122 lines
5.3 KiB
Markdown
122 lines
5.3 KiB
Markdown
|
|
||
|
# C# Language Design Meeting for June 17, 2020
|
||
|
|
||
|
## Agenda
|
||
|
|
||
|
1. Null-suppression & null-conditional operator
|
||
|
1. `parameter!` syntax
|
||
|
1. `T??`
|
||
|
|
||
|
## Discussion
|
||
|
|
||
|
### Null-suppression & null-conditional operator
|
||
|
|
||
|
Issue #3393
|
||
|
|
||
|
We generally agree that this is uninintended and unfortunate, essentially a spec bug. The
|
||
|
only question is whether to allow `!` at the end of a `?.`, as well as in the "middle". In
|
||
|
some sense
|
||
|
|
||
|
### `parameter!` syntax
|
||
|
|
||
|
This has been delayed because we haven't been able to agree on the syntax. The main contenders
|
||
|
are
|
||
|
|
||
|
```C#
|
||
|
void M(string param!)
|
||
|
void M(string param!!)
|
||
|
void M(string! param)
|
||
|
void M(string !param)
|
||
|
void M(checked string param)
|
||
|
void M(string param ?? throw)
|
||
|
void M(string param is not null)
|
||
|
void M(notnull string param)
|
||
|
void M(null checked string param)
|
||
|
void M(bikeshed string param)
|
||
|
void M([NullChecked("Helper")] string param)
|
||
|
/* contract precondition forms */
|
||
|
void M(string param) Requires.NotNull(param)
|
||
|
void M(string param) when param is not null
|
||
|
```
|
||
|
|
||
|
The simplest form, `void M(string param!)` is attractive, but looks very similar to the null
|
||
|
suppression operator. The biggest problem is that you can see them as having very different
|
||
|
meanings -- `!` in an expression silences nullable warnings, while `!` on a parameter does the
|
||
|
opposite, it actually produces an exception on nulls. However, the result of both forms is a
|
||
|
value which is treated by the compiler as not-null, so there is a way of seeing them as similar.
|
||
|
|
||
|
Moving it to the other side of the parameter name, `!param`, would resolve some of the similarity
|
||
|
with the null suppression operator, but it also looks a lot like the `not` prefix operator. There's
|
||
|
slightly less contradiction in these operators, but it still features a bit of syntactic overloading.
|
||
|
|
||
|
`string!` has a couple problems, including a suggestion that it's a part of the type (which it would not
|
||
|
be), and that sometimes you may want to use the operator on nullable types, like `AssertNotNull` methods.
|
||
|
It also wouldn't be usable in simple lambdas without types.
|
||
|
|
||
|
`checked` suggests integer over/underflow more than nullability.
|
||
|
|
||
|
`param!!` has some usefulness that we could provide a corresponding expression form -- `!`
|
||
|
suppresses null warnings, while `!!` actually checks for null and throws if it is found. On the
|
||
|
other hand, it also reads a bit strangely, especially since we're adding a new syntax form
|
||
|
instead of trying to reuse some forms we already have. On the other hand, the fact that it's
|
||
|
different enough to look different, while also short enough to be used commonly has a lot in
|
||
|
favor of it. In general we historically have a bias towards making new things strongly
|
||
|
distinguished from existing code, but shortly after introducing the feature we tend to wish that
|
||
|
things were less verbose and didn't draw as much attention in the code. On the other hand, the
|
||
|
nullable feature has a rule that it should not affect code semantics, while the purpose of this
|
||
|
feature is to affect code semantics. `param!!` could be seen as being too similar to other things
|
||
|
in the nullable feature, but to some people it also stands out because of the multiple operators.
|
||
|
|
||
|
We did a brief ranked choice vote and came up with the following ranking, not as definitive, just to
|
||
|
measure our current preferences:
|
||
|
|
||
|
1. `void M(string param!!)`
|
||
|
2. `void M(nullcheck string param)`
|
||
|
3. `void M([NullChecked(Helper)] string param)`
|
||
|
|
||
|
Many people don't have strong opinions, so we don't have a clear winner coming out.
|
||
|
|
||
|
**Conclusion**
|
||
|
|
||
|
We're getting closer to consensus, but we need to discuss this more and consider some of the
|
||
|
long-term consequences, as well as impact on other pieces of the language design.
|
||
|
|
||
|
### `T??`
|
||
|
|
||
|
Unfortunately there are multiple parsing ambiguities with `T??`:
|
||
|
|
||
|
```C#
|
||
|
(X??, Y?? y) t;
|
||
|
using (T?? t = u) { }
|
||
|
F((T?? t) => t);
|
||
|
```
|
||
|
|
||
|
This has left us looking back to the original syntax: `T?`. The original reason we rejected this
|
||
|
was that there could be confusion that `T?` means "maybe default," so that the type is nullable
|
||
|
if it's a reference type, but not nullable if it's a value type.
|
||
|
|
||
|
If we want to allow the `T?` syntax anyway, we need some syntax to specify that, for overrides,
|
||
|
we want the method with no constraints (unconstrained). This is because constraints cannot
|
||
|
generally be specified in overrides or explicit implementations, so previously `T?` always meant
|
||
|
`Nullable<T>`, but now it may not. We added the `class` constraint for nullable reference types,
|
||
|
but neither `T : class` nor `T : struct` help if the `T` is unconstrained. In essence, we need a
|
||
|
constraint that means unconstrained. Some options include:
|
||
|
|
||
|
```C#
|
||
|
override void M1<[Unconstrained]T,U>(T? x) // a
|
||
|
override void M1<T,U>(T? x) where T: object? // b
|
||
|
override void M1<T,U>(T? x) where T: unconstrained // c
|
||
|
override void M1<T,U>(T? x) where T: // d
|
||
|
override void M1<T,U>(T? x) where T: ? // e
|
||
|
override void M1<T,U>(T? x) where T: null // f
|
||
|
override void M1<T,U>(T? x) where T: class|struct // g
|
||
|
override void M1<T,U>(T? x) where T: class or struct // h
|
||
|
override void M1<T,U>(T? x) where T: cluct // joke
|
||
|
override void M1<T,U>(T? x) where T: default // i
|
||
|
```
|
||
|
|
||
|
**Conclusion**
|
||
|
|
||
|
The `default` constraint seems most reasonable. It would only be allowed in overrides and
|
||
|
explicit interface implementations, purely for the purpose of differentiating which method
|
||
|
is being overridden or implemented. Let's see if there are other problems.
|