record is a keyword that creates “the value itself” data type in one line. By combining modern features such as init only setter, value equality, and with expressions, the creation of DTO, event, and result objects is drastically reduced.
Learning Objectives
- Know the automatically created members (constructor, init property, Equals, ToString, Deconstruct) of
record - Know the difference between
record classandrecord struct - Perform non-destructive mutation in the
withmanner. - Know the meaning and use of
initonly setter - Know what value equality is and why it is useful
Key concepts
record Person(string Name, int Age);
This one line automatically creates:
public Person(string name, int age)Constructorpublic string Name { get; init; },public int Age { get; init; }properties (both init only)Equals,GetHashCode— Value basedToString—Person { Name = Alice, Age = 30 }format==,!=operators — compare valuesDeconstruct—var (n, a) = p;
2) record class vs record struct
record class (default) |
record struct |
|
|---|---|---|
| Category | reference type (heap) | value type (Stack/Inline) |
| Copy | Copy Reference | Copy Value |
| Equivalence | automatic value equality | automatic value equality |
| size | Large object OK | Small data (16-24 B) recommended |
| Yes | DTO, message | Point, Vector |
record struct is usually declared as readonly record struct to enforce immutability.
record class Person(string Name, int Age);
readonly record struct Point(int X, int Y);
with creates “a new instance that leaves the original as is but changes only some properties.”
Person p1 = new("Alice", 30);
Person p2 = p1 with { Age = 31 }; //p1 remains the same, p2 is a new object
record uses the clone method (<Clone>$) automatically generated by the compiler. with cannot be used on regular class.
4) init-only setters
class Person
{
public string Name { get; init; } = "";
}
- Assignment possible only at the time of object initialization (
new Person { Name = "Alice" }) - readonly after object is created
- Can be used in general classes other than records
Equals/GetHashCode of record are automatically value-based, so they can be used as is with the HashSet<Person> / Dictionary<Person, …> keys.
HashSet<Coord> set = [new(1,2), new(1,2)]; // Count == 1
If it was class, the two objects are different references, so both are entered.
See as example
Person p1 = new("Alice", 30);
Person p2 = new("Alice", 30);
Console.WriteLine($"p1 == p2 ? {p1 == p2}"); // True
Console.WriteLine(p1); //ToString Automatic
var (name, age) = p1; //Disassembly Automatic
Execution result
p1 == p2 ? True
p1 == p3 ? False
Person { Name = Alice, Age = 30 }
name=Alice, age=30
Note: A one-line declaration creates five automatic members. Creating DTO/message objects becomes really short.
Person original = new("Alice", 30, "Seoul");
Person aged = original with { Age = 31 };
Person moved = original with { City = "Busan", Age = 31 };
Execution result
original: Person { Name = Alice, Age = 30, City = Seoul }
aged : Person { Name = Alice, Age = 31, City = Seoul }
moved : Person { Name = Alice, Age = 31, City = Busan }
original is still 30 years old, Seoul
Note: Functional style "change state = create new object" pattern. It especially shines in concurrent code and LINQ pipelines.
internal sealed record PointClass(int X, int Y);
internal readonly record struct PointStruct(int X, int Y);
Console.WriteLine($"record class: c1 == c2 ? {c1 == c2} (reference? {ReferenceEquals(c1, c2)})");
Console.WriteLine($"record struct: s1 == s2 ? {s1 == s2}");
Execution result
record class: c1 == c2 ? True (reference? False)
record struct: s1 == s2 ? True
s1 = PointStruct { X = 1, Y = 2 }
shifted = PointStruct { X = 99, Y = 2 }, s1 = PointStruct { X = 1, Y = 2 }
Note: Both have value equality, so the result of == is the same. The difference is memory placement — struct is stored entirely in stack/inline, making it suitable for small data.
internal sealed class Person
{
public string Name { get; init; } = "";
public int Age { get; init; }
}
Person p = new() { Name = "Alice", Age = 30 };
//p.Name = "Bob"; // Compilation error — init cannot be changed after object initialization
Execution result
Alice, 30 years old
C# (320p)
Note: init is not a dedicated function of record. It can also be used in general classes to make them “immutable after creation.” When combined with required, "must specify" is also enforced.
internal sealed record Coord(int X, int Y);
Coord a = new(1, 2);
Coord b = new(1, 2);
HashSet<Coord> set = [a, b, new(3, 4)]; //a and b are the same, so remove duplicates
Execution result
a.Equals(b) = True
set.Count = 2
ToString: Coord { X = 1, Y = 2 }
a == b : True
a == c : False
Memo: It works correctly even if you use the Dictionary<Coord, ...> key. If it was class, you would have had to directly override Equals/GetHashCode.
Common Mistakes
- I forgot that the default record property is
initonly, and tried to change it top.Name = ...after creating it — compilation error. - Try the
withexpression on regularclass— for record only. - Declaring
record structwithoutreadonly— mutable struct causes subtle bugs. Basicallyreadonly record structrecommended. - Large objects to
record struct— It is actually slower because the entire object is copied each time. Usually only 16 bytes or less in struct. - Leave the mutable collection (
List<T>) inside the record — the list inside is still mutable. If you really need immutability, exposeIReadOnlyList<T>.
Cleanup
- Constructor/Properties/Equals/ToString/Deconstruct is automatically created with one line of
record - Thanks to value equality, it can be used as a HashSet/Dictionary key.
- Non-destructive copy as
with— suitable for functional style initis not dedicated to records but can also be used in general classes.readonly record structfor small, immutable value sets,record classfor others.
Try it yourself
cd src/RecordBasics && dotnet run
cd ../WithExpression && dotnet run
cd ../RecordVsClass && dotnet run
cd ../InitOnly && dotnet run
cd ../ValueEquality && dotnet run