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

«Что такое иммутабельный (неизменяемый) класс?» — вопрос из категории ООП, который задают на 29% собеседований Flutter Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Иммутабельный (неизменяемый) класс — это класс, экземпляры которого не могут изменять своё состояние после создания. Все его поля объявлены как 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 часто полагаются на иммутабельные классы для представления состояний, что облегчает их сравнение (==) и триггерит перерисовку виджетов только при реальном изменении данных.