C# and .NET path · Types and memory · Topic 3 of 14

Value types, reference types and boxing

Not started

Every type in C# is either a reference type (class, interface, delegate, record, arrays, string) or a value type (struct, enum, the numeric types, bool, char, record struct). The difference is what a variable holds: a reference type variable holds a pointer to an object on the managed heap, while a value type variable holds the value itself. That one fact decides what assignment does, where memory is allocated, and when the garbage collector gets involved.

Why it matters: Whether a type is a struct or a class decides what assignment copies, where the data lives, and whether a call quietly allocates on the heap.

  1. 1Classes are references

    A variable of a class type holds a reference to an object on the heap. Assigning it copies the reference, so both variables see the same object.

  2. 2Structs are values

    A struct's fields are stored inline: in the local, the array element or the containing object. Assigning it copies all the fields.

  3. 3Boxing allocates

    Converting a value type to object or to an interface copies it into a new heap object. In hot code that is garbage you didn't ask for.

  4. 4Small and immutable

    Use a struct for small, immutable values (readonly struct, record struct). Large or mutable structs are easy to copy by accident.

1

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.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.What does this print?
    var a = new P { X = 1 };var b = a;b.X = 99;Console.WriteLine(a.X);struct P { public int X; }
  2. 2.Which line boxes?
    int n = 42;long l = n;          // line 2object o = n;        // line 3string s = n.ToString(); // line 4
  3. 3.Why do List<int> and Dictionary<int, T> avoid boxing when the old ArrayList and Hashtable did not?

0 of 3 right so far. Get all three to finish this topic.