161 lines
8.6 KiB
Markdown
161 lines
8.6 KiB
Markdown
# 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](https://github.com/dotnet/csharplang/issues/102)
|
|
|
|
We plan to allow target typed `default` expressions, where the type is left off:
|
|
|
|
``` c#
|
|
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*.
|
|
|
|
``` c#
|
|
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:
|
|
|
|
``` c#
|
|
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`:
|
|
|
|
``` c#
|
|
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:
|
|
|
|
``` c#
|
|
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
|
|
|
|
``` c#
|
|
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:
|
|
|
|
``` c#
|
|
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:
|
|
|
|
``` c#
|
|
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](https://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.
|