C# records, with-expressions, and value semantics
Create immutable data types with value equality, copy them with changes, and compare with structs and classes.
On this page
C# classes and properties.
- Create immutable data types with value equality, copy them with changes, and compare with structs and classes.
- Predict the sample output and explain the state changes.
- Complete the practice task and check the boundary cases.
The concept
A record is a type designed for data. A positional record such as Point(int X, int Y) generates properties, a constructor, value-based equality, a readable ToString, and a Deconstruct method. Two records are equal when their fields are equal, even though they are different objects, unlike plain classes where == compares references by default.
Records are immutable by design. A with-expression makes a copy with some properties changed and leaves the original untouched. A struct is a value type: assigning it copies the whole value, while assigning a class instance copies the reference, so both variables point at the same object. Choose structs for small, short-lived values and classes or records otherwise.
Code example
using System;
record Point(int X, int Y);
struct Counter {
public int Value;
}
class Box {
public int Value;
}
class Program {
static void Main() {
var a = new Point(1, 2);
var b = new Point(1, 2);
Console.WriteLine(a == b);
Console.WriteLine(ReferenceEquals(a, b));
Console.WriteLine(a);
var c = a with { Y = 5 };
Console.WriteLine(c + " " + a);
var (x, y) = c;
Console.WriteLine(x + " " + y);
var s1 = new Counter { Value = 1 };
var s2 = s1;
s2.Value = 99;
var c1 = new Box { Value = 1 };
var c2 = c1;
c2.Value = 99;
Console.WriteLine("struct copy: " + s1.Value + ", class alias: " + c1.Value);
}
}True
False
Point { X = 1, Y = 2 }
Point { X = 1, Y = 5 } Point { X = 1, Y = 2 }
1 5
struct copy: 1, class alias: 99Follow the code
Open the walkthrough to inspect the example one step at a time.
Show guided walkthrough
using System; record Point(int X, int Y); struct Counter { public int Value;} class Box { public int Value;} class Program { static void Main() { var a = new Point(1, 2); var b = new Point(1, 2); Console.WriteLine(a == b); Console.WriteLine(ReferenceEquals(a, b)); Console.WriteLine(a); var c = a with { Y = 5 }; Console.WriteLine(c + " " + a); var (x, y) = c; Console.WriteLine(x + " " + y); var s1 = new Counter { Value = 1 }; var s2 = s1; s2.Value = 99; var c1 = new Box { Value = 1 }; var c2 = c1; c2.Value = 99; Console.WriteLine("struct copy: " + s1.Value + ", class alias: " + c1.Value); }}Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| One line declares the properties, constructor, equality, ToString, and deconstruction. | — | — | |
| Different objects, equal fields, so the records are equal. | — | True | |
| They are still two separate objects. | — | True False | |
| The generated ToString lists the properties. | — | True
False
Point { X = 1, Y = 2 } | |
| A copy with Y changed. The original record is untouched. | — | True
False
Point { X = 1, Y = 2 } | |
| c has Y = 5 and a still has Y = 2. | — | True
False
Point { X = 1, Y = 2 }
Point { X = 1, Y = 5 } Point { X = 1, Y = 2 } | |
| Deconstruct the record into two variables. | — | True
False
Point { X = 1, Y = 2 }
Point { X = 1, Y = 5 } Point { X = 1, Y = 2 } | |
| The two properties. | — | True
False
Point { X = 1, Y = 2 }
Point { X = 1, Y = 5 } Point { X = 1, Y = 2 }
1 5 | |
| s2 is a copy of the struct s1. Changing it does not touch s1. | — | True
False
Point { X = 1, Y = 2 }
Point { X = 1, Y = 5 } Point { X = 1, Y = 2 }
1 5 | |
| c2 refers to the same Box as c1, so c1 sees the change. | — | True
False
Point { X = 1, Y = 2 }
Point { X = 1, Y = 5 } Point { X = 1, Y = 2 }
1 5 | |
| The struct was copied, the class instance was shared. | — | True
False
Point { X = 1, Y = 2 }
Point { X = 1, Y = 5 } Point { X = 1, Y = 2 }
1 5
struct copy: 1, class alias: 99 |
Behind the scenes
How C# processes this code
C# compiles to code for the .NET runtime, which invokes the program entry point.
Cost and common mistakes
Equality compares each field, O(number of fields).
- A record is immutable only through its init-only properties. A record with a mutable field or collection can still change.
- Mutable structs are a source of bugs, because the copy that you modify is not the one you think it is.
- Record equality is shallow. If a property is a List, two records are equal only if they hold the same List object.
Try it yourself
What does a == c give?
Reveal answer and reasoning
False, because the Y values (2 and 5) differ.