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

Записи record

Перечисление хранит одно значение из списка. Часто нужно другое: держать вместе несколько связанных значений и сравнивать их целиком.

Представьте пропуск в офис: на нём имя и номер кабинета. Охранник на входе сравнивает данные, а не саму карточку. Если вы потеряли пропуск и вам напечатали новый с теми же данными, для охранника это тот же пропуск.

Объект класса сравнивается иначе. В теме о значимых и ссылочных типах == для двух объектов класса с одинаковыми полями давал false. Он проверяет, одна ли это карточка, а не одинаковы ли на ней данные.

Для данных, которые сравнивают по содержимому, в C# есть записи — типы, объявленные словом record:

C#
var a = new Point(1, 2);
var b = new Point(1, 2);
Console.WriteLine(a == b); // True
Console.WriteLine(a);      // Point { X = 1, Y = 2 }

record Point(int X, int Y);

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

  1. record Point(int X, int Y); объявляет запись Point с двумя свойствами: X и Y. Всё объявление умещается в одну строку.
  2. new Point(1, 2) создаёт запись так же, как new создаёт объект класса: X получает 1, Y — 2.
  3. a == b даёт true: a и b — две разные записи, но данные у них одинаковые.
  4. Console.WriteLine(a) печатает имя типа и все данные: Point { X = 1, Y = 2 }.

Что даёт одна строка record

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

  • свойства X и Y: их можно прочитать через a.X, но нельзя изменить после создания;
  • конструктор с параметрами X и Y, поэтому работают и new Point(1, 2), и вызов с именованными аргументами new Point(X: 1, Y: 2);
  • сравнение по данным: ==, != и Equals сравнивают свойства по очереди;
  • метод ToString, который возвращает строку Point { X = 1, Y = 2 } для печати.

Запись и класс

Сравните запись с классом, который хранит те же данные:

C#
var c1 = new Spot(1, 2);
var c2 = new Spot(1, 2);
Console.WriteLine(c1 == c2); // False
Console.WriteLine(c1);       // Spot

class Spot
{
    public int X;
    public int Y;
    public Spot(int x, int y) { X = x; Y = y; }
}

Данные у c1 и c2 одинаковые, но == сравнивает ссылки, а c1 и c2 ведут к двум отдельным объектам. Печать показывает только имя класса: метод ToString класса по умолчанию не знает, что лежит в полях.

Чем record отличается от class, часто спрашивают на собеседовании. Короткий ответ:

  • запись сравнивается по данным, а объект класса — по ссылке;
  • позиционная запись сама получает свойства, конструктор и ToString, который возвращает строку со всеми данными;
  • свойства позиционной записи нельзя изменить после создания.

Здесь аналогия с пропуском перестаёт работать. Настоящий пропуск не может лежать в двух карманах сразу. А запись — ссылочный тип, как класс: после var b = a; два имени ведут к одной и той же записи. Для Point это безопасно: свойства позиционной записи нельзя изменить, и через b никто не заменит X или Y у a.

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

Свойство записи попробовали изменить:

C#
var a = new Point(1, 2);
a.X = 5; // ✗

record Point(int X, int Y);
TEXT
error CS8852: Init-only property or indexer 'Point.X' can only be assigned in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor.

Программа не компилируется. Свойства позиционной записи — init-свойства: значение им дают только при создании. Чтобы получить точку с X = 5, создайте новую запись: a = new Point(5, a.Y);.

Главное

  • record Point(int X, int Y); объявляет запись со свойствами X и Y, конструктором и методом ToString, который даёт Point { X = 1, Y = 2 }.
  • Записи сравниваются по данным: две записи с одинаковыми свойствами равны, и == даёт true.
  • Оператор == сравнивает объекты класса по ссылке, поэтому два объекта с одинаковыми полями не равны.
  • Свойства позиционной записи нельзя изменить после создания: a.X = 5; не компилируется.