meetings/2017/LDM-2017-08-23.md
We discussed various aspects of nullable reference types
What exactly is the mechanism by which a nullable variable can be dereferenced without warning, when it's known not to be null?
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:
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.
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:
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.
! operatorThe 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.
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".
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.
There are different approaches with different safeties:
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.
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.
How are nullable reference types inferred?
Proposal:
null literal expression should contribute nullness to the inference, even though it doesn't otherwise contribute to the typeWe should consider this for nullable value types as well.
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:
! operator works here, since the whole point is you don't control initialization from user code.new() or struct?)The options to handle are painful. No conclusion.