← Назад к вопросам

Зачем нужен Record?

2.0 Middle🔥 162 комментариев
#Основы C# и .NET

Комментарии (2)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Зачем нужен Record в C#?

Record — это принципиально новый тип ссылочного типа данных, представленный в C# 9.0 и развитый в последующих версиях. Его основное предназначение — предоставление простого и лаконичного способа создания неизменяемых (immutable) объектов данных, которые по своей сути являются "контейнерами" для значений. Если класс — это о моделировании поведения и состояния с идентификацией, то record — о моделировании данных, где важны сами значения свойств, а не идентичность объекта в памяти.

Ключевые преимущества и причины использования

1. Семантика значений (Value Semantics) для равенства

Это главная причина. Для record равенство (Equals, ==, !=) по умолчанию определяется по значениям всех его свойств, а не по ссылке на объект в куче (как у class). Это делает record идеальным для DTO (Data Transfer Object), сообщений, конфигурационных объектов, результатов запросов.

// Сравнение record
public record PersonRecord(string Name, int Age);
var person1 = new PersonRecord("Alice", 30);
var person2 = new PersonRecord("Alice", 30);
var person3 = new PersonRecord("Bob", 25);

Console.WriteLine(person1 == person2); // True (значения равны)
Console.WriteLine(person1.Equals(person2)); // True
Console.WriteLine(person1 == person3); // False
Console.WriteLine(ReferenceEquals(person1, person2)); // False (разные объекты в памяти!)

// Сравнение class (для контраста)
public class PersonClass { public string Name; public int Age; }
var class1 = new PersonClass { Name = "Alice", Age = 30 };
var class2 = new PersonClass { Name = "Alice", Age = 30 };
Console.WriteLine(class1 == class2); // False (сравниваются ссылки!)

2. Встроенная неизменяемость (Immutability) и безопасность потоков

Свойства record, объявленные в позиционном синтаксисе (в заголовке), по умолчанию являются init-only. После создания объекта их нельзя изменить. Это делает код предсказуемым, предотвращает случайные модификации и по своей природе безопасным для использования в многопоточной среде (thread-safe), так как состояние такого объекта не может измениться после создания.

public record Invoice(string Number, decimal Amount, DateTime Date);

var invoice = new Invoice("INV-001", 100.50m, DateTime.UtcNow);
// invoice.Amount = 200; // Ошибка компиляции! Свойство инициализируется только при создании.

// Для создания модифицированной копии используется выражение `with` (неизменяемость в действии).
var correctedInvoice = invoice with { Amount = 90.25m };
Console.WriteLine(invoice); // Invoice { Number = INV-001, Amount = 100.50, ... }
Console.WriteLine(correctedInvoice); // Invoice { Number = INV-001, Amount = 90.25, ... }

3. Выражение with для неразрушающего изменения (Non-Destructive Mutation)

Это мощный инструмент для работы с неизменяемыми данными. Позволяет создать новый экземпляр record на основе существующего, изменив при этом только указанные свойства. Исходный объект остается неизменным. Это особенно полезно в функциональном стиле программирования.

public record Configuration(string Host, int Port, bool UseHttps);
var defaultConfig = new Configuration("localhost", 8080, false);

// Создаем новую конфигурацию для продакшена на основе дефолтной.
var prodConfig = defaultConfig with { Host = "api.example.com", UseHttps = true };
Console.WriteLine(prodConfig); // Configuration { Host = api.example.com, Port = 8080, UseHttps = True }

4. Лаконичный синтаксис и встроенные методы

Компилятор C# автоматически генерирует за вас "шаблонный" код:

  • Канонический конструктор для позиционных записей.
  • Методы ToString(), который выводит удобное для чтения представление типа и значений свойств.
  • Методы Equals(object), Equals(T) и операторы ==, !=, работающие на основе значений.
  • Метод GetHashCode(), рассчитанный на основе значений свойств.
  • Метод Deconstruct() для деконструкции на переменные.
public record Point(int X, int Y); // Всего одна строка объявления!

// Использование сгенерированного кода
var p = new Point(5, 10);
Console.WriteLine(p); // Выведет: Point { X = 5, Y = 10 }

// Деконструкция
var (x, y) = p;
Console.WriteLine($"X: {x}, Y: {y}"); // X: 5, Y: 10

Основные сценарии применения

  • DTO и ViewModels: Передача данных между слоями приложения (например, из API контроллера в клиент) без лишнего кода.
  • Неизменяемые модели данных в предметной области (Domain): Value Objects в DDD (Domain-Driven Design), где важна именно совокупность значений.
  • Результаты запросов и вычислений: Идеально для возврата сложных данных из методов, где важна целостность и неизменяемость.
  • Сообщения в системах обмена (CQRS, Event Sourcing): События (Events) и команды (Commands) по своей природе неизменяемы после создания.
  • Ключи в словарях (Dictionary): Благодаря корректно реализованным Equals и GetHashCode на основе значений, record идеально подходит на роль ключа.

Record class vs Record struct (C# 10)

  • Record class (по умолчанию): Ссылочный тип, размещается в управляемой куче. Поддерживает наследование.
  • Record struct: Тип значения, размещается в стеке (или встраивается). Более эффективен для небольших, часто создаваемых объектов, но лишен наследования.
public record class UserClass(string Login); // Ссылочный тип (явно)
public record struct Coords(double Lat, double Lng); // Тип значения

Итог

Record — это не замена классу, а специализированный инструмент в арсенале C#-разработчика. Он радикально сокращает бойлерплейт-код, обеспечивает безопасность и предсказуемость за счет неизменяемости и value-based равенства, способствует написанию чистого, надежного кода в многопоточных и распределенных системах. Если ваша основная задача — моделирование и передача данных, а не сложного поведения с изменяемым состоянием, record почти всегда будет лучшим и более выразительным выбором, чем обычный класс или структура.

Зачем нужен Record? | PrepBro