C# Unmanaged Constraint for Generics
The unmanaged constraint in C# restricts a generic type
parameter to types that are not managed by the garbage collector.
Introduced in C# 7.3, it ensures that a type is a non-nullable value
type containing no reference type fields at any level of nesting. This
article explains the technical definition of an unmanaged type,
demonstrates how to use the constraint in generic code, outlines the key
differences between struct and unmanaged, and
highlights common use cases involving low-level memory and native
interop.
What Is an Unmanaged Type?
In C#, a type is considered unmanaged if it satisfies all of the following conditions:
- It is a built-in primitive type:
sbyte,byte,short,ushort,int,uint,long,ulong,nint,nuint,char,float,double,decimal, orbool. - It is an
enumtype. - It is a pointer type.
- It is a user-defined
structthat contains only fields of unmanaged types.
If a struct contains any reference types—such as a
string, an array, or an object—it is not
unmanaged.
Syntax and Implementation
You declare the constraint by adding where T : unmanaged
to a generic class, interface, method, or delegate:
public unsafe class NativeBuffer<T> where T : unmanaged
{
private T* _pointer;
private int _length;
public NativeBuffer(int length)
{
_length = length;
_pointer = (T*)System.Runtime.InteropServices.NativeMemory.Alloc((nuint)(length * sizeof(T)));
}
public void Free()
{
if (_pointer != null)
{
System.Runtime.InteropServices.NativeMemory.Free(_pointer);
_pointer = null;
}
}
}Why Use the Unmanaged Constraint?
Before the unmanaged constraint, generic code could not
directly interact with pointers or native memory safely because the C#
compiler could not verify whether a generic type parameter contained
garbage-collected references.
Applying where T : unmanaged provides several specific
capabilities:
- Pointer Operations on Generic Types: You can
declare pointers to generic types (e.g.,
T*), perform pointer arithmetic, and use the address-of operator (&). - The
sizeofOperator: You can usesizeof(T)directly on the generic parameter without throwing a compile-time error. - Memory Allocation: It enables safe integration with
stackalloc,Span<T>,Memory<T>, and direct native heap allocations viaNativeMemoryorMarshal.
unmanaged vs.
struct
While the struct constraint requires a type to be a
value type, it does not guarantee that the type is free of managed
references:
// Valid: 'struct' allows reference types inside
public struct ManagedStruct
{
public string Name; // Reference type
}
// Allowed:
public void ProcessStruct<T>() where T : struct { }
ProcessStruct<ManagedStruct>();
// Compilation Error: ManagedStruct is not unmanaged
public void ProcessUnmanaged<T>() where T : unmanaged { }
// ProcessUnmanaged<ManagedStruct>(); The unmanaged constraint implicitly satisfies the
struct constraint, meaning that anything that satisfies
unmanaged also satisfies struct, but the
inverse is not true.
Practical Use Cases
- Native Interoperability (P/Invoke): Marshaling data between managed C# code and unmanaged C/C++ libraries.
- Game Development and Graphics: Constructing high-frequency data structures, such as vertex buffers or direct hardware buffers.
- Serialization and Network Protocols: Writing binary serializers that read and write directly to byte streams via pointer casting without heap allocations.