Every generic constraint in C# 14, what each enables, the order rules, and how constraints affect performance and API design.
Constraints tell the compiler what T can do so you can use it safely. C# has class and class? for reference types, struct for non-nullable value types, unmanaged for blittable structs, notnull, new() for a public parameterless constructor, base-class and interface constraints, where T : U, and Enum and Delegate constraints. C# 13 added the allows ref struct anti-constraint so T can be a Span<T>-like type. Interface constraints on struct T also avoid boxing because the JIT emits constrained calls.
Constraints (where T : ...) restrict which type arguments are allowed and, in return, let generic code use capabilities of T — call interface members, create instances, use null, or treat it as unmanaged memory. I'd walk through each constraint with a real reason to use it.
| Constraint | Means | Example of when you need it |
|---|---|---|
where T : class |
Non-nullable reference type (with NRT on) | Generic caches storing T? as "missing"; Interlocked.CompareExchange<T> |
where T : class? |
Any reference type, nullable or not | Same, when callers may use nullable types |
where T : struct |
Non-nullable value type | T? meaning Nullable<T>: T? Parse<T>(string s) where T : struct, IParsable<T> |
where T : unmanaged |
Value type with no references (recursively) | stackalloc T[n], MemoryMarshal.Cast<byte, T>, sizeof(T), pointers, binary serialisation |
where T : notnull |
Non-nullable reference or value type | Dictionary keys: Dictionary<TKey,TValue> where TKey : notnull |
where T : new() |
Has a public parameterless constructor | Factories: T Create<T>() where T : new() => new T(); |
where T : BaseClass |
Derives from (or is) a class | Generic repositories over AggregateRoot<TId> |
where T : IInterface |
Implements an interface | Max<T>(...) where T : IComparable<T>; generic math where T : INumber<T> |
where T : U (naked type) |
T derives from/implements another type parameter |
AddRange<TDerived>(IEnumerable<TDerived> items) where TDerived : T |
where T : Enum (C# 7.3) |
An enum type | Generic enum helpers: GetDisplayName<T>(T value) where T : struct, Enum |
where T : Delegate / MulticastDelegate (C# 7.3) |
A delegate type | Combining/removing generic delegates safely |
where T : allows ref struct (C# 13) |
Anti-constraint: permits ref struct type arguments | Generic APIs that should accept Span<T>-like types: IEnumerable-like patterns, Func<T, TResult> in .NET 9 |
where T : default |
Used in overrides/explicit impls to resolve T? ambiguity |
Overriding a method with unconstrained T? |
Constraints combine: where T : class, IEntity, new(). Order rules: class/struct/unmanaged/notnull/base class first, interfaces next, new() last; allows ref struct with other constraints in the same clause.
Interface constraint — call members without boxing:
public static T Max<T>(ReadOnlySpan<T> items) where T : IComparable<T>
{
var max = items[0];
foreach (var i in items[1..]) if (i.CompareTo(max) > 0) max = i;
return max;
}
struct + IParsable — return Nullable<T>:
public static T? TryParseOrNull<T>(string? s) where T : struct, IParsable<T>
=> T.TryParse(s, CultureInfo.InvariantCulture, out var v) ? v : null;
unmanaged — reinterpret bytes:
public static T Read<T>(ReadOnlySpan<byte> bytes) where T : unmanaged => MemoryMarshal.Read<T>(bytes);
Enum:
public static string Describe<T>(T value) where T : struct, Enum => value.ToString();
allows ref struct (C# 13):
public static TResult Apply<T, TResult>(T value, Func<T, TResult> f) where T : allows ref struct => f(value);
// can now be called with ReadOnlySpan<char>
T only has object members.A generic enum helper took object and used reflection to read [Display] attributes; it was called millions of times in report generation and boxed every value. Rewriting it as GetDisplayName<T>(T value) where T : struct, Enum with a static generic cache (static class EnumNames<T>) built once per enum type removed the boxing and reflection per call, and invalid calls (passing a non-enum) became compile errors.
Tests breadth (do they know the full set, including newer unmanaged, notnull, allows ref struct) and depth (why constraints matter for boxing and code generation).
Green flags: explains constrained calls and boxing; knows ordering rules; picks unmanaged for binary work; knows new() cost.
Red flags: only knows class and new(); thinks notnull is enforced at run time; cannot explain what a constraint enables inside the method.
Constraints are stored in metadata on generic parameters (GenericParamConstraint table plus flags like ReferenceTypeConstraint, NotNullableValueTypeConstraint, DefaultConstructorConstraint). unmanaged is emitted as struct + modreq(UnmanagedType) + an attribute so older compilers can't misuse it. notnull and class? are compiler-level nullable annotations. allows ref struct sets a new flag the runtime (.NET 9+) understands.
new() and performancenew T() in generic code historically compiled to Activator.CreateInstance<T>() — reflection-based and slow. Modern runtimes optimise it significantly (and for value types it's just default), but for hot paths a factory delegate (Func<T>) or static abstract static T Create() member is faster and more flexible (supports constructor parameters).
Constraints aren't part of the method signature, so you can't overload by constraint. C# 7.3 improved resolution: candidates whose constraints aren't satisfied are discarded early in more cases, reducing confusing errors. Still, design APIs with different names rather than relying on constraint-based selection.
unmanaged in detailA type is unmanaged if it's a primitive numeric type, char, bool, an enum, a pointer, or a struct whose fields are all unmanaged (recursively, including generic structs since C# 8). This guarantees no GC references, so the memory can be copied as raw bytes, stack-allocated, pinned trivially, and passed to native code. Generic math types like int, double, Half and Int128 are unmanaged.
allows ref struct and what it unlocksRef structs couldn't be type arguments because generic code might box them, store them in fields, or capture them. The anti-constraint promises the generic code won't do those things (the compiler enforces ref-safety rules for T inside). .NET 9 annotated many BCL types (Func<>, Action<>, IEnumerable<T>-like interfaces in some cases, SearchValues, comparers) so spans can flow through generic APIs, enabling more zero-allocation code.
T? means "default of T" (nullable for references, unchanged for values).where T : struct makes T? Nullable<T>.where T : class makes T? a nullable reference.where T : notnull warns when nullable types are used as arguments.Max<T> with and without IComparable<T> constraint (using Comparer<T>.Default) and benchmark with int.where T : unmanaged with stackalloc T[16] and sizeof(T).where T : struct, Enum and a static generic cache.allows ref struct and call it with ReadOnlySpan<char>.using System.Numerics;
using System.Runtime.InteropServices;
namespace Generics.Constraints;
public abstract class EntityBase
{
public Guid Id { get; init; } = Guid.NewGuid();
}
public sealed class Customer : EntityBase
{
public string Name { get; set; } = "";
}
public readonly record struct Header(int MessageType, int Length);
public interface IHasLength
{
int Length { get; }
}
public ref struct Utf8Token(ReadOnlySpan<byte> bytes) : IHasLength
{
private readonly ReadOnlySpan<byte> _bytes = bytes;
public int Length => _bytes.Length;
}
public static class Constrained
{
public static T Sum<T>(ReadOnlySpan<T> values) where T : INumber<T>
{
var total = T.Zero;
foreach (var v in values) total += v;
return total;
}
public static T ReadStruct<T>(ReadOnlySpan<byte> buffer) where T : unmanaged
=> MemoryMarshal.Read<T>(buffer);
public static T CreateEntity<T>() where T : EntityBase, new() => new T();
public static IReadOnlyList<string> EnumNames<T>() where T : struct, Enum
=> Enum.GetNames<T>();
public static void AddAll<T, TDerived>(ICollection<T> target, IEnumerable<TDerived> items) where TDerived : T
{
foreach (var item in items) target.Add(item);
}
// allows ref struct must be the last item in the constraint clause (C# 13+)
public static int Length<T>(T text) where T : IHasLength, allows ref struct
=> text.Length;
}
public static class Program
{
public static void Main()
{
Console.WriteLine(Constrained.Sum<int>([1, 2, 3]));
Console.WriteLine(Constrained.Sum<decimal>([1.5m, 2.25m]));
Span<byte> bytes = stackalloc byte[8];
BitConverter.TryWriteBytes(bytes, 7);
BitConverter.TryWriteBytes(bytes[4..], 128);
Console.WriteLine(Constrained.ReadStruct<Header>(bytes));
Console.WriteLine(Constrained.CreateEntity<Customer>().Id);
Console.WriteLine(string.Join(",", Constrained.EnumNames<DayOfWeek>()));
Console.WriteLine(Constrained.Length(new Utf8Token("hello"u8)));
}
}
What happens: if (value is IComparable c) inside generic methods.
Why it is wrong: boxing and runtime failures.
Do instead: add the interface constraint.
new()What happens: generic factories require parameterless constructors.
Why it is wrong: forces public parameterless constructors and bypasses DI/validation.
Do instead: accept a Func<T> factory or use static abstract factories.
What happens: where T : EntityBase when only an Id property is needed.
Why it is wrong: excludes structs and other types; couples to a hierarchy.
Do instead: constrain to a small interface.
What happens: M<T>() where T : class and M<T>() where T : struct.
Why it is wrong: same signature — compile error.
Do instead: use different names.
notnull on keysWhat happens: generic caches allow nullable keys.
Why it is wrong: Dictionary throws on null keys at run time.
Do instead: where TKey : notnull.
T?What happens: expecting T? to be Nullable<T> without a struct constraint.
Why it is wrong: for value types, unconstrained T? is just T.
Do instead: add where T : struct or design separate overloads.
Enum constraint without structWhat happens: where T : Enum alone.
Why it is wrong: allows System.Enum itself (a reference type), causing boxing.
Do instead: where T : struct, Enum.
unmanaged for types that contain referencesWhat happens: trying to MemoryMarshal.Cast structs with string fields.
Why it is wrong: compile error — and reinterpretation would be unsafe.
Do instead: keep binary-serialisable structs free of references.
Constraints are the tool that turns generics from "object with nicer syntax" into zero-cost abstractions. unmanaged plus spans gives safe binary parsing, interface constraints plus struct T give inlined calls, and INumber<T> gives one numeric algorithm for every type. The flip side is API stability: constraints are part of the signature, so I am conservative in libraries. allows ref struct is still young; I use it in low-level libraries but rarely in application code.
Generics are usually zero-cost, but how depends on T. For value types the JIT generates separate, fully specialised code, so there is no boxing and typeof(T) == typeof(int) folds to a constant. For reference types all instantiations share one canonical body, which looks up type-specific data such as statics and typeof(T) through a runtime dictionary. new T() and generic virtual methods take slower paths. The cost of specialisation is code size and JIT time, which matters for Native AOT, where every instantiation must be known at build time.
An interface is a contract: a set of members a type promises to provide, with no instance state. A class or struct can implement many interfaces. Since C# 8 an interface can carry default implementations (used only through the interface, and they need .NET Core 3.0+), plus static, private and protected members. Since C# 11 it can declare static abstract / static virtual members, which power generic math (INumber<T>, T.Zero). Explicit implementation (void IDisposable.Dispose()) hides a member unless you cast. Calls through an interface use interface dispatch, which the JIT can often devirtualise. Casting a struct to an interface boxes it; a generic constraint (where T : IComparable<T>) avoids the box.
A struct is a value type: a variable holds the data itself, assignment and parameter passing copy it, and it is stored inline (in a local, a field or an array element) rather than as a separate heap object. Structs cannot inherit (they are sealed) but can implement interfaces. default(T) always gives all-zero fields, even when you write a parameterless constructor (allowed since C# 10). readonly struct guarantees immutability and avoids defensive copies. ref struct (like Span<T>) can live only on the stack; since C# 13 it can implement interfaces and be used with allows ref struct generics. Converting a struct to object or an interface boxes it. Mutable structs cause lost updates because you often change a copy.
How to tell it (STAR)
**S:** Our device telemetry pipeline was storing wrong readings for one packet type. **T:** Find out why and stop it happening again. **A:** I traced it to a struct that gained a reference field, which the generic reader allowed because it only required `struct`. I changed the constraint to `unmanaged`, switched to MemoryMarshal, and added round-trip tests. **R:** The bug class became a compile-time error and the parser got [percentage] faster.
Last updated 30 Oct 2026.