A generic type or method takes type parameters (C# 2; C# specification clauses 8.4, 8.5
and 15.2.3). Unlike Java’s generics, C# generics are not erased: the type arguments exist
at run time, and List<int> stores real int values with no
boxing.
Declaring generic types and methods
public class Box<T>
{
public T Value { get; set; }
public Box(T value) => Value = value;
}
public static class Util
{
public static T Last<T>(IList<T> items) => items[^1];
public static void Swap<T>(ref T a, ref T b) { T t = a; a = b; b = t; }
}
var box = new Box<int>(3);
int n = Util.Last(new[] { 1, 2, 3 }); // T inferred as int
- Classes, structs, interfaces, delegates, records and methods can be generic. Enums, properties, indexers, events, operators, fields and constructors cannot declare type parameters of their own, though they may use those of the enclosing type.
- The compiler infers a method’s type arguments from its arguments when it can (clause 12.6.3). It never infers them from the return type or for type declarations.
- Types and methods are distinguished by their number of type parameters, so
BoxandBox<T>can coexist. - Each closed constructed type has its own set of static fields and its own run of
the static constructor:
Box<int>andBox<string>do not share them. default(T)ordefaultgives the default value of the type argument. An unconstrainedTcannot be assignednull, and twoTvalues cannot be compared with==, but a value can be compared tonull.
Open and closed types
A type with a type argument is a constructed type (clause 8.4). An
open type involves a type parameter; a closed type does not. An
unbound generic type has no type arguments at all and appears only in
typeof(List<>) and, since C# 14, nameof(List<>).
T? in generic code
Since C# 9, T? is allowed on an unconstrained T, meaning that
the value may be the default of a reference type. For a value type T,
T? stays T unless the struct constraint makes it
Nullable<T>. If T is itself a Nullable,
T? is the same type.
Constraints
A where clause limits the type arguments allowed, and lets code use the
members they guarantee (clause 15.2.5).
type_parameter_constraints_clause:
where type_parameter : type_parameter_constraints
| Constraint | The type argument must be | Since |
|---|---|---|
where T : struct | A non-nullable value type. Implies new() | 2 |
where T : class | A reference type; non-nullable in a nullable context | 2 |
where T : class? | A reference type, nullable or not | 8 |
where T : notnull | A non-nullable type; a violation is a warning rather than an error | 8 |
where T : unmanaged | A non-nullable unmanaged type. Implies struct | 7.3 |
where T : new() | Has a public parameterless constructor | 2 |
where T : Base, Base? | Derived from the class, or that class | 2 |
where T : IFoo, IFoo? | Implements the interface; may be repeated | 2 |
where T : U | Derived from another type parameter | 2 |
where T : System.Enum, System.Delegate | An enum type, or a delegate type, as a base class constraint | 7.3 |
where T : default | Used only on overrides and explicit implementations, to state that neither class nor struct applies | 9 |
where T : allows ref struct | May be a ref struct; an anti-constraint, see below | 13 |
Some constraints exclude each other, and they must be in a fixed order:
- At most one of
struct,class,class?,notnullandunmanaged, and it must come first. - A base class constraint cannot be combined with those five, and at most one is allowed.
new()must be last, apart from anti-constraints, and cannot be combined withstructorunmanaged.defaultcannot be combined withstructorclass.allows ref structmust come after all other constraints, and cannot be combined withclassorclass?.
public class Repository<TEntity, TKey>
where TEntity : class, IEntity<TKey>, new()
where TKey : notnull
{
public TEntity Create() => new TEntity();
}
With a class constraint, == always compares references, even
when the type argument overloads it. A self-referencing constraint such as
where T : IAddable<T> lets an interface name its implementing type; see
static abstract
members.
allows ref struct
allows ref struct (C# 13) permits a
ref struct
as the type argument. The generic code must then follow ref-safety rules for that type
parameter: it cannot box an instance or store it in a field outside a
ref struct. The clause is not inherited, and such a type parameter can be
passed on only to one that also allows it.
Variance
Interfaces and delegates can mark type parameters as variant (C# 4, clause 19.2.3). Generic classes and structs cannot. Variance applies only to reference conversions.
| Modifier | Name | May appear | Allows |
|---|---|---|---|
out T | Covariant | Only in output positions, such as return types | IEnumerable<string> to IEnumerable<object> |
in T | Contravariant | Only in input positions, such as parameters | IComparer<object> to IComparer<string> |
| none | Invariant | Anywhere | No conversions |
public interface IProducer<out T> { T Get(); }
public interface IConsumer<in T> { void Put(T item); }
IProducer<string> s = /* ... */ null!;
IProducer<object> o = s; // covariant
IEnumerable<int> i = new int[1];
// IEnumerable<object> bad = i; // error: int is a value type
The libraries use variance on interfaces such as IEnumerable<out T>,
IReadOnlyList<out T> and IComparer<in T>, and on
delegates such as Func<in T, out TResult> and Action<in T>.