- Unconstrained type parameter annotation
- Covariant returns
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:
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:
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
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:
-
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 ifT
is a struct or unconstrained, so finding the safe place to ask is difficult. -
Unresolved design problems. In overriding
T?
meanswhere T : struct
, going back to the beginning ofNullable<T>
. This already caused problems withT? where T : class
in C# 8, which is why we introduced a feature where you could specifyT? 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 unconstrainedT?
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. -
Confusion with a different feature. If we use the
T?
syntax, the following would be legal:
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
:
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.
We looked at the proposal in #2844.
There's a proposal that for the following
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
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.