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 неизменяемые records
| Класс | 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, "lena@mail.ru", 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 = "ekaterina@school.com" };
// Проверим объекты:
Console.WriteLine(student); // Student { Name = Екатерина, Age = 19, Email = kate@school.com }
Console.WriteLine(updatedStudent); // Student { Name = Екатерина, Age = 19, Email = ekaterina@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> { "Math", "Physics" });
var s2 = s1 with { };
s1.Subjects.Add("C#"); // Опа, теперь и s2.Subjects включает "C#"
Вот почему для truly immutable state лучше использовать только простые типы или коллекции, которые сами по себе неизменяемы (ImmutableList<T> и др. из System.Collections.Immutable).
Если вы хотите гарантировать настоящую неизменяемость, используйте эти коллекции или делайте ручное глубокое копирование.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ