Ответ
Иммутабельный (неизменяемый) класс — это класс, экземпляры которого не могут изменять своё состояние после создания. Все его поля объявлены как final, и он не предоставляет никаких методов, которые могли бы изменить значения этих полей.
Принципы создания иммутабельного класса в Dart:
- Все поля —
final: Это гарантирует, что поле может быть присвоено только один раз — в конструкторе. - Конструктор может быть
const: Если все поля final и конструктор помечен какconst, это позволяет компилятору создавать canonicalized (канонические) экземпляры, что экономит память и улучшает производительность. - Отсутствие сеттеров и мутирующих методов: Нет методов типа
setName(). - Защита от изменяемых полей: Если класс содержит коллекции или другие объекты, необходимо обеспечить, чтобы и они были иммутабельны, или возвращать их защищённые копии.
Пример иммутабельного класса в Dart:
import 'package:collection/collection.dart'; // Для глубокого сравнения и копирования
class ImmutablePerson {
final String name;
final int age;
final List<String> hobbies; // Ссылка на изменяемый список!
// Конструктор с const возможен, если все поля final и примитивны/иммутабельны.
// Список hobbies делает const конструктор невозможным в данном виде.
ImmutablePerson(this.name, this.age, [List<String>? hobbies])
: hobbies = List.unmodifiable(hobbies ?? []); // Защитная обёртка
// Метод для "изменения" состояния, возвращающий новый экземпляр
ImmutablePerson copyWith({String? name, int? age, List<String>? hobbies}) {
return ImmutablePerson(
name ?? this.name,
age ?? this.age,
hobbies ?? this.hobbies,
);
}
@override
String toString() => 'Person(name: $name, age: $age, hobbies: $hobbies)';
}
void main() {
// Создание экземпляра
var person1 = ImmutablePerson('Alice', 30, ['reading', 'hiking']);
print(person1); // Person(name: Alice, age: 30, hobbies: [reading, hiking])
// Попытка изменить список hobbies извне не сработает
// person1.hobbies.add('gaming'); // Throws: Unsupported operation: Cannot add to an unmodifiable list
// "Изменение" через создание новой копии (паттерн copy-with)
var person2 = person1.copyWith(age: 31);
print(person2); // Person(name: Alice, age: 31, hobbies: [reading, hiking])
print(identical(person1, person2)); // false (это разные объекты)
}
Преимущества иммутабельности в контексте Flutter/Dart:
- Потокобезопасность: Иммутабельные объекты можно свободно передавать между изолятами (isolates) без синхронизации.
- Предсказуемость: Состояние объекта не может неожиданно измениться, что упрощает отладку и рассуждение о коде.
- Кэширование и оптимизация:
const-конструкторы позволяют компилятору кэшировать и повторно использовать экземпляры. - Ключ для состояний: Идеально подходят для использования в качестве ключей в
Mapили значений вSet, так как ихhashCodeне меняется. - Состояние в управлении состоянием: Библиотеки вроде
flutter_blocилиriverpodчасто полагаются на иммутабельные классы для представления состояний, что облегчает их сравнение (==) и триггерит перерисовку виджетов только при реальном изменении данных.
Ответ 18+ 🔞
Э, слушай, вот тебе история про такие классы, которые как монахи — дали обет неизменности и держат его до конца. Иммутабельный класс, ёпта. Это когда ты создал объект, а он тебе такой: "Всё, братан, я законченный персонаж. Хочешь что-то поменять — создавай нового меня, а этот я останусь таким, каким ты меня задумал". Красота, да?
Представь себе, ты написал класс ImmutablePerson. И все его поля — final. Это как пригвоздить их гвоздями к полу. Один раз присвоил в конструкторе — и всё, пиши пропало, больше туда не подступишься. Никаких сеттеров, никаких методов вроде setName(), которые бы эту святыню нарушали. Чистота, блядь.
А теперь смотри, где собака зарыта. Допустим, у тебя в этом святом классе есть поле List<String> hobbies. Список, сука. А список по умолчанию — штука изменяемая, хитрая жопа. Ты сделал поле final, значит, ссылку на список менять нельзя. Но сам-то список, на который эта ссылка указывает, — его можно спокойно add()-ами испоганить извне! И твой "иммутабельный" объект нихуя не иммутабельный, а просто пиздопроебибна какая-то.
Так как же делать по-человечески? Вот принципы, ёб твою мать:
- Все поля —
final. Это база, без этого нихуя не получится. - Конструктор можно делать
const. Но только если все поля не простоfinal, а ещё и примитивы (типаint,String) или тоже иммутабельные объекты. Если там затесался изменяемый список — забудь проconst, не выйдет. - Никаких мутирующих методов. Только геттеры да методы, которые возвращают новый объект.
- Защита от изменяемых полей — это святое. Если внутри есть список или мапа, ты должен либо обернуть её в
List.unmodifiable(...), либо возвращать копию, когда её запрашивают. Чтобы снаружи не могли нашкодить.
Вот, глянь, как это выглядит в коде. Блоки кода не трогаю, они священны.
import 'package:collection/collection.dart';
class ImmutablePerson {
final String name;
final int age;
final List<String> hobbies; // Опа-на, список!
// Конструктор. Видишь `const`? Нету! Потому что список — манда с ушами, изменяемая.
// Но мы его сразу заворачиваем в непробиваемую броню.
ImmutablePerson(this.name, this.age, [List<String>? hobbies])
: hobbies = List.unmodifiable(hobbies ?? []); // Вот! Теперь снаружи не добавишь и не удалишь нихуя.
// Хочешь "поменять" возраст? Не вопрос! Но мы не меняем этот объект.
// Мы создаём НОВЫЙ, с теми же данными, кроме тех, что ты указал.
// Это паттерн "copy-with", и он охуенно удобен.
ImmutablePerson copyWith({String? name, int? age, List<String>? hobbies}) {
return ImmutablePerson(
name ?? this.name,
age ?? this.age,
hobbies ?? this.hobbies,
);
}
@override
String toString() => 'Person(name: $name, age: $age, hobbies: $hobbies)';
}
void main() {
// Создаём объект
var person1 = ImmutablePerson('Alice', 30, ['reading', 'hiking']);
print(person1);
// Пробуем нахулиганить извне. Не выйдет, получим по рукам!
// person1.hobbies.add('gaming'); // Выбросит исключение: Unsupported operation
// Вместо этого делаем "изменение" — создаём копию с новым возрастом.
var person2 = person1.copyWith(age: 31);
print(person2);
print(identical(person1, person2)); // false — это вообще два разных объекта, ёпта!
}
И зачем весь этот цирк, спросишь? А преимуществ — овердохуища:
- Потокобезопасность на раз. Кидай эти объекты между изолятами (isolates) — и ни одного мьютекса не понадобится. Они же неизменны, с ними нихуя не случится.
- Предсказуемость. Состояние не может внезапно поменяться у тебя за спиной. Отладка превращается из адской пытки в почти медитативный процесс.
- Оптимизация. Если бы у нас тут не было списка, можно было бы сделать
constконструктор. Тогда компилятор кэшировал бы одинаковые объекты и не плодил лишних, экономил память. - Идеально для состояния. Все эти модные библиотеки для управления состоянием во Flutter —
bloc,riverpod— они обожают иммутабельные классы. Потому что сравнить два состояния (==) — дело двух секунд, и перерисовка виджетов сработает только когда данные реально поменялись, а не когда кто-то внутри объекта пошевелил булками.
В общем, учи матчасть. Сначала кажется, что возни много, но потом, когда приложение не глючит из-за случайного изменения состояния где-то не там, понимаешь — оно того стоит. Сам от себя охуеешь, насколько проще жить.