Learning outcome
By the end of this lesson, you should be able to explain why records exist, when they are a good fit, and how the compiler changes their behavior compared with ordinary classes and structs. You should also be able to answer interview questions about equality, hashing, immutability, inheritance, and with expressions without relying on memorized slogans.
Intuition
A record is a type for data. The key promise is that two instances with the same meaningful data should often be considered equal. That is different from a plain class, where equality usually means “same object in memory.” Records also make copying safer and more convenient by supporting nondestructive mutation through with expressions.
Think of a record as a type where the data matters more than object identity. That makes records a natural fit for DTOs, messages, configuration snapshots, and small domain values. It is not a universal replacement for classes. If identity is important, such as in ORM-tracked entities, a record may be the wrong choice.
Deep dive
C# records come in two flavors: record class and record struct. The keyword record by itself means record class, which is a reference type. record struct is a value type. The underlying kind still matters: reference types copy references, while value types copy data.
Records add compiler-generated members that make data modeling easier:
For a record class, the compiler generates value-based Equals, GetHashCode, ==, !=, and a readable ToString. For a record struct, the same value-based equality exists, but the type is still a value type. That means assignment copies the data, not a shared object reference.
The interview trap is assuming all records are immutable. They are not automatically immutable. Record class positional properties are init-only by default, which encourages immutability, but you can still add mutable members if you want. Record structs are mutable by default unless you declare them readonly.
with expressions are another major record feature. They create a copy of an existing record and apply selected property changes to the copy. The original value remains unchanged. This is called nondestructive mutation.
The important detail is that with does not deep clone everything. If a record contains a reference like an array or a list, the reference is copied, not the contents. That means two records can still share mutable inner objects.
Value equality is property-by-property. Two distinct record instances can be equal if their declared values match. But equality is only as deep as the values inside the record. If one property is a reference type, the equality of that property depends on that property’s own equality semantics.
Failure modes
Common mistakes interviewers like to probe:
- Assuming a record class and a plain class behave the same for equality. They do not.
- Assuming
withmutates the original object. It creates a new one. - Assuming record equality performs a deep comparison of all nested objects. It usually compares property values, and nested reference members may still be compared by reference unless they override equality.
- Using records for entities where identity is the point. That often causes confusing equality behavior in tracking and caching scenarios.
- Forgetting that mutable members inside a record can break hash-based collections if values change after insertion.
- Treating
record structas “just a class with nicer syntax.” It has value semantics, so copies behave differently.
Interview drill
- What problem do records solve?
- Model data with built-in value equality, concise syntax, and useful copy semantics.
- Is
recorda class or a struct?
recordalone meansrecord class, which is a reference type.record structis a value type.
- How is equality different between a plain class and a record class?
- A plain class usually uses reference equality. A record class uses value equality generated by the compiler.
- Are record structs immutable?
- No. They are value types, but not immutable by default. Use
readonly record structif you want immutability.
- What does
withdo?
- It creates a copy of an existing record and applies specified changes to the copy.
- Does
withdeep clone nested reference objects?
- No. Nested references are typically copied as references.
- Why are records useful with dictionaries or hash sets?
- Their generated
GetHashCodeand equality members make them suitable as keys when their values are stable.
- When should you avoid records?
- When identity matters more than value, such as tracked entities or objects with lifecycle and mutation-heavy behavior.
- Can records participate in inheritance?
- Yes, especially
record classtypes. That is one reason to prefer them over record structs for polymorphic models.
- What is the biggest pitfall in an interview answer?
- Saying “records are immutable classes.” That is incomplete and sometimes false.
Revision checklist
- Know the difference between reference equality and value equality.
- Know that
recordmeansrecord classunlessstructis specified. - Know that record classes generate equality members and a helpful
ToString. - Know that
withcreates a copy, not a mutation. - Know that nested mutable members are not automatically deep-cloned.
- Know when records are a good fit and when ordinary classes are better.
Production code
The example below shows a practical record for an immutable-ish money value, value equality, reference comparison, and a with expression that creates a discounted copy.
Code walkthrough
Money is declared as a positional record, so the compiler creates constructor parameters and properties for Amount and Currency. Because it is a record class, two separate instances with the same values compare equal.
price1 == price2 prints True because records compare values, not object identity. ReferenceEquals(price1, price2) prints False because they are still different heap objects. The with expression produces discounted by copying price1 and replacing only Amount.
This is the core interview takeaway: records give you value semantics on top of the underlying type kind. They are not magic immutability, and they are not a replacement for every class. They are a tool for data-centric design where equality should mean “same content.”
<!-- tin:verified-code:start -->
Executable code examples
Records, value equality, and with-expressions
Program.cs
using System;
var price1 = new Money(25.00m, "USD");
var price2 = new Money(25.00m, "USD");
var discounted = price1 with { Amount = 20.00m };
Console.WriteLine(price1 == price2);
Console.WriteLine(ReferenceEquals(price1, price2));
Console.WriteLine(price1);
Console.WriteLine(discounted);
public record Money(decimal Amount, string Currency);
<!-- tin:verified-code:end -->