csharplang/meetings/2020/LDM-2020-06-17.md
2020-06-18 16:31:16 -07:00

5.3 KiB

C# Language Design Meeting for June 17, 2020

Agenda

  1. Null-suppression & null-conditional operator
  2. parameter! syntax
  3. 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

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??:

(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:

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.