Records — это новый тип данных, представленный в 2021 году в C# 9 и .NET Core 5.
public record Person(string Name, int Id);
Записи — это третий способ определения типов данных в C#; два других — class
и struct
.
Поскольку это довольно новая идея в .NET, мы должны потратить некоторое время на эксперименты с ней и попытаться понять ее возможности и функциональность.
В этой статье мы рассмотрим 8 свойств записей, которые необходимо знать перед их использованием, чтобы извлечь максимальную пользу из этого нового типа данных.
- 1 — Записи неизменяемы
- 2- Записи реализуют равенство
- 3- Записи можно клонировать или обновлять с помощью ‘with’
- 4- Записи могут быть структурами и классами
- 5 — Записи на самом деле не являются неизменяемыми
- 6- Записи могут иметь подтипы
- 7 — Записи могут быть абстрактными
- 8- Запись может быть опечатана
- Дополнительные ресурсы
- Подведение итогов
1 — Записи неизменяемы
По умолчанию записи неизменяемы. Это означает, что, создав один экземпляр, вы не сможете изменить ни одно из его полей:
var me = new Person("Davide", 1);
me.Name = "AnotherMe"; // won't compile!
Эта операция не является законной.
Даже компилятор жалуется:
Init-only property or indexer ‘Person.Name’ can only be assigned in an object initializer, or on ‘this’ or ‘base’ in an instance constructor or an ‘init’ accessor.
2- Записи реализуют равенство
Другим основным свойством записей является то, что они реализуют равенство «из коробки».
[Test]
public void EquivalentInstances_AreEqual()
{
var me = new Person("Davide", 1);
var anotherMe = new Person("Davide", 1);
Assert.That(anotherMe, Is.EqualTo(me));
Assert.That(me, Is.Not.SameAs(anotherMe));
}
Как вы видите, я создал два экземпляра Person
с одинаковыми полями. Они считаются равными, но не являются одним и тем же экземпляром.
3- Записи можно клонировать или обновлять с помощью ‘with’
Итак, если нам нужно обновить поле записи, что мы можем сделать?
Мы можем использовать ключевое слово with
:
[Test]
public void WithProperty_CreatesNewInstance()
{
var me = new Person("Davide", 1);
var anotherMe = me with { Id = 2 };
Assert.That(anotherMe, Is.Not.EqualTo(me));
Assert.That(me, Is.Not.SameAs(anotherMe));
}
Посмотрите на me with { Id = 2 }
: эта операция создает клон me
и обновляет поле Id
.
Конечно, вы можете использовать with
для создания нового экземпляра, идентичного исходному.
[Test]
public void With_CreatesNewInstance()
{
var me = new Person("Davide", 1);
var anotherMe = me with { };
Assert.That(anotherMe, Is.EqualTo(me));
Assert.That(me, Is.Not.SameAs(anotherMe));
}
4- Записи могут быть структурами и классами
В основном, записи действуют как классы.
public record Person(string Name, int Id);
Иногда это не то, что вам нужно. Начиная с C# 10 вы можете объявлять записи как структуры:
public record struct Point(int X, int Y);
Очевидно, что все, что мы видели раньше, остается в силе.
[Test]
public void EquivalentStructsInstances_AreEqual()
{
var a = new Point(2, 1);
var b = new Point(2, 1);
Assert.That(b, Is.EqualTo(a));
//Assert.That(a, Is.Not.SameAs(b));// does not compile!
}
Ну, почти все: вы не можете использовать Is.SameAs()
, потому что, поскольку структуры являются типами значений, два значения всегда будут разными значениями. Компилятор сообщит вам об этом, выдав ошибку, которая гласит:
Ограничение SameAs всегда не работает для типов значений, так как фактическое и ожидаемое значение не могут быть одной и той же ссылкой.
5 — Записи на самом деле не являются неизменяемыми
Мы видели, что вы не можете обновлять существующие записи. Это не совсем верно.
Это утверждение верно в случае «простых» записей, таких как Person
:
public record Person(string Name, int Id);
Но все меняется, когда мы используем другой способ определения Записей:
public record Pair
{
public Pair(string Key, string Value)
{
this.Key = Key;
this.Value = Value;
}
public string Key { get; set; }
public string Value { get; set; }
}
Мы можем явно объявить свойства Записи, чтобы сделать ее более похожей на обычные классы.
Используя этот подход, мы все еще можем использовать функциональность автоматического равенства Записей
[Test]
public void ComplexRecordsAreEquatable()
{
var a = new Pair("Capital", "Roma");
var b = new Pair("Capital", "Roma");
Assert.That(b, Is.EqualTo(a));
}
Но мы можем обновить одно поле, не создавая совершенно новый экземпляр:
[Test]
public void ComplexRecordsAreNotImmutable()
{
var b = new Pair("Capital", "Roma");
b.Value = "Torino";
Assert.That(b.Value, Is.EqualTo("Torino"));
}
Кроме того, только простые типы являются неизменяемыми, даже при использовании базового определения Record.
Тип ComplexPair
— это Record, который принимает в определении список строк.
public record ComplexPair(string Key, string Value, List<string> Metadata);
Этот список строк не является неизменным: вы можете добавлять и удалять элементы по своему усмотрению:
[Test]
public void ComplexRecordsAreNotImmutable2()
{
var b = new ComplexPair("Capital", "Roma", new List<string> { "City" });
b.Metadata.Add("Another Value");
Assert.That(b.Metadata.Count, Is.EqualTo(2));
}
В примере ниже видно, что я добавил новый элемент в список Metadata
без создания нового объекта.
6- Записи могут иметь подтипы
Интересной особенностью является то, что мы можем создавать иерархию записей очень простым способом.
Помните определение Person
?
public record Person(string Name, int Id);
Так вот, вы можете определить подтип так же, как вы это делаете с обычными классами:
public record Employee(string Name, int Id, string Role) : Person(Name, Id);
Конечно, все правила Boxing и Unboxing остаются в силе.
[Test]
public void Records_CanHaveSubtypes()
{
Person meEmp = new Employee("Davide", 1, "Chief");
Assert.That(meEmp, Is.AssignableTo<Employee>());
Assert.That(meEmp, Is.AssignableTo<Person>());
}
7 — Записи могут быть абстрактными
…и да, у нас могут быть абстрактные записи!
public abstract record Box(int Volume, string Material);
Это означает, что мы не можем создавать новые Записи, тип которых помечен как Абстрактный.
var box = new Box(2, "Glass"); // cannot create it, it's abstract
Напротив, для инстанцирования новых объектов нам необходимо создавать конкретные типы:
public record PlasticBox(int Volume) : Box(Volume, "Plastic");
Опять же, все правила, которые мы уже знаем, остаются в силе.
[Test]
public void Records_CanBeAbstract()
{
var plasticBox = new PlasticBox(2);
Assert.That(plasticBox, Is.AssignableTo<Box>());
Assert.That(plasticBox, Is.AssignableTo<PlasticBox>());
}
8- Запись может быть опечатана
Наконец, записи могут быть помечены как опечатанные.
public sealed record Point3D(int X, int Y, int Z);
Пометить запись как запечатанную означает, что мы не можем объявлять подтипы.
public record ColoredPoint3D(int X, int Y, int Z, string RgbColor) : Point3D(X, Y, X); // Will not compile!
Это может быть полезно при открытии типов для внешних систем.
Дополнительные ресурсы
Как обычно, несколько ссылок, которые вы можете прочитать, чтобы узнать больше о записях в C#.
Первая из них — это учебник с сайта Microsoft, который обучает основам Records:
🔗 Создание типов записей | Microsoft Docs
Второй — великолепная статья Гэри Вудфайна, в которой он исследует внутренние аспекты C# Records и многое другое:
🔗C# Records — The good, bad & ugly | Gary Woodfine.
Наконец, если вам интересны мелочи о C#, которые мы используем, но редко изучаем, вот статья, которую я написал некоторое время назад о GUIDs в C# — вы найдете там много интересного!
🔗5 вещей, которые вы не знали о Guid в C# | Code4IT
Эта статья впервые появилась на Code4IT
Подведение итогов
В этой статье мы рассмотрели 8 вещей, которые вы, вероятно, не знали о Records в C#.
Записи являются довольно новым явлением в экосистеме .NET, поэтому мы можем ожидать новых обновлений и функциональных возможностей.
Есть ли что-то еще, что мы должны добавить? Или, может быть, что-то, чего вы не ожидали?
Счастливого кодинга!
🐧