Выражение with
Свойства позиционной записи нельзя изменить после создания. Но данные в жизни меняются: сотрудник переезжает в другой кабинет, и пропуск должен показывать новый номер.
Старый пропуск при этом не исправляют ручкой, а печатают новый. Имя переносят со старой карточки, номер кабинета пишут новый. Старая карточка остаётся такой, какой была.
Для записей так работает выражение with. Оно создаёт новую запись — копию исходной, в которой заменены указанные свойства:
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);Давайте разберём программу по строкам:
new Pass("Anna", 7)создаёт пропуск Анны с кабинетом7.anna with { Room = 12 }копирует все свойстваannaв новую запись и заменяет в нейRoomна12. Новая запись попадает вmoved.- Запись
annaне изменилась: печать показываетRoom = 7. moved— другая запись, сRoom = 12, а имяAnnaв неё скопировано.anna == movedдаётfalse: записи различаются в одном свойстве.
Из чего состоит with
Выражение anna with { Room = 12 } состоит из трёх частей:
- слева — исходная запись, здесь
anna; - слово
with; - в фигурных скобках — свойства, которые нужно заменить, в виде
Room = 12.
Несколько замен разделяют запятыми: anna with { Name = "Ann", Room = 3 }.
Свойства, которых нет в скобках, копируются как есть. Результат — значение, как у любого выражения: его сохраняют в переменную или сразу передают дальше. Исходная запись не меняется никогда.
Фигурные скобки после with работают как инициализатор объекта из темы о классах: они задают свойства новой копии, пока та создаётся. Поэтому with может задать даже init-свойство, а старую запись не трогает.
Кроме записей, with работает со структурами.
Копия поверхностная
На собеседовании часто спрашивают, насколько глубокую копию делает with. Ответ: копия поверхностная. Каждое свойство копируется так же, как при присваивании: число — числом, а ссылка — ссылкой.
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);Разберём по строкам:
- В
aлежат имя и ссылка на список с одним элементом"tea". Тип конструкции["tea"]здесь задаёт параметрItems: конструктор ждётList<string>. withзаменяет в копии имя, а изItemsкопирует только ссылку, не сам список.b.Items.Add("cake")добавляет элемент в этот единственный список.- Поэтому
a.Items.Countравно2: записьaпрежняя, но её список тоже изменился. a.Items == b.Itemsдаётtrue: оба свойства ведут к одному списку.
Здесь аналогия с пропуском требует оговорки: новая карточка напечатана целиком, но не всё, к чему она ведёт, копируется. Представьте, что на пропуске указан номер шкафчика. Новый пропуск получает тот же номер, а не новый шкафчик, и вещи в шкафчике у двух пропусков общие.
Если копии нужен свой список, передайте ей новый: a with { Items = new List<string>(a.Items) }. Код new List<string>(a.Items) создаёт отдельный список с теми же элементами.
Частая ошибка
with попробовали применить к объекту класса:
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.Программа не компилируется: with нельзя применить к объекту класса. Для записи компилятор сам пишет скрытый метод, который делает копию, а у класса такого метода нет. Если объекты вашего типа копируют и сравнивают по данным, объявите его как record, а не class.
Главное
r with { Room = 12 }создаёт новую запись: копиюr, в которой заменены перечисленные свойства.- Исходная запись не меняется, а свойства, не названные в скобках, копируются как есть.
- Копия поверхностная: свойство-ссылка копируется как ссылка, и две записи делят один список.
withработает с записями и структурами, но не с классами:CS8858.