C# Design Notes for Feb 10, 2014

Topics: C# Language Design
Coordinator
Mar 28, 2014 at 11:47 PM
Edited Mar 28, 2014 at 11:48 PM

C# Design Notes for Feb 10, 2014

Notes are archived here.

Agenda

  1. Design of using static <design adopted>
  2. Initializers in structs <allow in certain situations>
  3. Null-propagation and unconstrained generics <keep current design>

Design of using static

The “using static” feature was added in some form to the Roslyn codebase years ago, and sat there quietly waiting for us to decide whether to add it to the language. Now it’s coming out, it’s time to ensure it has the right design.

Syntax

Should the feature have different syntax from namespace usings, or should it be just like that, but just specifying a type instead? The downside of keeping the current syntax is that we need to deal with ambiguities between types and namespaces with the same name. That seems relatively rare, though, and sticking with current syntax definitely makes it feel more baked in:
using System.Console;
as opposed to, e.g.:
using static System.Console;

Conclusion

We’ll stick with the current syntax.

Ambiguities

This leads to the question of how to handle ambiguities when there are both namespaces and types of a given name. We clearly need to prefer namespaces over types for compatibility reasons. The question is whether we make a choice at the point of the specified name, or whether we allow “overlaying” the type and the namespace, disambiguating at the next level down by preferring names that came from the namespace over ones from the type.

Conclusion

We think overlaps are sufficiently rare that we’ll go with the simple rule: A namespace completely shadows a type of the same name, and you can’t import the members of such a type. If this turns out to be a problem we’re free to loosen it up later.

Which types can you import?

Static classes, all classes, enums? It seems it is almost always a mistake to import non-static types: they will have names that are designed to be used with the type name, such as Create, FromArray, Empty, etc., that are likely to appear meaningless on their own, and clash with others. Enums are more of a gray area. Spilling the enum members to top-level would often be bad, and could very easily lead to massive name clashes, but sometimes it’s just what you want.

Conclusion

We’ll disallow both enums and non-static classes for now.

Nested types

Should nested types be imported as top-level names?

Conclusion

Sure, why not? They are often used by the very members that are being “spilled”, so it makes sense that they are spilled also.

Extension methods

Should extension methods be imported as extension methods? As ordinary static methods? When we first introduced extension methods, a lot of people asked for a more granular way of applying them. This could be it: get the extension methods just from a single class instead of the whole namespace. For instance:
using System.Linq.Enumerable;
Would import just the query methods for in-memory collections, not those for IQueryable<T>.
On the other hand, extension methods are designed to be used as such: you only call them as static methods to disambiguate. So it seems wrong if they are allowed to pollute the top-level namespace as static methods. On the other other hand, this would be the first place in the language where an extension method wouldn’t be treated like a static method.

Conclusion

We will import extension methods as extension methods, but not as static methods. This seems to hit the best usability point.

Initializers in structs

Currently, field initializers aren’t allowed in structs. The reason is that initializers look like they will be executed every time the struct is created, whereas that would not be the case: If the struct wasn’t new’ed, or it was new’ed with the default constructor, no user defined code would run. People who put initializers on fields might not be aware that they don’t always run, so it’s better to prevent them.

It would be nice to have the benefits of primary constructors on structs, but that only really flies if the struct can make use of the parameters in scope through initializers. Also, we now have initializers for auto-properties, making the issue worse. What to do?

We can never prevent people from having uninitialized structs, and the struct type authors still need to make sure that an uninitialized struct is meaningful. However, if a struct has user-defined constructors, chances are they know what they’re doing and initializers wouldn’t make anything worse. However, initializers would only run if the user-defined constructors don’t chain to the default constructor with this().

Conclusion

Let’s allow field and property initializers in structs, but only if there is a user-defined constructor (explicit or primary) that does not chain to the default constructor. If people want to initialize with the default constructor first, they should call it from their constructor, rather than chain to it.
struct S0 { public int x = 5; } // Bad
struct S1 { public int x = 5; S1(int i) : this() { x += i; } } // Bad
struct S2 { public int x = 5; S2(int i) { this = new S2(); x += i; } } // Good
struct S3(int i) { public int x = 5 + i; } // Good

Unconstrained generics in null-propagating operator

We previously looked at a problem with the null-propagating operator, where if the member accessed is of an unconstrained generic type, we don’t know how to generate the result, and what its type should be:
var result = x?.Y;
The answer is different when Y is instantiated with reference types, non-nullable value types and nullable value types, so there’s nothing reasonable we can do when we don’t know which.
The proposal has been raised to fall back to type object, and generate code that boxes (which is a harmless operation for values that are already of reference type).

Conclusion

This seems like a hack. While usable in some cases, it is weirdly different from the mainline semantics of the feature. Let’s prohibit and revisit if it becomes a problem.
Apr 18, 2014 at 11:59 AM
I would propose the use of the existing default(T) operator for the null propogation issue.
May 1, 2014 at 11:09 PM
I would propose having the ?. pair with : to specify the default value (if nullable, it could also pair with ??). The desired default may null or default(T) 75% of the time, but I see no reason not to deal with the other 25%.
Coordinator
May 1, 2014 at 11:28 PM
The ?. operator always checks for null (not default value) and can therefore only be applied to reference types and nullable value types. It also always produces null when it finds null in the receiver, so the output is always a reference type or a nullable value type. In a?.b if the member b has a non-nullable value type T, then the result of the whole expression is lifted to type T?.

If you want something else as the result you can always tag a ?? at the end:
customer?.Orders[0].Amount ?? 0
The expression customer?.Orders[0].Amount is of type int? (assuming Amount is declared to be int). The null-coalescing ?? operator peels the nullability off, so that the whole expression is int.

Semantically it is similar to:
((customer == null) ? customer.Orders[0].Amount : null) ?? 0
Except that customer is only evaluated once.

Ending with a ?? operator is probably common, and the compiler could optimize to:
(customer == null) ? customer.Orders[0].Amount :  0
I don't know if we'll do this optimization but we certainly could.
May 1, 2014 at 11:51 PM
Edited May 1, 2014 at 11:56 PM
Having ?. yield a T? when the right hand operator doesn't obey a class constraint will make the operator useless when the right hand operator is of an unconstrained generic type. Having it yield a default(T) would make the operator almost useless in many cases when the right-hand operator is a value type. Allowing it to yield a value specified after a : or ?? would alleviate both problems and vastly increase the usefulness of the operator in many other cases as well. The : or ?? term could be omitted in cases where either the right-hand operator was a reference type or Nullable<T>, but even in those cases I would think including those operators would make it visually clearer that the code in question could return null even if the right-hand member never would.

See my post on https://roslyn.codeplex.com/discussions/543895 for more explanation.