Menu
Coddy logo textTech

Запечатанные классы (Dart 3)

Часть раздела Объектно-ориентированное программирование путешествия по Dart на Coddy — урок 85 из 110.

В Dart 3 появились запечатанные классы, которые ограничивают список классов, способных расширять или реализовывать их. Когда вы помечаете класс как sealed, только классы в том же файле могут быть его прямыми подтипами. Это создает закрытый набор известных подтипов.

sealed class Result {}

class Success extends Result {
  final String data;
  Success(this.data);
}

class Failure extends Result {
  final String error;
  Failure(this.error);
}

Сила изолированных классов (sealed classes) заключается в проверке на полноту (exhaustiveness checking). Когда вы используете switch для изолированного типа, компилятор знает все возможные подтипы и гарантирует, что вы обработаете каждый из них:

String handleResult(Result result) {
  return switch (result) {
    Success(data: var d) => 'Got: $d',
    Failure(error: var e) => 'Error: $e',
  };
  // Значение по умолчанию не требуется — компилятор знает, что все случаи учтены!
}

void main() {
  var success = Success('Hello');
  var failure = Failure('Not found');
  
  print(handleResult(success));  // Got: Hello
  print(handleResult(failure));  // Error: Not found
}

Если вы добавите новый подтип в запечатанный класс (sealed class), компилятор пометит каждый оператор switch, который его не обрабатывает. Это делает запечатанные классы идеальными для моделирования состояний, результатов или любого сценария, где имеется фиксированный набор вариантов. В отличие от абстрактных классов, запечатанные классы гарантируют, что никакой внешний код не сможет добавить непредвиденные подтипы.

challenge icon

Задание

Легко

Давайте создадим обработчик сетевых запросов, используя sealed-классы! Вы создадите типизированную систему, которая моделирует различные состояния сетевого ответа, гарантируя правильную обработку каждого возможного результата.

Вы организуете свой код в двух файлах:

  • response.dart: Создайте иерархию sealed-классов, которая представляет все возможные состояния сетевого ответа:
    • Sealed-класс с именем NetworkResponse, который служит базой для всех типов ответов
    • Класс Loading, который расширяет NetworkResponse и содержит поле String message (например, "Fetching data...")
    • Класс Success, который расширяет NetworkResponse и содержит поле String data и поле int statusCode
    • Класс Error, который расширяет NetworkResponse и содержит поле String errorMessage и поле int errorCode
  • main.dart: Импортируйте файл с ответами и создайте функцию, которая обрабатывает все типы ответов с помощью выражения switch:
    • Создайте функцию handleResponse(NetworkResponse response), которая возвращает String
    • Используйте выражение switch с сопоставлением с образцом (pattern matching) для обработки каждого случая:
      • Для Loading: верните Status: [message]
      • Для Success: верните Success ([statusCode]): [data]
      • Для Error: верните Error ([errorCode]): [errorMessage]
    • Создайте три экземпляра ответов и выведите результат обработки каждого:
      • Loading с сообщением Please wait...
      • Success с данными User profile loaded и кодом состояния 200
      • Error с сообщением об ошибке Not found и кодом ошибки 404

Прелесть sealed-классов заключается в том, что компилятор гарантирует обработку каждого возможного типа ответа — ветка по умолчанию (default) не требуется!

Ожидаемый вывод:

Status: Please wait...
Success (200): User profile loaded
Error (404): Not found

Шпаргалка

Запечатанные (sealed) классы ограничивают список классов, которые могут их расширять или реализовывать. Только классы в том же файле могут быть прямыми подтипами, создавая закрытый набор известных вариантов.

sealed class Result {}

class Success extends Result {
  final String data;
  Success(this.data);
}

class Failure extends Result {
  final String error;
  Failure(this.error);
}

Запечатанные классы позволяют выполнять проверку на исчерпываемость (exhaustiveness checking) в выражениях switch. Компилятор знает все возможные подтипы и гарантирует, что вы обработаете каждый из них:

String handleResult(Result result) {
  return switch (result) {
    Success(data: var d) => 'Got: $d',
    Failure(error: var e) => 'Error: $e',
  };
  // No default case needed - all cases are covered
}

Если вы добавите новый подтип, компилятор пометит каждый оператор switch, который его не обрабатывает. Это делает запечатанные классы идеальными для моделирования состояний, результатов или любого фиксированного набора вариантов, где вам нужны гарантии времени компиляции того, что все случаи обработаны.

Попробуйте сами

import 'response.dart';

// TODO: Создайте функцию handleResponse(NetworkResponse response),
// которая возвращает String
// Используйте выражение switch с сопоставлением с образцом для обработки каждого случая:
// - Для Loading: вернуть "Status: [message]"
// - Для Success: вернуть "Success ([statusCode]): [data]"
// - Для Error: вернуть "Error ([errorCode]): [errorMessage]"

void main() {
  // TODO: Создайте экземпляр Loading с сообщением "Please wait..."
  
  // TODO: Создайте экземпляр Success с данными "User profile loaded" и кодом состояния 200
  
  // TODO: Создайте экземпляр Error с сообщением об ошибке "Not found" и кодом ошибки 404
  
  // TODO: Выведите результат обработки каждого ответа
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование