csharplang/proposals/csharp-9.0/unconstrained-type-parameter-annotations.md

3.6 KiB

Unconstrained type parameter annotations

Summary

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) { ... }

? annotation

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>()
}

default constraint

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.

Design meetings