Constrain Generics to Reference or Value Types in C
In C#, generic type parameters can be restricted to specific
categories of data using generic constraints declared with the
where keyword. Constraining a generic parameter to either a
reference type or a value type ensures compile-time type safety,
dictates whether the type can be null, and grants access to specific
operations associated with that category. This article explains how to
apply the class constraint for reference types and the
struct constraint for value types, along with their
practical usage and rules.
Constraining to
Reference Types (where T : class)
To restrict a generic type parameter to reference types only, append
the where T : class constraint to the class, interface, or
method signature. This ensures that the type argument must be a
reference type, such as a class, interface, delegate, record, or
array.
public class ReferenceContainer<T> where T : class
{
public T Value { get; set; }
public void CheckNull()
{
if (Value is null)
{
Console.WriteLine("Value is null.");
}
}
}Rules and Behaviors for
class:
- Valid Types: Any custom class, string, interface, delegate, or array.
- Invalid Types: Primitive types (
int,bool,double), customstructdefinitions, and enumerations (enum). - Nullability: If nullable reference types are
enabled,
where T : classrequires a non-nullable reference type in C# 8.0 and later. Usewhere T : class?if the type argument is allowed to be nullable.
Constraining to
Value Types (where T : struct)
To restrict a generic type parameter to value types, use the
where T : struct constraint. This limits the type parameter
to non-nullable value types.
public class ValueContainer<T> where T : struct
{
public T Value { get; set; }
public void Reset()
{
// Value types always have a default parameterless value
Value = default;
}
}Rules and Behaviors for
struct:
- Valid Types: Numeric primitives (
int,float,decimal),bool,char,enum, and customstructtypes. - Invalid Types: Classes, strings, interfaces,
delegates, and
Nullable<T>types (e.g.,int?). - Default Constructor: The
structconstraint implicitly satisfies thenew()constraint, meaning you can instantiateTusingnew T().
Specialized Value Type Constraints
C# provides additional, more specific value-type constraints:
unmanagedConstraint:where T : unmanagedrestrictsTto types that are value types and contain no reference type fields at any level of nesting. This is useful for low-level interop and pointer manipulation.notnullConstraint:where T : notnullrestrictsTto non-nullable types, encompassing both non-nullable value types and non-nullable reference types.
Combining Constraints
When combining multiple constraints on a single type parameter, the
type constraint (class or struct) must always
appear first in the constraint list:
// Valid: class constraint comes first
public void ProcessReference<T>(T item) where T : class, IComparable<T>, new()
{
}
// Valid: struct constraint comes first
public void ProcessValue<T>(T item) where T : struct, IComparable<T>
{
}Note that class and struct are mutually
exclusive and cannot be applied to the same type parameter.