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 за замовчуванням незмінні, тобто значення їхніх властивостей задають один раз під час створення й надалі не змінюють (сетери приватні). Можна оголошувати й змінювані записи, але типово — незмінність.
- Порівняння за значенням: якщо два 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; // @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);
Але для передавання даних майже завжди використовують класичну форму record, тобто посилальний тип. Детальніше про record struct — у найближчих лекціях :P
Порівняння class, struct, record
| Тип | Незмінність за замовчуванням | Порівняння за значенням | Легка деструктуризація | Автоматичний ToString |
|---|---|---|---|---|
| class | Ні | Ні (за посиланням) | Ні | Ні |
| struct | Ні | Так | Ні | Ні |
| record | Так | Так | Так | Так |
9. Особливості та типові помилки
Чимало новачків плутаються в нюансах використання record. Наприклад, іноді очікують, що зміна поля в одного об’єкта record змінить інший (як у класів із копіюванням посилання). Ні! Крім того, коли ви пишете with, памʼятайте, що завжди створюється копія, а не змінюється оригінал. Записи ідеально підходять для логіки, де ваш застосунок дбає про чистоту даних і передбачуваність.
Так, якщо ви оголосили властивості з init замість set, їх теж можна встановлювати лише під час створення або за допомогою with, але не пізніше.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ