Quote of the Day: "Now (T)default
means the same as default(T)
!"
We continued to flesh out the designs for features currently considered for C# 7.1.
- Default expressions (design adopted)
- Field target on auto-properties (yes)
- private protected (yes, if things work as expected)
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.
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 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.
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.
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
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.
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.
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.
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
.
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.
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.
Adopt default
expressions with the design decisions described above, targeting C# 7.1.
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.
Adopt the feature with a current target of C# 7.1.
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.
If all those things work out well enough, we do want the feature.