According to Microsoft, reference types are allocated on the heap and garbage-collected, whereas value types are allocated on the stack (or inline in a containing type) and deallocated when the stack unwinds. This difference in allocation strategy is what makes value type allocations generally cheaper. But at the CLR level, what exactly are classes and structs?
Keywords vs CLR
All components in .NET derive from System.Object. That means everything — including structs and enums — is ultimately an object and therefore a class. Consider this source:
public class MyClass { }
public struct MyStruct { }
public enum MyEnum { }
class Program
{
static void Main(string[] args)
{
long value = 20;
}
}Decompiling to IL reveals what the CLR actually sees. A class in IL extends System.Object:
.class public auto ansi beforefieldinit StructVsClass.MyClass
extends [mscorlib]System.Object
{
.method public hidebysig specialname rtspecialname instance void .ctor () cil managed
{
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: nop
IL_0007: ret
}
}A struct extends System.ValueType — which is itself a class:
// A struct extends System.ValueType — which itself is a class
.class public sequential ansi sealed beforefieldinit StructVsClass.MyStruct
extends [mscorlib]System.ValueType
{
.pack 0
.size 1
}An enum extends System.Enum — also a class:
// An enum extends System.Enum — which is also a class
.class public auto ansi sealed StructVsClass.MyEnum
extends [mscorlib]System.Enum
{
}Even a primitive long becomes int64 in IL, which maps to System.Int64 — a sealed ValueType:
// The long variable in Main becomes int64 in IL, which extends System.ValueType
.method private hidebysig static void Main (string[] args) cil managed
{
.entrypoint
.locals init (
[0] int64 'value'
)
IL_0000: nop
IL_0001: ldc.i4.s 20
IL_0003: conv.i8
IL_0004: stloc.0
IL_0005: ret
}
// From mscorlib — System.Int64 is a sealed ValueType:
.class public sequential ansi sealed serializable beforefieldinit System.Int64
extends System.ValueType
implements System.IComparable,
System.IFormattable,
System.IConvertible,
class System.IComparable`1<int64>,
class System.IEquatable`1<int64>
{ }Memory Layout — Pointer Evidence
The key practical difference is memory layout. A struct holds its field values directly at its own address, whereas a class stores a reference to its fields elsewhere on the heap. You can verify this with unsafe pointer arithmetic:
// Classes store a reference; structs embed the value inline.
// With unsafe code you can observe this directly via pointer addresses.
public class MyClass { public int Value; }
public struct MyStruct { public int Value; }
static void Main(string[] args)
{
var myClass = new MyClass();
var myStruct = new MyStruct();
unsafe
{
// Class: the object pointer and the field pointer differ
TypedReference clsTR = __makeref(myClass);
IntPtr clsPtr = **(IntPtr**)(&clsTR);
Console.WriteLine(quot;Class object address : 0x{clsPtr.ToString("x")}");
fixed (int* clsValPtr = &myClass.Value)
{
Console.WriteLine(quot;Class field address : 0x{((long)clsValPtr).ToString("x")}");
Console.WriteLine(quot;Class object == field : {(long)clsPtr == (long)clsValPtr}");
}
// Struct: the struct address IS the field address (value is inline)
MyStruct* strPtr = &myStruct;
int* strValPtr = &myStruct.Value;
Console.WriteLine(quot;Struct object address : 0x{((long)strPtr).ToString("x")}");
Console.WriteLine(quot;Struct field address : 0x{((long)strValPtr).ToString("x")}");
Console.WriteLine(quot;Struct object == field : {strPtr == strValPtr}");
}
}
// Output:
// Class object address : 0x1f4a9c20
// Class field address : 0x1f4a9c28 ← different (header + method table pointer offset)
// Class object == field : False
// Struct object address : 0x56f9e4b0
// Struct field address : 0x56f9e4b0 ← same — value lives at the struct's own address
// Struct object == field : TrueFor the struct the object address and the field address are identical — the value is embedded directly. For the class they differ because the object header and method table pointer precede the field data on the heap.
