Программирование SwiftОбработка ошибокМладший разработчик iOS на Swift

Спрогнозируйте, какой обработчик выполнится при вызове charge , и объясните роль динамического типа ошибки....

Спрогнозируйте, какой обработчик выполнится при вызове charge(), и объясните роль динамического типа ошибки.

enum PaymentError: Error {
    case declined(reason: String)
    case unavailable
}

func charge() throws {
    let error: Error = PaymentError.declined(reason: "limit")
    throw error
}

do {
    try charge()
} catch PaymentError.declined(let reason) where reason == "limit" {
    print(" лимит")
} catch PaymentError.declined {
    print("отклонено")
} catch {
    print("другая ошибка")
}
Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Выполнится первый обработчик, поэтому программа выведет лимит. Несмотря на то что ошибка была сохранена в переменной типа Error, при сопоставлении catch Swift использует её фактический динамический тип PaymentError, значение reason и условие where.

Обработчики проверяются сверху вниз, и выполняется первый подходящий. Поэтому более конкретные шаблоны следует располагать перед общим catch.

Исторический контекст

В Swift ошибки представлены обычными значениями, соответствующими протоколу Error, а механизм throws отделяет успешное выполнение от неуспешного без возврата специального кода состояния. Такой подход позволяет передавать структурированную причину сбоя и обрабатывать её через сопоставление с типом и содержимым значения.

Поскольку переменная типа Error может содержать ошибку любого конкретного типа, обработчику нужен механизм проверки фактического типа. Шаблоны в catch решают эту задачу без анализа строковых сообщений.

Постановка проблемы

Если обработать все ошибки одним catch, вызывающий код потеряет возможность различать, например, отказ платежа, временную недоступность сервиса и внутреннюю ошибку. Это приводит к неправильным действиям: временный сбой может быть показан пользователю как окончательный отказ, а повторная попытка может быть выполнена для ошибки, которую повторять бессмысленно.

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

Подробное решение

После throw error Swift передаёт управление ближайшему подходящему catch. Сопоставление выполняется по фактическому значению ошибки, а не только по статическому типу переменной, через которую она была передана. Поэтому преобразование PaymentError к Error не уничтожает информацию о конкретном типе.

В первом обработчике одновременно проверяются три условия:

  1. ошибка имеет тип PaymentError;
  2. её случай — declined;
  3. связанное значение reason равно "limit".

Все условия выполняются, поэтому выбирается первый блок. Второй обработчик также подходит для случая declined, но до него управление уже не дойдёт.

Условие where позволяет уточнить сопоставление. Если бы причина была "fraud", первый блок не подошёл бы, а второй обработал бы тот же случай declined. Последний catch без шаблона соответствует любой оставшейся ошибке и обычно используется как резервная ветка.

Порядок обработчиков имеет значение: сначала размещают случаи с наиболее узким условием, затем более общие. Нельзя полагаться на текст ошибки для бизнес-логики, поскольку текст предназначен для диагностики и может меняться; для программного решения нужны отдельные случаи перечисления, структуры или другие типизированные данные.

enum PaymentError: Error { case declined(reason: String) case unavailable } func handle(_ error: Error) { switch error { case PaymentError.declined(let reason) where reason == "limit": print("нужен другой способ оплаты") case PaymentError.declined: print("платёж отклонён") default: print("сбой сервиса") } }

Вызов handle с ошибкой PaymentError.declined(reason: "limit") выберет первую ветку switch. Тот же принцип сопоставления применяется и в catch, но catch дополнительно управляет передачей ошибки между областями обработки.

Ситуация из практики

Платёжный SDK возвращает ошибки от сервера и должен решить, показывать ли пользователю сообщение, повторять ли запрос или просить другой способ оплаты. Рассматривались три варианта.

Первый вариант — возвращать строку с сообщением сервера. Он прост, но хрупок: изменение текста ломает проверки, локализация усложняет сравнение, а разные причины могут случайно иметь похожие сообщения.

Второй вариант — один тип ошибки с кодом и набором необязательных полей. Он допускает много состояний, но часть комбинаций может быть некорректной: например, код отказа без причины или код сетевой ошибки с платёжными реквизитами.

Выбран вариант с перечислением PaymentError и отдельными случаями с ассоциированными значениями. Клиентский код использует специализированные catch с where: окончательный отказ отображается пользователю, временная недоступность допускает повтор, а неизвестная ошибка попадает в журнал и обрабатывается резервной веткой. В результате логика не зависит от текста ответа и явно покрывает поддерживаемые причины.

Что кандидаты часто упускают

  1. Что произойдёт, если первым поставить catch { ... }?

    Он станет универсальным обработчиком и подойдёт для любой ошибки. Управление завершит do-catch после его выполнения, поэтому расположенные ниже специализированные обработчики недостижимы с точки зрения выполнения. Специализированные ветки нужно ставить выше общего обработчика.

  2. Сохраняется ли возможность сопоставления после присваивания ошибки переменной типа Error?

    Да. Error — это тип-обобщение для значений, соответствующих протоколу, но само значение сохраняет фактический динамический тип. Поэтому ошибка, созданная как PaymentError, может быть сопоставлена с catch PaymentError... после передачи через Error.

  3. Чем опасно использовать where для важных бизнес-правил внутри catch?

    where удобен для локального уточнения уже смоделированной ошибки, но чрезмерно сложные условия делают обработку трудной для тестирования и чтения. Если правило зависит от нескольких полей или повторяется в разных местах, его лучше выразить отдельным случаем ошибки либо методом модели, чтобы допустимые состояния были видны в типе и проверялись централизованно.