Add design notes

This commit is contained in:
Mads Torgersen 2017-10-05 20:15:39 -07:00
parent ca982efb54
commit 95c9267d0d
11 changed files with 832 additions and 2 deletions

View file

@ -0,0 +1,130 @@
# C# Language Design Notes for Aug 16, 2017
***Warning: These are raw notes, and still need to be cleaned up. Read at your own peril!***
"It's an open question whether we go out with a bang`!`"
## Agenda
# The dammit operator
Proposal:
1. *Target typed*: `e!` implicitly converts to `T` if `e` does, but without nullability warnings
- `string s = null!;`
- `string s = default!`
- `string s = GetNameOrNull()!;`
- `List<string> l = GetList<string?>()!;`
- `List<string?> l = GetList<string>()!;`
2. *Inherent type*: if the type of `e` is a nullable reference type `T?`, then the inherent type of `e!` is `T`
- `var s = GetNameOrNull()!;`
- `GetNameOrNull()!.Length;`
3. *Default expressions*: if `T` is a non-nullable reference type, then `default(T)!` suppresses the warning normally given by `default(T)`
For 2, an alternative is to have a dedicated `!.` and `![...]` operator, cousins of `?.` and `?[...]`. Then you wouldn't get to factor out to a local with `var`.
3 is a bit of a corner case. Most people would choose to just rewrite it to something else - there are plenty of options. But `default(T)` is a good strategy for code generators.
We could generalize to `!` silencing all nullability warnings even in subexpressions. No.
If `!` is applied in a place that yields no nullability warnings, does that lead to a warning? No.
We can make `!!` and error. If you really want to (we don't believe there's *any* scenario, other than swearing) you can parenthesize, `(e!)!`.
An alternative is to make the type of `e!` oblivious, if we choose to embrace a notion of oblivious. That's attractive in that it makes a type for "something that doesn't yield warnings", but it's also viral - could lead to many things not being checked. Option to be considered in future.
# Discussion about var and the type of nullable things known not to be null
``` c#
T t = ...;
if (t != null)
{
int s = t.ToString();
}
string? s = ...;
var s1 = s;
if (s != null)
{
var s2 = s;
int l = s.Length;
}
```
``` c#
string s1 = "Hello";
var s2 = s1; //
```
This could be safe:
``` c#
void M1(ref string s);
void M2(ref string? s);
var s = "foo";
if (s == null);
{
M1(ref s);
}
else
{
M2(ref s);
}
```
Three discussions:
1. What is the type of the value of a local variable `string? s` in a scope where it is known to be non-null? Does it contribute the type `string` to e.g. type inference, or does it remain `string?`
2. What is the meaning of `var`? Does it infer its nullability along with the rest of its type, from the initializer, or does it remain able to be assigned `null` even when initializer is not-null?
3. Should we reconsider completely abandoning the notion of nullness being part of a local's type at the top level?
1. could even consider for value parameters
``` c#
T M<T>(T t);
string? s = "abc";
var l = M(s).Length;
```
# 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
# 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`?)
# Dotted names problems

View file

@ -0,0 +1,37 @@
# C# Language Design for Aug 21, 2017
***Warning: These are raw notes, and still need to be cleaned up. Read at your own peril!***
# Type classes
Multiple implementations (ints and groups)
- You can explicitly provide the type argument for an implicit type parameter
- (but it might get messy)
Need explicit instance
- Nothing fundamental preventing a more structural approach
- Two levels of inference possible:
- a: explicit instance, infer members
- b: implicit instance even
To bridge to existing interface-based abstractions, you can just provide a very general, generic instance
Need an implicit type parameter but don't use it. Maybe a bit too magical. Might be better to require dotting off of the implicit type parameter. For operators that would be nice, though.
Instance members? need some syntax like extension methods, maybe, or like explicit interface implementation.
Concepts can be for more than one type, so they are not always tied to a single domain type. This may be a step too far, but it does have real value: Graph algorithms that have both Node and Edge types.
Main competitor, conceptually, would be something that allows for interfaces to play the role of concepts. That comes with challenges of its own, and lots of limitations. But that sort of the thing you have to justify why you're not.
Could you use this to make the environment of a lambda a struct? Combined with closures as structs, passed by ref.

View file

@ -0,0 +1,134 @@
# 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
1. How does flow analysis silence the warning
2. Problems with dotted names
# How does flow analysis silence the warning
``` 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.
``` c#
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:
``` 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.
## 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.
# 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:
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`?)
# 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.
``` c#
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.

View file

@ -0,0 +1,74 @@
# C# Language Design Notes for Aug 28, 2017
***Warning: These are raw notes, and still need to be cleaned up. Read at your own peril!***
## Agenda
# Ref-like types safety rules
https://github.com/dotnet/csharplang/blob/master/proposals/span-safety.md#draft-language-specification
The need for ref-like types quickly arises, when you need to flow more than one span, etc.
The fact that span is array like is not very interesting for the safety rules. The interesting thing is that it embeds a reference. Indexing is not a core concern in the general case.
Once you can embed refs in struct, the question: How can you assign them?
With assigment, you can now assign to ref parameters of ref-like structs, so every assignment is a potential return.
"Let's not refer to local data" does not work here:
- we *want* the feature to refer to local data. stackalloc, etc.
- lightweight params, etc.
- we want to protect our future ability to allow this even if we could live with it now
So: We need to allow local data inside ref-like structs.
We tie a scope to variables holding ref-like structs. That scope is based on what the variable is initialized with. We call this the "escape level".
Essentially, on the assignment we check that the assigned-to variable's scope is no wider than the assigned value's scope.
The rules assume that everyone plays by them.
Special craziness for multiple arguments, some which are refs, some which are refs to ref-like:
``` c#
void M(ref Span<int> x, ref int y)
{
// One of two things must be true about this:
// 1. This is an error
// 2. The caller has guaranteed this is safe
x = new Span<int>(ref y);
}
```
We need to do this on the caller, who knows most about the situation. The caller therefore has to assume that there is cross-assignment in the callee.
Q: Could you track the escape level during flow analysis, changing it locally? It's possible we could, but it seems extremely complex. We propose strict rules now; a flow analysis later would be more permissive, if we can figure it out. That's an important property!
"ref safe to escape" is how far you can escape the variable. "safe to escape" is how far you can escape a value. Only relevant if that value is a ref-like struct that can contain a ref.
Quality of error messages: We'll do a decent job, but a) it rarely gets complicated/cascading, b) we can improve them later if they turn out not to be good enough. Rust has error messages about this kind of thing that are just stellar, but this is bread-and-butter code in Rust, whereas it's exceptional here.
Defer: Should uninitialized ref-struct locals be allowed?
Defer: Should default values of ref-struct types be allowed?
Methods can return by-ref, ref-like and by-ref ref-like! We say that by-ref ref-like parameters are not returnable!
Open issue with ref dynamic: Treat them like in parameters, in how we deal with compiler-generated temps.
Language restrictions:
- Local functions - it's a shame that we can't factor out this stuff to local functions, because of the closure rule. But we can fix it later if necessary.

View file

@ -0,0 +1,84 @@
# C# Language Design Notes for Aug 30, 2017
***Warning: These are raw notes, and still need to be cleaned up. Read at your own peril!***
# IAsyncDisposable
`IAsyncDisposable` is sort of required, due to the way we translate `foreach`. Also it's useful in a lot of situations.
Don't take a `CancellationToken`, since when you cancel an operation you don't want to also cancel the clean-up.
Contract should be similar to `Dispose`, where you can call multiple times (sequentially) without problem.
If an enumerator (for instance) implements both sync and async, the client is only required to call one of them. The expansion of foreach will prefer sync or async naturally through the expansion.
Does the CT given to an enumerator apply during disposal? That would need to be up to the implementation. Standard guidance would be not to cancel any of what the disposal does. Advanced implementors might use the fact that the enumerator was canceled to skip some work.
# Alternative IAsyncEnumerable pattern
There's an alternative that does "explicit chunking", by asynchronously yielding synchronous enumerables, to be consumed in a nested loop.
Tests show that this is a lot more efficient than the simple design.
We are cautiously leaning in this direction. As long as we can work out iterators, which are not fully explored.
# ConfigureAwait
Can use extension methods on IAE
# foreach
Proposal to support foreach over enumera_tors_, not just enunera_bles_.
Should enumerators be first class currency? There's a path we can take where everything is still enumerables, and you can get enumerables from other enumerables representing e.g. something with a CT, a certain starting point, etc.
Downsides: If you're just foreaching over an enumerator, is it your responsibility to dispose it? Also, should LINQ then be implemented over those as well?
Let's not open that Pandora's box for now. Let's stick with enumerable.
Consider a ToEnumerable on enumerators.
## Syntax options
Need to think about it in connection with `using` also.
We'll stick with `foreach await ( ... )` for now.
## pattern-based
Similar to today. We get that one layer of optimization. There's more wrapping here (MoveNextAsync, WithCancellation), so we quickly still end up with interface dispatch.
# iterators
Same as current iterators, except with an `async` keyword, and IAsyncEnumerable/tor return type. Needs to feel exactly like putting iterators and async methods together.
Could consider custom builders for this. Skip for now.
If we do the fancier version of IAE we need to find out how to compile. Need to spec the contract completely and then follow that. (Probably some sequences of calls would be unspecified).
## Cancellation
There are two competing models around this.
1. Take CT into GetEnumerator, and find a way to syntactically expose it in an iterator body
2. Don't pass CT's into the enumerator at all; pass them to iterator methods
We actually want to start out with 2, and see if that gets us into trouble
# LINQ
There are 200 overloads on enumerable, and most would need to be duplicated on IAE. Then, mixing sync and async would add another axis of this.
Ix has an implementation of these already, and they would adjust to what we decide.
If you have that, query expressions would work to some degree, when the bodies are sync.
However, we don't currently allow `await` in query clauses, because the lambdas we target don't have the `async` modifier.
Fine to not do it now. At some point we would want to deeply investigate how to get those awaits in.

View file

@ -0,0 +1,50 @@
# C# Language Design Notes for Sep 25. 2017
***Warning: These are raw notes, and still need to be cleaned up. Read at your own peril!***
# Ref readonly locals
We have nowhere to put a ref readonly result. Ref readonly locals are like ref locals, except that they don't allow mutation of the ref'ed variable.
```
var x = a[1];
ref readonly var r = ref a[1];
```
We could have `var` infer the `readonly` as well. It wouldn't be breaking to add later.
Why do we allow the silent copying of readonly struct values in these new scenarios? Do we like that?
No, but for consistency. People will need to use analyzers already to catch the existing cases. Those analyzers should just have this feature in there as well.
We agree that this is a reasonable feature to have, and the design is right.
Like ref locals, these aren't currently reassignable, but there's no dependence on that. We could change it later. There's then technically room for an extra `readonly` in front.
For
``` c#
MyRefTaker(42);
MyRefTaker(ref MyRefReturner());
ref readonly int r = 42;
ref readonly int r = ref MyRefReturner();
b ? 42 : MyRefReturner()
return ref r;
ref readonly int x = a[1];
```
Discussion about whether the implicitness is a good thing.
There are other options: require "ref" in arguments, require "in" in arguments. We still want the implicit ref in parameters.
For operators, during overload resolution we ignore the refness.
## Conclusion
Let's flip to requiring `ref` in argument position. `ref 42` and `ref x+y` etc are allowed.
(Other small decisions)

View file

@ -0,0 +1,110 @@
# C# Language Design Notes for Sep 27, 2017
***Warning: These are raw notes, and still need to be cleaned up. Read at your own peril!***
QOTD: `in int i = in init(in it);`
We changed our minds to allow and require explicit `ref` for `ref readonly` arguments.
``` c#
const int x = 42;
foo(ref x);
```
In the compiler, the order of things change, which is the most churning fix.
Technically not hard to do.
## Concern 1
Is 'ref' the right keyword? Putting `ref` signals to the caller that mutation may be happening. You need to understand what you are passing it *to* in order to know whether it's safe from mutation.
In other cases, like `return ref x` or `ref readonly r = ref x` the `readonly` is nearby, and it doesn't cause the same concern.
## Concern 2
Also, the `ref` in front of rvalues feels wrong.
``` c#
Foo(ref 42);
Foo(ref await FooAsync());
```
There's an argument that `ref` is about *how* the value is passed, not about the guarantees of the callee. On the other hand we use both `out` and `ref`, where the only difference is the guarantee.
Compromise position:
Use keyword to pass by ref, no keyword to pass "by value" (really by creating a new local and putting the value in).
Warn (or error) when an lvalue is being passed without the keyword, that could have been passed by ref.
* Is it important to be able to see in the code if something is passed by ref?
* Is it important to be able to see in the code if something is copied?
* Is it important that the parameter passing mode reflects the contract?
* Is it important that the same keyword is used for passing and returning?
Other compromise:
- No modifier: Do you best
- Modifier: Require ref, don't copy
The keyword would be `in`.
Should it then also be `in` in parameter declarations? A danger is that people misunderstand it for "being explicit" about value parameters, whereas `ref readonly` leaves no such room for interpretation. On the other hand, this may be a case where we'd overoptimize for newcomers to the feature, leaving too much syntax for too little benefit. A bit of education, maybe some warnings, maybe analyzers...
## Conclusion
- At the parameter declaration site, use `in`. `ref readonly` is no longer allowed.
- At the call site, `in` is optional. If it is used, the argument has to an lvalue that can be passed directly by ref. If not, then the compiler does its best: copy only if necessary.
No change to `ref readonly` return signature, or to `return ref`.
No change to `ref readonly int r = ref ...`
We would consider allowing `ref readonly int r = 42;` in the future, but it is only useful once we have ref reassignment.
We could consider a warning for `in` parameters of small types (reference types and built-in value types, maybe), but sometimes you do need that. Better left to an analyzer.
What about ambiguity:
``` c#
M(in int x){}
M(int x){}
M(42); // how do you resolve in direction of value
```
We could deal with this through a tie breaker in overload resolution, but it's probably not worth it.
If you have an API like that, there's going to be a method overload you cannot call. We could always add it later.
This scenario isn't entirely esoteric: if I want to update my API from value-passing to in-passing, then either:
- I add an overload. Then all existing call sites are broken on recompilation.
- I replace the current overload. The all existing assemblies are broken until recompilation.
Instead, you can add an optional parameter, change parameter names etc. to give another means of distinguishing.
Delegate conversion: No contravariance in the type of parameters, unlike value parameters. THis is because the CLR doesn't know about it. It's similar to the restriction on out parameters.
Conditional: If one of the branches is readonly, the whole thing is readonly. That means that calling a mutating member on it would mutate a copy, *regardless* of whether the actual branch chosen was readonly or not:
``` c#
void M(bool b, in x, ref y)
{
(b ? ref x : ref y).M(); // M is always called on a copy, even if b is false
}
```
# ref structs
## Syntax
Partial has to keep being the last modifier; ref can for now only be right before `struct` or `partial struct`.
Long term we want `ref` to float freely as a modifier. Just may not get to do the work now.

View file

@ -0,0 +1,69 @@
# Milestone philosophy
***Warning: These are raw notes, and still need to be cleaned up. Read at your own peril!***
7.3 is a bucket for next steps with pattern matching.
Non-exhaustive list
- recursive patterns
- non-null patterns
- switch expression
- negated if-condition
8.0 is for major language features
# Discussion on how we get input
We should solicit problems, not just solutions
# Triage
## 945
Could make it always prefer by-value as a tie-breaker.
## 933
Motivating scenarios:
1. Hold on to the content variable in linked list elements
2. assign one as a default and have an if overwrite it with another
Syntax! Should we be putting `ref` in front of the LHS, or just the RHS?
``` c#
ref r = ref v; // or
r = ref v;
```
Not requiring ref lets us:
- Be more terse
- Work as an expression (because no expression starts with ref)
Requiring makes it syntactically clear whether you are assigning to `r` itself (in the ref space) or to the variable currently pointed to by `r` (in the value space). Also, what the hell does code mean if e.g. a ref-reassigning expression occurs as a ref or out argument?
``` c#
M(out r = ref v); //What?
```
We'd just recommend parenthesizing the assignment, like we recommend everywhere else assignments are used as expressions.
There's a limit which is that there's no proper default, so we'd still always require initialization, picking up the lifetime from the initializer. This is a bit painful when you want it to have global lifetime (no good default to provide).
We should instead allow you to not have an initializer. We do definite assignment analysis. It has global lifetime.
``` c#
ref readonly tmp = ref Get();
M(in tmp);
```
Annoying that there's sort of three different ways to talk about a `ref readonly`: `ref readonly`, `ref` and `in`.
Should we switch parameter to `ref readonly`? Allow choice.
No: Let's keep having only one way of doing it, and let's have that way be consistent with what you say at the call site.

View file

@ -0,0 +1,123 @@
# C# Language Design Review, Oct 4, 2017
*Quote of the Day:*
> "You don't get to use this with your grandfather's Oldsmobile"
## Agenda
We looked at nullable reference types with the reviewers, Anders Hejlsberg and Kevin Pilch.
1. Overall philosophy
2. Switches
3. Libraries
4. Dotted names
5. Type narrowing
6. The dammit operator
7. Array covariance
8. Null warnings
9. Special methods
10. Conclusion
# Overall philosophy
Think of this feature as a linting tool, an analyzer. It will help you find many bugs, but it will not guarantee anything. It is important that it does not lead to too much inconvenience, and does not yell too much over existing code. Too much whining at programmers makes them turn the feature off. First appearances are everything.
We should not think of the feature as dialable, with multiple switches or settings. We should design our way to the best balance, and stick to it. One switch: On or off.
Don't worry too much about unannotated libraries. Push on the library owners to get annotations, and live with the lack of them until that happens. In many cases the feature will still be useful even on unannotated libraries, because the default of assuming non-null is often going to be correct.
# Switches
Have just one on/off switch for the warnings. The annotations should be allowed regardless of whether the warnings are on or off.
New projects should have it on by default, existing projects probably off.
# Libraries
We should push to get our libraries upgraded to have annotations. Since it's only about adding attributes, it's possible we can do something with reference assemblies, leaving the actual binaries untouched.
Even if some libraries aren't upgraded, or not at the same pace, it's not a disaster, and we shouldn't hold up the feature waiting for a sufficient amount of libraries to be ready. Instead, use the availability and (hopefully) popularity of the feature to drive libraries to annotate.
# Dotted names
The flow analysis should track dotted names. We have experience from TypeScript, and customers there would definitely complain if we didn't. We also know that it is common in existing code bases to check a dotted name (e.g. a property) for null, then dereference it. In particular, of course, "dotted" names with an implicit `this.` are common.
It would be a disaster for existing code not to track the null state of dotted names.
If we do, what does it take to invalidate assumptions about a dotted name? In principle, any intervening call can cause a property to change. Even another thread could do that!
But assuming the worst on this is just going to lead to a lot of pain. We have to be lenient, and assume that dotted names remain the same unless something in the dotted chain is assigned to, or maybe passed by out or ref.
There's going to be a type of subtle bug that we won't catch as a result of this lenience. But the price of catching it is too high in most cases, in terms of the amount of perfectly safe existing code that it would flag.
# Type narrowing
We are currently tracking nullness of variables separately from the type system. Even when a nullable variable is known not to be null at a given place in the source code, it's *type* is still nullable, and that's what we feed into e.g. type inference around it.
This allows us to handle things that are *not* necessarily nullable, such as type parameters. However, when we *do* know the type is nullable and not null, it feels like we're throwing away useful information not to narrow the type.
We should consider a hybrid, where we narrow the type to non-nullable when we can.
Similarly for use of the `!` (dammit) operator. Yes it should silence warnings on conversions and dereferencing. But it should also narrow the type of the expression to non-nullable when possible.
# The dammit operator
TypeScript also has this, and it is often a nuisance that it doesn't "stick" - it applies only locally to the expression it is used on. It might be nice with some sort of assertion that sticks throughout the scope of the variable. We should consider it.
It's a little suspicious to use the same operator for silencing warnings and narrowing the type, but probably better than having two different ones. Casts aren't good for suppressing warnings, because they also imply a runtime check (which we may not want, since the suppression of the warning may be because you *want* to allow a null).
# Array covariance
We currently treat arrays as invariant with respect to element nullability.
``` c#
arrayOfNullable = arrayOfNonNullable; // warning
```
But arrays are covariant, albeit unsafely. We should consider applying the same covariance to nullability, for consistency. I.e., we would allow the above code without warning. That is *even though* a null check won't be part of the runtime type check that arrays do on write. The alternative is worse.
# Null warnings
we should warn on the majority of cases where a null value makes it into a nonnullable variable. However, there are cases where it simply gets too harsh on existing code.
Places where we should warn:
- constructors that don't initialize fields of nonnullable reference type
- converting a null literal to a nonnullable reference type
- passing or assigning a nullable reference value to a non-nullable reference type
- `default(T)` expressions when `T` is a nonnullable reference type
The last one could also yield a `T?` and no warning, but that would just lead to other warnings further down the line. Besides, default isn't used very much on reference types.
On the other hand we should *not* warn on array creation expressions, even though they create a whole array of forbidden null values:
``` c#
var array = new string[10];
```
These are numerous in current code, and very often they are fine. Besides, the code one would have to write instead is quite unappetizing!
We *could* maybe find special cases, where we could warn, e.g. if a newly created array of nonnullable element type is read from without ever having been written to at all.
# Special methods
Some methods have a special relationship with null: if `string.IsNullOrEmpty` returns false, then the argument was not null. If a `TryGet` method returns false, then the resulting out parameter may be null.
TypeScript has a notion of user-defined predicates, which are methods that claim to establish membership of a given type for a given value. We may try to think along similar lines, and consider whether methods can somehow convey extra knowledge about nullability.
# Conclusion
This is going to be a great feature, and people will love it. Have a list of known holes, and make clear that there's no guarantee.
We're not going to get everything in the world. It's not possible! And even some of the possible stuff is too inconvenient.
There are only so many greenfield projects in this very established world. Don't be discouraged by low adoption in the beginning. These things take time.

View file

@ -614,3 +614,21 @@ We looked at the interaction between generics and nullable reference types
1. Unconstrained type parameters
2. Nullable constraints
3. Conversions between constructed types
## Oct 4, 2017
[C# Language Design Review, Oct 4, 2017](LDM-2017-10-04.md)
We looked at nullable reference types with the reviewers, Anders Hejlsberg and Kevin Pilch.
1. Overall philosophy
2. Switches
3. Libraries
4. Dotted names
5. Type narrowing
6. The dammit operator
7. Array covariance
8. Null warnings
9. Special methods
10. Conclusion

View file

@ -1,9 +1,10 @@
Nullable reference types in C#
==============================
This feature is intended to:
The goal of this feature is to:
* Allow developers to express whether a variable, parameter or result of a reference type is intended to be null or not.
* Provide optional warnings when such variables, parameters and results are not used according to their intent.
* Provide warnings when such variables, parameters and results are not used according to that intent.
Expression of intent