C#64 min total · 17 parts
C# Fundamentals: Value Types, Reference Types, Boxing, and the Type System
Part 13 of 17 · ~2 min
static Members: Shared Per Type, Not Per Instance
class Shipment
{
static int _sequence = 0;
public readonly int Number;
public Shipment() { Number = ++_sequence; }
}
var s1 = new Shipment();
var s2 = new Shipment();
var s3 = new Shipment();
Console.WriteLine(s3.Number); // 3 — every Shipment increments the one shared counter
A static field is owned by the type declaration itself, not by any particular object built from it — however many Shipments get constructed, there's only ever the one _sequence counting them. That's a completely separate question from value-vs-reference: this chapter is about who's holding the storage, not about what happens when you assign it to a new variable. Where the two topics keep bumping into each other is thread safety — because whatever a mutable static field points at, it's the same object being handed to every caller, on every thread, for as long as the process keeps running, and nothing about the static keyword itself protects that from being read and written at the same time.
// A real bug pattern: a mutable static field used as scratch space for a batch of adjustments,
// silently shared across concurrent requests hitting the same warehouse API
class AdjustmentBatcher
{
static List<StockCount> _pending = new(); // shared across every call, every thread
public List<StockCount> ApplyBatch(IEnumerable<StockCount> incoming)
{
_pending.Clear();
_pending.AddRange(incoming);
return _pending; // two concurrent requests can interleave and hand back each other's items
}
}
Two warehouse-floor devices hitting this endpoint at nearly the same instant can genuinely interleave: one request's Clear() wipes out items the other request already added, and either caller can walk away holding a list that's some scrambled mix of both batches — with no exception anywhere to point at what happened.
There are really only three ways out of this once you spot it: stop mutating the shared value at all, give each caller its own private copy instead of one shared one, or wrap access to it with a lock (or swap it for a collection built to handle concurrent access safely). And it's not only the literal static keyword that creates this exposure — a class registered with dependency injection as a Singleton (worth reading up on in Building REST APIs with ASP.NET Core if the lifetime terms are unfamiliar) gets exactly one instance for the entire application too, so any mutable field on it is under the identical threat, static or not.