Содержание
Начиная с версии C# 9 мы получили возможность использовать ещё один ссылочный тип данных — записи (record
). Для того, чтобы разобраться с тем, зачем нам потребовался ещё один ссылочный тип данных наряду с классами, стоит немного разобраться с тем, как производится сравнение различных типов данных.
Операции сравнения у ссылочных типов и типов значений
Если мы проверяем на равенство две переменных типа значений, например, два числа, то сравнение производится именно по значению (побитово). Например, следующая операция сравнения вернет true
:
int x = 10; double y = 10.0; Console.WriteLine(x == y); //true
несмотря на то, что переменные имеют различные типы данных, их значения равны. Это вполне очевидно и понятно даже человеку далекому от программирования. Другое дело — сравнение ссылочных типов. Здесь действует следующее правило: два ссылочных типа равны, если они ссылаются на один и тот же объект в памяти. Что это значит — рассмотрим на следующем примере:
A a = new A(); //выделили память под первый объект a.x = 10; A b = new A();//выделили память под второй объект b.x = 10; Console.WriteLine(a == b); public class A { public int x; }
здесь мы создаем два объекта типа A
и присваиваем полям x
одно и тоже значение — 10
. И далее мы сравниваем два объекта между собой, выводя результат сравнения в консоль. Несмотря на то, что с точки зрения бизнес-логики перед нами два абсолютно одинаковых объекта (поля x
одинаково), результат сравнения будет false
так как переменные ссылаются на два разных объекта в памяти.
Так вот, что касается типа record
, то первое его отличие от классов заключается в том, что два объекта record
равны, если они имеют одинаковый тип и хранят одинаковые значения. То есть, в первом приближении record
сочетает в себе как возможности ссылочных типов, так и типов значений. Теперь познакомимся с этим типом данных поближе.
Объявление record
Для объявления записи используется ключевое слово record
и изначально так как это ключевое слово применялось только классам, то слово class
можно опускать при определении записи. То есть следующие два варианта объявления полностью идентичны:
record class FirstRecord { public int x; public int y; } record SecontRecord { public int x; public int y; }
Теперь мы можем воспользоваться этими записями так же, как и любым други ссылочным типом, например
FirstRecord firstRecord = new FirstRecord() { X = 10, Y = 20 };
На этом шаге мы подходим к тому, что отличает запись (record
) от других ссылочных типов.
Сравнение record
Вначале вернемся к сравнению ссылочных типов данных и посмотрим на результаты следующих сравнений:
FirstRecord point_1 = new FirstRecord() { X = 10, Y = 20 }; FirstRecord point_2 = new FirstRecord() { X = 10, Y = 20 }; SecondRecord point_3 = new SecondRecord() { X = 10, Y = 20 }; Console.WriteLine(point_1.Equals(point_2)); Console.WriteLine(point_1.Equals(point_3)); record FirstRecord { public int X { get; set; } public int Y { get; set; } } record SecondRecord { public int X { get; set; } public int Y { get; set; } }
Здесь оба типа записей имеют один и тот же набор свойств, а три созданных объекта имею одинаковые значения этих свойств. Для сравнения объектов между собой мы использовали метод Equals()
, который, как мы знаем, по умолчанию сравнивает ссылки на объекты. Посмотрим на результат сравнения. Вот, что мы увидим в консоли:
False
Так как объекты point_1
и point_2
имеют одинаковый тип (FirstRecord
) и одинаковое значение свойств, то в консоли мы видим True
— объекты равны. В случае второго сравнения — мы передаем в Equals() объект другого типа, поэтому закономерно получаем результат сравнения False
.
Также, если две записи имеют одинаковый тип, то мы можем их сравнивать, используя обычный оператор проверки на равенство:
point_1 == point_2
Операция сравнения двух записей, помимо того, что показывает преимущества record по сравнению с обычными классами, также говорит нам о том, что в таких типах данных, как минимум, переопределен метод Equals()
. Рассмотрим, что ещё отличает record class
от обычного class
.
Стандартный набор конструкторов и методов record
Для каждого типа record
компилятором автоматически добавляются следующие возможности:
- наследование от интерфейса
IEquatable<T>
и его реализацию (для проверки объектов на равенство); - параметризированный конструктор для инициализации всех свойств записи (при использовании позиционного
record
); - операции сравнения
==
и!=
; - методы
Equals()
иGetHashCode()
; - метод
ToString()
, который возвращает имя типа записи и содержимое записи в формате JSON; - деконструктор объекта;
- специальные методы и свойства, доступные только компилятору.
Например, если бы мы вместо record
в примере выше захотели использовать обычные классы, то нам пришлось бы создать примерно такой класс:
class Point : IEquatable<Point> { public bool Equals(Point? other) { //реализация своего метода } public override bool Equals(object? obj) { //реализация своего метода } public override string ToString() { //реализация своего метода } public override int GetHashCode() { //реализация своего метода } // Операторы сравнения public static bool operator ==(Point a, Point b) => a.Equals(b); public static bool operator !=(Point a, Point b) => !a.Equals(b); }
Это только часть того, что бы нам пришлось добавлять в наш класс самостоятельно. То есть, использование record
значительно облегчает нам работу, если нам требуется создать тип данных, объекты которого будут сравниваться между собой.
Позиционный record
Кроме уже привычной формы определения класса, мы можем использовать следующую форму определения record
:
record FirstRecord(int X, int Y);
Такие записи ещё называют позиционными. В этом случае компилятор создаст для такого типа конструктор с параметрами и деконструктор. Теперь создание нового объекта будем выглядеть следующим образом:
FirstRecord point_1 = new FirstRecord(10, 20); FirstRecord point_2 = new(10, 20);
При необходимости, мы можем совместить определение записи и сделать одни свойства позиционными, а другие — определить как обычно:
record FirstRecord(int X, int Y) { public int Z { get; set; } }
Значение свойству, которое определяется позиционно может быть присвоено только в инициализаторе объекта, в свойствах this
или base
в конструкторе экземпляра или в методе доступа init
. То есть, мы можем сделать вот такое присваивание значений свойствам, используя конструктор:
FirstRecord point_1 = new FirstRecord(10, 20);
однако, такой код даже не скомпилируется:
FirstRecord point_2 = new FirstRecord(10, 20); point_1.X = 100;
При этом, другие свойства record
мы можем спокойно изменять — здесь действуют те же правила, что и для обычных классов:
FirstRecord point_2 = new FirstRecord(10, 20); point_1.Z = 100; //значение этого свойства мы можем поменять
Позиционные структуры record
В C# 10 появилась возможность создавать структуры record
, то есть типы record struct
. В отличие от классов, для таких типов мы должны обязательно указывать ключевое слово struct
. Отличие record-структуры от record-класса заключается в том, что у структуры мы можем изменять позиционные свойства. Например, следующий код будет скомпилирован:
RecordStruct point_1 = new RecordStruct(10, 20); point_1.X = 50; point_1.Z = 100;
Для свойств record-структур, задаваемых позиционно, компилятор генерирует обычные методы чтения/записи. Чтобы запретить изменение позиционных свойств, нам необходимо использовать при определении такой структуры ключевое слово readonly
:
RecordStruct point_1 = new RecordStruct(10, 20); point_1.X = 50; //ошибка point_1.Z = 100;//ошибка readonly record struct RecordStruct(int X, int Y) { public int Z { get; init; } }
Соответственно, для readonly-структур все свойства должны быть только для чтения и значения таких свойств должны устанавливаться в конструкторе или методах this
, base
и init
.
Итого
Начиная с C# 9 появилась возможность создания нового ссылочного типа — записи (record
). Такие типы данных позволяют сравнивать объекты между собой не по ссылкам (как обычно), а по значениям свойств для чего при определении record
компилятор автоматически добавляет необходимые методы и переопределяет операторы равенства в типе. Так же, начиная с C# 10 мы получили возможность создания record-структур, включая структуры только для чтения.