C#64 min total · 17 parts
C# Fundamentals: Value Types, Reference Types, Boxing, and the Type System
Part 17 of 17 · ~3 min
Common Mistakes Worth Remembering
- Handing a
structinto a method and expecting to see whatever the method did to it afterward — a plain parameter only ever gets a copy; nothing comes back unless that parameter was declaredref. - Writing
sku.Trim();on its own line and moving on, as if it did something — nothing aboutstringever mutates; the trimmed result exists for exactly one expression and then vanishes if nobody assigns it anywhere. - Trusting that a mutating method call through a
List<T>indexer is safe just because the build succeeded. It compiles clean and runs clean, and the mutation still lands on a copy nobody keeps — only a direct field write through that same indexer gets stopped at compile time, withCS1612. - Dropping value types into
ArrayList,Hashtable, or any other collection typed toobject, without noticing that every single one just bought itself a heap allocation. - Shipping an
Equalsoverride with no matchingGetHashCode(or the reverse) —DictionaryandHashSetkeep compiling, keep running, and just quietly stop finding entries that are plainly sitting in them. - Reaching for a
staticfield as a convenient scratch variable without working out what two callers landing on it at the same instant, on two different threads, actually do to each other. - Believing a collection variable's assignment duplicated what was inside it — it copied the handle to the same underlying storage, and nothing about the contents is independent until something is explicitly cloned.
- Treating "stack" and "value type" as interchangeable labels for the same thing — a value type gets promoted to the heap the moment it becomes a class field, gets closed over by a lambda, or has to survive across an
await, with the type itself never having changed at all.
The same bugs, wearing different clothes
None of the above is really about warehouses. Here's the two sharpest ones again, in a completely unrelated system — a kiosk jukebox that queues songs for a shared speaker:
struct QueuedTrack
{
public string Title;
public int PlayCount;
public void MarkPlayed() { PlayCount++; }
}
var queue = new List<QueuedTrack> { new QueuedTrack { Title = "Track A", PlayCount = 0 } };
queue[0].MarkPlayed(); // compiles fine, mutates a throwaway copy
Console.WriteLine(queue[0].PlayCount); // 0 — nothing actually happened
Same silent no-op, zero relationship to shipments or stock counts. And the shared-static-state bug shows up just as easily here as it did in the batching example:
class KioskSession
{
static List<string> _recentlyPlayed = new(); // meant to be "recent for THIS kiosk"
public void Enqueue(string title) { _recentlyPlayed.Add(title); }
}
Two physical kiosks in the same store, each running its own KioskSession, end up sharing one _recentlyPlayed list — kiosk two's "recently played" panel shows songs a completely different machine queued, because static was scoped to the type, not to either kiosk's own session, and nothing about that mistake announces itself until someone notices the wrong songs on the wrong screen.
That's really the test of whether any of this stuck: pull the warehouse away entirely and the shape of the bug should still be obvious on sight. If it was, the model you built genuinely transferred — it was never really about SKUs and shipments, only borrowed them to become concrete for a while. From here, the more useful next move is writing this kind of reasoning yourself against fresh problems in the code lab, and then noticing the same value-vs-reference questions resurface one layer up, at the boundary of an actual HTTP request, in Building REST APIs with ASP.NET Core.
Practice this
Continue learning
- Interview & Career PrepThe Non-Technical Half of the Interview: Behavioral Questions, the STAR Method, and What Recruiters Are Actually Scoring
- AI & LLM EngineeringAI & LLM Engineering Fundamentals: Prompting, RAG, Embeddings, and Function Calling
- TypeScriptTypeScript Fundamentals: Types, Interfaces, Generics, and Why It Catches Bugs Before Runtime