[!INCLUDESpecletdisclaimer]
Champion issue: #3297
Allow nullable annotations for type parameters that are not constrained to value types or reference types: T?
.
static T? FirstOrDefault<T>(this IEnumerable<T> collection) { ... }
In C#8, ?
annotations could only be applied to type parameters that were explicitly constrained to value types or reference types.
In C#9, ?
annotations can be applied to any type parameter, regardless of constraints.
Unless a type parameter is explicitly constrained to value types, annotations can only be applied within a #nullable enable
context.
If a type parameter T
is substituted with a reference type, then T?
represents a nullable instance of that reference type.
var s1 = new string[0].FirstOrDefault(); // string? s1
var s2 = new string?[0].FirstOrDefault(); // string? s2
If T
is substituted with a value type, then T?
represents an instance of T
.
var i1 = new int[0].FirstOrDefault(); // int i1
var i2 = new int?[0].FirstOrDefault(); // int? i2
If T
is substituted with an annotated type U?
, then T?
represents the annotated type U?
rather than U??
.
var u1 = new U[0].FirstOrDefault(); // U? u1
var u2 = new U?[0].FirstOrDefault(); // U? u2
If T
is substituted with a type U
, then T?
represents U?
, even within a #nullable disable
context.
#nullable disable
var u3 = new U[0].FirstOrDefault(); // U? u3
For return values, T?
is equivalent to [MaybeNull]T
;
for argument values, T?
is equivalent to [AllowNull]T
.
The equivalence is important when overriding or implementing interfaces from an assembly compiled with C#8.
public abstract class A
{
[return: MaybeNull] public abstract T F1<T>();
public abstract void F2<T>([AllowNull] T t);
}
public class B : A
{
public override T? F1<T>() where T : default { ... } // matches A.F1<T>()
public override void F2<T>(T? t) where T : default { ... } // matches A.F2<T>()
}
For compatibility with existing code where overridden and explicitly implemented generic methods could not include explicit constraint clauses, T?
in an overridden or explicitly implemented method is treated as Nullable<T>
where T
is a value type.
To allow annotations for type parameters constrained to reference types, C#8 allowed explicit where T : class
and where T : struct
constraints on the overridden or explicitly implemented method.
class A1
{
public virtual void F1<T>(T? t) where T : struct { }
public virtual void F1<T>(T? t) where T : class { }
}
class B1 : A1
{
public override void F1<T>(T? t) /*where T : struct*/ { }
public override void F1<T>(T? t) where T : class { }
}
To allow annotations for type parameters that are not constrained to reference types or value types, C#9 allows a new where T : default
constraint.
class A2
{
public virtual void F2<T>(T? t) where T : struct { }
public virtual void F2<T>(T? t) { }
}
class B2 : A2
{
public override void F2<T>(T? t) /*where T : struct*/ { }
public override void F2<T>(T? t) where T : default { }
}
It is an error to use a default
constraint other than on a method override or explicit implementation.
It is an error to use a default
constraint when the corresponding type parameter in the overridden or interface method is constrained to a reference type or value type.