В Swift ошибки это значения, а не исключения в стиле C++ или Java. На собесе спросят про try? и try!, порядок defer, Result, typed throws и стратегию ошибок в приложении.
Объявление и бросание
Ошибка это любой тип, соответствующий протоколу Error. Обычно enum со связанными значениями:
enum LoginError: Error {
case wrongPassword
case blocked(minutes: Int)
}
func login(_ email: String, _ password: String) throws -> Session {
guard password.count >= 8 else { throw LoginError.wrongPassword }
...
}
Вызов и обработка
Каждая точка, где может вылететь ошибка, помечается try:
do {
let session = try login(email, password)
show(session)
} catch LoginError.blocked(let minutes) {
showBlocked(minutes)
} catch {
showError(error) // неявная константа error
}
catch принимает паттерны, как case в switch, включая let e as DecodingError.
Три формы try:
try: ошибка пробрасывается выше (вызывающая функция тожеthrows) или ловится вdo/catch;try?: результат становится опционалом, ошибка теряется;try!: при ошибке приложение падает.
С Swift 5 try? не создаёт двойной опционал: для функции, возвращающей Int?, результат Int?.
defer
defer откладывает блок до выхода из текущей области по любому пути: return, throw, break. Несколько defer выполняются в обратном порядке.
func work() -> Int {
var x = 1
defer { print("defer 1", x) }
defer { x += 10; print("defer 2") }
x = 5
return x // вернёт 5: значение вычислено до defer
}
// defer 2
// defer 1 15
defer внутри do выполнится при выходе из do, то есть до блока catch.
rethrows
Функция с rethrows бросает, только если бросает переданное замыкание. Поэтому map { $0 * 2 } не требует try, а map { try parse($0) } требует.
Result
Result<Success, Failure> делает результат значением, которое можно сохранить и передать дальше. Удобно в callback-API:
func load(completion: @escaping (Result<User, Error>) -> Void)
let r = Result { try decoder.decode(User.self, from: data) }
let user = try r.get()
В синхронном и async-коде идиоматичнее throws.
Typed throws (Swift 6)
func parse(_ s: String) throws(ParseError) -> Int
В catch переменная error сразу типа ParseError, функция не может бросить другой тип. Обычный throws равен throws(any Error), а throws(Never) значит «не бросает». Хорошо подходит для внутренних модулей и обобщённого кода, а в публичном API тип ошибки становится частью контракта, и новые случаи ломают клиентов.
Цена ошибок
Swift передаёт ошибку через выделенный регистр, как особый возвращаемый результат, без раскрутки стека по таблицам. Поэтому бросание дешёвое, и ошибки нормальная часть потока управления.
assert, precondition, fatalError
Debug (-Onone) | Release (-O) | |
|---|---|---|
assert | проверяется | выброшен |
precondition | проверяется | проверяется |
fatalError | всегда | всегда |
fatalError возвращает Never, поэтому годится как выход из guard. Эти функции для ошибок программиста, а не для данных извне.
Ошибки для пользователя
Реализуйте LocalizedError с errorDescription, иначе localizedDescription вернёт малополезный системный текст. Отмену (CancellationError, URLError.cancelled) пользователю не показывают.
Стратегия в приложении
- Сетевой слой бросает технические ошибки: нет сети, таймаут, статус, декодирование.
- Репозиторий переводит их в доменные: «сессия истекла», «нет связи».
- View model решает, как показать: экран ошибки с «Повторить», тост поверх кеша или ничего.
- Всё логируется с контекстом, без персональных данных.
Частые ошибки
try?при декодировании: непонятно, какое поле сломалось.try!для данных из сети.- Ожидать, что
deferизменит уже вычисленное возвращаемое значение. - Показывать пользователю ошибку отмены при уходе с экрана.
Что спросят на собесе
- Разница
try,try?,try!. - В каком порядке выполняются
deferи что вернёт функция? - Что такое
rethrows, когда нуженResult? - Что дают typed throws в Swift 6?
- Чем
assertотличается отprecondition?