C# 9 – Записи (record). Особенности использования

C# logoВ C# 9 был добавлен новый ссылочный тип – записи (record), основные свойства которого были рассмотрены в прошлой части. А теперь речь пойдет об особенностях его использования.

Неизменяемость (immutability)

Записи по умолчанию рассматриваются как неизменяемые объекты. Однако, важно понимать, что в данном случае обеспечивается неизменяемость только свойств самой записи. Например, значение свойства типа string изменить невозможно, так как сам тип неизменяемый. При этом также нельзя задать новый экземпляр коллекции для свойства типа ICollection<T>, но можно добавлять, удалять и изменять элементы существующей коллекции.

Таким образом, полностью неизменяемой записью будет являться запись, у которой все её свойства также неизменяемых типов (значимого, строкового, записи или защищенные от изменений классы).

При этом необходимо помнить, что разработчик может сделать запись и изменяемой, определив методы set для её свойств.

Наследование

Записи могут наследовать другие записи, но при этом запрещено наследование между классами и записями.

Наследование записи может быть записано в форме аналогичной наследованию классов.

public record Client : User { … }

При этом свойства и методы базовой записи будут унаследованы, а методы из стандартного набора – сгенерированы компилятором или могут быть явно определены разработчиком.

Но, как и в случае я объявлением записи, существует компактный синтаксис для наследования. Выглядит он следующим образом:

[public | internal] record [имя типа]([список свойств])
    : [имя базового типа]([значения для инициализации свойств базового типа]);

Например:

public record User(int Id, string Name);
public record Customer (int Id, string CustomerId, string CustomerName)
    : User(Id, "No name");

В данном случае запись Customer:

  • получит как унаследованные свойства Id и Name, так и новые CustomerName, CustomerId;
  • свойства Id, CustomerId и CustomerName будут заданы при помощи параметров конструктора;
  • свойство Name получит значение "No name".

Стоит отметить что:

  • У конструктора наследника будут параметры только инициализации свойств, которые явно указанны объявлении записи. А свойства базовой записи будут инициализированы значениями и свойствами, указанными в перечне после имени её типа.
  • В объявлении наследника можно указать свойство из базовой записи, для того, чтобы инициализировать её через конструктор.
  • Значение любого свойства может быть переопределено в инициализаторе объекта, т.к. все свойства получат метод доступа init.

Сравнение записей

Как уже было отмечено, компилятор добавляет в код записей реализацию сравнения. Оно выполняется по следующему правилу: две записи равны, если они одного типа и их свойства равны.

var u1 = new User(1, "John Doe");
var u2 = new User(1, "John Doe");

Console.WriteLine(u1 == u2); // true
Console.WriteLine(u1.Equals(u2)); // true

При этом наследник записи считается как отдельный тип, даже если перечень и значения его свойств полностью совпадают с экземпляром базового типа:

public record User(int Id, string Name);
public record Client(int Id, string Name) : User(Id, Name);
…
var u1 = new User(1, "John Doe");
var u2 = new Client(1, "John Doe");

Console.WriteLine(u1 == c1); // false
Console.WriteLine(u1.Equals(c1)); // false

Конструктор копирования

Конструктор копирования по умолчанию создается защищенным. Разработчик может не только переопределить его поведение, но и сделать его публичным. Например, у копии записи User свойство Id всегда будет устанавливаться в значение по умолчанию:

public record User(int Id, string Name)
{
    public User(User src)
    {
        this.Id = default;
        this.Name = src.Name;
    }
}

Деконструктор

У всех записей есть деконструктор. Он может быть определен разработчиком или автоматически сгенерирован компилятором. В последнем случае можно отметить следующие особенности:

  • Порядок возвращаемых свойств соответствует порядку их объявления.
  • При наследовании деконструктор будет содержать только свойства, явно объявленные в наследнике.

В следующем примере унаследованное свойство Name будет отсутствовать в деконструкторе Customer:

public record User(int Id, string Name);
public record Customer(int Id, string CustomerId, string CustomerName)
   : User(Id, "No name");
… 
var customer = new Customer(1, "C01", "Customer");
(int id, string customerId, string customerName) = customer;

Сходство с классами

Аналогично классам, записи могут:

  • Реализовывать интерфейсы.
  • Иметь атрибуты как у самой записи, так и у её свойств.
  • Использовать обобщенные типы (generics), в том числе и с ограничениями по типу (generic constraints).
  • Быть частичными (partial) и закрытыми для наследования (sealed).
  • Содержать любые методы, определенные разработчиком (а не только методы из стандартного списка).

В качестве примера рассмотрим код, который демонстрирует эти возможности и при этом использует короткую форму записи:

public interface IContainer<TData>
{
    TData Data { get; }
}

public partial record UserContainer<TProperties>([Required] User Data, TProperties UserProps)
    : IContainer<User>
    where TProperties : class;

public partial record UserContainer<TProperties>
{
    public string GetProperty(string name) { … }
}

Добавить комментарий