1. Проблема передачи данных
Допустим, у нас есть приложение для школы, и нам нужно передавать информацию о студенте между разными модулями: имя, год рождения, класс. Как мы обычно поступаем?
Мы создаём класс типа:
public class Student
{
public string Name { get; set; }
public int YearOfBirth { get; set; }
public string Class { get; set; }
}
Выглядит привычно. Но у такого подхода есть несколько проблем:
- Чтобы сравнить двух студентов на равенство, по умолчанию будет сравниваться только ссылка на объект. То есть два студента с одинаковыми полями, но разными объектами — не равны!
- Класс можно изменить после создания, что иногда ведёт к ошибкам (особенно если объект уже где-то используется);
- Много «шаблонного» кода: конструкторы, методы сравнения, копирования (клонов), ToString.
И как вы уже, наверное, догадались: C# может избавить нас от этих забот! Встречайте — record.
2. Что такое record?
record — это особый тип в C#, который проектировался специально для хранения данных. У записи (record) есть две главные фичи:
- Неизменяемость (immutability): объекты типа record по умолчанию неизменяемы, то есть значения их свойств устанавливаются один раз при создании и больше не меняются (точнее, set-сеттеры приватные). Хотя можно объявлять и изменяемые записи, по умолчанию — неизменяемость.
- Сравнение по значению: если два record-объекта имеют одинаковые значения во всех своих полях, они считаются равными (== и .Equals() работают по-другому!).
На самом деле, record — это идеальная обёртка для передачи данных между слоями приложения (например, из БД в контроллер, из контроллера в представление и т.д.).
3. Синтаксис record
Самый простой способ — позиционный синтаксис
Когда нам нужно просто передать набор значений, мы объявляем тип с помощью одной короткой строчки:
public record Student(string Name, int YearOfBirth, string Class);
Что происходит под капотом? Компилятор сам сгенерирует для нас:
- Автоматические свойства только для чтения (с приватным сеттером);
- Конструктор, принимающий все параметры;
- Методы сравнения и копирования;
- Классный ToString, который нормально форматирует вывод!
Использование позиционного record
Давайте попробуем воспользоваться этим новым типом в нашем школьном приложении:
var student1 = new Student("Иван", 2008, "8А");
var student2 = new Student("Мария", 2008, "8Б");
Доступ к свойствам осуществляется как обычно (только их менять нельзя):
Console.WriteLine($"{student1.Name}, {student1.YearOfBirth}, {student1.Class}");
Попытка изменить свойство после создания
student1.Name = "Петр"; // Ошибка! Свойство только для чтения.
Если вы такую строку раскомментируете — компилятор тут же начнёт возмущаться: невозможно установить значение для только для чтения.
Вот так выглядит автоматически сгенерированный ToString
Console.WriteLine(student1); // Выведет: Student { Name = Иван, YearOfBirth = 2008, Class = 8А }
Всё красиво и понятно даже без ручного форматирования!
4. Сравнение record-объектов
Напомню: если создать два разных объекта классического класса с одинаковыми данными, они всё равно будут не равны друг другу:
var a = new Student("Иван", 2008, "8А");
var b = new Student("Иван", 2008, "8А");
Console.WriteLine(a == b); // Для класса: false
Если же Student — это record, то равенство работает так, как вы бы и хотели:
public record Student(string Name, int YearOfBirth, string Class);
var a = new Student("Иван", 2008, "8А");
var b = new Student("Иван", 2008, "8А");
Console.WriteLine(a == b); // Для record: true!
То есть две записи с одинаковыми полями приравниваются друг к другу даже если это два разных объекта в памяти.
5. Как это выглядит внутри
Студенты часто удивляются, сколько всего компилятор делает за нас в случае с record. Давайте сравним для наглядности, какой бы код пришлось писать вручную для класса и что делает record.
Старый добрый класс написанный руками
public class Student
{
public string Name { get; }
public int YearOfBirth { get; }
public string Class { get; }
public Student(string name, int yearOfBirth, string @class)
{
Name = name;
YearOfBirth = yearOfBirth;
Class = @class; //ссылка на свой собственный класс
}
public override bool Equals(object? obj)
{
if (obj is not Student other) return false;
return Name == other.Name && YearOfBirth == other.YearOfBirth && Class == other.Class;
}
public override int GetHashCode()
{
return HashCode.Combine(Name, YearOfBirth, Class);
}
public override string ToString()
{
return $"Student {{ Name = {Name}, YearOfBirth = {YearOfBirth}, Class = {Class} }}";
}
}
Неудивительно, что программисты становятся параноиками — мы ведь по 10 раз пишем одно и то же!
Record — одна строка
public record Student(string Name, int YearOfBirth, string Class);
6. Record и неизменяемость: что можно, а что нельзя
У record-ов по умолчанию свойства только для чтения, и это здорово предотвращает кучу багов. Но если очень хочется (например, вы пишете для очень старого API), можно объявить и изменяемые записи:
public record MutableStudent
{
public string Name { get; set; }
public int YearOfBirth { get; set; }
public string Class { get; set; }
}
Теперь эти поля можно менять, но вы теряете часть преимуществ (безопасность, например).
7. Деструктуризация record
Поскольку позиционный синтаксис очень похож на кортежи, можно легко деструктурировать запись:
var student = new Student("Иван", 2008, "8А");
var (name, year, className) = student;
Console.WriteLine($"{name} - {year}, {className}"); // Иван - 2008, 8А
Компилятор генерирует метод Deconstruct для каждого позиционного record, и это облегчает работу с LINQ, switch-паттернами и вообще делает жизнь приятнее.
8. Record — это класс или struct?
По умолчанию record — это ссылочный тип, как класс. То есть все особенности поведения ссылочных типов (хранение в куче, копирование ссылки и т.д.) работают обычным образом.
Если хочется значимый тип (value type), то в C# и для этого есть свой оператор — всегда можно написать:
public record struct Point(int X, int Y);
Но для передачи данных почти всегда используется классическая форма records, то есть ссылочный тип. Подробнее про record struct — в ближайших лекциях :P
Сравнение class, struct, record
| Тип | Иммутабельность по умолчанию | Сравнение по значению | Легкая деструктуризация | Авто ToString |
|---|---|---|---|---|
| class | Нет | Нет (по ссылке) | Нет | Нет |
| struct | Нет | Да | Нет | Нет |
| record | Да | Да | Да | Да |
9. Особенности и типичные ошибки
Многие новички путаются в нюансах использования records. Например, иногда ожидают, что изменение поля у одного объекта record изменит другой (как у классов с копированием ссылки). Нет! Кроме того, когда пишете with, помните, что всегда создаётся копия, а не изменяется исходник. Записи идеально подходят для логики, где ваше приложение заботится о чистоте данных и предсказуемости.
Да, если вы объявили поля с init вместо set, их тоже можно задавать только при создании или с помощью with, но не позже.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ