Что такое sealed class в Dart?

Ответ

Sealed class (запечатанный класс) в Dart — это класс, который явно ограничивает своё наследование. Все его прямые подтипы должны быть объявлены в том же файле (и библиотеке), что и сам sealed class. Это создаёт замкнутую иерархию типов, известную компилятору, что позволяет использовать исчерпывающий (exhaustive) pattern matching.

Основная цель: Моделирование дискретных, известных на этапе компиляции вариантов, таких как состояния загрузки, результаты операций или узлы AST.

Пример: Моделирование результата сетевого запроса

// Все возможные подтипы Result определены здесь и только здесь.
sealed class Result<T> {}

class Success<T> extends Result<T> {
  final T data;
  Success(this.data);
}

class Error<T> extends Result<T> {
  final Exception exception;
  Error(this.exception);
}

class Loading<T> extends Result<T> {}

// Использование с исчерпывающим switch
Widget buildResultWidget(Result<String> result) {
  return switch (result) {
    Success(data: final data) => Text('Данные: $data'),
    Error(exception: final e) => Text('Ошибка: ${e.toString()}'),
    Loading() => CircularProgressIndicator(),
    // Компилятор знает, что все случаи покрыты. Если добавить новый подтип Result,
    // switch выдаст ошибку компиляции, пока его не обработать.
  };
}

Преимущества:

  • Безопасность: Компилятор гарантирует обработку всех вариантов в switch и if-case.
  • Читаемость: Структура данных явно документирована в коде.
  • Удобство рефакторинга: При добавлении нового варианта компилятор укажет все места, которые нужно обновить.

Ответ 18+ 🔞

Э, слушай, а вот это sealed class в Dart — это вообще огонь, я тебе скажу. Представь себе такую ситуацию: у тебя есть какой-то тип, и ты заранее, как бог, знаешь, какие у него могут быть варианты. И ты говоришь компилятору: «Смотри, ёпта, вот этот класс — он запечатанный. Все его дети будут только вот в этом файле, и больше нихуя нигде». И компилятор тебе верит, потому что доверия ебать ноль ко всем остальным, и начинает мозговать.

Зачем это, спросишь? Ну, чтобы не было этих вечных «а вдруг кто-то в другом углу проекта наследуется и всё сломает». Ты моделируешь чёткие, дискретные состояния. Типа, результат запроса: либо успех, либо ошибка, либо грузится. Или узлы какого-нибудь дерева разбора. Всё просто и ясно, как божий день.

Смотри, как это выглядит на практике:

// Всё, приехали. Все наследники Result живут тут и только тут.
sealed class Result<T> {}

class Success<T> extends Result<T> {
  final T data;
  Success(this.data);
}

class Error<T> extends Result<T> {
  final Exception exception;
  Error(this.exception);
}

class Loading<T> extends Result<T> {}

Красота, да? Три варианта, и всё. Теперь самое вкусное — использование. Берёшь switch и просто пишешь:

Widget buildResultWidget(Result<String> result) {
  return switch (result) {
    Success(data: final data) => Text('Данные: $data'),
    Error(exception: final e) => Text('Ошибка: ${e.toString()}'),
    Loading() => CircularProgressIndicator(),
    // Компилятор тут такой: «О, я всё вижу, всё случаи покрыты. Молодец, чувак».
    // А если ты завтра добавишь новый подтип Result, он тебе сразу вьебет ошибку компиляции,
    // пока не обработаешь и его. Никаких сюрпризов в рантайме!
  };
}

В чём, блядь, соль-то?

  • Безопасность на уровне компилятора. Это не тот случай, когда «ой, забыл кейс обработать». Компилятор сам тебе мозги поправит. Волнение ебать — нулевое.
  • Читаемость пиздец. Открыл файл — и сразу видишь всю иерархию, как на ладони. Никаких «а этот класс откуда вылез, ядрёна вошь».
  • Рефакторинг — одно удовольствие. Захотел добавить новый вариант типа Partial<T>? Добавляй. Компилятор тут же начнёт орать на всех switch, где его нет, и ты спокойно всё допишешь. Удивление пиздец, как же раньше без этого жили.

В общем, вещь. Когда нужно жёстко ограничить возможные состояния — это твой выбор. Чисто, надёжно, и компилятор за тебя думает. Э сабака сука, удобно же!