Back to Blog
C#

C# where T : class — Reference Type Constraints in Generics

c# where t class: Understand the C# where T : class constraint: syntax, compile-time guarantees, nullability interaction, common mistakes, and runtime behavior.

C#GenericsType Constraints.NETReference Types
A C# generic class declaration showing the where T : class constraint restricting type parameters to reference types.

c# where t class requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

In C#, a generic type parameter can be restricted with constraints that tell the compiler what kinds of types are allowed. The where T : class constraint requires that any type argument substituted for T be a reference type. This single constraint affects compile-time checking, nullability analysis, and the runtime behavior of the generic type.

The Syntax of the class Constraint

The constraint is declared after the type parameter list:

public class Cache<T> where T : class { private readonly Dictionary<string, T> _items = new(); public void Add(string key, T item) { _items[key] = item; } }

The constraint applies to the type parameter T and is placed at the end of the class declaration. The same syntax works for generic methods, interfaces, and delegates.

For a generic method:

public static T? FindFirst<T>(IEnumerable<T> source, Func<T, bool> predicate) where T : class { foreach (var item in source) { if (predicate(item)) { return item; } } return null; }

What the Constraint Guarantees at Compile Time

When you write where T : class, the compiler enforces that any type argument used with the generic type or method must be a reference type. That includes classes, interfaces, delegates, arrays, and the string type. Value types such as int, double, struct types, and enums are rejected at compile time.

This guarantee has a direct effect on what you can do inside the generic body. You can assign null to a variable of type T (subject to nullability settings), compare a T value against null, and use default(T) knowing that it evaluates to null rather than a zeroed value type.

public static bool IsNull<T>(T value) where T : class { return value is null; }

Without the constraint, this method would not compile because T could be a value type, and null is not a valid comparison for all value types.

A Practical Example: Generic Repository Methods

A common use is a generic data-access helper where entities are reference types:

public interface IEntity { int Id { get; } } public class EntityRepository<T> where T : class, IEntity { private readonly Dictionary<int, T> _store = new(); public void Save(T entity) { _store[entity.Id] = entity; } public T? GetById(int id) { return _store.TryGetValue(id, out var entity) ? entity : null; } }

The class constraint here works together with the IEntity interface constraint. The method GetById returns T?, which is meaningful only because T is known to be a reference type. If T could be a value type, the nullable return annotation would behave differently, and returning null would not be possible.

Nullability and the class Constraint

In projects with nullable reference types enabled, the meaning of where T : class becomes more precise. The constraint where T : class means the type argument must be a non-nullable reference type. If you want to allow nullable reference types as type arguments, you must write where T : class?.

public class NullableContainer<T> where T : class? { public T? Value { get; set; } }

The distinction matters when you call generic code with a type that may be null. A method constrained with where T : class will produce a compiler warning if you pass a nullable reference type as the type argument, because the constraint promises that T is non-nullable.

Common Mistakes and Edge Cases

One frequent mistake is applying the constraint to a type that is used with value types. If you have a generic utility that should work with both int and string, the class constraint is too restrictive. The where T : notnull constraint allows both reference types and non-nullable value types, which is often the better choice for such utilities.

Another edge case involves default(T). With the class constraint, default(T) is null. Without the constraint, default(T) is the zero value for value types. Code that relies on default(T) being null must include the constraint.

public static T? GetDefault<T>() where T : class { return default; }

Runtime Behavior and Performance

The constraint is enforced at compile time; it does not add runtime checks. The JIT compiler generates specialized code for each value type used with a generic type, but reference types share a single instantiation because all reference types have the same representation. This means using reference types as type arguments does not cause boxing, and the constraint itself has no measurable runtime cost.

The practical performance consideration is the opposite: the class constraint prevents value types from being used, which means you cannot accidentally introduce boxing by passing a struct to a generic method that stores it as object. The constraint keeps the generic body free of boxing-related allocation when the type argument is a reference type.

Choosing Between class, struct, and notnull

ConstraintAllowed type argumentsdefault(T)Nullable annotation
where T : classNon-nullable reference typesnullT? allowed
where T : class?Reference types, including nullablenullT? allowed
where T : structNon-nullable value typesZeroed valueT? means Nullable<T>
where T : notnullReference types and non-nullable value typesDepends on TNot allowed

Use where T : class when the generic logic depends on reference semantics, such as null checks, reference equality, or returning null as a sentinel. Use where T : notnull when the generic code must accept both reference and value types but still reject null. Use where T : struct when the generic logic relies on value-type semantics like Nullable<T>.

The choice of constraint is a contract with callers. It tells them which types are valid and lets the compiler catch invalid usage before the code runs. Choosing the narrowest constraint that satisfies the generic body keeps the API honest and prevents callers from passing types that would break the implementation.

c# where t class: Practical Usage and Code Examples | RYUSLOG DEV