$ sudo teach IT

Когда 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.