CodeOath
← All posts
C#65 min total · 17 parts

C# Fundamentals: Value Types, Reference Types, Boxing, and the Type System

Contents — Part 14 of 17: Mutable Structs: Why They're a Trap
Part 14 of 17 · ~1 min

Mutable Structs: Why They're a Trap

Structs can have mutable fields, but combining "value type" (copies on assignment) with "mutable" produces some of the most confusing bugs in the language, because a copy silently diverges from the original the moment either one is mutated:

struct Counter2
{
    public int Value;
    public void Increment() { Value++; }
}

// Mutating a struct returned from a property or indexer often doesn't do what it looks like
List<Counter2> counters = new List<Counter2> { new Counter2() };
counters[0].Increment(); // Compile error in modern C# — indexers return a temporary copy,
                          // and the compiler now refuses to let you mutate a temporary you can't observe

Counter2 c = counters[0];
c.Increment(); // mutates ONLY the local copy `c` — counters[0] is untouched
Console.WriteLine(counters[0].Value); // still 0

The counters[0].Increment() line is a real historical foot-gun that the compiler now catches for indexers on List<T> specifically (list indexers return a copy, not a reference, so mutating it would silently do nothing) — but the underlying issue, a struct copy diverging from its source the moment either is mutated, is exactly why the broadly accepted guidance is: make structs immutable. Give every field a value only through the constructor, expose no mutating methods, and use with-style "return a new struct with this field changed" instead — records structs get this for free via with.