C#64 min total · 17 parts
C# Fundamentals: Value Types, Reference Types, Boxing, and the Type System
Part 14 of 17 · ~2 min
Mutable Structs: Why They're a Trap
StockCount as written earlier has mutable fields, and "value type" plus "mutable" turns out to be a genuinely dangerous combination — a copy quietly drifts away from its source the instant either one changes.
struct StockCount
{
public string Sku;
public int OnHand;
public void Adjust(int delta) { OnHand += delta; } // a mutating instance method
}
var counts = new List<StockCount> { new StockCount { Sku = "WDG-100", OnHand = 5 } };
counts[0].OnHand = 6; // compile error CS1612 — List<T>'s indexer returns a copy, and this
// is a direct field assignment through that copy, which the compiler catches
That's confirmed against the compiler exactly as shown: CS1612: Cannot modify the return value of 'List<StockCount>.this[int]' because it is not a variable. List<T>'s indexer is a property, not direct element access, so what it hands back isn't a variable the compiler can write into — and for a direct field or property assignment like the one above, the compiler catches that and refuses to compile it.
Here's the part that's easy to assume works the same way, and genuinely doesn't — worth checking rather than guessing, because the two cases look almost identical on the page:
counts[0].Adjust(1); // compiles with ZERO errors or warnings
Console.WriteLine(counts[0].OnHand); // still 5 — the call ran, but only against a throwaway copy
No error. No warning. It just runs against an invisible temporary copy of the struct that gets discarded the instant Adjust returns, and the list is left completely unchanged. This is a real, verified gap the compiler leaves open: it catches a direct field assignment through a non-variable value-type expression, but it does not catch a method call on that same expression, even when the method plainly mutates its own fields. The two lines above look like the same category of mistake and only one of them gets stopped at compile time.
The safe pattern is to pull the struct out, mutate the local copy, and write it back explicitly:
var current = counts[0];
current.Adjust(1);
counts[0] = current; // an explicit assignment back into the list — this DOES work
Console.WriteLine(counts[0].OnHand); // 6
That three-line dance gets old fast, which is exactly why the durable fix isn't "remember to write it out longhand" — it's removing mutation from the struct's design entirely. Set every field once, in the constructor, and never write a method that assigns to one afterward; when a value needs to change, build and hand back a different instance carrying the new value instead of altering the one you were given. Reach back a few chapters and record struct is already doing precisely that, generating the "new instance, new value" behavior for you the moment you use with.