Records and Pattern Matching
C# classes were built for objects with behavior: they have identity, and two distinct objects with identical contents are still two distinct objects. But a huge share of code describes not objects but data: DTOs, events, results, keys. For those, contents matter and identity does not. That is exactly what records provide: a record is still a reference type, but the compiler synthesizes value-based equality, a copy constructor for with, and a Deconstruct for the positional form.
The other half of the topic is pattern matching. It grew from a bare is test into a full language for describing the shape of data: type, property, positional, relational and list patterns, combinable with and / or / not, plus the switch expression that returns a value instead of running statements. The two halves work together: a record's Deconstruct is exactly what a positional pattern latches onto. The traps here are subtle and beloved by interviewers: record equality compares the type (the hidden EqualityContract) before the fields; with copies shallowly; init is a compiler contract, not a run-time freeze; a class's primary-constructor parameters are not properties; a non-exhaustive switch expression throws rather than returning default. The layer-by-layer breakdown is below.
Topic map
- Records —
recordas a reference type with synthesized value equality,EqualityContract,with, and the positional form. - Init-only properties — the assignment window limited to initialization, and how
initdiffers from areadonlyfield. - Required members —
requiredforces the caller to set the member, and the compiler is what checks it. - Primary constructors — on a record the parameters become properties; on a plain
classthey do not. - Pattern matching — type, property, positional, relational and list patterns, and what they compile to.
- Switch expressions —
pattern => value, exhaustiveness,null, andSwitchExpressionException.
Common Mistakes and Traps
| Mistake | Consequence |
|---|---|
Treating a record as a value type | A record (i.e. a record class) is a heap reference type; only a record struct is a value type |
| Expecting record equality to walk the object graph | The comparison is member-wise: a List<T> or array member is compared by reference, not by contents |
Not knowing about EqualityContract | A base and a derived record with identical fields are never equal — not a bug, but what keeps == symmetric |
Expecting a deep copy from with | The copy is shallow: objects the record references are shared with the original |
Thinking init freezes the object at run time | The compiler enforces the restriction; the contents of a List<T> behind an init property remain mutable |
Thinking an object initializer can assign a readonly field | It cannot: a readonly field is written only in its declaration or a constructor |
Thinking required implies init | They are orthogonal: a required member may stay fully mutable |
Expecting a run-time exception from required | It is a compile error at the call site; a constructor that sets them itself needs [SetsRequiredMembers] |
Expecting a class's primary-constructor parameters to become public properties | They do not — they are parameters captured into scope; only a positional record makes properties |
Thinking null matches a type pattern | It never does: o is string s on null is false, which is why a match guarantees a non-null value |
| Thinking patterns work through reflection | The compiler builds a tree of ordinary type tests and member reads — there is no run-time reflection |
Expecting default from a non-exhaustive switch expression | An uncovered value throws SwitchExpressionException; the compiler warns about it (CS8509) |
Considering an enum exhausted once every declared member has an arm | A cast such as (Color)99 produces a value no member names — you need a _ arm |
Interview relevance
Records and patterns are a mandatory block on any modern C# interview, and the questions are almost always about the compiler's mechanics: what exactly it synthesizes and what happens to that at run time. A candidate who answers "a record is a class with auto-generated Equals, GetHashCode, a copy constructor and an EqualityContract" is already speaking the interviewer's language.
Typical checks:
- How a
recorddiffers from aclasswhen two instances are compared, and why. - Why
EqualityContracttakes part in equality, and what follows from it for record inheritance. - What
withdoes, and how deep the copy is. - What the positional form
record Point(int X, int Y)synthesizes. - How
initdiffers from areadonlyfield, and who enforces the restriction. - What
requiredactually forces, and why a constructor needs[SetsRequiredMembers]. - What a primary constructor declares on a non-record class.
- Which pattern forms exist, and how they behave with
null. - What a non-exhaustive
switchexpression does with an uncovered value.
Common wrong answer: "A class's primary constructor creates public properties from its parameters." It does not. On a plain class those are constructor parameters captured into scope for the whole body: visible in field initializers and method bodies, but no obj.Name appears from the outside. Only a positional record turns parameters into properties — and that difference is precisely how they check whether you understand that records and classes were given one syntactic form with two different semantics.