175 lines
5.4 KiB
Markdown
175 lines
5.4 KiB
Markdown
|
|
||
|
# C# Language Design Meeting for March 6th, 2019
|
||
|
|
||
|
## Agenda
|
||
|
|
||
|
Open issues:
|
||
|
|
||
|
1. Pure checks in the switch expression
|
||
|
2. Nullable analysis of unreachable code
|
||
|
3. Warnings about nullability on expressions with errors
|
||
|
4. Handling of type parameters that cannot be annotated
|
||
|
5. Should anonymous type fields have top-level nullability?
|
||
|
6. Element-wise analysis of tuple conversions
|
||
|
|
||
|
## Discussion
|
||
|
|
||
|
### Pure checks in switch expression
|
||
|
|
||
|
Example:
|
||
|
|
||
|
```C#
|
||
|
void M(object x)
|
||
|
{
|
||
|
_ = x switch
|
||
|
{
|
||
|
1 => x.ToString(),
|
||
|
{} => x.ToString(), // is this a "pure" null test?
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
The original proposal was that certain tests, like `is {}` are "pure" null
|
||
|
checks because there is no reason to perform such a test unless you are
|
||
|
checking if the input is null. The switch expression was not decided, but it
|
||
|
may be confusing to have a pattern check for null in an `is` expression, but
|
||
|
not produce the same nullable semantics for the same pattern in `switch`.
|
||
|
|
||
|
However, it's possible that the user didn't intend `{}` to be a null test in
|
||
|
the previous example, but just a "catch-all" case. If we use the original
|
||
|
interpretation of "pure null test" then this would not meet the bar, as there
|
||
|
is a meaning which could not be a null test. However, this would produce
|
||
|
a different meaning for `{}` in an `is` expression, as opposed to a `switch`.
|
||
|
|
||
|
**Conclusion**
|
||
|
|
||
|
`{}` is proving contentious as a "pure" null check in a switch. One
|
||
|
alternative which we can all agree on is removing `{}` as a pure null check
|
||
|
in the `is` expression, as well. This would remove the problem of consistency
|
||
|
all together.
|
||
|
|
||
|
### Nullable analysis of unreachable code
|
||
|
|
||
|
```C#
|
||
|
static void M(bool b, string s)
|
||
|
{
|
||
|
var t = (b || true) ? s : null;
|
||
|
t.ToString(); // warning?
|
||
|
}
|
||
|
```
|
||
|
|
||
|
**Conclusion**
|
||
|
|
||
|
Diagnostics will not be produced because of nullability analysis on
|
||
|
expressions which are non-nullable. This conforms to what we do for definite
|
||
|
assignment, where everything is treated as defintely assigned in unreachable
|
||
|
code.
|
||
|
|
||
|
We accept a proposed rule where all expressions which are unreachable are
|
||
|
non-nullable (even `null`), in service of the above principle.
|
||
|
|
||
|
### Warnings about nullability on expressions with errors
|
||
|
|
||
|
Should we provide warnings about nullability in cases where one of the
|
||
|
symbols being considered has an error, like a variable which was declared
|
||
|
but never initialized?
|
||
|
|
||
|
**Conclusion**
|
||
|
|
||
|
We will not provide nullable diagnostics on symbols with errors, because we
|
||
|
do not know exactly how the types will be altered to remove the errors, so
|
||
|
the warnings may not be useful and are less important than the errors already
|
||
|
reported.
|
||
|
|
||
|
### Handling of type parameters that cannot be annotated
|
||
|
|
||
|
`e?.M()` used a statement is fine and we shouldn't care about the return type.
|
||
|
Similarly, `e ?? M()`.
|
||
|
|
||
|
The main unfortunate case is `default`, and the most annoying case is probably
|
||
|
implementing code like `GetFirstOrDefault()` where the only way to implement
|
||
|
it is to suppress the warning. Moreover, if the method is annotated with
|
||
|
`MaybeNull` it seems like you have to mention that the value could be null
|
||
|
twice: once in the signature of the method, and once in the body.
|
||
|
|
||
|
**Conclusion**
|
||
|
|
||
|
We like the list as is. We considered using annotations like `MaybeNull` in
|
||
|
things like `return` statements, but aren't willing to go that far right now.
|
||
|
More broadly, we may want to consider whether the annotations should apply to
|
||
|
the implementer of a method, instead of just the users of the method, but we
|
||
|
are not going to do so now.
|
||
|
|
||
|
### Should anonymous type fields have top-level nullability?
|
||
|
|
||
|
```C#
|
||
|
static T Identity<T>(T t) => t;
|
||
|
|
||
|
static void F(string x, string? y)
|
||
|
{
|
||
|
var a = new { x };
|
||
|
a.x.ToString(); // ok
|
||
|
|
||
|
a = new { x = y }; // warning?
|
||
|
a.x.ToString(); // warning
|
||
|
|
||
|
Identity(a).x.ToString(); // warning?
|
||
|
}
|
||
|
```
|
||
|
|
||
|
Our primary concern here is LINQ query expression, which functionally produce
|
||
|
anonymous types that are then consumed through further functions.
|
||
|
|
||
|
**Conclusion**
|
||
|
|
||
|
Yes, we want to track the inferred type of the anonymous type, so for
|
||
|
|
||
|
```C#
|
||
|
void M(string x, string? y)
|
||
|
{
|
||
|
var a = new { x };
|
||
|
|
||
|
a = new { x = y };
|
||
|
}
|
||
|
```
|
||
|
|
||
|
`a` would be of type `{ string! x }` and the subsequent assignment would
|
||
|
produce a warning.
|
||
|
|
||
|
### Element-wise analysis of tuple conversions
|
||
|
|
||
|
We discussed the behavior of nested nullability in tuple expressions for
|
||
|
the following example:
|
||
|
|
||
|
```C#
|
||
|
// current behavior:
|
||
|
static T Id<T>(T t) => t;
|
||
|
static void M(string? x, string y)
|
||
|
{
|
||
|
(string, string) t = (x, y); // warning: (string?, string) mismatch
|
||
|
t.Item1.ToString(); // warning: Item1 may be null
|
||
|
var x = Id(t); // what is the inferred type of x?
|
||
|
|
||
|
}
|
||
|
```
|
||
|
|
||
|
The warnings are present regardless of the design. The only question is if
|
||
|
the type of `x` is `(string, string)` or `(string?, string)`. If tuples
|
||
|
behaved like the underlying `ValueTuple`, the inferred type would be
|
||
|
`(string, string)` since the tracking of the `Item1` field does not change
|
||
|
the type of the container.
|
||
|
|
||
|
However, if we pretended that the elements of the tuple were individual
|
||
|
local variables, we would infer `(string?, string)` because the tracked state
|
||
|
of a local variable is used in type inference.
|
||
|
|
||
|
**Conclusion**
|
||
|
|
||
|
The inferred type is `(string, string)`. Aside from difficulties in nailing
|
||
|
down exactly what the semantics of "tracked like locals" means (presumably it
|
||
|
wouldn't apply to tuple fields of a second type?) there is a simplicity to
|
||
|
making tuples, constructed types, and anonymous types behave roughly
|
||
|
identical through type inference.
|
||
|
|
||
|
|