Паттерны и 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.