Unconstrained type parameter annotations

Note

This article is a feature specification. The specification serves as the design document for the feature. It includes proposed specification changes, along with information needed during the design and development of the feature. These articles are published until the proposed spec changes are finalized and incorporated in the current ECMA specification.

There may be some discrepancies between the feature specification and the completed implementation. Those differences are captured in the pertinent language design meeting (LDM) notes.

You can learn more about the process for adopting feature speclets into the C# language standard in the article on the specifications.

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