Когда do/catch — это слишком много
Вы уже умеете объявлять свои ошибки через Error и ловить их конструкцией do/catch. Но не всегда нужен полный разбор: иногда вам просто нужно значение по умолчанию, если что-то пошло не так, иногда вы точно знаете, что ошибки быть не может, а иногда код должен выполниться в любом случае — и при успехе, и при провале. Для этих трёх случаев в Swift есть свои инструменты: try?, try! и defer. Отдельно разберём тип Result — способ хранить «успех или ошибка» как обычное значение, без немедленного throws.
try? — ошибка превращается в nil
Если вам не важно, какая именно ошибка произошла, а важен только сам факт «получилось или нет», используйте try?. Он превращает вызов throws-функции в Optional: при успехе вы получаете значение, обёрнутое в Optional, при ошибке — nil. Сама ошибка при этом теряется, её нельзя посмотреть.
enum ParseError: Error {
case notANumber
}
func parseCount(_ text: String) throws -> Int {
guard let value = Int(text) else { throw ParseError.notANumber }
return value
}
let result = try? parseCount("42")
print(result)
Тип result — Int?, а не Int: даже если функция при успехе возвращает не опциональный тип, try? всё равно оборачивает результат в Optional, потому что вызов мог провалиться. Программа выше напечатает Optional(42).
Частый приём: try? вместе с ??
Чаще всего try? используют не сам по себе, а в паре с оператором ??, чтобы сразу подставить значение по умолчанию:
let count = (try? parseCount("abc")) ?? 0
print(count)
Здесь parseCount бросает ошибку, try? превращает её в nil, а ?? подставляет 0. На экране будет 0, и никакого catch писать не пришлось. Обратите внимание на скобки вокруг try? parseCount("abc") — без них компилятор применит ?? не к тому выражению.
try! — «ошибки точно не будет»
Иногда вы точно уверены, что вызов не бросит ошибку — например, парсите строку, которую сами же перед этим проверили. В этом случае можно написать try!. Он говорит компилятору: «не оборачивай в опционал, я гарантирую успех».
let fixed = try! parseCount("100")
print(fixed)
Здесь fixed имеет обычный тип Int, без опционала. Но если гарантия окажется ложной и функция всё-таки бросит ошибку, программа не перейдёт в catch — она аварийно завершится с fatal error, как при принудительном разворачивании nil через восклицательный знак. Используйте try! только тогда, когда провал вызова означает баг в вашем коде, а не ожидаемую ситуацию вроде плохого ввода пользователя.
defer — код, который выполнится в любом случае
Блок defer откладывает выполнение кода до момента выхода из текущей области видимости — неважно, дошла ли функция до конца, вышла раньше через return или прервалась через throw. Это удобно для «уборки»: закрыть файл, освободить ресурс, вывести лог о завершении операции.
func processOrder(id: Int) {
print("Начали обработку заказа \(id)")
defer { print("Закончили обработку заказа \(id)") }
if id < 0 {
print("Некорректный id, выходим")
return
}
print("Заказ обработан")
}
При вызове processOrder(id: -1) строка из defer всё равно напечатается перед выходом из функции, хотя выполнение прервалось на return внутри if. Блоков defer в одной функции может быть несколько — тогда они выполняются в порядке, обратном объявлению: последний объявленный defer сработает первым.
func demo() {
defer { print("первый defer") }
defer { print("второй defer") }
print("тело функции")
}
demo()
// тело функции
// второй defer
// первый defer
Такой порядок логичен: если вы в начале функции открыли файл, а потом ещё и захватили блокировку, освобождать их нужно в обратном порядке — сначала блокировку, потом файл. defer сам следит за этой очерёдностью.
Result — ошибка как значение
И throws, и try? требуют вызвать функцию прямо сейчас, чтобы узнать результат. Но иногда результат нужно сохранить и обработать позже — например, передать его дальше по коду или сравнить несколько результатов между собой. Для этого в стандартной библиотеке есть перечисление Result<Success, Failure> с двумя кейсами: .success(Success) и .failure(Failure), где Failure обязан соответствовать протоколу Error.
enum MathError: Error {
case divisionByZero
}
func safeDivide(_ a: Int, _ b: Int) -> Result {
guard b != 0 else { return .failure(.divisionByZero) }
return .success(a / b)
}
let outcome = safeDivide(10, 2)
switch outcome {
case .success(let value):
print("Результат: \(value)")
case .failure(let error):
print("Ошибка: \(error)")
}
outcome здесь — обычное значение типа Result, его можно сохранить в массив, передать в другую функцию или сравнить. Функция при этом не бросает ошибку — она просто возвращает готовый результат одним из двух видов.
Result и throws вместе
Если внутри вам всё же удобнее работать с throws-функцией, оберните её вызов в do/catch и соберите результат вручную:
func divide(_ a: Int, _ b: Int) throws -> Int {
guard b != 0 else { throw MathError.divisionByZero }
return a / b
}
func safeDivide(_ a: Int, _ b: Int) -> Result {
do {
return .success(try divide(a, b))
} catch let error as MathError {
return .failure(error)
} catch {
return .failure(.divisionByZero)
}
}
А чтобы пойти в обратную сторону — достать значение из Result и снова получить обычный механизм ошибок — есть метод get(). Он возвращает значение из .success либо бросает ошибку из .failure, поэтому вызывать его нужно через try:
func printOutcome(_ outcome: Result) {
do {
let value = try outcome.get()
print("Значение: \(value)")
} catch {
print("Не получилось: \(error)")
}
}
Частые ошибки
- Ставить
try!там, где ошибка вполне ожидаема (например, при разборе ввода пользователя). Одна неудачная попытка — и всё приложение аварийно завершается. - Забывать скобки вокруг
try?при использовании с??: записьtry? parseCount("x") ?? 0без скобок компилятор поймёт иначе, чем вы задумывали. - Злоупотреблять
try?там, где причина ошибки важна для решения, что делать дальше. Молча превращая всё вnil, вы теряете информацию, которая могла бы помочь пользователю или логам. - Считать, что
deferвыполнится немедленно в момент объявления. На самом деле его тело откладывается до конца текущей области видимости, а не выполняется по порядку строк. - Забывать, что
get()уResult— throws-метод: его нужно вызывать сtry, а не как обычный доступ к значению.
Резюме
try?превращает результат throws-функции вOptional:nilпри ошибке, значение при успехе; удобно сочетать с??.try!убирает опционал, но при реальной ошибке приводит к аварийному завершению программы — используйте только при полной уверенности в успехе.deferоткладывает блок кода до выхода из функции, независимо от того, как она завершилась; несколькоdeferвыполняются в обратном порядке объявления.Resultхранит «успех или ошибку» как обычное значение с кейсами.successи.failure, которое можно передавать и сохранять.- Метод
get()уResultдостаёт значение из.successили бросает ошибку из.failure— мост обратно к обычномуthrows.
Проверьте себя
4 вопроса
Безопасный возраст
Дана готовая функция parseAge(_:), которая разбирает строку в возраст: бросает ParseError.notANumber, если строка не число, и ParseError.negative, если число отрицательное.
Напишите функцию safeAge(_ text: String) -> Int, которая вызывает parseAge через try? и возвращает результат, а если разбор не удался — возвращает 0.
Деление через Result
Дана throws-функция divide(_:_:), которая бросает MathError.divisionByZero при делении на нуль.
Напишите функцию safeDivide(_ a: Int, _ b: Int) -> Result<Int, MathError>, которая вызывает divide и возвращает .success с результатом при удаче или .failure(.divisionByZero) при ошибке. Используйте do/catch внутри функции — сама функция ошибок не бросает, она возвращает Result.