JavaRush /Курси /C# SELF /Знайомство з record і...

Знайомство з record і позиційним синтаксисом

C# SELF
Рівень 19 , Лекція 1
Відкрита

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) є дві ключові особливості:

  1. Незмінність (immutability): об’єкти типу record за замовчуванням незмінні, тобто значення їхніх властивостей задають один раз під час створення й надалі не змінюють (сетери приватні). Можна оголошувати й змінювані записи, але типово — незмінність.
  2. Порівняння за значенням: якщо два record-об’єкти мають однакові значення в усіх своїх полях, вони вважаються рівними (== і .Equals() працюють інакше!).

Насправді record — ідеальний варіант для передачі даних між шарами застосунку (наприклад, з БД до контролера, з контролера — до подання тощо).

3. Синтаксис record

Найпростіший спосіб — позиційний синтаксис

Коли потрібно просто передати набір значень, оголошуємо тип одним коротким рядком:


public record Student(string Name, int YearOfBirth, string Class);
Позиційний синтаксис запису (record)

Що відбувається під капотом? Компілятор сам згенерує для нас:

  • автоматичні властивості лише для читання (з приватним сетером);
  • конструктор, який приймає всі параметри;
  • методи порівняння й копіювання;
  • зручний 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!
Порівняння record-об’єктів за значенням

Тобто два записи з однаковими полями вважаються рівними, навіть якщо це різні об’єкти в пам’яті.

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);
Record — усе те саме, але одним рядком!

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А
Деструктуризація позиційного record

Компілятор генерує метод Deconstruct для кожного позиційного record, і це полегшує роботу з LINQ, патернами switch та загалом спрощує роботу.

8. Record — це клас чи struct?

За замовчуванням record — це посилальний тип, як клас. Тобто всі особливості поведінки посилальних типів (зберігання в купі, копіювання посилання тощо) працюють звичайним чином.

Якщо потрібен тип значення (value type), то в C# і для цього є окрема форма — можна написати:


public record struct Point(int X, int Y);
Record struct — тип значення

Але для передавання даних майже завжди використовують класичну форму record, тобто посилальний тип. Детальніше про record struct — у найближчих лекціях :P

Порівняння class, struct, record

Тип Незмінність за замовчуванням Порівняння за значенням Легка деструктуризація Автоматичний ToString
class Ні Ні (за посиланням) Ні Ні
struct Ні Так Ні Ні
record Так Так Так Так

9. Особливості та типові помилки

Чимало новачків плутаються в нюансах використання record. Наприклад, іноді очікують, що зміна поля в одного об’єкта record змінить інший (як у класів із копіюванням посилання). Ні! Крім того, коли ви пишете with, памʼятайте, що завжди створюється копія, а не змінюється оригінал. Записи ідеально підходять для логіки, де ваш застосунок дбає про чистоту даних і передбачуваність.

Так, якщо ви оголосили властивості з init замість set, їх теж можна встановлювати лише під час створення або за допомогою with, але не пізніше.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ