Перейти к содержимому
Теория · Перечисления, записи и паттерны · урок 4 из 8

Выражение with

Свойства позиционной записи нельзя изменить после создания. Но данные в жизни меняются: сотрудник переезжает в другой кабинет, и пропуск должен показывать новый номер.

Старый пропуск при этом не исправляют ручкой, а печатают новый. Имя переносят со старой карточки, номер кабинета пишут новый. Старая карточка остаётся такой, какой была.

Для записей так работает выражение with. Оно создаёт новую запись — копию исходной, в которой заменены указанные свойства:

C#
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);

Давайте разберём программу по строкам:

  1. new Pass("Anna", 7) создаёт пропуск Анны с кабинетом 7.
  2. anna with { Room = 12 } копирует все свойства anna в новую запись и заменяет в ней Room на 12. Новая запись попадает в moved.
  3. Запись anna не изменилась: печать показывает Room = 7.
  4. moved — другая запись, с Room = 12, а имя Anna в неё скопировано.
  5. anna == moved даёт false: записи различаются в одном свойстве.

Из чего состоит with

Выражение anna with { Room = 12 } состоит из трёх частей:

  • слева — исходная запись, здесь anna;
  • слово with;
  • в фигурных скобках — свойства, которые нужно заменить, в виде Room = 12.

Несколько замен разделяют запятыми: anna with { Name = "Ann", Room = 3 }.

Свойства, которых нет в скобках, копируются как есть. Результат — значение, как у любого выражения: его сохраняют в переменную или сразу передают дальше. Исходная запись не меняется никогда.

Фигурные скобки после with работают как инициализатор объекта из темы о классах: они задают свойства новой копии, пока та создаётся. Поэтому with может задать даже init-свойство, а старую запись не трогает.

Кроме записей, with работает со структурами.

Копия поверхностная

На собеседовании часто спрашивают, насколько глубокую копию делает with. Ответ: копия поверхностная. Каждое свойство копируется так же, как при присваивании: число — числом, а ссылка — ссылкой.

C#
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);

Разберём по строкам:

  1. В a лежат имя и ссылка на список с одним элементом "tea". Тип конструкции ["tea"] здесь задаёт параметр Items: конструктор ждёт List<string>.
  2. with заменяет в копии имя, а из Items копирует только ссылку, не сам список.
  3. b.Items.Add("cake") добавляет элемент в этот единственный список.
  4. Поэтому a.Items.Count равно 2: запись a прежняя, но её список тоже изменился.
  5. a.Items == b.Items даёт true: оба свойства ведут к одному списку.

Здесь аналогия с пропуском требует оговорки: новая карточка напечатана целиком, но не всё, к чему она ведёт, копируется. Представьте, что на пропуске указан номер шкафчика. Новый пропуск получает тот же номер, а не новый шкафчик, и вещи в шкафчике у двух пропусков общие.

Если копии нужен свой список, передайте ей новый: a with { Items = new List<string>(a.Items) }. Код new List<string>(a.Items) создаёт отдельный список с теми же элементами.

Частая ошибка

with попробовали применить к объекту класса:

C#
var h = new House();
var g = h with { Floors = 3 }; // ✗

class House { public int Floors = 1; }
TEXT
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.