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

Сопоставление с образцом в Swift

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

Что такое pattern matching

Сопоставление с образцом — способ проверить значение и одновременно извлечь из него части — связать переменные, сравнить с константой, отфильтровать диапазон. В Swift это поведение switch, if, guard, for-in и присваивания.

Главная польза для прикладного кода:

Базовый switch уже разобран в управляющих конструкциях; здесь — расширенные приёмы.


switch без провала между case

В Swift нет неявного fallthrough между case (если не указан fallthrough явно). Каждый case завершается сам по себе — это убирает класс ошибок из C/Java.

Исчерпывающность: для enum без default компилятор проверяет, что обработаны все случаи. При добавлении нового case в enum проект не соберётся, пока вы не обновите switch — защита при эволюции API.

enum Status {
case idle, loading, ready, failed
}
func label(for status: Status) -> String {
switch status {
case .idle: return "Ожидание"
case .loading: return "Загрузка"
case .ready: return "Готово"
case .failed: return "Ошибка"
}
}

Разбор:

  • enum Status задаёт фиксированный набор допустимых состояний.
  • Функция label(for:) возвращает строку для каждого case через switch.
  • Отсутствие default заставляет компилятор проверить полноту обработки всех вариантов.
  • Такой подход делает изменения enum безопасными: новый case потребует обновить обработчик.

Связка значений и where

В case можно привязать части значения к локальным константам:

let point = (x: 3, y: 4)
switch point {
case (0, 0):
print("начало координат")
case let (x, 0):
print("на оси X: \(x)")
case let (0, y):
print("на оси Y: \(y)")
case let (x, y) where x == y:
print("диагональ (\(x), \(y))")
default:
print("обычная точка")
}

Разбор:

  • point — кортеж с именованными полями x и y.
  • В switch используются шаблоны — точное совпадение, захват значений через let, и default.
  • Конструкция where x == y добавляет дополнительное условие после успешного сопоставления.
  • Пример показывает, как pattern matching одновременно проверяет форму данных и извлекает полезные части.

where добавляет произвольное условие к уже сопоставленному шаблону.


Enum с associated values

Перечисление может нести данные в каждом случае — удобная модель "результат операции":

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

Разбор:

  • LoadState хранит не только статус, но и связанные данные (progress, data, message).
  • В switch значения associated values извлекаются через let.
  • String(format: ...) форматирует прогресс в проценты, а data.count даёт размер полученных данных.
  • Модель состояния в виде enum помогает избежать разрозненных флагов и противоречивых комбинаций.

Один switch заменяет цепочку if let + проверок типа.


Опционал как enum

Optional — это enum с .none и .some(Wrapped). Поэтому к опционалам применимы те же приёмы.


if let / guard let

Самый частый паттерн — распаковка с узкой областью видимости (if let) или ранний выход (guard let). См. типы данных.

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

let response: LoadState = .loaded(data: Data())
if case .loaded(let data) = response {
process(data)
}

Разбор:

  • if case проверяет один конкретный шаблон без полного switch.
  • При совпадении case .loaded переменная data автоматически извлекается из associated value.
  • Такой синтаксис компактен для точечных проверок конкретного состояния.

Аналог "если статус именно loaded — достать data".


guard case let

Тот же смысл с ранним выходом:

func requireData(from state: LoadState) throws -> Data {
guard case .loaded(let data) = state else {
throw AppError.notReady
}
return data
}

Разбор:

  • guard case проверяет нужный шаблон и обеспечивает ранний выход при несоответствии.
  • В ветке else выбрасывается доменная ошибка AppError.notReady.
  • После guard компилятор знает, что data доступна, и функция возвращает её без дополнительных проверок.

switch по опционалу

let code: Int? = 404
switch code {
case .none:
print("код не задан")
case .some(200):
print("OK")
case .some(let c) where c >= 400:
print("ошибка \(c)")
default:
print("другой код")
}

Разбор:

  • Int? сопоставляется как enum Optional: .none или .some(...).
  • Конкретный статус 200 выделен отдельной веткой для приоритетной обработки.
  • where c >= 400 фильтрует диапазон ошибок, одновременно сохраняя код в переменной c.
  • Пример полезен, когда нужно выразить несколько бизнес-веток для optional-значения.

На практике для опционалов чаще if let / ?? / ?.; switch уместен, когда вариантов много.


Сопоставление в цикле for-in

Фильтрация с распаковкой:

let states: [LoadState] = [.idle, .loading(progress: 0.5), .failed(message: "timeout")]
for case .loading(let p) in states {
print("прогресс \(p)")
}

Разбор:

  • for case объединяет фильтрацию и итерацию: цикл заходит только в элементы, подходящие под шаблон .loading.
  • Значение progress извлекается в p и сразу используется в теле цикла.
  • Такой стиль убирает лишние if case внутри обычного for.

Обрабатываются только элементы, подходящие под шаблон.


Result и цепочка ошибок

Result<Success, Failure> — enum для успеха/неудачи. Его разбирают тем же switch:

func handle(_ result: Result<User, NetworkError>) {
switch result {
case .success(let user):
showProfile(user)
case .failure(let error):
showAlert(error.localizedDescription)
}
}

Разбор:

  • Result<User, NetworkError> явно кодирует два исхода: успех с User или ошибка NetworkError.
  • В switch для .success и .failure извлекаются значения и передаются в соответствующие UI-функции.
  • Это типобезопасная альтернатива неявным флагам или nullable-данным для обработки ответа.

Связка с обработкой ошибок: throws + do/catch для исключений, Result — для явного значения в типе возврата.


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

ЗадачаИнструмент
Все варианты enumswitch без default
Один конкретный caseif case let / guard case let
Есть / нет значенияif let, guard let, ??
Фильтр в коллекцииfor case, compactMap
Ошибка как значениеResult + switch

Типичные ошибки

  1. default там, где enum фиксирован — скрывает забытый новый case при рефакторинге.
  2. Принудительный ! вместо паттерна — краш вместо исчерпывающей ветки.
  3. Слишком общий case let x — теряется смысл; лучше именованные associated values.
  4. Дублирование логики в if case и switch — вынести в одну функцию с switch.

Связанные материалы


Практика применения pattern matching

Наибольшая польза этой техники проявляется в коде, где много состояний и переходов — загрузки данных, навигация, обработка команд.


Где использовать в первую очередь

  • enum состояния экрана (idle/loading/success/failure).
  • Разбор Result из сети или базы.
  • Фильтрация массивов enum-значений через for case.

Рекомендации по стилю

  1. Для фиксированного enum используйте switch без default.
  2. Давайте осмысленные имена associated values (case failed(message: String)).
  3. Большие case-ветки выносите в отдельные функции.

Связанные темы


Сквозной мини-кейс — экран оплаты с enum состояния

Модель состояния экрана и одна функция отображения:

enum CheckoutState {
case idle
case validating
case ready(total: Decimal, currency: String)
case processing
case success(orderID: String)
case failure(message: String)
}

@MainActor
func uiMessage(for state: CheckoutState) -> String {
switch state {
case .idle:
return "Введите данные карты"
case .validating:
return "Проверяем форму…"
case let .ready(total, currency):
return "К оплате: \(total) \(currency)"
case .processing:
return "Обрабатываем платёж…"
case let .success(orderID):
return "Заказ \(orderID) оформлен"
case let .failure(message):
return message
}
}

Разбор:

  • CheckoutState моделирует полный жизненный цикл оплаты в одном enum.
  • Функция uiMessage(for:) на @MainActor гарантирует безопасное обновление UI на главном потоке.
  • switch покрывает все состояния — ввод, проверка, готовность, обработка, успех и ошибка.
  • Ветви с let извлекают полезные данные (total, currency, orderID, message) для пользовательских сообщений.
  • Такой подход упрощает сопровождение экрана: вместо множества булевых флагов есть один источник истины — текущее состояние.

Компилятор заставит обработать каждый case. При добавлении .cancelled проект не соберётся, пока ветку не допишут — это защита при эволюции API.

Для одного частного случая вместо полного switch уместен if case let .failure(message) = state.


Анти-паттерны и типичные ошибки

ОшибкаЧто происходитКак лучше
default в switch по enumновый case молча попадает в defaultубрать default, обработать все cases
Цепочка if let из 5 уровней"пирамида" вложенностиguard let или один switch
value! после if caseкраш при ошибке моделиguard case let с throw
Одинаковая логика в 3 if caseдублированиеобщий switch в функции
switch по String вместо enumопечатки в runtimeenum + associated values
Забытый break в C-стилев Swift не нужен; путаница у новичковкаждый case — отдельный блок

Расширяет раздел "Типичные ошибки" в начале этой статьи.