csharplang/meetings/2017/LDM-2017-08-23.md

139 lines
6.1 KiB
Markdown
Raw Normal View History

2017-10-06 05:15:39 +02:00
# C# Language Design Notes for Aug 23, 2017
## Agenda
We discussed various aspects of nullable reference types
2017-10-06 05:15:39 +02:00
1. How does flow analysis silence the warning
2. Problems with dotted names
3. Type inference
4. Structs with fields of non-nullable type
2017-10-06 05:15:39 +02:00
# 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?
2017-10-06 05:15:39 +02:00
``` 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.
2017-10-06 05:15:39 +02:00
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?' ?
2017-10-06 05:15:39 +02:00
```
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.
2017-10-06 05:15:39 +02:00
## 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.
2017-10-06 05:15:39 +02:00
# Type inference
How are nullable reference types inferred?
2017-10-06 05:15:39 +02:00
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.