Records & Pattern Matching
Records and value-based equality, positional records, pattern matching forms, switch expressions, init-only and required members, primary constructors.
12 questions
JuniorTheoryVery commonWhat does a with expression do on a record, and how deep is the copy?
What does a with expression do on a record, and how deep is the copy?
A with expression produces a new instance — a shallow copy of the source with the listed members replaced; the source itself is untouched. The compiler calls a synthesized copy constructor, so objects the record references are shared, not cloned.
Common mistakes
- ✗Thinking
withmutates the original instead of returning a new instance - ✗Expecting a deep copy — objects referenced by the record are shared with the copy
- ✗Assuming members you omit are reset to their defaults rather than carried over
Follow-up questions
- →Why can a
withexpression assigninit-only properties that are otherwise frozen? - →Can you customize what a
withcopy does by writing the copy constructor yourself?
MiddleTheoryVery commonHow does a record differ from a class when you compare two instances?
How does a record differ from a class when you compare two instances?
A record is a reference type, but the compiler synthesizes Equals, GetHashCode and == comparing its type and every field, so two records with equal contents are equal. A plain class keeps reference equality: two instances are never equal.
Common mistakes
- ✗Thinking a
recordis a value type — arecord classis a reference type - ✗Expecting record equality to walk the object graph; a
List<T>member still compares by reference - ✗Assuming
==on a record still compares references
Follow-up questions
- →Why does a record's synthesized equality compare the type as well as the fields?
- →What happens to a record's value equality when one of its members is an array?
JuniorTheoryCommonWhat does the is type pattern test, and how does it treat null?
What does the is type pattern test, and how does it treat null?
The is type pattern checks a value's runtime type and, on a match, assigns it to a new typed variable — if (o is string s) — replacing a test-then-cast pair. null never matches a type pattern, so a match also guarantees the value is not null.
Common mistakes
- ✗Thinking
nullmatches a type pattern — it never does - ✗Believing
ischecks the static type rather than the runtime type - ✗Forgetting the declared variable is definitely assigned only where the pattern matched
Follow-up questions
- →How does the declared variable's scope differ between
o is string sand a negatedis nottest? - →How does a property pattern build on the type pattern to look inside the matched value?
MiddleTheoryCommonWhat does a positional record give you that a record with regular properties does not?
What does a positional record give you that a record with regular properties does not?
A positional record Point(int X, int Y) synthesizes public init-only properties, the matching constructor and a Deconstruct, so it fits positional patterns. Hand-written properties give none of that, but let you pick accessibility or required.
Common mistakes
- ✗Thinking positional parameters become fields rather than init-only properties
- ✗Believing hand-written record properties lose value equality — every record has it
- ✗Expecting a
Deconstructon a record whose properties were written by hand
Follow-up questions
- →How do you keep the positional syntax but change one property's accessibility or add validation?
- →Why does a hand-written record property not work in a positional pattern?
MiddleTheoryCommonHow does a switch expression differ from a switch statement?
How does a switch expression differ from a switch statement?
A switch expression produces a value: each arm is pattern => expression, with no case, break or fall-through, so it can be assigned or returned. A switch statement runs statements. An unmatched value throws SwitchExpressionException.
Common mistakes
- ✗Thinking an unmatched
switchexpression falls back todefaultinstead of throwing - ✗Expecting
breakor fall-through — arms are expressions, not statements - ✗Believing patterns and
whenguards work only in the statement form
Follow-up questions
- →What does the compiler report when a
switchexpression is not exhaustive? - →When is a
switchstatement still the better choice?
JuniorTheoryOccasionalHow does an init-only property setter differ from a readonly field?
How does an init-only property setter differ from a readonly field?
An init accessor allows assignment only while the object is initialized — constructor, object initializer, or with expression. A readonly field is assignable only in its declaration or a constructor, so an object initializer can never touch it.
Common mistakes
- ✗Thinking
initfreezes the object at runtime, when the compiler enforces it at compile time - ✗Believing an object initializer can assign a
readonlyfield the way it assigns aninitproperty - ✗Assuming
initmakes the property's contents deeply immutable, not just its assignment
Follow-up questions
- →How does a
withexpression manage to assigninit-only properties on the copy it makes? - →Does
initmake a property that holds a mutableList<T>actually immutable?
MiddleTheoryOccasionalWhat do type, property, relational and list patterns each match?
What do type, property, relational and list patterns each match?
A type pattern tests the runtime type; a property pattern inspects members ({ Age: > 18 }); a positional one deconstructs it; a relational one compares to a constant, with and/or; a list pattern matches a sequence's shape ([1, .., 9]).
Common mistakes
- ✗Thinking patterns are matched by run-time reflection rather than compiled type and member tests
- ✗Not knowing a list pattern supports a slice
..and nested patterns inside it - ✗Believing patterns cannot be combined with
and,orandnot
Follow-up questions
- →How does a positional pattern find the
Deconstructmethod it needs? - →When does the compiler consider a set of patterns exhaustive?
MiddleTheoryOccasionalWhat does a primary constructor on a non-record class actually declare?
What does a primary constructor on a non-record class actually declare?
On a class the primary-constructor parameters are constructor parameters captured into scope for the whole body — usable in field initializers and members. They do NOT become public properties; only a positional record does that.
Common mistakes
- ✗Expecting a class's primary-constructor parameters to become public properties, as on a record
- ✗Thinking primary constructors are available only on records
- ✗Assuming every parameter is captured into a field even when no member uses it
Follow-up questions
- →When does the compiler actually capture a primary-constructor parameter into a field?
- →How do you validate a primary-constructor argument before it is stored?
MiddleTheoryOccasionalWhat does required on a member enforce, and who performs that check?
What does required on a member enforce, and who performs that check?
required forces callers to assign the member in the object initializer, or it will not compile — the compiler checks, not the runtime. It is orthogonal to init: the member may stay mutable. A constructor setting it needs [SetsRequiredMembers].
Common mistakes
- ✗Thinking
requiredimpliesinit— arequiredmember can stay mutable - ✗Expecting a run-time exception; the enforcement is a compile error at the call site
- ✗Forgetting that a constructor which already sets the member needs
[SetsRequiredMembers]
Follow-up questions
- →How does
requiredbehave when a derived type adds or hides members? - →Why can reflection or a deserializer still leave a
requiredmember unset?
SeniorTheoryOccasionalWhy is a base record never equal to a derived record with identical field values?
Why is a base record never equal to a derived record with identical field values?
A record's synthesized equality compares a hidden EqualityContract property — the runtime type — before the fields. The derived record overrides it, so the contracts differ and equality fails both ways. Comparing the type keeps == symmetric.
Common mistakes
- ✗Thinking record equality compares only fields and ignores the runtime type
- ✗Assuming a base-typed comparison of a derived instance can succeed
- ✗Believing derived records fall back to reference equality
Follow-up questions
- →How would you make a base record accept a derived instance as equal, and what breaks then?
- →What does
EqualityContractlook like on a sealed record?
SeniorTheoryRareHow do required members coexist with a constructor that already sets them?
How do required members coexist with a constructor that already sets them?
A constructor does not satisfy required by default: callers still assign the members in an initializer. [SetsRequiredMembers] says it sets them all, lifting that. A class's primary constructor does not — its parameters are not properties.
Common mistakes
- ✗Thinking a constructor that assigns a
requiredmember exempts callers without[SetsRequiredMembers] - ✗Expecting a class's primary-constructor parameters to satisfy
requiredmembers - ✗Applying
[SetsRequiredMembers]to a constructor that does not actually set every one of them
Follow-up questions
- →What goes wrong when
[SetsRequiredMembers]is put on a constructor that misses one member? - →How does a derived type's constructor inherit the required-member obligation from its base?
SeniorTheoryRareWhat makes a switch expression exhaustive, and how do null and unmatched values behave?
What makes a switch expression exhaustive, and how do null and unmatched values behave?
The compiler proves coverage from the arms' patterns; when it cannot, it warns (CS8509) and an unmatched value throws SwitchExpressionException. A type pattern never matches null, so a maybe-null input needs a null arm or a _ arm.
Common mistakes
- ✗Expecting a non-exhaustive expression to return
defaultinstead of throwing - ✗Assuming an
enumis exhaustive once every declared member has an arm — a cast can produce any value - ✗Thinking a type pattern matches
null, so no explicitnullarm is needed
Follow-up questions
- →Why can an
enumvariable hold a value that no declared member names? - →How does adding a
_arm change what the compiler can tell you when a new case appears?