Підрозділ 8.10
Records
Розглядає records у C#: record-класи й record-структури, init-властивості, порівняння за значенням, оператор with, позиційні records і успадкування.
8.10. Records
У більшості програм існують об'єкти двох принципово різних призначень. Одні — сутності з ідентичністю: конкретний пацієнт, конкретний лікар. Їх ідентифікують не за значеннями полів, а за посиланням на конкретний об'єкт у пам'яті. Два різні пацієнти з однаковим іменем — це різні люди. Для таких об'єктів добре підходить звичайний клас з посилальною рівністю.
Інші — контейнери значень (value objects): діагноз, дата прийому, результат аналізу, адреса, медичний запис. Тут два об'єкти з однаковим вмістом семантично рівні. «Діагноз "гіпертонія"» і ще один «діагноз "гіпертонія"» — це одне й те саме. Для таких об'єктів ми хочемо структурну рівність (як у string) і незмінність після створення. Раніше для цього доводилось вручну перевизначати Equals, GetHashCode, ToString та реалізовувати operator ==. Records (C# 9) вирішують цю задачу: компілятор генерує весь цей код автоматично.
Що компілятор генерує автоматично
Один рядок позиційного record:
public record Patient(string Name, int Age);замінює близько 50 рядків класу з усіма необхідними членами.

Конструктор — приймає всі параметри позиційного запису.
init-властивості — автоматично генеруються з readonly-семантикою: public string Name { get; init; }. Встановити можна тільки при конструюванні або в ініціалізаторі об'єкта — після цього значення зафіксоване.
Структурна рівність — Equals, ==, != порівнюють за значеннями всіх властивостей, а не за посиланням.
GetHashCode — генерується на основі значень властивостей, узгоджений з Equals.
ToString — виводить Patient { Name = Іван, Age = 45 } — зручно для логування та діагностики.
Deconstruct — дозволяє декомпозицію var (name, age) = patient;.
<Clone>() — внутрішній метод для підтримки оператора with.
Модифікатор init
init — це особливий тип сеттера, введений у C# 9. Властивість з init можна встановити тільки в двох місцях:
- у конструкторі;
- в ініціалізаторі об'єкта (
new Patient { Name = "Іван" }).
Після виходу з конструктора або ініціалізатора значення «замерзає» — будь-яка подальша спроба присвоєння спричиняє помилку компіляції. Це принципово відрізняється від set (можна змінити будь-коли) і від readonly-поля (можна встановити тільки в конструкторі, але не в ініціалізаторі).
public record Patient
{
public string Name { get; init; } = "";
public int Age { get; init; }
}
var p = new Patient { Name = "Іван", Age = 45 }; // OK — ініціалізатор
p.Name = "Марія"; // помилка компіляції — init вже завершеноСинтаксис: повний та позиційний
Повний синтаксис — схожий на клас, але з ключовим словом record:
public record Patient
{
public string Name { get; init; }
public int Age { get; init; }
public Patient(string name, int age)
{
Name = name;
Age = age;
}
}Позиційний синтаксис — вся інформація в одному рядку:
public record Patient(string Name, int Age);Компілятор сам створює конструктор, init-властивості та Deconstruct. Позиційний синтаксис — це не скорочення повного, це повноцінна альтернатива. Якщо потрібні додаткові члени — їх можна дописати у тілі:
public record Patient(string Name, int Age)
{
public bool IsAdult => Age >= 18;
public string Diagnosis { get; set; } = ""; // звичайна мутабельна властивість
}Оператор with
with — оператор немутабельного оновлення. Він створює копію record зі зміненими вказаними властивостями:
var original = new Patient("Іван Петренко", 45);
var updated = original with { Age = 46 }; // новий об'єкт; Name = "Іван Петренко"original при цьому не змінюється. Це ключова особливість: замість того, щоб мутувати об'єкт, ми отримуємо новий з оновленими даними. Такий підхід — «незмінні дані + нові версії» — є основою функціонального програмування і безпечний при роботі з кількома потоками.
Внутрішньо with викликає <Clone>() і потім встановлює init-властивості через ініціалізатор. Оператор with працює тільки з record — для звичайних класів він недоступний.
Структурна рівність
Для record компілятор генерує рівність за значеннями, а не за посиланням. Два різні об'єкти з однаковими даними рівні:
Порівняємо з класом:
Успадкування records
Record-класи можна успадковувати так само, як звичайні класи. Але є важлива деталь: при рівності враховується фактичний тип об'єкта. Компілятор генерує захищену властивість EqualityContract, яка повертає typeof(Patient), typeof(Doctor) тощо. При порівнянні типи повинні збігатися.
EqualityContract — це зв'язуюча ланка, яка гарантує: Person { Name="Tom", Age=37 } та Employee { Name="Tom", Age=37 } не рівні навіть при однакових значеннях полів. Це правильна поведінка: різні типи — різна ідентичність.
record struct vs record class
Починаючи з C# 10, кортежний тип record можна визначити і як структуру:
public record struct BloodPressure(int Systolic, int Diastolic);Ключова відмінність від record class:
| Характеристика | record class |
record struct |
readonly record struct |
|---|---|---|---|
| Тип даних | reference (heap) | value (stack) | value (stack) |
| Null | може бути null | ні | ні |
| Mутабельність (позиційні) | init — immutable | set — mutable | init — immutable |
| Успадкування | так | ні | ні |
with |
так | так | так |
| Структурна рівність | так | так | так |
Важлива різниця: для позиційного record struct компілятор генерує звичайні set-сеттери (мутабельні), а для record class — init-сеттери (immutable). Щоб зробити record struct незмінним, потрібне readonly:
public readonly record struct BloodPressure(int Systolic, int Diastolic);record struct доречний для невеликих value objects, які часто копіюються: точки, координати, вимірювання — там де value type дає виграш у продуктивності.
Підсумкова таблиця: record vs інші підходи
record |
Анонімний тип | Кортеж | Клас | |
|---|---|---|---|---|
| Іменований тип | ✓ | ✗ | ✓ (частково) | ✓ |
| Передача між методами | ✓ | ✗ (тільки object) | ✓ | ✓ |
| Структурна рівність | ✓ (авто) | ✓ (авто) | ✓ (авто) | ✗ (потрібно визначити) |
| Immutable | ✓ (init) | ✓ | ✗ (мутабельні) | ✓ (потрібно визначити) |
with |
✓ | ✗ | ✗ | ✗ |
| Успадкування | ✓ (class) | ✗ | ✗ | ✓ |
| ToString (стан) | ✓ (авто) | ✓ (авто) | ✓ (авто) | ✗ (потрібно визначити) |
| Boilerplate | мінімальний | немає | немає | максимальний |
Records — ідеальний інструмент для DTO (Data Transfer Objects), value objects у доменній моделі, результатів запитів, конфігураційних об'єктів: скрізь, де об'єкт несе дані і ідентифікується за своїм вмістом, а не за посиланням на конкретний екземпляр.