1. Що таке незмінність?
Почнімо з аналогії: уявіть, що ви — бухгалтер, який щодня формує звіт про продажі. Як справжній профі, ви не змінюєте звіт минулого тижня, а створюєте новий — на основі попереднього, але з оновленими даними. Так само і з незмінними об’єктами у програмуванні: після створення такий об’єкт більше не змінюється, а будь-які «зміни» означають створення нової копії.
Незмінність (immutability) — це властивість об’єкта не змінюватися після ініціалізації. Усі його властивості стають «замороженими»: якщо хочете інше значення — створіть новий об’єкт.
Навіщо це потрібно?
- Забезпечує безпеку: якщо ваш об’єкт ніхто не може випадково змінити, він не зіпсує ваші дані. Таке часто трапляється у багатопотокових програмах, коли кілька потоків намагаються щось змінювати одночасно.
- Спрощує налагодження: якщо об’єкт не змінюється, ви точно знаєте, що відбувалося після його створення.
- Зручно передавати дані, особливо у розподілених системах, де копії можуть розходитися.
- Дозволяє робити «знімки стану» (snapshots) — історія змін стає очевидною.
2. Незмінність типів record
Коли ви оголошуєте звичайний class, його властивості за замовчуванням змінні (mutable). Приклад:
public class UserProfile
{
public string Name { get; set; }
public int Age { get; set; }
}
var user = new UserProfile { Name = "Іван", Age = 25 };
user.Age = 26; // Усе гаразд — звичайний клас: змінюємо вік "на льоту"
У типів record ситуація інша: їх призначено для зберігання незмінних даних.
public record UserProfile(string Name, int Age);
// Створюємо об’єкт:
var user = new UserProfile("Іван", 25);
// Пробуємо змінити вік:
user.Age = 26; // Помилка компіляції: властивість лише для читання!
Властивості позиційного record оголошуються лише для читання (init-only). Ви не можете змінити їх після створення, але можете використати with-вираз, щоб створити нову копію зі зміненою властивістю.
Змінні класи vs незмінні record
| Клас | Позиційний record |
|---|---|
Властивості за замовчуванням |
Незмінні |
| Як змінити Звернення до властивості |
Лише створення нової копії |
| Порівняння об’єктів За посиланням (ReferenceEquals) |
За значенням (Equals) |
| Зручно для передавання даних Не завжди |
Так |
3. with-вирази
Можливо, ви подумали: «record — це круто, але як тепер жити, якщо їх не можна змінювати?» Ось тут і з’являється магія with-виразів!
with — це спеціальний синтаксис, який дозволяє створити нову копію record, змінюючи лише потрібні властивості.
Тобто: «Взяти цей об’єкт, зробити його копію, але тут підправити кілька властивостей.»
Найпростіший приклад
var user1 = new UserProfile("Анна", 30);
// ... але життя не стоїть на місці, і Анна стала старшою
var user2 = user1 with { Age = 31 };
// user1 без змін, user2 — копія, але на рік старша
Console.WriteLine(user1); // UserProfile { Name = Анна, Age = 30 }
Console.WriteLine(user2); // UserProfile { Name = Анна, Age = 31 }
Під капотом
Це не мутант-клон, а новий об’єкт, створений за допомогою спеціального автогенерованого методу Clone(), який створює копію і підставляє нові значення.
Якби with-вирази були у житті, ви могли б прокидатися не в «старому втомленому тілі», а в копії себе з налаштованим настроєм і більшими м’язами (але тільки якби ви були record).
4. Трохи про вкладеність і копіювання
Якщо record містить інші record — усе гаразд:
public record Address(string City, string Street);
public record Student(string Name, int Age, string Email, Address Home);
var a1 = new Address("Київ", "Хрещатик");
var s1 = new Student("Олена", 21, "olena@mail.ua", a1);
var s2 = s1 with { Home = a1 with { Street = "Січових Стрільців" } };
Тут усе працюватиме по-справжньому незмінно, адже вкладений Address — теж record.
5. Останні нюанси
Позиційні record = компактність
Record можна оголошувати у «короткій» формі (позиційний синтаксис). Тоді всі властивості автоматично отримують init-only.
public record Course(string Name, int Credits);
var c1 = new Course("C#", 5);
var c2 = c1 with { Credits = 6 };
Аналогія з властивостями лише для читання (init-only)
У record можна явно оголошувати властивості так:
public record Student
{
public string Name { get; init; }
public int Age { get; init; }
}
Такі властивості теж можна змінювати лише під час ініціалізації (або за допомогою with).
6. Практика: демо-застосунок
Напишімо нашу навчальну «онлайн-школу». Припустімо, у нас уже є record для студента:
public record Student(string Name, int Age, string Email);
Класика: хтось помилився в адресі електронної пошти, а студент уже створив обліковий запис. Як «оновити» email? Звісно, за допомогою with!
var student = new Student("Катерина", 19, "kate@school.com");
var updatedStudent = student with { Email = "kateryna@school.com" };
// Перевіримо об’єкти:
Console.WriteLine(student); // Student { Name = Катерина, Age = 19, Email = kate@school.com }
Console.WriteLine(updatedStudent); // Student { Name = Катерина, Age = 19, Email = kateryna@school.com }
7. Типові помилки та підводні камені
Тепер трохи про те, де студенти найчастіше помиляються під час роботи з незмінними record.
- По-перше, чимало хто вважає, що with змінює вихідний об’єкт. Насправді вихідний об’єкт не змінюється; новий створюється зі зміненими полями. Через це легко потрапити в пастку й втратити нові значення.
- По-друге, пам’ятайте: якщо всередині record є вкладені змінні об’єкти (наприклад, масив або List), то with-вираз не виконує глибокого копіювання! Ваша колекція залишиться спільною для обох копій.
public record Student(string Name, int Age, List<string> Subjects);
var s1 = new Student("Олег", 22, new List<string> { "Математика", "Фізика" });
var s2 = s1 with { };
s1.Subjects.Add("C#"); // Ого, тепер і s2.Subjects містить "C#"
Ось чому для справді незмінного стану краще використовувати лише прості типи або колекції, які самі по собі незмінні (ImmutableList<T> та інші з System.Collections.Immutable).
Якщо ви хочете гарантувати справжню незмінність, використовуйте ці колекції або виконуйте ручне глибоке копіювання.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ