Перейти к основному содержимому

Паттерны и switch в Dart 3

Разработчику Архитектору

О чём эта статья

Dart 3 — сопоставление с образцом — switch как выражение, record, списки, sealed class. Компилятор проверяет исчерпывающие ветки.

Опора: типы, управление.


Паттерны и switch в Dart 3

Начиная с Dart 3, оператор switch перестал быть только "выбором по константе". Он стал частью системы сопоставления с образцом (pattern matching) — можно разбирать значения по форме — константа, тип, поля record, элементы списка, ключи словаря. Компилятор проверяет исчерпывающий разбор для sealed-иерархий и расширенных enum.

Связанные темы: управляющие конструкции, типы и record, классы и ООП.


Switch statement и switch expression

Оператор switch выполняет ветку и завершает её без break (в отличие от C/Java fall-through запрещён):

enum Status { pending, done, failed }

String label(Status s) {
switch (s) {
case Status.pending:
return 'В очереди';
case Status.done:
return 'Готово';
case Status.failed:
return 'Ошибка';
}
}

Разбор:

  • enum Status задаёт закрытый набор возможных состояний.
  • В switch (s) каждая ветка case возвращает человекочитаемую подпись для конкретного состояния.
  • В Dart fall-through между ветками запрещён, поэтому каждая ветка самостоятельна и безопасна.
  • Такой подход удобен для отображения статусов в UI и логах.

Выражение switch возвращает значение — удобно присваивать результат:

String label(Status s) => switch (s) {
Status.pending => 'В очереди',
Status.done => 'Готово',
Status.failed => 'Ошибка',
};

Разбор:

  • Это switch expression: выражение сразу вычисляет строку и возвращает её.
  • Стрелочная функция => делает код компактным для простой логики сопоставления.
  • Каждая ветка имеет форму паттерн => результат, поэтому структура хорошо читается сверху вниз.
  • Формат особенно удобен, когда нужно присвоить результат переменной без лишнего return-блока.

Обе формы требуют, чтобы каждая ветка завершалась значением (в expression) или управляющим переходом (в statement).


Сопоставление с константами и типами

Паттерн может быть литералом, константой или проверкой типа:

String describe(Object value) => switch (value) {
0 => 'ноль',
int n when n < 0 => 'отрицательное: $n',
int n => 'целое: $n',
String s when s.isEmpty => 'пустая строка',
String s => 'строка: $s',
_ => 'что-то ещё',
};

Разбор:

  • Входной параметр Object value допускает значения разных типов.

  • Паттерны int n и String s одновременно проверяют тип и связывают значение с локальной переменной.

  • when ... добавляет guard-условие для более точной фильтрации внутри уже совпавшего паттерна.

  • _ => 'что-то ещё' служит универсальной веткой по умолчанию для всех прочих случаев.

  • when — дополнительное условие (guard) после сопоставления формы.

  • _ — подстановочный паттерн "любое значение", обычно в последней ветке.

Оператор is внутри switch по-прежнему работает в обычном коде; в Dart 3 типовой паттерн int n одновременно проверяет тип и вводит переменную.


Record и именованные поля

Record удобно разбирать по позиции или по именам:

Код ITЗагрузка примера кода…

Разбор:

  • parseId возвращает record (String, int), где первый элемент — имя, второй — возраст.
  • В switch (pair) паттерны разбирают запись по позициям — ('', _), (var name, var age).
  • when age < 0 добавляет валидацию возраста как guard для отдельной ветки. См. Проверка и валидация.
  • Блок demo показывает именованный record (name: ..., score: ...) и доступ к полям через паттерн по именам.
  • Конструкция (name: var n, score: var s) связывает значения полей с локальными переменными n и s.

Синтаксис (name: var n, score: var s) связывает именованные поля record с локальными переменными в ветке.


Списки и Map

Список — по длине и элементам:

String summarize(List<int> data) => switch (data) {
[] => 'нет данных',
[var only] => 'одно значение: $only',
[var first, var second, ...] => 'начало: $first, $second, всего ${data.length}',
};

Разбор:

  • [] — паттерн пустого списка.
  • [var only] — список ровно из одного элемента.
  • [var first, var second, ...] — список длиной минимум два, где ... захватывает "хвост".
  • Такой разбор длины и структуры списка делает ветвление компактным без ручных if (data.length ...).

... — rest-паттерн: "остальные элементы" (имя после ... опционально).

Map — по набору ключей:

String role(Map<String, String> user) => switch (user) {
{'role': 'admin', 'name': var n} => 'администратор $n',
{'role': var r, 'name': var n} => '$n ($r)',
_ => 'неизвестный профиль',
};

Разбор:

  • Паттерн {'role': 'admin', 'name': var n} сопоставляет карту с конкретными ключами и значениями.
  • var n извлекает имя пользователя в локальную переменную ветки.
  • Вторая ветка ловит любые другие роли через var r.
  • _ — запасная ветка для карт без ожидаемой структуры.

Порядок веток важен: более конкретные паттерны должны идти раньше общих.


Sealed class и исчерпывающий switch

sealed class ограничивает наследников одним файлом (или библиотекой). Компилятор знает полный набор подтипов и требует обработать каждый в switch:

Код ITЗагрузка примера кода…

Разбор:

  • sealed class Result<T> закрывает иерархию: новые подтипы можно добавить только в этом файле.
  • Ok и Err — конкретные варианты результата операции.
  • В switch паттерны Ok(value: var v) и Err(message: var m) разбирают поля подтипов.
  • Компилятор проверяет исчерпываемость: если забыть ветку, анализатор предупредит.

Если добавить новый подкласс Result и забыть ветку в switch, анализатор выдаст предупреждение. Тот же приём применяют для enhanced enum с полями и для иерархий состояний UI (загрузка / данные / ошибка).


Объекты и деструктуризация

Для обычных классов согласованность паттерна зависит от того, поддерживает ли тип сопоставление (часто через enum или record внутри). Практичный приём — хранить варианты в sealed + final class или возвращать record из функции:

({bool ok, String text}) load() {
// ...
return (ok: true, text: 'готово');
}

void handle() {
switch (load()) {
case (ok: true, text: var t):
print(t);
case (ok: false, text: var e):
print('сбой: $e');
}
}

Разбор:

  • ({bool ok, String text}) — именованный record, который функция load() возвращает как результат.
  • switch (load()) сразу разбирает форму данных без цепочки if/else.
  • Ветка (ok: true, text: var t) обрабатывает успешный сценарий.
  • Ветка (ok: false, text: var e) обрабатывает ошибку и печатает текст сбоя.

Связь с null safety

Паттерны учитывают nullable-типы. Сопоставление case null: отделяет отсутствие значения; для полей record типа String? guard when помогает сузить тип:

String? title;

final display = switch (title) {
null => '(без названия)',
var t when t.length > 40 => '${t.substring(0, 40)}…',
var t => t,
};

Разбор:

  • String? title допускает отсутствие значения.
  • case null: обрабатывает пустой заголовок отдельной веткой.
  • when t.length > 40 добавляет guard для длинных строк.
  • Последняя ветка var t => t возвращает исходный текст без изменений.

Операторы ?., ?? из статьи про операторы остаются для простых случаев; switch с паттернами уместен, когда вариантов несколько и логика ветвления объёмная.


Когда что выбирать

ЗадачаИнструмент
Два–три исхода, одно выражениеТернарный ? : или ??
Enum или sealed-иерархияswitch expression
Разбор JSON-подобной структуры (record / Map)Паттерны в switch
Длинная цепочка if / else if по типуswitch с типовыми паттернами

Паттерны не заменяют полиморфизм методов — поведение, привязанное к классу, по-прежнему оформляют методами и миксинами. switch уместен там, где нужно явно перечислить формы данных на границе слоя (парсинг, маппинг API, reducer состояния).


Практическая заметка для Flutter

В UI часто встречается разбор AsyncSnapshot, union-состояний загрузки или кодов ответа API. switch expression с sealed типами делает дерево виджетов читаемым: каждая ветка — отдельный подвиджет без вложенных if. Подробнее о приложениях — в статье про Flutter.