188 lines
No EOL
6.4 KiB
Markdown
188 lines
No EOL
6.4 KiB
Markdown
|
|
# 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. |