csharplang/meetings/2020/LDM-2020-01-08.md

188 lines
6.4 KiB
Markdown
Raw Normal View History

2020-01-09 05:40:58 +01:00
# C# Language Design Meeting for Jan. 8, 2020
## Agenda
1. Unconstrained type parameter annotation
2. Covariant returns
## Discussion
### Unconstrained type parameter annotation T??
You can only use `T?` when is constrained to be a reference type or a value type. On the other
hand, you can only use `T??` when `T` is unconstrained.
The question is what to use for the following:
```C#
abstract class A<T>
{
internal abstract void F<U>(ref U?? u) where U : T;
}
class B1 : A<string>
{
internal override void F<U>(ref U?? u) => default; // Is ?? allowed or required?
}
class B2 : A<int>
{
internal override void F<U>(ref U?? u) => default; // Is ?? allowed or required?
}
class B3 : A<int?>
{
internal override void F<U>(ref U?? u) => default; // Is ?? allowed or required?
}
```
Our understanding is that this would be:
```C#
abstract class A<T>
{
internal abstract void F<U>(ref U?? u) where U : T;
}
class B1 : A<string>
{
internal override void F<U>(ref U?? u) => default;
// We think the correct annotation is
// void F<U>(ref U? u) where U : class
// because the type parameter is no longer unconstrained. The `where U : class`
// constraint is required, as U? would otherwise mean U : struct
}
// We may want to allow U?? even in the above case, so
class B1 : A<string>
{
internal override void F<U>(ref U?? u) => default; // allowed?
// This would not require the `where U : class` constraint because `U??` cannot
// be confused with `Nullable<U>`
}
class B2 : A<int>
{
internal override void F<U>(ref U?? u) => default;
// The correct annotation is
// void F<U>(ref U u)
// We could allow
// void F<U>(ref U?? u)
// although it would be redundant
}
class B3 : A<int?>
{
internal override void F<U>(ref U?? u) => default;
// The correct annotation is
// void F<U>(ref U u)
// We could allow
// void F<U>(ref U?? u)
// although it would be redundant
```
In the above, we're wondering whether we should allow `??` without warning even in cases where
there's existing syntax to represent the semantics. One benefit is that you could write
```C#
abstract class A<T>
{
internal abstract F<U>(ref U?? u) where U : T;
}
class B1 : A<string>
{
internal override F<U>(ref U?? u) { }
}
```
Since the override cannot be confused with `U?`, there's no need for the `where U : class`. On the
other hand, the benefit seems marginal and it's not needed to represent the semantics. It could
also be added later.
**Conclusion**
We like the syntax `??` to represent the "maybe default" semantics. We think that `??` should be
allowed in the cases where we have other syntax to represent the semantics. A warning will be
provided and hopefully a code fix to move the code to the "more canonical" form. The syntax `??`
is only legal on type parameters in a nullable-enabled context.
We considered using the `?` syntax the represent the same semantics, but ruled it out for a couple reasons:
1. It's technically difficult to achieve. There are two technical limitations in the compiler.
The first is design history where a type parameter ending in `?` is assumed to be a struct. This
has been true in the compiler all the way until nullable reference types in the last release. The
second problem is that many pieces of C# binding are very sensitive to being asked if a type is a
value or reference type and asking the question can often lead to cycles if asked before the answer
is absolutely necessary. However, `T?` means different things if `T` is a struct or unconstrained, so
finding the safe place to ask is difficult.
2. Unresolved design problems. In overriding `T?` means `where T : struct`, going back to the beginning of
`Nullable<T>`. This already caused problems with `T? where T : class` in C# 8, which is why we introduced
a feature where you could specify `T? where T : class` in an override, contrary to past C# design where
constraints are not allowed to be re-specified in overrides. To accommodate overloads for unconstrained
`T?` we would have to introduce a new type of explicit constraint meaning *unconstrained*. We don't have
a syntax for that, and don't particularly want one.
3. Confusion with a different feature. If we use the `T?` syntax, the following would be legal:
```C#
class C
{
public T? M<T>() where T : notnull { ... }
}
```
what you may think is that `M` returns a nullable reference type if `T` is a reference type, and a nullable
value type if `T` is a value type (i.e., `Nullable<T>`). However, that's *not* what this feature is. This
feature is essentially *maybe default*, meaning that `T?` may contain the value `default(T)`. For a reference
type this would be `null`, but for a value type this would be the zero value, not `null`.
Moreover, this seems like a useful feature, that would be ruled out if we used the syntax for something else.
Consider a method similar to `FirstOrDefault`, `FirstOrNull`:
```C#
public static T? FirstOrNull<T>(this IEnumerable<T> e) where T : notnull
```
The benefit is that there is a single sentinel value, `null`, and that the full set of values in a struct
could be represented. In `FirstOrDefault`, if the input is `IEnumerable<int>` there is no way to distinguish
a non-empty enumerable with first value of `0`, or an empty enumerable. With `FirstOrNull` you can distinguish
these cases in a single call, as long as `null` is not a valid value in the type.
Due to CLR restrictions it is not possible to implement this feature in the obvious way, so this feature may
never be implemented, but we would like to prevent confusion and keep the syntax available in case we ever
figure out how we'd like to implement it.
### Covariant returns
We looked at the proposal in #2844.
There's a proposal that for the following
```C#
interface I1
{
object M();
}
interface I2 : I1
{
string M() => null;
}
```
`I2.M` would be illegal as it is, because this is an interface *implementation*, not an
*override*. Interface implementation would not change return types, while overriding would. Thus
we would allow
```C#
interface I1
{
object M();
}
interface I2 : I1
{
override string M() => null;
}
```
which would allow the return type to change. However, it would override *all* `M`s, since
explicit implementation is not allowed.
**Conclusion**
We like it in principle and would like to move forward. There may be some details around
interface implementation vs. overriding to work out.