The unmanaged constraint C# 7.3 restricted a type parameter to types with no references at all, so the compiler could treat them as raw bytes. But it only accepted a type parameter, never a constructed generic type, so Wrapper<int> was rejected even though it contains nothing but an int.
C# 8.0 extends the rule. A constructed generic type satisfies unmanaged when every one of its type arguments does, recursively.
Code
csharp
struct Wrapper<T> where T : unmanaged
{
public T Value;
public int Tag;
}
// Wrapper<long> is unmanaged because long is
static int SizeOf<T>() where T : unmanaged => Unsafe.SizeOf<T>();
var size = SizeOf<Wrapper<long>>(); // 16csharp
struct Wrapper<T> where T : unmanaged
{
public T Value;
public int Tag;
}
// Rejected: a constructed type never satisfied the constraint
// var size = SizeOf<Wrapper<long>>();Notes
- The constraint propagates:
Wrapper<Wrapper<int>>is also unmanaged, because both levels are - A generic type with any reference-type field is never unmanaged, whatever its type arguments, because
unmanagedmeans "contains no references anywhere in the layout" - This is what makes
sizeof(T)legal over generic wrapper types, which previously required a non-generic struct per element type sizeof(T)still needs anunsafecontext or thesizeofoperator's generic form, soUnsafe.SizeOf<T>()is the usual alternative in safe codenintandnuintare unmanaged even though their size depends on the platform, sosizeofis evaluated at runtime for a generic parameter- See native integers C# 9.0 for the built-in pointer-sized types
- See the spans, memory & allocation guide for working with this memory without allocating