Records, patterns и sealed-классы Dart 3

Главные новинки Dart 3 — records, паттерны и sealed-классы — делают код короче, безопаснее и выразительнее.

Суть: records возвращают несколько значений без отдельного класса, паттерны (patterns) разбирают данные на части одной строкой, а sealed-классы дают компилятору проверять, что вы обработали все варианты.

Паттерны работают не только в присваивании, но и в условиях через конструкцию if-case. Запись if (json case {'name': String name}) { ... } одновременно проверяет, что в Map есть ключ name со строковым значением, и сразу извлекает его в переменную. Это резко сокращает разбор данных из сети, где раньше требовалась цепочка проверок типов и приведений. Паттерны делают такой код и короче, и безопаснее.

Раньше, чтобы вернуть из функции два значения, приходилось городить отдельный класс или возвращать список и помнить, что под каким индексом. Dart 3 решает это records — лёгкими кортежами с именованными или позиционными полями. А паттерны позволяют тут же разложить record на переменные.

// record как тип возврата: координаты
(int, int) findPosition() => (10, 20);

// деструктуризация через pattern:
final (x, y) = findPosition();
print('x=$x y=$y');     // x=10 y=20

// record с именованными полями:
({String name, int age}) getUser() => (name: 'Аня', age: 20);
final user = getUser();
print(user.name);       // Аня

Круглые скобки (10, 20) создают record, а final (x, y) = ... — это паттерн, который мгновенно раскладывает его по переменным. Никаких индексов и классов ради двух чисел.

Как работают sealed-классы под капотом

Sealed-класс — это запечатанный класс: все его наследники должны лежать в том же файле. Это даёт компилятору суперсилу: он знает все возможные варианты и в switch требует обработать каждый. Если вы добавите новый подтип и забудете его в switch — код не скомпилируется. Это идеально для моделирования состояний экрана: «загрузка», «успех», «ошибка».

              sealed class Result
             /        |         \
        Loading    Success     Failure     все подтипы известны
                                            компилятору

   switch (result) {
     Loading() => spinner,
     Success(:final data) => show(data),   деструктуризация в switch
     Failure(:final msg)  => error(msg),
   }
   // забыли вариант? -> ОШИБКА компиляции (исчерпывающесть)
sealed class Result {}
class Loading extends Result {}
class Success extends Result { final String data; Success(this.data); }
class Failure extends Result { final String msg;  Failure(this.msg); }

String render(Result r) => switch (r) {
  Loading()             => 'Загрузка...',
  Success(:final data)  => 'Данные: $data',
  Failure(:final msg)   => 'Ошибка: $msg',
};

Запись Success(:final data) — это паттерн с извлечением поля: он одновременно проверяет тип и достаёт значение data. Так состояние экрана превращается в интерфейс безопасно и без единого if-is.

# Аналог sealed + switch на Python: реестр состояний экрана
def render(result):
    kind = result['kind']
    if kind == 'loading':
        return 'Загрузка...'
    elif kind == 'success':
        return f"Данные: {result['data']}"
    elif kind == 'failure':
        return f"Ошибка: {result['msg']}"
    else:
        raise ValueError('необработанный вариант: ' + kind)

print(render({'kind': 'loading'}))
print(render({'kind': 'success', 'data': '42 поста'}))
print(render({'kind': 'failure', 'msg': 'нет сети'}))

Частые ошибки

  • Класть наследников sealed-класса в другой файл — это запрещено, вся защита держится на том, что варианты собраны вместе.
  • Добавлять default в switch по sealed-классу без нужды — он гасит проверку исчерпывающести.
  • Путать record и класс. Record — лёгкий безымянный кортеж; для богатой логики всё же нужен класс.

Best practices

  • Используйте records для функций, которым нужно вернуть пару-тройку значений, — без церемоний с классами.
  • Моделируйте состояния (loading/success/error) sealed-классами — это устранит целый класс багов «забыл обработать вариант».
  • Применяйте паттерны в switch для одновременной проверки типа и извлечения данных.

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

Итог: records, паттерны и sealed-классы — это лицо современного Dart 3. Они особенно сильны в управлении состоянием Flutter-экранов, где важно гарантированно обработать все ситуации. Запомните связку sealed + switch — вернёмся к ней в разделе про состояние.

Проверьте себя
1. Зачем нужны records в Dart 3?
AЧтобы хранить только числа
BЧтобы вернуть несколько значений без создания отдельного класса
CЧтобы заменить циклы
DЧтобы рисовать виджеты
2. Какое главное преимущество sealed-класса в switch?
AОн работает быстрее
BКомпилятор требует обработать все возможные подтипы (исчерпывающесть)
CОн не требует наследников
DОн отключает null-safety