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, or bool.
  • It is an enum type.
  • It is a pointer type.
  • It is a user-defined struct that 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:

  1. Pointer Operations on Generic Types: You can declare pointers to generic types (e.g., T*), perform pointer arithmetic, and use the address-of operator (&).
  2. The sizeof Operator: You can use sizeof(T) directly on the generic parameter without throwing a compile-time error.
  3. Memory Allocation: It enables safe integration with stackalloc, Span<T>, Memory<T>, and direct native heap allocations via NativeMemory or Marshal.

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.