csharplang/meetings/2017/LDM-2017-03-07.md
2017-04-05 17:19:42 -07:00

8.6 KiB

C# Language Design Notes for Mar 7, 2017

Quote of the Day: "Now (T)default means the same as default(T)!"

Agenda

We continued to flesh out the designs for features currently considered for C# 7.1.

  1. Default expressions (design adopted)
  2. Field target on auto-properties (yes)
  3. private protected (yes, if things work as expected)

Default expressions

github.com/dotnet/csharplang/issues/102

We plan to allow target typed default expressions, where the type is left off:

int i = default(int); // Current
int i = default;      // New
var i = default;      // Error: no target type

In essence, default is like null: an inherently typeless expression, that can acquire a type through implicit or explicit conversion. Unlike null, default can be converted to any type T, including non-nullable value types, and is equivalent to default(T) for that T. For types where both null and default are allowed, they both have the same value, namely null.

Constants

Like null and default(T), default is a constant expression for certain types.

const string S = null;    // sure
const string S = default; // sure
const int? NI = null;     // error
const int? NI = default;  // error

This is just a consequence of the existing limitations on which types can be constants.

Constant patterns

Constant patterns are interpreted in context of the type of the expression they are tested against. For instance, if the left hand side of an is expression is of an enum type E, 0 is the zero value of that enum type, not the int zero:

E myE = ...;

if (e is 0) ... // 0 means (E)0, not (int)0;

For null, this means that the null value created by the constant pattern is of the reference or nullable value type of the left hand side. A null pattern is not allowed if the type of the left hand side is a non-nullable value type, or a type parameter not known to be a reference type.

As a special dispensation, however, null is allowed as a constant pattern even if the type of the left hand side is not one that allows constants.

We will allow default as a constant pattern when the type of the matched expression is a reference type, a nullable value type or a constant type.

case default

As a special case (no pun intended), this decision means that it would be permitted to write case default: in a switch statement! That is surely going to be confused with a default: label, and is almost certainly either a bug or a very bad idea. For this particular syntactic pattern, therefore, we should yield a warning. The warning goes away if you parenthesize case (default): or add a type case default(T): or similar.

Optional parameters

Expressions of the form default(T) are explicitly allowed as default arguments of optional parameters even when non-constant. Therefore, so is default:

void M(MyStruct ms = default) { ... } // sure

Special forms

Today null is specially allowed in a number of situations, where it is treated as a value even though it is not given a type. We have to decide whether default is similarly allowed in those places. Our general position is "no": While null conceptually has a specific value, even though its representation varies, default conceptually has different values depending on context. Therefore it doesn't make sense to treat it as a value in typeless contexts.

Specifics listed out below.

Equality

We allow null == null as a special case, called out specifically in the language spec. We don't think we should allow default == default. Even though it would probably be true regardless of the type, it doesn't make sense with no type.

is-expressions

null is allowed on the left hand side of is expressions:

bool b = null is T; // allowed, always false

Should default similarly be allowed? It is important to note that the type T in these expressions is not a context type for the expression: the compiler does not convert the expression to the type. For instance, when you write

if (0 is DateTime) { ... }

The result is always false (the compiler even warns about it), even though 0 converts to the enum DateTime. This is because 0 is interpreted on its own, not through a literal conversion, and on its own it has the type int. The int zero is not a DateTime, so the result is false. It is essentially as if the expression was boxed to object, and then checked at runtime against the type.

By the same reasoning, if we allowed default on the left hand side it would not mean default(T) but default(object). That would surely be confusing to the programmer:

if (0 is int)            { ... } // True: 0 is an int 
if (default(int) is int) { ... } // True: 0 is still an int
if (default is int)      { ... } // Would be false! default(object) is null, which is not an int.

For this reason, we will not allow default on the left hand side of an is-expression.

as-expressions

null is allowed on the left hand side of as expressions:

T t = null as T; // allowed, always null

Should default similarly be allowed? In case of as, the type is restricted to be a reference type or a nullable value type. Therefore default always meaning null wouldn't be surprising in the same way that it would be for the is operator.

In this particular case, therefore, it seems alright to allow default, essentially as an alias for null.

throw statements and expressions

You are allowed to throw null in C#. It leads to a runtime error, which ironically causes a NullReferenceException to be thrown. So it is a sneaky/ugly idiom for throwing a NullReferenceException.

There's no good reason to allow this for default. We don't think the user has any intuition that that should work, or about what it does.

Using default versus null

When should I use default, and when should I use null? Are we setting up for style wars?

One proposal to circumvent this issue is to not adopt the default syntax, but instead use null - even for non-nullable value types. Not only would this be confusing, it would also be breaking: It would allow null to convert to e.g. int, which would affect overload resolution.

Another idea is to allow default only when the type is not known to be a reference or nullable value type. So the two would be mutually exclusive, and you'd never have a choice.

Let's not restrict it in the language. It would be a matter of style, and an IDE might even give you code style options about it. The recommendation would probably be null when we can, default otherwise, and default(T) when we have to. But you can imagine for instance using default for nullable things for uniformity, where a swath of code initializes multiple things, some nullable and some non-nullable. Not allowing that seems unnecessarily restrictive.

Conclusion

Adopt default expressions with the design decisions described above, targeting C# 7.1.

Field target on auto-properties

github.com/dotnet/csharplang/issues/42

Through several releases we have neglected to make the field target on attributes work right when applied to auto-implemented properties. It should attach the attribute to the underlying generated field, just as it does when applied to field-like events.

There are extremely esoteric ways in which this can be a breaking change; essentially when someone exploits unintended leniency in today's compiler. There is absolutely no benefit from such exploitation, and we don't imagine anyone would rely on it. If it turns out we're wrong, we can reintroduce the leniency specifically for the breaking case, but we don't think it is worth it here.

Conclusion

Adopt the feature with a current target of C# 7.1.

Private protected

We have a complete feature design and a very nearly complete implementation. What we need in order to go forward is to test out a lot of things:

  • Do completion, refactorings, etc. work in IDEs?
  • How does it work with languages that don't understand it, including F# and older versions of C#
  • How much would it confuse other tools such as CCI?
  • Does this fully and completely work as expected in the runtime?

Even though the feature is already allowed in C++/CLI, there's reason to suspect it wasn't exercised all that broadly, so there may be corner cases that don't work as expected.

This feature would need to be added to both C# and VB for API parity reasons. We would need to do the work to implement it in VB.

Conclusion

If all those things work out well enough, we do want the feature.