138 lines
6.1 KiB
Markdown
138 lines
6.1 KiB
Markdown
# C# Language Design Notes for Aug 23, 2017
|
|
|
|
## Agenda
|
|
|
|
We discussed various aspects of nullable reference types
|
|
|
|
1. How does flow analysis silence the warning
|
|
2. Problems with dotted names
|
|
3. Type inference
|
|
4. Structs with fields of non-nullable type
|
|
|
|
|
|
# How does flow analysis silence the warning
|
|
|
|
What exactly is the mechanism by which a nullable variable can be dereferenced without warning, when it's known not to be null?
|
|
|
|
``` c#
|
|
void M<T>(string? s, T t)
|
|
{
|
|
if (s != null) WriteLine(s.Length); // How is the warning silenced?
|
|
if (t != null) WriteLine(t.ToString()); // How is the warning silenced?
|
|
}
|
|
```
|
|
|
|
So far we've said that when a nullable variable is known to not be null, it's *value* is simply considered to be of the underlying nonnullable type. So for `s` in the above example, inside the `if` where it is tested, its value is of type `string`. Thus, the dereference does not earn a warning.
|
|
|
|
However, this doesn't immediately work for type parameters. In the example above, we don't *know* that `T` is a nullable reference type, we just have to assume it. Therefore, it does not *have* an underlying nonnullable type. We could invent one, say `T!`, that means "nonnull `T` if `T` was nullable", but it starts to get complex.
|
|
|
|
An alternative mechanism is to say that the type of a variable and its value *does not change*. The null state tracking does not work through changing the type, but simply by directly silencing null warnings on "dangerous" operations.
|
|
|
|
This works for both `s` and `t` above.
|
|
|
|
With the new proposal, you can better imagine separating out the null warnings to an analyzer, because the type system understanding of the `?` would be logically separated from the flow analysis.
|
|
|
|
IntelliSense will be a challenge:
|
|
|
|
``` c#
|
|
void M(string s) => ...;
|
|
|
|
string? s = "Hello";
|
|
|
|
M(s); // Does IntelliSense confuse you here if the type of 's' is shown as 'string?' ?
|
|
```
|
|
|
|
But there's non-trivial experience work in the IDE no matter what we do.
|
|
|
|
|
|
## Impact on type inference
|
|
|
|
``` c#
|
|
string? n = "Hello";
|
|
var s = n; // 'string' or 'string?' ?
|
|
```
|
|
|
|
If null state affects the type, then `s` above is of type `string`, because `n` is known to be non-null at the point of assignment. If not, then it is `string?` (but currently known not to be null).
|
|
|
|
This also affects which type is contributed to generic type inference:
|
|
|
|
``` c#
|
|
List<T> M<T>(T t) => new List<T>{ t };
|
|
|
|
void N(string? s)
|
|
{
|
|
if (s != null) WriteLine(M(s)[0].Length); // 1
|
|
if (s != null) { var l = M(s); l.Add(null); } // 2
|
|
}
|
|
```
|
|
|
|
If the type of `s` changes to `string` in the non-null context, then the calls to `M` infer `T` to be `string`, and return `List<string>`. Thus, `//1` is fine, but `//2` yields a warning that `null` is being passed to a non-null type.
|
|
|
|
Conversely, if the type of `s` remains `string?` in a non-null context, then the calls to `M` infer `T` to be `string?`, and return `List<string?>`. Thus, `//2` is fine, but `//1` yields a warning about a possible null dereference.
|
|
|
|
|
|
## Impact on `!` operator
|
|
|
|
The currently proposed `!` operator changes the type of a nullable value to a non-nullable one. The deeper philosophy behind this, though, is simply that `!` should do the same to the value of a variable (that is not target typed) as a non-null null-state would have done.
|
|
|
|
``` c#
|
|
string? n = GetStringOrNull();
|
|
|
|
var l = n!.Length; // why no warning?
|
|
var s = n!;
|
|
```
|
|
|
|
With the current proposal, `n!` would be of type `string`, and there'd be no warning on the dereference because of that.
|
|
|
|
With the new proposal, `n!` would be of type `string?`, but the `!` would itself silence the warning on dereference.
|
|
|
|
In a way the new proposal unifies the target-typed and non-target-typed meanings of `!`. It is never about changing the type of the expression, just about silencing null warnings.
|
|
|
|
We need to get more specific about exactly what it means that it "silences the warnings".
|
|
|
|
## Conclusion
|
|
|
|
Let's roll with the new approach. As always, we'll keep an eye on whether that leads to a good experience, and are willing to revisit.
|
|
|
|
|
|
# Dotted names problems
|
|
|
|
There are different approaches with different safeties:
|
|
|
|
- Don't track dotted names at all (safest): this pushes you to introduce a local for every null test of every field or property
|
|
- Track dotted names but invalidate when prefix is "manipulated" (passed or called a method on): doesn't catch aliasing or indirect mutation
|
|
- Track dotted names and assume it's still valid no matter how prefix is manipulated: bigger risk that it's wrong
|
|
|
|
The problem is that people have code today that checks on dotted names.
|
|
|
|
Should we have a dial? Otherwise we need to decide how we weigh safety vs convenience.
|
|
|
|
## Conclusion
|
|
In the prototype, since dotted names aren't implemented, let's try the more restrictive approach. People will let us know where this is too painful.
|
|
|
|
|
|
# Type inference
|
|
|
|
How are nullable reference types inferred?
|
|
|
|
Proposal:
|
|
|
|
- Consider nullness an orthogonal aspect to the rest of the type being inferred
|
|
- If any contributing type is nullable, that should contribute nullness to the inference
|
|
- A `null` literal expression should contribute nullness to the inference, even though it doesn't otherwise contribute to the type
|
|
|
|
We should consider this for nullable value types as well.
|
|
|
|
|
|
# Structs with fields of non-nullable type
|
|
|
|
Structs can be created without going through a declared constructor, all fields being set to their default value. If those fields are of non-nullable reference type, their default value will still be null!
|
|
|
|
It seems we can chase this in three ways:
|
|
|
|
1. Not at all. We just aren't that ambitious.
|
|
2. We warn on all fields of structs that have non-nullable reference types. That's a *lot*! How do you "fix" it? Make them nullable? No version of the `!` operator works here, since the whole point is you don't control initialization from user code.
|
|
3. We warn whenever a struct that has such fields is created as a default value. In other words, we treat the type the same as a non-null reference type, recursively. (And we warn on passing for a type parameter constrained by `new()` or `struct`?)
|
|
|
|
## Conclusion
|
|
The options to handle are painful. No conclusion.
|