Что такое иммутабельный (неизменяемый) класс?

Ответ

Иммутабельный (неизменяемый) класс — это класс, экземпляры которого не могут изменять своё состояние после создания. Все его поля объявлены как final, и он не предоставляет никаких методов, которые могли бы изменить значения этих полей.

Принципы создания иммутабельного класса в Dart:

  1. Все поля — final: Это гарантирует, что поле может быть присвоено только один раз — в конструкторе.
  2. Конструктор может быть const: Если все поля final и конструктор помечен как const, это позволяет компилятору создавать canonicalized (канонические) экземпляры, что экономит память и улучшает производительность.
  3. Отсутствие сеттеров и мутирующих методов: Нет методов типа setName().
  4. Защита от изменяемых полей: Если класс содержит коллекции или другие объекты, необходимо обеспечить, чтобы и они были иммутабельны, или возвращать их защищённые копии.

Пример иммутабельного класса в 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()-ами испоганить извне! И твой "иммутабельный" объект нихуя не иммутабельный, а просто пиздопроебибна какая-то.

Так как же делать по-человечески? Вот принципы, ёб твою мать:

  1. Все поля — final. Это база, без этого нихуя не получится.
  2. Конструктор можно делать const. Но только если все поля не просто final, а ещё и примитивы (типа int, String) или тоже иммутабельные объекты. Если там затесался изменяемый список — забудь про const, не выйдет.
  3. Никаких мутирующих методов. Только геттеры да методы, которые возвращают новый объект.
  4. Защита от изменяемых полей — это святое. Если внутри есть список или мапа, ты должен либо обернуть её в 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 — они обожают иммутабельные классы. Потому что сравнить два состояния (==) — дело двух секунд, и перерисовка виджетов сработает только когда данные реально поменялись, а не когда кто-то внутри объекта пошевелил булками.

В общем, учи матчасть. Сначала кажется, что возни много, но потом, когда приложение не глючит из-за случайного изменения состояния где-то не там, понимаешь — оно того стоит. Сам от себя охуеешь, насколько проще жить.