Как сделать класс иммутабельным в Dart?

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

Ответ

Иммутабельный класс в Dart — это класс, состояние которого нельзя изменить после создания. Вот как его правильно реализовать:

Базовый иммутабельный класс:

class ImmutableUser {
  final String name;
  final int age;
  final List<String> roles;

  const ImmutableUser({
    required this.name,
    required this.age,
    List<String>? roles,
  }) : roles = List.unmodifiable(roles ?? []);

  @override
  bool operator ==(Object other) {
    return identical(this, other) ||
      other is ImmutableUser &&
      runtimeType == other.runtimeType &&
      name == other.name &&
      age == other.age &&
      const ListEquality().equals(roles, other.roles);
  }

  @override
  int get hashCode => name.hashCode ^ age.hashCode ^ roles.hashCode;

  @override
  String toString() => 'ImmutableUser(name: $name, age: $age, roles: $roles)';
}

Метод copyWith для создания модифицированных копий:

class ImmutableUser {
  // ... поля и конструктор из примера выше

  ImmutableUser copyWith({
    String? name,
    int? age,
    List<String>? roles,
  }) {
    return ImmutableUser(
      name: name ?? this.name,
      age: age ?? this.age,
      roles: roles ?? this.roles,
    );
  }
}

// Использование:
final user1 = ImmutableUser(name: 'Алексей', age: 30, roles: ['admin']);
final user2 = user1.copyWith(age: 31);
print(user1); // ImmutableUser(name: Алексей, age: 30, roles: [admin])
print(user2); // ImmutableUser(name: Алексей, age: 31, roles: [admin])

Иммутабельная коллекция с вложенными объектами:

import 'package:collection/collection.dart';

class ImmutableProject {
  final String id;
  final String title;
  final ImmutableUser owner;
  final List<ImmutableUser> members;

  const ImmutableProject({
    required this.id,
    required this.title,
    required this.owner,
    List<ImmutableUser>? members,
  }) : members = List.unmodifiable(members ?? []);

  ImmutableProject copyWith({
    String? id,
    String? title,
    ImmutableUser? owner,
    List<ImmutableUser>? members,
  }) {
    return ImmutableProject(
      id: id ?? this.id,
      title: title ?? this.title,
      owner: owner ?? this.owner,
      members: members ?? this.members,
    );
  }

  // Для глубокого сравнения используем DeepCollectionEquality
  @override
  bool operator ==(Object other) {
    return identical(this, other) ||
      other is ImmutableProject &&
      runtimeType == other.runtimeType &&
      id == other.id &&
      title == other.title &&
      owner == other.owner &&
      const DeepCollectionEquality().equals(members, other.members);
  }

  @override
  int get hashCode => Object.hash(id, title, owner, 
      const DeepCollectionEquality().hash(members));
}

Freezed package для автоматической генерации:

// pubspec.yaml: freezed: ^2.0.0
// pubspec.yaml: freezed_annotation: ^2.0.0

import 'package:freezed_annotation/freezed_annotation.dart';

part 'user.freezed.dart';
part 'user.g.dart';

@freezed
class User with _$User {
  const factory User({
    required String name,
    required int age,
    @Default([]) List<String> roles,
  }) = _User;

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}

// Freezed автоматически генерирует:
// - copyWith
// - toString
// - equals/hashCode
// - toJson/fromJson
// - конструктор const

Преимущества иммутабельных классов:

  1. Потокобезопасность — можно безопасно использовать в асинхронном коде
  2. Предсказуемость — состояние не меняется неожиданно
  3. Кэширование — const-конструкторы позволяют кэшировать экземпляры
  4. Отладка — проще отслеживать изменения через copyWith
  5. Совместимость с ValueNotifier/ChangeNotifier — иммутабельные объекты идеально подходят для реактивного программирования

Важные нюансы:

  • Используйте List.unmodifiable() или UnmodifiableListView для защиты коллекций
  • Для глубокой иммутабельности все вложенные объекты тоже должны быть иммутабельными
  • Const-конструкторы работают только если все поля final и сами являются compile-time константами