Hacker News new | past | comments | ask | show | jobs | submit login

Out variables seem like a mis-feature, especially when they are adding a better alternative (Tuples) in the same release.

It's great to finally have tuples, but the c/java style syntax is showing it's age compared to something like scala which has return type declarations at the end, which I find much more readable.

Literal improvements will be a godsend for writing database migrations.

Ref returns and locals look like a source of constant headaches. It's much harder to reason with code when the data can be modified by random others bits of code. Currently I can look for all references to an array to find everywhere that modifies it now I can't.




Hey there, C# language designer here.

> Out variables seem like a mis-feature, especially when they are adding a better alternative (Tuples) in the same release.

Out variables are really great when working with existing code that predates Tuples (for example, the entire BCL). We don't just introduce language features that will only work well for new code. We also want to make the experience of coding against existing APIs feel better.

> Ref returns and locals look like a source of constant headaches.

That 'headache' has to be weighed against the needs of some parties to write very fast code and to have as little overhead as possible. ref returns enables a much more pleasant coding experience that enables these sorts of speedups in these specialized scenarios.

> It's much harder to reason with code when the data can be modified by random others bits of code.

There is a simple solution to this of course, don't expose your data through ref returns :)

For many (likely most) developers, ref returns simply won't be something that ever hits their radar. Similar to stackallocs and other esoteric features, this is really a targeted improvement for people who really like working in .Net but are finding it hard to squeeze out essential perf in some core scenarios.

The .net runtime supports this features fantastically. We wanted to make it available to people who like the power and safety of C#, but occasionally need some low level hooks to get that extra perf boost as well.


> Out variables are really great when working with existing code that predates Tuples (for example, the entire BCL). We don't just introduce language features that will only work well for new code. We also want to make the experience of coding against existing APIs feel better.

I disagree on this point. If there is a language feature that has been superseded by a better alternative, continuing to add sugar to the old feature only serves to perpetuate its use.

It embiggens the language while providing relatively little value in return. Furthermore, it adds confusion as to what should be considered idiomatic.

Tuples and deconstruction are so clearly better than out variables, I would have thought it would make more sense to deprecate the "out" feature entirely. Or, at the very least, not make it easier to use them.

Awkward and outdated features should be painful to use.

I also felt that this was a very odd addition. Even odder, the article doesn't even list tuples and deconstruction first.


Did you guys look into alternative syntax for return types? It really is a huge eyesore, and impediment to reading, when you have more than just a typename and a couple of modifiers like * and [] attached to it.

With tuples especially, it's even worse, because the method name gets squished between two very similarly looking ()s, which is very different from how methods have historically looked in code. If I were scanning code fast, I'm not sure I would even read it as a function declaration.

C++ adopted its "auto ... -> ..." syntax a while ago - granted, they had a forcing function in form of decltype(), but many people also use it for readability reasons with complex return types. I hope C# follows suit; or, better yet, comes up with a unified type-after-name syntax a la ES6, that can be used everywhere in the language, while retaining existing syntax for back-compat purposes.


What would you suggest?

public TResult SomeMethod() where TResult : (string name, int id)


The most obvious approach would be to repurpose C++ syntax, since it's already been around for a while, and C# syntax is generally pretty close to C++ in many respects. So:

  public SomeMethod(bool b) -> (string name, int id) { ... }
However, this may be undesirable due to confusion with => for lambdas and expression-bodied methods, especially in:

  public SomeMethod(bool b) -> (string name, int id) => ...
: is the next obvious candidate, and would unify the syntax with TypeScript and many other languages. But given that it's already used for labels and named arguments, I'm not sure there's enough room there to reuse it also for types.

:: is another decent choice in terms of familiarity coming from other languages (Fortran, Haskell etc). But, unfortunately, it's already taken for extern alias, and I don't think this could be easily disambiguated in many contexts.

Now, if this is narrowly scoped to method return type only (i.e. we're not trying to invent a syntax that could later be used in a similar way to swap the type and the name in other places, like arguments and variables), and only as a fallback for when the usual "Type Name" arrangement has poor readability, perhaps take a hint from Ada and reuse "return"?

  public SomeMethod(bool b) return (string name, int id) { ... }
A tad verbose, but if it's intended to be used sparingly, primarily with tuple-returning methods and deeply nested generics, I think that's okay - tuples themselves are pretty verbose when components are named.

Or maybe borrow "as" from VB? It looks like it could be extended to other kinds of declarations in the future in a straightforward manner, without conflicting with its existing use for casts:

  // Just for method return types
  public SomeMethod(bool b) as (string name, int id) { ... }

  // For everything
  public SomeMethod(b as bool) as (name as string, id as int) {
    var x as float;
    TryFoo(out var y as bool);
    ...
    switch (obj) {
      case foo as Foo:
        ...
    }
  }


I like the as syntax, it may conflict with casting though when used on variables.

    public SomeMethod(bool b) -> (string name, int id) { ... }
When was this added to c++? I think I need to brush up on my lower level skills.


With regards to "as" conflicting, I think it shouldn't be a problem because the cast operator is binary. So if you already know that something is a declaration, the current token is the identifier that's being declared (or closing parenthesis of the parameter list), and the following token is "as", it cannot be a valid expression. Consider:

  var x as int;
"var" makes it a declaration, so "x" is the identifier, and "as" has to be the type specifier.

  var x = y as int;
"=" clearly separates the identifier from the initializer, and the latter is an expression, so "as" is the cast operator.

Similar reasoning applies to other places where the two can appear - function parameter list, return type, field and property declarations etc.

So far as I can tell, by the time we get to the point where "as" would be used to specify the type of the declared entity, we will already know that it's a declaration, and that the only other token that can follow in existing grammar is "=", "{", or "=>" (for variable and field initializers and default parameter values, property getters and setters, and lambdas and expression-bodied members, respectively), and none of these start an expression. In all other contexts, "as" would be an operator.


It was added in C++11 when they added decltype(), so that you could reference arguments when computing return type. It's slightly different though, in that you need to specify the return type in the usual position as "auto" first, and then you can use "->" after the parameter list. So:

  template<typename A1, typename A2>
  auto foo(A1 a1, A2 a2) -> decltype(a1 + a2) {
    return a1 + a2;
  }


Honestly, I'd like to have seen ref returns implemented as attributed out params (syntactic sugar). As it stands I can't see me using them in any of places that I originally planned to - I am exactly the target market for that feature. Still, I'm sure something interesting can be done with this.


> That 'headache' has to be weighed against the needs of some parties to write very fast code and to have as little overhead as possible. ref returns enables a much more pleasant coding experience that enables these sorts of speedups in these specialized scenarios.

I'm worried this could result in a loss of focus, you can't make everyone happy all the time. c# is a fantastic language to develop applications in (web or desktop) but it's never going to be the fastest language around or be a systems level language. For me some of the other features listed here (like immutable records) would be a much better fit for where c# excels already.


The Midori project proved otherwise.

One of the reasons we are stuck with C and C++ is because other language vendors, including Microsoft, dropped the ball regarding performance of type safe languages.

Do you think C++ would have been kept around if Java and the CLR had been AOT compiled to native code with focus on compiler optimisations since day one?

I really appreciate the efforts of the .NET team in adopting the Midori lessons in the standard .NET stack.


Some things can be rather tricky to AOT-compile, especially when you don't have a fixed "world" (i.e. when code can be dynamically loaded, as is the case with both Java classes and CLR assemblies).

Consider virtual generic methods, for example. If your set of types is not statically bound, you can't allocate the appropriate number of vtable slots in advance, because you don't know all the instantiations.


C++ has the same problem.

I was already using dlls with plugins in Windows 3.1 and C++.


C++ doesn't have this problem, because it doesn't allow virtual function templates.

For that matter, it doesn't allow any templates across ABI boundary, except when you manually instantiate them (extern template) - and then only for those explicit instantiations. So it is effectively impossible to have a generic C++ API using templates that is not statically linked.


Of course it does, as I said I was doing that.

Borland C++ for Windows 3.x had an initial implementation for templates as they started to be discussed at ANSI and also supported exporting classes across dlls, providing both producer and consumer were Borland C++.


How exactly did that implementation work for templates?


No idea, not something I was too deep into. Back on those days I just used them.

Here is the link for the Borland C++ compiler documentation, I was actually using the Turbo C++ version for Windows 3.1.

https://archive.org/details/bitsavers_borlandborn3.1Programm...

The BIDS, Borland International Data Structures were the template based library that replaced the object based one and could be accessible as a DLL as well.

To export classes from DLL one would do something like

    // On the DLL side
    class _export MyClass {...}

    // On the consumer side
    class huge MyClass {...}

Described on page 336 of the manual.

If you check the the templates documentation, the generated code code could be externalized, page 152, via the -Jgx.

I was only a BIDS user across DLLs, but I can easily imagine a combination of _export/huge and -Jgd/-Jgx being what was necessary to make it all work.


Okay, so that was something that I have mentioned in passing earlier:

"it doesn't allow any templates across ABI boundary, except when you manually instantiate them (extern template)"

So you can export template instantiations, yes. But you cannot export the template itself. For fairly obvious reasons - to instantiate a template requires compiling C++ code after substituting the tempalte parameters, so to export it across ABI boundary would require including the entirety of the C++ language (or at least a substantial subset - I guess you could desugar a bunch of stuff like, say, lambdas before) in that ABI.

And virtual templates are a whole other kettle of fish, because every instantiation of a virtual template would require a new slot in the vtable - or else the generic would have to dispatched by a single method, and the caller would have to supply runtime type information. In practice, C++ just forbids virtual templates outright.

In C#, this all works just fine, because generics still exist on bytecode level, which is what gets exported - and at runtime, the JIT will instantiate the generic by compiling that bytecode to native code, separately for every instantiation (in practice they unify them for reference types, because pointers are always of the same size; but value types get distinct JIT-compiled versions each).

For virtual generics, I'm actually not entirely sure how this works, but I would assume that vtable slots are allocated in batches and filled with instantiations as needed - and when you run out of slots and need to reallocate the vtable (and potentially shift indices of other methods in it), you can just JIT-recompile the bytecode for any method that happens to use the class with affected vtable.


Well maybe you don't care, but there are folks that do (I'm on that list) ;-) I want the speed of C but the safety of C#.

There are many pieces of code that you probably use every day that go to great lengths to squeeze as much power as possible. Why not help these folks help us? I'm glad there is more focus on performance because we all benefit. (Hoping to see more on the CLR side.)


> I want the speed of C but the safety of C#

Rust or D seem like the best bet here


Why c# and not rust or something similar that has a lower level focus?


Because some of us believe in GC enabled systems programming languages, as done already at Xerox PARC (Cedar), DEC (Modula-2+/Modula-3), ETHZ (Oberon, Active Oberon, Component Pascal) and MSR (Sing# / System C#).

What is missing is a company like Microsoft to push it down to mainstream developers, at all costs, like it happen to all luddites.


> I want the speed of C but the safety of C#

Try OCaml.


>I want the power of a Bugatti but the efficiency of a Prius.

Use a tractor.


Adding features for the the subset of developers that need optional performance enhancements is great. However, it does seem ripe for misuse. As in the developer who makes everything a ref reference because they read it makes it "faster." Would there be a way to get Visual Studio to warn about this?


Sure. Write a Roslyn analyzer to help out here.

Note: it's not like you can just sprinkle 'ref' on the return type of your methods willy nilly. Like 'ref'/'out' parameters, it requires you to do very specific things to keep your code safe/legal. As such, it's unlikely to just be added by people because, by and large, most code won't be equipped to actually handle it properly.

The codebases that will want to use this are already ones that have done a lot of work that makes them amenable to ref. i.e. game engines where you have large arrays of structs that you want to operate on in an efficient manner and whatnot.


I think it's great that you guys are adding a lot of high performance features that are beneficial to game engines. Any news you can share with us on a certain very-popular-game-engine-running-a-very-old-version-of-the-clr? ;)


Not .NET developer here but the usual way is to use some sort of linting tool to "disable" the esoteric features.


These tend to become cargo cult practices, "thou shalt never" that get in the way of those few places where they are useful.


I don't think so. The purpose of compiler warnings, lint-like tools, static analyzers and such is to bring developer attention to some block of code. Not 'Hey, you should not do it this way', but 'Hey, is this thing here as intended by you or just a typo or mistake?'.

I like the way its implemented in Perl. Use 'use strict' by default, and guard the block where you really need something unusual by 'no strict something' - refs, vars, subs - so neither compiler nor people reading your code never being confused whether is it a mistake or author's intention.


In C# one can always

  #pragma warning disable ${list of warnings}
and

  #pragma warning restore ${list of warnings}
https://msdn.microsoft.com/en-us/library/441722ys.aspx

Although frankly the list of warnings to disable and restore consists of warning numbers, not names, which is not super convenient.


>Out variables seem like a mis-feature, especially when they are adding a better alternative (Tuples) in the same release.

You can't retro-fit multiple-returns (via Tuples) onto existing library functions, so for places where you're forced into using out parameters, this is a slight improvement.


Sure you can - you can introduce a new overload resolution rule that lets you treat trailing out params on a method as tuple members, so a method

    TReturn Foo(T1 param1, out T2 param2) 
can be called as if it were

    (TReturn, T2) Foo(T1 param1)
You'd need a keyword to trigger this kind of overload resolution at the call site to ensure back compatibility - maybe you'd have to call

   (var x, var y) = out Foo(param); 
or something. That way all that legacy library code gets a free upgrade to your new language feature.

Since the C# team decided not to do that, you can actually implement a version of this at a library level, by static importing a family of generic methods like this:

   Func<TParam, (T1, T2)> Tuplify<TParam,T1,T2>(Func<TParam, out T1, T2> outFunc) {...}
for every pattern of out params you want to convert, then you can just call

   (var x, var y) = Tuplify(Foo)(param);


> it's age compared to something like scala which has return type declarations at the end, which I find much more readable.

Scala or .... VB.net!


LMAO

-Anthony D. Green, Program Manager, Visual Basic


I'll begrudgingly admit it got a couple of things right.


Out parameters are also useful in constructors. I often use them as a way to limit the scope in which an object can be modified, which lets me have mostly immutable objects.

    IEnumerable<Student> GetStudents(SqlCommand cmd)
    {
        var rdr = cmd.ExecuteReader(); //select * from student left join courses on...

        var memo = new Dictionary<int, Student.Completion>(); //Completion lets us add courses to a student

        while(rdr.Read())
        {        
            if(!memo.TryGetValue(rdr.GetInt32("student_id"), var out complete)
                 memo.Add(new Student(rdr out complete).ID, complete);

            complete.AddCourse(rdr); //might be a no-op
        }

        return from kv in memo select kv.Value.Student; //each student object is unmodifiable.
    }


So I'm taking a look at this line:

  if(!memo.TryGetValue(rdr.GetInt32("student_id"), var out complete)
    memo.Add(new Student(rdr out complete).ID, complete);
I think I understand your point, but I found this code really hard to read. You don't use the first var out complete in the TryGetValue, right? And the Student c'tor returns itself as an out parameter?

If I understand you correctly, you like this because you don't have to declare a new student before you add it? I.e. the alternative would be

  if(!memo.ContainsKey(rdr.GetInt32("student_id")))
    var student = new Student(rdr);
    memo.Add(student.ID, student);
I guess I would prefer this over the former. Also, why not enforce your student ID constraint in SQL instead of putting everything in a dictionary only to take it back out again? That would simplify your code to the point of just being a map from rdr->Student. Furthermore, if all I had was the Student c'tor, I would never guess that was the intent of the out parameter. This seems more anti-pattern than pattern.

I would rather put a simple IEnumerable in front of SqlDataReader so that you could just do:

  foreach(var row in rdr) yield return new Student(row)
This doesn't obviate your use case, however, which is to inline a variable where it's needed in multiple places in that line because you can save yourself an explicit declaration. In this case, however, I think that the increased readability justifies the explicit declaration.


Hi, thanks for taking the time to comment.

> You don't use the first var out complete in the TryGetValue, right?

There is only one complete variable; we declare it in TryGetValue, and it's definitely used in the last line of the while statement, but might first be used after the if statement.

> And the Student c'tor returns itself as an out parameter?

The Student constructor does not return itself as an out parameter, rather it returns an object that can modify an internal list of courses.

The idea is that when we iterate through a result set from a database, some of the rows are going to correspond to a new student object, and some are going to correspond to a course that belongs to the student. Crucially, we are only allowed modify the Student object (or whatever) during iteration, and the collection that we return will only contain immutable/unmodifiable Student objects.

I've used this technique to populate deeply nested structures (lots of joins and nested joins) using only one query.

> I would rather put a simple IEnumerable in front of SqlDataReader so that you could just do:

    > foreach(var row in rdr) yield return new Student(row)
Just to be clear, the reason that I cannot do that is because the Student object might not be completely "hydrated" until we finish iterating through the result set, because it might contain a bunch of nested objects that also need to be instantiated from one or more DB records.

> Also, why not enforce your student ID constraint in SQL instead of putting everything in a dictionary only to take it back out again?

I'm not quite sure I understand this (which is probably my fault), but rest assured that we use nothing but SQL (specifically, DDL) to enforce data integrity.


Thank you, in turn, for replying. I'm of the opinion that out variables have little value if you have tuples and deconstruction, but I wanted to understand your point. I'm not sure I do, unfortunately. I'm little out of practice with C#, so bear with me.

Back to this code:

  if(!memo.TryGetValue(rdr.GetInt32("student_id"), var out complete)
    memo.Add(new Student(rdr out complete).ID, complete);
There's a parens missing on the end of the if, correct? Also I can't parse the student c'tor:

  new Student(rdr out complete)
I was assuming there is a comma in there somewhere. Does this compile? What does "out" do here? I thought you were getting a new out variable, but that's not correct since you wouldn't be able to name it the same in the same scope.

Also, I don't see how "complete" is ever non-null. If the ID isn't in the dictionary, then TryGetValue returns false and "complete" is null. Then you add the null "complete" to the dictionary, and throw away the Student object (which apparently does other side effects) once you have its id? If ID is in the dictionary, you get back what you inserted, which is still null.

And then you call .AddCourse on the possibly null reference? I'm lost.

Can you post the code again? Maybe I'm just missing something due to a syntax error.


You're right, there were missing tokens; sorry about that.

Here is the equivalent C# 6 version of the code (i.e. something very similar to the pattern that I currently use):

        IEnumerable<Student> GetStudents(SqlCommand cmd)
	{
		var rdr = cmd.ExecuteReader();
		var memo = new Dictionary<int, Student.Completion>();

		while(rdr.Read())
		{
			Student.Completion completion; //this declaration will be unnecessary in C# 7
			
			if(!memo.TryGetValue(rdr.GetInt32("student_id"), out completion))
				memo.Add(new Student(rdr, out completion).ID, completion);
			
			completion.AddCourse(rdr); //completion is *guaranteed* to be non-null
		}
		
		return from kv in memo select kv.Value.Student;
	}
As you can see, it only differs from the C# 7 version by one line.

The first thing to note is that, per the C# spec, the `out` parameters of a method must be definitely assigned before the method returns[1]. It just so happens that the constructor of the `Student` class always creates a new `Completion` object and assigns it to the `out` parameter. Now, theoretically, an `out` parameter could be assigned a null reference (as in the case of TryGetValue), but in practice it's trivial to guarantee that it will be non-null (as in the case of our Student cstor).

In the line,

    memo.Add(new Student(rdr, out completion).ID, completion);
we first call the Student cstor, which assigns a non-null value to completion, so that by the time `memo.Add` is called, the completion variable is guaranteed to be non-null.

Also, `Student.Completion` is a class that is defined inside of the `Student` class. As such, it has access to all of Student's members (private and public). But, in order to do anything to an instance of Student, a Completion instance must have field which references that Student instance (unlike the case of Java's inner classes, which are a bit more powerful I think). That is why this line is possible:

     return from kv in memo select kv.Value.Student //`Value` is an instance of Student.Completion
Here is the basic definition of the Student class:

        sealed class Student
	{
		public int ID { get; } //this is a readonly property, meaning it can only be modified in a cstor
		public FullName { get; } //readonly property

		private List<Course> courses = new List<Courses>(); 

                public IEnumerable<Course> Courses => from c in courses select c;
		
		public Student(IDataReader rdr,  out Completion completion)
		{
			ID = rdr.GetInt32("student_id");
			Fullname =  rdr.GetString("fullname");
			
			completion = new Completion(this);
		}
		
		public sealed class Completion
		{
			public Student Student { get; } //readonly property.
			
			public Completion(Student student)
			{
				this.Student =  student;
			}
			
			public void AddCourse(IDataReader rdr)
			{
				if(rdr.GetInt32("student_id) == Student.ID)
					student.courses.Add(new Course(rdr));
			}
		}
	}

 
Like I said in my earlier comment, my immediate goal was to be able to create a set of immutable objects with arbitrary nestings from the result of a single SQL query that may have an arbitrary number of joins (which is how we represent nesting relationally). The above example just has just one nested property, but I have production code in which objects have many more nested properties. For example, the `Course` class in the aforementioned example might have its own `Completion` class for adding `CourseAssignment` instances (e.g. select from Student left join Course on ... left join CourseAssignment on ...).

But, the really big idea is that I wanted an object-capability system[2][3]. Getting objects be immutable "almost everywhere"[4] is a nice side benefit.

[1] http://www.ecma-international.org/publications/files/ECMA-ST... (section 12.1.6)

[2] https://en.wikipedia.org/wiki/Object-capability_model

[3] https://www.youtube.com/watch?v=EGX2I31OhBE

[4] https://en.wikipedia.org/wiki/Almost_everywhere (in this case "almost everywhere" is with respect to the set of all possible execution paths).


Thanks for continuing to indulge me in this conversation. So, if I understand you correctly, your data looks something like this:

    StudentId|    Name |CourseId
    ============================
            1|      Bob|       1
            2|     Jane|       1
            1|      Bob|       2
            1|      Bob|       3
            2|     Jane|       4
With many more columns of course. The point being that it's a denormalized listing of student-course pairs. If so, you'd be able to get immutability by reading the query result into a data table and doing something like:

    var result = from row in dt.AsEnumerable() 
    group row by row.Field<int>("StudentId") into grp 
    select new Student(
        grp.Key,
        grp.First().Field<string>("Name"),
        from courseRow in grp select new Course(courseRow)
        );
This seems to satisfy all of the conditions that you list (namely immutable objects), but with the added advantages of not needing a inner Completion class, and a number of less lines of code.

Furthermore this is less coupled because now the Student class doesn't need to worry about DataReaders or DataTables or which column names to read from.

I would also argue that this version is a lot easier to read and reason about.

I think you could make a similar transformation for any circumstance in which you wanted to use an out variable in the fashion you outline.

Does that make sense? Is there another case in which you would advocate for our variables in new code?


Yeah, I think lots of uses of out in the .Net framework are from the early days and a little bit of a mistake. Since they can't remove them this is suppose to make them easier to work with. TryGetValue comes to mind, but I agree overall out variables always felt clunky


Although F# manages to convert all those TryGetXXX methods into tuple-returning transparently, so it can be done. Just off the top of my head, you could probably write an extension method to wrap the original into the new format pretty easily.


> Literal improvements will be a godsend for writing database migrations.

Can you elaborate on this; both how it helps and why you're using C# for database migrations?


My go to migrator is fluent migrator (https://github.com/schambers/fluentmigrator) or flywaydb if anyone objects to the c# (https://flywaydb.org/).

Each migration gets a number, typically time encoded. If I was to write one now it would have the attribute [Migration(201608251157)]. With the new literals this will become [Migration(20160825_1157)], amazing how much readability a single underscore can make.


>c/java style syntax is showing it's age compared to something like scala which has return type declarations at the end, which I find much more readable

I hope there are better examples of Scala's syntactical advantages than this one, because it seems like the 'egyptian-style' vs 'next line style' brace bracket debate...


Yes there are plenty. The correct order of type declarations just ties in with the better syntax for generics and leads to an easier to read, more consistent language.


Interesting; thanks. F# and Scala are pretty high on my to-learn list.


I've seen some p/invoke examples where out variables are used as a c pointer.


out/ref still have their uses when interfacing with COM or P/invoke.




Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact

Search: