csharplang/meetings/2017/LDM-2017-08-09.md
2017-08-10 17:14:56 -07:00

123 lines
7.3 KiB
Markdown

# C# Language Design Notes for Aug 9, 2017
## Agenda
We discussed how nullable reference types should work in a number of different situations.
1. Default expressions
2. Array creation
3. Struct fields
4. Unconstrained type parameters
# Default expressions
There's an argument to be made that `default(string)` should have the type `string?`. After all, we know it is going to produce a null value! On the other hand, there's also an argument that `default(T)` should produce a `T` for any `T`, and even that `default(string)` is just bad practice, that should be warned on. After all, it's very much like saying `(string)null`, which *would* get a warning.
## Conclusion
In keeping with the resolution to the local variables question from [Monday's meeting](https://github.com/dotnet/csharplang/blob/master/meetings/2017/LDM-2017-08-07.md), we hold to an emerging principle that *"the type you say is the type you get"*. That means that `default(string)` should have the type `string`. We should probably also warn on it, since we *are* producing a null value of a non-nullable type. The fix for the warning is to put a `?` on the type (if you want it nullable), or use a `!` (if you don't).
It is not clear that `default(string)!` would actually silence the warning. After all it is the `default(string)` expression itself that causes the warning, not a conversion of it. For `!` to work here, we would probably need to special case it.
For the new target-typed version of the `default` expression, it would simply work the same way as a `null` literal: if the target type is a nonnullable reference type we'll warn, unless there's a trailing `!`.
# Array creation
Similar to default expressions there's an argument (but weaker) that `new string[10]` should have the type `string?[]`. After all, it produces an array full of nulls! On the other hand there's also an argument that `new T[10]` should continue to produce a `T[]` for any `T`, and maybe even (somewhat more weakly) that `new string[10]` is just bad practice that should be warned on.
## Conclusion
In keeping with the resolution for default expressions above, array creation expressions should have the type they say they create: `new string[10]` creates a `string[]`.
It's more open whether it should produce a warning. If it doesn't, then arrays leave a gaping hope in the "null protection" story: you can say `(new string[10])[0].Length` and get a null reference exception without a warning. There is no way that we can establish reliably that a given new `string[]` is initialized to all non-nulls, and it is not clear that this would always be desirable. Many safe uses of `string[]` would simply keep track of which elements have been assigned (with non-null strings) at a given time, and only allow access to those.
If we do warn on array creation, that's going to hit every single array creation expression today, except when the element type is a value type. That seems harsh! On the other hand, it gives the developer *somewhere* to realize that there's danger, and that they may want to consider an array with nullable elements. How to silence the warning if not? Maybe you can apply `!` to the array creation expression. Again, this is a more general version of `!` that applies not just to the nullability of the value itself (the array), but also types that it is constructed from. Thus, `new string[10]!` would suppress the warning.
We are going to adopt the warning for now in the prototype, since we believe we'll learn more from it. However, we are not very confident that this warning will stay on by default. It may also be one of those warnings that's better served by a standalone analyzer for folks who want to be stricter (maybe combined with some attempts to track initialization), or at least have its own opt-in in the compiler.
# Struct fields
We've been saying that we want to warn when non-nullable reference fields are not initialized by a constructor.
``` c#
class C
{
string s1;
string s2 = "Hello";
string s3; // warning: not initialized to non-null value
public C(string s) => s1 = s;
}
```
However, all structs can occur uninitialized, with default values in all fields. Does that mean it should be a warning for a struct to have *any* fields of non-nullable reference type?
``` c#
struct S
{
public string s; // warn anyway?
public S(string s) => this.s = s;
}
S[] a = ...; // array of uninitialized S's
var l = a[0].s.Length; // null reference exception
```
This would lead to a *lot* of warnings in existing source code! It feels similar to the array warning, in that it would invalidate the *only* way of doing things in pre-C# 8.0 code. Unlike the array situation, however, there is no way to silence the warning with a `!` to say "I know what I'm doing!"
We did not decide what to do here.
# Unconstrained generics
Unconstrained type parameters should be allowed to take nullable type arguments. Even though we've previously said that "unconstrained" is the same as "constrained to `object`", we should now say that it's the same as "constrained to `object?`".
This leads to type parameters `T` where *we don't know* if they are nullable reference types or not. Thus, we have to exercise caution and apply warnings "from both sides": on the one hand they may be null, so we should warn on unguarded reference. On the other, they may *not* be nullable, so we should warn on things that might make them null.
Of course, you cannot assign `null` to an unconstrained type parameter today (it might be instantiated with a value type), but there are still default expressions:
``` c#
T M<T>()
{
return default(T); // warning
return default; // warning
}
```
Now this is a problem: default expressions were added to the language *specifically* so you could use them in a generic setting! And now we would say you cannot use them? That seems harsh! Of course you can silence them every time you use them, but that's a nuisance.
*Not* having a warning would be a hole. We could consider it anyway (as we might for `new string[]`), and just say that this sacrifices safety for convenience.
Another option is to come up with a new type constructor over T that means "nullable if reference type". We could overload the postfix `?` if it's not too confusing. so `T?` on an unconstrained generic type would mean:
- `V` if `T` is a value type `V` (either nullable or nonnullable)
- `N` if `T` is a nullable reference type `N`
- `C?` if `T` is a nonnullable reference type `C`
It's a little subtle, and has some weird consequences, especially if we use the `T?` syntax, since it wouldn't *always* be a nullable type:
``` c#
void M<T>()
{
T? t = null; // would be an error!
}
```
Also `T?` would mean different things depending on whether `T` has a `struct` constraint or not:
``` c#
T? M1<T>(T t) => t;
T? M2<T>(T t) where T : struct => t;
var i1 = M1(1); // 'int'
var i2 = M2(2); // 'int?'
```
This is quite confusing, but might just speak to having another syntax than `T?`. On the other hand, that's *yet* another syntax, and extra complexity in the language.
It's worth noting that `T?` would be useful as the return type of `FirstOrDefault` and its brethren among the query operators, which otherwise wouldn't have a good way of expressing their signature. That would at least address *one* of a handful of patterns that aren't well served by nullable reference types as currently proposed.
We didn't reach a verdict on this topic.