Records, init, with, required, primary constructors
↑ 2.1 C♯: продвинутый язык · ← Предыдущая · → Следующая
Зачем это на собесе
Records,
init,requiredи primary constructors — «современный C#» в одном вопросе. Спросят, чем record отличается от class, как устроено равенство, что делаетwith, зачемinitиrequired, чем primary constructor у record отличается от primary constructor у класса.
Объяснение
record: тип с равенством по значению
public record Patient(Guid Id, string LastName, DateOnly BirthDate); // record class, позиционный
public readonly record struct Money(decimal Amount, string Currency); // record struct, иммутабельный
var a = new Patient(id, "Иванов", new(1990, 1, 1));
var b = a with { LastName = "Петров" }; // копия с изменением, оригинал не тронут
a == new Patient(id, "Иванов", new(1990, 1, 1)); // true: сравнение по значениям свойств
a.ToString(); // Patient { Id = ..., LastName = Иванов, BirthDate = ... }
var (i, l, d) = a; // деконструкция (позиционный record)Компилятор генерирует: конструктор, свойства init, Equals/GetHashCode, ==/!=, ToString (PrintMembers), Deconstruct, <Clone>$ для with, EqualityContract.
- record class — ссылочный тип (по умолчанию
record=record class). - record struct — значимый;
readonly record structиммутабелен,record structбезreadonlyимеет изменяемые свойства. - Наследование: record наследуется только от record (
record Student(string Course) : Person(...)); равенство учитывает точный тип.
init и required
public class Patient
{
public required string LastName { get; init; } // обязателен при создании, дальше только чтение
public string? Phone { get; init; }
public Guid Id { get; init; } = Guid.NewGuid();
}
var p = new Patient { LastName = "Иванова" }; // без LastName не скомпилируется
// p.LastName = "X"; // ошибка: init-onlyinit— сеттер, который вызывается только в инициализаторе объекта,withи конструкторе.required(C# 11) — компилятор потребует задать член при создании, работает и без конструктора; полезно для DTO и десериализации.[SetsRequiredMembers]на конструкторе сообщает, что он задаёт все required-члены.System.Text.Json(.NET 7+) учитываетrequired: при отсутствии поля в JSON бросит исключение.
Primary constructors (C# 12)
public class ReportService(IDataCollector collector, ILogger<ReportService> log) // у класса
{
public Task<byte[]> BuildAsync(Guid id) { log.LogInformation("Build {Id}", id); return collector.CollectAsync(id); }
}- У класса и struct: параметры — захваченные переменные (не свойства, не readonly). Доступны в теле, изменяемы.
- У record: параметры автоматически становятся публичными init-свойствами и участвуют в равенстве и деконструкции.
- Если нужен
readonly, присвойте в поле:private readonly IDataCollector _c = collector;.
Когда что использовать
| Задача | Выбор |
|---|---|
| DTO, сообщения, запросы/ответы API | record или record class |
| Value object (деньги, диапазон, идентификатор) | readonly record struct (мелкий) или record |
| Сущность с идентичностью и изменяемым состоянием (EF Core) | обычный class |
| Сервис с зависимостями | класс с primary constructor |
| Конфигурация, привязка из JSON | класс с required/init свойствами или record |
Нюансы и подводные камни
- Равенство record — по значениям всех свойств. Для полей-коллекций сравнение идёт по ссылке, содержимое не сравнивается.
- Record с изменяемыми свойствами (
{ get; set; }) теряет смысл: хэш-код может измениться, и объект «потеряется» вHashSet. withделает поверхностную копию: вложенные ссылочные объекты общие.- Сущности EF Core лучше не делать record: change tracker опирается на ссылочную идентичность и
Equalsпо значению создаёт неожиданности. Для проекций и DTO — самое место. - Primary constructor класса не делает параметры полями: если использовать их в нескольких методах, компилятор захватывает в скрытое поле, и это может удивлять при рефакторинге.
- В DI primary constructor работает как обычный: параметры внедряются.
Практика
- Переписать DTO-класс пациента на record с
required-свойством и проверитьwith. - Сравнить два record с
List<int>внутри и объяснить результат. - Создать
readonly record struct DateRangeс валидацией в конструкторе.
Вопросы с ответами
Чем record отличается от class?
Компилятор генерирует равенство по значению,
ToString,Deconstruct,with-копирование и позиционныеinit-свойства. Record предназначен для данных, а не для сущностей с идентичностью.
Что делает
with?Создаёт неглубокую копию record или struct с изменёнными свойствами. Оригинал не меняется.
Чем
initотличается отsetи отreadonly?
initможно вызывать только при создании объекта (инициализатор,with, конструктор).set— в любое время.readonly-поле — только в конструкторе и объявлении.
Что делает
required?Заставляет компилятор требовать присвоения члена при создании объекта. Без него код не скомпилируется.
Чем primary constructor записи отличается от primary constructor класса?
У record параметры становятся публичными init-свойствами и участвуют в равенстве. У class параметры — просто захваченные переменные, свойств не создаётся.
Связанные темы
- Предыдущая: Extension methods и fluent API · Следующая: Span и Memory: stackalloc, ref struct
- Enum, record, tuple: Enum, record, tuple, деконструкция
- Equals и GetHashCode: Equals, GetHashCode, сравнение объектов
- Классы и конструкторы: Классы, конструкторы, свойства, индексаторы
- DDD: value object: DDD: тактический — entity, value object, aggregate, domain events