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

Паттерны и 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 'Ошибка';
}
}

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

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

Обе формы требуют, чтобы каждая ветка завершалась значением (в 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',
_ => 'что-то ещё',
};
  • when — дополнительное условие (guard) после сопоставления формы.
  • _ — подстановочный паттерн «любое значение», обычно в последней ветке.

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


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

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

(String, int) parseId(String raw) {
final parts = raw.split(':');
return (parts[0], int.parse(parts[1]));
}

String format((String, int) pair) => switch (pair) {
('', _) => 'пустой ключ',
(var name, var age) when age < 0 => 'некорректный возраст для $name',
(var name, var age) => '$name — $age лет',
};

void demo() {
final named = (name: 'Анна', score: 90);
final msg = switch (named) {
(name: 'Анна', score: >= 85) => 'отлично, $name',
(name: var n, score: var s) => '$n: $s баллов',
};
print(msg);
}

Синтаксис (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}',
};

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

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

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

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


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

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

sealed class Result<T> {}

final class Ok<T> extends Result<T> {
final T value;
Ok(this.value);
}

final class Err<T> extends Result<T> {
final String message;
Err(this.message);
}

String explain(Result<int> r) => switch (r) {
Ok(value: var v) => 'успех: $v',
Err(message: var m) => 'ошибка: $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');
}
}

Связь с 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,
};

Операторы ?., ?? из статьи про операторы остаются для простых случаев; 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.


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).