5.4 KiB
C# Language Design Notes for Aug 23, 2017
Warning: These are raw notes, and still need to be cleaned up. Read at your own peril!
Agenda
- How does flow analysis silence the warning
- Problems with dotted names
How does flow analysis silence the warning
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.
List<T> M<T>(T t) => new List<T>{ t };
void N(string? s)
{
if (s != null) WriteLine(M(s)[0].Length); // Warning or not?
if (s != null) { var l = M(s); l.Add(null); }
}
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.
It is also
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.
Impact on type inference
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.
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.
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.
Type inference
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:
- Not at all. We just aren't that ambitious.
- 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. - 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()
orstruct
?)
Dotted names problems
There are different approaches with different safeties:
- Don't track dotted names at all (safest): pushes you to introduce a local
- Track dotted names but invalidate when prefix is "manipulated" (passed or called a method on): doesn't catch aliasing, indirect mutation
- Track dotted names and assume it's still valid no matter how prefix is manipulated: big risk that it's wrong
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.
if (View.SuperView is var! superview) { // superview }
if !(View.SuperView is UIView superview) { return; }
(Say more about syntactic options)
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.