The with expression
The properties of a positional record cannot be changed after creation. But data in real life does change: an employee moves to another room, and the pass must show the new number.
Nobody corrects the old pass with a pen; a new one is printed. The name is carried over from the old card, and the new room number is written in. The old card stays as it was.
For records, the with expression works this way. It creates a new record—a copy of the original with the listed properties replaced:
var anna = new Pass("Anna", 7);
var moved = anna with { Room = 12 };
Console.WriteLine(anna); // Pass { Name = Anna, Room = 7 }
Console.WriteLine(moved); // Pass { Name = Anna, Room = 12 }
Console.WriteLine(anna == moved); // False
record Pass(string Name, int Room);Let's take the program apart line by line:
new Pass("Anna", 7)creates Anna's pass with room7.anna with { Room = 12 }copies all the properties ofannainto a new record and replacesRoomthere with12. The new record goes intomoved.- The record
annahas not changed: printing showsRoom = 7. movedis a different record, withRoom = 12, and the nameAnnacopied into it.anna == movedgivesfalse: the records differ in one property.
The parts of with
The expression anna with { Room = 12 } has three parts:
- on the left, the original record, here
anna; - the word
with; - in braces, the properties to replace, written as
Room = 12.
Several replacements are separated by commas: anna with { Name = "Ann", Room = 3 }.
Properties that are not in the braces are copied as they are. The result is a value, like that of any expression: you store it in a variable or pass it on right away. The original record never changes.
The braces after with work like an object initializer from the theme on classes: they set the properties of the new copy while it is being created. That is why with can set even an init property, and it leaves the old record alone.
Besides records, with works with structs.
The copy is shallow
Interviewers often ask how deep a copy with makes. The answer: the copy is shallow. Each property is copied the same way as in an assignment: a number as a number, and a reference as a reference.
var a = new Order("Anna", ["tea"]);
var b = a with { Name = "Bob" };
b.Items.Add("cake");
Console.WriteLine(a.Items.Count); // 2
Console.WriteLine(a.Items == b.Items); // True
record Order(string Name, List<string> Items);Let's go line by line:
aholds a name and a reference to a list with one element,"tea". Here the parameterItemsgives["tea"]its type: the constructor expects aList<string>.withreplaces the name in the copy, and fromItemsit copies only the reference, not the list itself.b.Items.Add("cake")adds an element to that single list.- That is why
a.Items.Countis2: the recordais the same, but its list changed too. a.Items == b.Itemsgivestrue: both properties lead to one list.
The pass analogy needs a caveat here: the new card is printed in full, but not everything it leads to is copied. Imagine the pass carries a locker number. The new pass gets the same number, not a new locker, and the things in the locker are shared by both passes.
If the copy needs a list of its own, give it a new one: a with { Items = new List<string>(a.Items) }. The code new List<string>(a.Items) creates a separate list with the same elements.
Common mistake
Someone tried to apply with to a class object:
var h = new House();
var g = h with { Floors = 3 }; // ✗
class House { public int Floors = 1; }error CS8858: The receiver type 'House' is not a valid record type and is not a struct type.The program doesn't compile: you cannot apply with to a class object. For a record, the compiler writes a hidden method that makes the copy, and a class has no such method. If objects of your type are copied and compared by their data, declare it as a record, not a class.
Key points
r with { Room = 12 }creates a new record: a copy ofrwith the listed properties replaced.- The original record doesn't change, and properties not named in the braces are copied as they are.
- The copy is shallow: a reference property is copied as a reference, and two records share one list.
withworks with records and structs, but not with classes:CS8858.