JavaRush /Курси /C# SELF /Відмінності між record, class і struct

Відмінності між record, class і struct

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

1. Навіщо взагалі зʼявилися record

У C# (і .NET загалом) тривалий час основними будівельними блоками були класи (class) і структури (struct). Та кожен із них не ідеальний для певних завдань. Класи — посилальні, змінні та зазвичай порівнюються за посиланням. Структури — значимі типи (копіюються під час передавання), за замовчуванням порівнюються побайтово й найчастіше змінні (доки не зʼявилися readonly struct).

А якщо вам потрібен простий і природний запис даних, який зручно порівнювати за значенням, швидко клонувати і не перейматися тим, що ваш обʼєкт хтось десь змінив, — доводилося вигадувати чимало власних рішень або користуватися бібліотеками на кшталт ValueTuple чи навіть System.Tuple. Але всі вони не такі виразні та зручні, як хотілося б.

Тому в C# 9 зʼявився record — тип даних, що поєднує стислість оголошення, безпечну незмінність і поведінку, налаштовану під порівняння за значенням.

2. Усі чотири типи на одній сторінці

class struct record record struct
Категорія Посилальний тип Значимий тип Посилальний тип Значимий тип
Змінність За замовчуванням — так За замовчуванням — так За замовчуванням — ні (init) За замовчуванням — ні (init)
Порівняння За посиланням (==) За значенням (поля) За значенням (поля/властивості) За значенням (поля)
Клонування Тільки вручну Тільки вручну Є вбудована підтримка (with) Є вбудована підтримка (with)
Наслідування Так Ні Так Ні
Незмінність Потрібно реалізувати Потрібно реалізувати Дуже просто реалізувати Дуже просто реалізувати
Синтаксис Найдовший Короткий Найкоротший (позиційний) Досить короткий
Використання в колекціях За посиланням Копії За посиланням Копії

Візуальна схема

+----------------+     +----------------+     +--------------------+
|    class       |     |    struct      |     |      record        |
+----------------+     +----------------+     +--------------------+
| Reference Type |     | Value Type     |     | Reference Type     |
| Mutable        |     | Mutable        |     | Immutable (init)   |
| == : Reference |     | == : By Fields |     | == : By Value      |
+----------------+     +----------------+     +--------------------+

3. Суть різниці між record, class і struct

Поведінка в памʼяті: посилання чи значення?

  • class і record — це посилальні типи. Коли ви їх передаєте в метод, копіюється посилання на обʼєкт.
  • struct і record struct — значимі типи. Вони завжди копіюються побайтово (якщо тільки ви явно не передаєте їх за посиланням).

class PointClass { public int X; public int Y; }
struct PointStruct { public int X; public int Y; }
record PointRecord(int X, int Y);
record struct PointRecordStruct(int X, int Y);

void Demo()
{
    var pc = new PointClass { X = 1, Y = 2 };
    var ps = new PointStruct { X = 1, Y = 2 };
    var pr = new PointRecord(1, 2);
    var prs = new PointRecordStruct(1, 2);

    ChangeY(pc);    // pc.Y зміниться!
    ChangeY(ps);    // ps.Y не зміниться — копія!
    ChangeY(pr);    // pr.Y зміниться!
    ChangeY(prs);   // prs.Y не зміниться — копія!
}

void ChangeY(dynamic p) { p.Y = 99; }

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

Порівняння: як дізнатися, що два обʼєкти рівні?

  • class: порівнюються за посиланням (== — true, тільки якщо це один і той самий обʼєкт у памʼяті), якщо не перевизначити Equals.
  • struct: порівнюються за значенням усіх полів (за замовчуванням).
  • record: порівнюються за значенням усіх полів/властивостей, заданих у первинному конструкторі.
class Foo { public int A; public int B; }
record Bar(int A, int B);

var foo1 = new Foo { A = 42, B = 1 };
var foo2 = new Foo { A = 42, B = 1 };
var bar1 = new Bar(42, 1);
var bar2 = new Bar(42, 1);

Console.WriteLine(foo1 == foo2); // False! Різні обʼєкти
Console.WriteLine(bar1 == bar2); // True! Значення збігаються

Цікавий факт:
З record struct ще цікавіше: у них порівняння «за значенням», як у звичайних struct, але з синтаксисом і можливостями record.

4. Незмінність: хто скільки готовий гарантувати?

Порівнюємо безпечність обʼєктів:

  • class: за замовчуванням легко змінюваний, якщо явно не зробити всі поля readonly.
  • struct: аналогічно, але можна оголосити як readonly struct — тоді ані поля, ані властивості не можна змінювати.
  • record: зазвичай властивості оголошують із модифікатором init, тобто задати їх можна лише в конструкторі або в ініціалізаторі обʼєкта (with). Це зручно для безпечної передачі даних.
  • record struct: те саме; можна визначити readonly record struct і отримати незмінність значимих типів з усіма перевагами record.

record Person(string Name, int Age);

var p1 = new Person("Олексій", 23);
// p1.Age = 24; // Помилка! Лише init

var p2 = p1 with { Age = 24 }; // Працює! Створює копію з оновленими даними

Коли у вас великий проєкт і десятки сутностей, record допомагають уникнути багатьох помилок, повʼязаних із «хтось десь змінив поле — і все впало».

5. Синтаксис: як виглядають оголошення і як не заплутатися


// class
public class Product
{
    public int Id { get; init; }
    public string Name { get; init; }
}

// struct
public struct Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

// record
public record Product(int Id, string Name);

// record struct
public record struct Point(int X, int Y);
Порівняння синтаксису оголошення різних типів

Як бачите, запис у record-типу максимально короткий. Ви отримуєте конструктор, деконструктор, рівність, ToString() і багато іншого «з коробки».

Синтаксис Отримуєш безкоштовно Чи можна наслідувати
class майже нічого Так
struct майже нічого Ні
record ToString, Equals, Deconstruct, with Так
record struct ToString, Equals, Deconstruct, with Ні

6. Клонування і оператор with

Лише у record і record struct є спеціальний оператор with, який дає змогу без зайвих зусиль копіювати обʼєкт зі зміною окремих властивостей.


record User(string Name, int Age);

var user1 = new User("Ірина", 28);
var user2 = user1 with { Age = 29 }; // user2.Name == "Ірина", user2.Age == 29

У випадку класу (class) доведеться реалізувати метод копіювання вручну, і якщо забути скопіювати якесь поле — виникатимуть помилки.

7. Наслідування: хто кого

  • class: підтримує стандартне наслідування (ієрархії класів, віртуальні методи, абстракції тощо).
  • struct: не підтримує (може лише реалізовувати інтерфейси).
  • record: підтримує наслідування, але з певними обмеженнями (наприклад, наслідування можливе лише між record-типами, не з класами).
  • record struct: не підтримує, як і звичайні struct.
record Animal(string Name);
record Dog(string Name, string Breed) : Animal(Name); // Добре!

class Vehicle { }
class Car : Vehicle { } // Добре!

// структура не може наслідувати структуру

Докладніше про наслідування — на наступному рівні.

8. Де які типи застосовуються на практиці

  • class: великі обʼєкти з багатою поведінкою, довгим життєвим циклом, змінним станом та ієрархією (наприклад, представлення бізнес-логіки, UI-компоненти).
  • struct: дрібні обʼєкти-значення, де важливі максимально швидке копіювання, відсутність участі GC і мінімальні накладні витрати (наприклад, координати, кольори, суми — те, що легко й швидко копіювати).
  • record: DTO (Data Transfer Object), обʼєкти-параметри, параметри конфігурації, незмінні стани, результати обчислень, які зручно порівнювати за вмістом.
  • record struct: значимі типи, де потрібні незмінність і поведінка, як у record, але без зайвих виділень у купі (heap).

9. Типові помилки й підводні камені

Часом розробники вважають, що record — це просто «заміна класу». Це не так! Якщо ви створюєте обʼєкт, який має змінювати свій стан у процесі життя — використовуйте class.
Якщо ви хочете порівнювати обʼєкти за посиланням (наприклад, у патерні «одинак» або коли життєвий цикл обʼєкта критично важливий) — використовуйте class.
Якщо ви створюєте обʼєкт-значення, який має поводитися як число чи точка на координатній сітці — struct або record struct.
Якщо ви працюєте з незмінними, легко порівнюваними обʼєктами, які часто передаються між шарами застосунку, зберігаються в колекціях, логуються й серіалізуються — ваш вибір — record.

Також памʼятайте: якщо ви оголосили record-клас із властивостями лише для читання, але забули про вкладені обʼєкти — вкладені поля все одно можуть змінюватися, якщо вони самі змінні.


record Student(string Name, List<int> Grades);

var s1 = new Student("Антон", new List<int>() {5,5,5});
var s2 = s1 with { };

s2.Grades.Add(2); // Обидва обʼєкти посилаються на той самий список! s1.Grades == s2.Grades
1
Опитування
Проблема з DTO, рівень 19, лекція 4
Недоступний
Проблема з DTO
DTO, record і with
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ