How it works
The explanation, with examples you can run.
Copy or share
var a = new PointS(1, 2);var b = a; // copies the whole structb.X = 99;Console.WriteLine($"struct: a.X={a.X} b.X={b.X}"); var c = new PointC(1, 2);var d = c; // copies the referenced.X = 99;Console.WriteLine($"class: c.X={c.X} d.X={d.X}"); struct PointS { public int X, Y; public PointS(int x, int y) { X = x; Y = y; } }class PointC { public int X, Y; public PointC(int x, int y) { X = x; Y = y; } }This prints struct: a.X=1 b.X=99 and class: c.X=99 d.X=99. The same copying happens when you pass a struct to a method, return it, or read it out of a List<T> indexer: you get a copy, which is why list[0].X = 5 on a list of mutable structs is a compile error.
"Structs live on the stack" is only half true. A struct lives wherever its container is: a local in a stack frame (or a CPU register), an element inside an array on the heap, a field inside a class object. A class instance always lives on the heap, has an object header and a pointer to its method table (16 bytes of overhead on 64-bit), and is reclaimed by the GC.
Boxing
Sometimes a value has to be treated as an object: stored in an object variable, passed as an interface it implements, or put into a non-generic collection. The runtime then boxes it: allocates a heap object, copies the value in, and hands back a reference. Unboxing copies it back out.
long before = GC.GetAllocatedBytesForCurrentThread();object boxed = 0;for (int i = 0; i < 1000; i++) boxed = i; // boxes on every assignmentConsole.WriteLine(GC.GetAllocatedBytesForCurrentThread() - before);On .NET 10 x64 this reported 24,024 bytes: 1,001 boxes of 24 bytes each (header, method table pointer, and the 4-byte int padded to 8). The same loop adding into an int allocated nothing. Common hidden sources of boxing are string.Format and logging calls with object parameters, calling an interface method on a struct through the interface type, and Equals(object) on structs that don't override it.
Choosing
Prefer a class by default. Choose a struct for a small value that is logically one thing (a coordinate, a money amount, a key), is usually immutable, and is copied less often than it is created. Mark it readonly struct so the compiler stops you mutating it and avoids defensive copies. Pass large structs with in or ref if profiling shows copying matters.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.