Merge pull request #1487 from dotnet/design-notes

Add design notes
This commit is contained in:
Mads Torgersen 2018-05-01 13:18:34 -07:00 committed by GitHub
commit 55f74b7374
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 346 additions and 0 deletions

View file

@ -0,0 +1,151 @@
# C# Language Design Notes for Apr 25, 2018
## Agenda
# Warn on nullable ref type argument when T has `class` constraint
``` c#
static void F<T>(T t) where T : class { t.ToString(); /* no warning */ }
F(maybeNull); // warning: string? does not satisfy 'class' constraint
F<string?>(string.Empty); // warning: string? does not satisfy 'class' constraint
```
This is a real danger, and we should be warning here.
Note that in both cases the warning is about constraint checking: even in the second line we infer `string?` and it warns on constraint check.
We should probably have the error message say that this is because of nullability.
# Warn on nullable reference type argument when T has non-nullable reference type or interface type constraint
``` c#
static void F<T>(T t) where T : IDisposable { }
F(maybeNull); // warning: Stream? does not satisfy 'IDisposable' constraint
```
Yes, warn for same reasons.
We also want to warn (or error) on inconsistent nullability in constraints. Specifically if one constraint is known to be non-nullable and another is known to be nullable.
What about
``` c#
class C<T, U, V> where T: class? where U : T, IDisposable where V : T, IDisposable?
{
// Fine
}
class D<T, U, V> where T: class where U : T, IDisposable where V : T, IDisposable?
{
// Warn on V
}
```
``` c#
... where T : Node<T>?
... where T : Node<T?> // both currently allowed, but not where T : Node<T?>?
... where T : class, INode<T?>
... where T : class?, INode<T>?
```
``` c#
static void F<T>(T t) where T : IFoo<string?> { }
IFoo<string> foo;
F(foo); // warning, unless IFoo is covariant
```
Another kind of clash:
``` c#
interface IBar : IFoo<string> {}
static void F<T>(T t) where T : IFoo<string?>, IBar { }
```
We want to warn on this: maybe find shared base interfaces between the two constraints, and check if they have consistent nullability.
Other, slightly more complex example, same conclusion:
``` c#
interface IBar : IFoo<string?> {}
static void F<T, U>(T t) where T : IFoo<U>, IBar where U : class{ }
```
# Allow nullable reference types and interface types as constraints
`where T : Person?, IDisposable?`
Yes
# Allow `class?` constraint
Yes
How do we express in metadata?
Let's put top-level nullability as an attribute on the type parameter itself, rather than on the constraints. We can noodle more on this later.
# Allow `object` constraint
``` c#
static void F<T>(T t) where T : object { }
F(maybeNull); // warning: constraint violated
```
## Need to talk about relationship to value types soon
``` c#
int? i = null;
object o = i;
o.ToString();
```
# Allow `object?` explicitly?
It's the "most general" constraint that is implied by unconstrained generics. Today we disallow `object` because of that.
We don't know that this is a terribly useful restriction today, but we'll keep doing it (now with `object?`) unless we get evidence to the contrary.
# Warn on dereference of T when T is unconstrained?
Yes, if a type parameter `T` can be instantiated with a nullable reference type, then we should track null state and warn on unguarded dereference.
The warning when occurring on unconstrained generics might suggest using the `object` constraint.
# Warn on assignment to `object` of unconstrained `T`?
Yes. This can be a `W` (cosmetic) warning when the `object` variable is a local.
# default(T) with unconstrained T
``` c#
T M<T>()
{
T t = default(T); // W warning
return default(T); // safety warning
}
```
This is something completely safe. I don't want it to warn!:
``` c#
T M<T>()
{
var t = default(T);
if (something) t = somethingelse;
if (t != null) WriteLine(t.ToString());
}
```
This could be an argument for `T?` that is not about special methods. Or it is an argument against having the W warnings at all.
Let's keep this example around and revisit. But for now, let's consider `default(T) to be potentially null, and therefore warn on its unguarded use.
# Annotations
We would like to deal with methods with special null semantics, such as string.IsNullOrEmpty, TryGetValue, Assert.NotNull, Object.Equals, Object.ReferenceEquals.
We'll come back to this later.

View file

@ -0,0 +1,195 @@
# C# Language Design Notes for Apr 30, 2018
# Switch expressions
Current syntax:
``` c#
e switch
{
1 => "one",
2 => "two",
var x when x > 2 => "too many",
_ => "too few"
}
```
No default: use `_`.
If the compiler thinks there are cases you don't handle, it'll warn. If you actually don't handle a case we throw an exception (NRE for prototype, something else in the long run).
Also discussed:
- use `match` instead of `switch`
- keyword first, followed by parens, like `switch` statement
- or without parens
- optional implicit default at the end?
The current thing is nice for error recovery.
The lack of statements inside may be a frustration, but that's orthogonal. Let's leave it for now.
## Exploration
Is there a way to generalize the syntax from the conditional (used to be ternary!) operator?
``` c#
e ?
true => e1 ? x : y :
false => e2
e
? true => e1
: false => e2
// Interpret ? as following the tested expression, and : as a separator of test/result pairs
e
? 1 => "one"
: 2 => "two"
: var x when x > 2 => "too many"
: _ => "too few"
// Interpret ? as introducing test/result pairs, and : as introducing the fallback result
e
? 1 => "one"
? 2 => "two"
? var x when x > 2 => "too many"
: "too few"
e
? 1 -> "one"
? 2 -> "two"
? var x when x > 2 -> "too many"
: "too few"
// Compromise - terser
e ? { 1: "one", 2: "two", var x when x > 2: "two many", "too few" }
// Formatted
e ?
{
1: "one",
2: "two",
var x when x > 2: "two many",
"too few"
}
e switch
{
1: "one",
2: "two",
var x when x > 2: "two many",
"too few"
}
// Some of these in context of a var
var x = e
? 1 -> "one"
: 2 -> "two"
: var x when x > 2 -> "too many"
: _ -> "too few";
var x = e ?
{
1: "one",
2: "two",
var x when x > 2: "two many",
"too few"
};
var x = e switch
{
1: "one",
2: "two",
var x when x > 2: "two many",
"too few"
};
// Some of these as one-liners in a method call
M(x switch { null => 0, _ => x.Length }); // 1
M(x switch { null: 0, x.Length }); // 2
M(x ? null -> 0 : _ -> x.Length); // 3
M(x ? { null: 0, x.Length }); // 4
M(x ? null -> 0 : x.Length); // 5
M(x ? { null -> 0, x.Length }); // 6
```
Argument against 1:
``` c#
strings.Select(x => x switch { null => 0, _ => x.Length }); // Lots of => with different meaning
```
Argument against `->`: Has meaning in unsafe code
Argument against `:` as used in 4: Clashes with other uses of `:`. But
Where input is an expression rather than a variable:
``` c#
M(e switch { null => 0, var x => x.Length }); // 1 - 0
M(e switch { null: 0, var x: x.Length }); // 2 - 13 - 6
M(e ? null -> 0 : var x -> x.Length); // 3 - 1
M(e ? { null: 0, var x: x.Length }); // 4 - 6 - 3
M(e ? { null -> 0, var x -> x.Length }); // 6 - 0
```
We might want to allow the last thing to be a default value without pattern, but not in the prototype.
So the prototype will have version 2.
Conditional operators and switch expressions on bool are semantically equivalent.
The fact that you know the number of colons today means you can have fewer parentheses than you would get away with here.
The `=>` is probably not right in a `?:` style syntax. It would have to be something else that more clearly signals pattern/result pairs.
# Property pattern
``` c#
if (e is { Name: "Mads", Employer: { ID: string id } }) { WriteLine(id); } // 1 - Current
if (e is { Name = "Mads", Employer = { ID = string id } }) { WriteLine(id); } // 2
if (e is { Name == "Mads", Employer == { ID == string id } }) { WriteLine(id); } // 3
if (e is { Name is "Mads", Employer is { ID is string id } }) { WriteLine(id); } // 4
```
1 is what we have implemented, but clashes a little with what we just decided for switch expressions.
2 mirrors object initializers the most
3 implies equality, but clashes in meaning with `==` elsewhere
4 emphasizes `is` as a means for applying patterns
``` c#
var result = person switch
{
{ Name: "Mads", Employer: { ID: string id } }: id,
(_, id: var pid){ Name: "Matt" }: pid,
_: null
};
```
There's a clash but maybe it doesn't feel too bad. The second pattern with three different meanings of `:` is certainly not common.
``` c#
var result = person switch
{
{ Name is "Mads", Employer is { ID is string id } }: id,
(_, var pid){ Name is "Matt" }: pid,
_: null
};
```
Decision: stay with `:` for prototype, remains open question though!