Записи record
Перечисление хранит одно значение из списка. Часто нужно другое: держать вместе несколько связанных значений и сравнивать их целиком.
Представьте пропуск в офис: на нём имя и номер кабинета. Охранник на входе сравнивает данные, а не саму карточку. Если вы потеряли пропуск и вам напечатали новый с теми же данными, для охранника это тот же пропуск.
Объект класса сравнивается иначе. В теме о значимых и ссылочных типах == для двух объектов класса с одинаковыми полями давал false. Он проверяет, одна ли это карточка, а не одинаковы ли на ней данные.
Для данных, которые сравнивают по содержимому, в C# есть записи — типы, объявленные словом record:
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);Давайте разберём программу по строкам:
record Point(int X, int Y);объявляет записьPointс двумя свойствами:XиY. Всё объявление умещается в одну строку.new Point(1, 2)создаёт запись так же, какnewсоздаёт объект класса:Xполучает1,Y—2.a == bдаётtrue:aиb— две разные записи, но данные у них одинаковые.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 }для печати.
Запись и класс
Сравните запись с классом, который хранит те же данные:
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.
Частая ошибка
Свойство записи попробовали изменить:
var a = new Point(1, 2);
a.X = 5; // ✗
record Point(int X, int Y);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;не компилируется.