Skip to main content
CodeOath
← All posts

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 struct into 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 declared ref.
  • Writing sku.Trim(); on its own line and moving on, as if it did something — nothing about string ever 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, with CS1612.
  • Dropping value types into ArrayList, Hashtable, or any other collection typed to object, without noticing that every single one just bought itself a heap allocation.
  • Shipping an Equals override with no matching GetHashCode (or the reverse) — Dictionary and HashSet keep compiling, keep running, and just quietly stop finding entries that are plainly sitting in them.
  • Reaching for a static field 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.