178 lines
4.2 KiB
Markdown
178 lines
4.2 KiB
Markdown
# C# Language Design for February 13th, 2019
|
|
|
|
## Agenda
|
|
|
|
Nullable Reference Types: Open LDM Issues https://github.com/dotnet/csharplang/issues/2201
|
|
|
|
## Discussion
|
|
|
|
### Track assignments through `ref` with conditional expressions
|
|
|
|
What is the nullability of `ref` variables when assigned through conditional expressions?
|
|
|
|
```cs
|
|
string? x = "";
|
|
string? y = "";
|
|
(b ? ref x : ref y) = null;
|
|
x.ToString(); // warning?
|
|
y.ToString(); // warning?
|
|
```
|
|
|
|
```cs
|
|
string? x = null;
|
|
string? y = null;
|
|
(b ? ref x : ref y) = "";
|
|
```
|
|
|
|
One option is to assume nullability after a `ref` has been taken to a variable. However,
|
|
that would mean that a `ref` variable declared non-nullable could become nullable, which
|
|
seems too heavy-handed.
|
|
|
|
Similarly, disabling flow analysis for variables which are taken as the target of a `ref`
|
|
feels like violating our model, which is largely based on flow analysis.
|
|
|
|
Alias analysis, on the other hand, seems to complicated and any reliable implementation
|
|
would be too difficult for users to understand.
|
|
|
|
**Conclusion**
|
|
|
|
Let's reach a middle ground. Assignment between any two identifiers copies
|
|
the state and is then tracked separately. This is also true for `ref` locals.
|
|
So,
|
|
|
|
```C#
|
|
string? x = "";
|
|
string? y = "";
|
|
(b ? ref x : ref y) = null;
|
|
x.ToString(); // warning
|
|
y.ToString(); // warning
|
|
```
|
|
|
|
But the equivalent using `ref` locals does not.
|
|
|
|
```C#
|
|
string? x = "";
|
|
string? y = "";
|
|
if (b)
|
|
{
|
|
ref string? rx = ref x;
|
|
rx = null;
|
|
}
|
|
else
|
|
{
|
|
ref string? ry = ref y;
|
|
ry = null;
|
|
}
|
|
x.ToString(); // no warning
|
|
y.ToString(); // no warning
|
|
```
|
|
|
|
### Nullability of conditional access with unconstrained type parameters
|
|
|
|
What is the nullability of `x?.F()`?
|
|
|
|
```cs
|
|
class C<T, U>
|
|
where T : U
|
|
where U : C<T, U>?
|
|
{
|
|
static void M(U x)
|
|
{
|
|
U y = x?.F();
|
|
T z = x?.F();
|
|
}
|
|
T F() => throw null;
|
|
}
|
|
```
|
|
|
|
This question seems interesting even without the type parameters and also
|
|
contains a nested question about reachability.
|
|
|
|
Is the `null` case of `?.` reachable even if the expression is non-nullable?
|
|
And if it is, what is the null state of the LHS?
|
|
|
|
```C#
|
|
string x = "";
|
|
x?.ToString(); // warning?
|
|
```
|
|
|
|
**Conclusion**
|
|
|
|
The `null` case of `?.` is always reachable, meaning the result is always
|
|
maybe null e.g.,
|
|
|
|
```C#
|
|
var y = x?.M(); // y is maybe null here, if possible
|
|
```
|
|
|
|
Moreover, the LHS is considered maybe null in the null branch, so by normal
|
|
flow analysis, after the expression is evaluated a variable on the LHS will
|
|
be considered maybe null.
|
|
|
|
```C#
|
|
string x = "";
|
|
x?.ToString(); // warning that x is maybe null
|
|
```
|
|
|
|
### `!` operator on L-values
|
|
|
|
Where is `!` allowed?
|
|
|
|
* `M(out x!);` (note this also definitely assigns to `x` through the `!`)
|
|
|
|
* `M(out (x!));`
|
|
|
|
* `M(out (RefReturning()!));`
|
|
|
|
* `x! = y;`
|
|
|
|
* `M(out string x!);`
|
|
|
|
Current implementation is to allow in `out` scenarios, but disallow in assignment scenarios.
|
|
|
|
We dislike allowing it in regular assignment. We like allowing it in simple
|
|
`out` parameters. We're ambivalent on `M(out string x!)`, but it's
|
|
not easily representable in the syntax model and is very similar to the
|
|
related feature `parameter!`, with the opposite meaning.
|
|
|
|
**Conclusion**
|
|
|
|
Only allow `!` in simple `out` parameters with no declaration.
|
|
|
|
### `is` nullability in `false` case
|
|
|
|
See [dotnet/roslyn#30297](https://github.com/dotnet/roslyn/issues/30297)
|
|
|
|
```cs
|
|
string s = string.Empty;
|
|
if (!(s is object)) { s.ToString(); /* could warn? */ }
|
|
if (!(s is string)) { s.ToString(); /* could warn? */ }
|
|
```
|
|
|
|
There are variants of this scenario with `string!` and `string~`. Should `is`
|
|
update the nullability in both branches or should the one branch be treated
|
|
as unreachable?
|
|
|
|
The problematic code is probably more like:
|
|
|
|
```C#
|
|
void M(string s)
|
|
{
|
|
if (s is IComparable t)
|
|
{
|
|
}
|
|
s.ToString(); // warning
|
|
}
|
|
```
|
|
|
|
Here the user may not have meant to do a null check, but gets the
|
|
side-effects of doing so anyway.
|
|
|
|
**Conclusion**
|
|
|
|
It seems important to respect a deliberate null check from the
|
|
user, even if the input variable is non-nullable. As a next step we
|
|
need to define exactly which tests we think are "deliberate" null
|
|
checks. For instance, `x is null` is certainly a null check, but
|
|
pattern matching may or may not be a *deliberate* null check, even
|
|
if the code contains a null check.
|