Structs, value and reference types

C# · Practical · 12 min

Many subtle bugs and performance issues come from not knowing whether a variable holds a value or a reference.

C# has two kinds of types. Value types (int, double, bool, DateTime, enums, and any struct) store their data directly in the variable. Reference types (class, string, arrays, delegates, record class) store a reference to an object on the heap.

The difference shows up on assignment and when passing arguments: copying a struct copies all its fields, so changing the copy doesn't affect the original. Copying a class reference makes two variables point to the same object.

Declare a struct with struct, ideally readonly struct so all fields are immutable and the compiler can avoid defensive copies. Good struct candidates are small (a few fields), immutable, and value-like: points, money amounts, ranges.

Other differences:

  • Structs always have a default value (all fields zeroed) and cannot be null unless written T?.
  • Struct Equals compares fields by default (reflection-based and slow, so override it or use record struct); class Equals compares references.
  • Local struct variables usually live on the stack or inline in their containing object, avoiding GC allocations.

Boxing happens when a value type is converted to object or an interface: the runtime allocates a heap object holding a copy. Unboxing copies it back. Boxing in hot loops costs allocations; generics like List<int> avoid it, unlike the old non-generic ArrayList.

A common trap: mutating a struct obtained from a property or list element modifies a copy.

Example

var a = new PointS(1, 2);
var b = a;            // copies the value
b.X = 10;
Console.WriteLine($"{a.X} {b.X}");   // 1 10

var c = new PointC { X = 1 };
var d = c;            // copies the reference
d.X = 10;
Console.WriteLine($"{c.X} {d.X}");   // 10 10

object boxed = a;     // boxing: heap copy
a.X = 99;
Console.WriteLine(((PointS)boxed).X); // 1

var list = new List<PointS> { new(1, 2) };
// list[0].X = 5;     // error CS1612: would modify a temporary copy

struct PointS(int x, int y) { public int X = x; public int Y = y; }
class PointC { public int X; }

Remember

  • Structs copy their data on assignment; classes copy references
  • Keep structs small and immutable (readonly struct)
  • Converting a value type to object or an interface boxes it
  • Mutating a struct copy does not change the original

Watch out for

  • Making a large mutable struct and being surprised that changes 'disappear' on copies.

Check yourself

  1. Which is a value type in C#?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/value-types