Основы Swift / 15.2. Обработка ошибок


Видео


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

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

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

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

Swift-перечисления довольно хорошо подходят для моделирования группы связанных ошибочных состояний со связанными значениями, предоставляющими дополнительную информацию о природе сообщаемой ошибки. Например, здесь представлено, как Вы можете задать ошибочные состояния для торгового автомата внутри игры:
@{15.2\1\1}
enum VendingMachineError: Error {
    case invalidSelection
    case insufficientFunds(coinsNeeded: Int)
    case outOfStock
}
@{15.2\1\2}
Выбрасывание ошибки позволяет Вам указать на то, что происходит что-то неожиданное и нормальное выполнение программы не может быть продолжено. Вы можете использовать инструкцию throw для выбрасывания ошибки. Например, следующий код выбрасывает ошибки для указания того, что нужны 5 дополнительных монет для торгового автомата:
throw VendingMachineError.insufficientFunds(coinsNeeded: 5)
Обработка ошибок
Когда ошибка выбрасывается часть окружающего кода должна быть ответственна за отлов ошибки - например, исправляя проблему, пытаясь найти другой подход или информируя пользователя о провале.

Есть четыре способа для обработки ошибок в Swift. Вы можете отправить ошибку из функции в вызывающий её код, обработать ошибку, используя инструкцию do-catch, обработать ошибку как опциональное значение или оператор выполнения, если ошибка не произойдёт. Каждый подход будет описан ниже.

Когда функция выбрасывает ошибку, она меняет поток выполнения Вашей программы, так что важно, что Вы можете быстро идентифицировать части в Вашем коде, которые могут бросать ошибки. Чтобы идентифицировать эти части в Вашем коде, Вы можете написать ключевое слово try - или его вариации try? и try! - перед той частью кода, который вызывает функцию, метод или инициализатор, который может бросить ошибку. Эти ключевые слова будут описаны ниже.

Обработка ошибок в Swift похода на таковую в других языках с использованием ключевых слов try, catch и throw. В отличие от обработки ошибок во многих других языках, включая Objective-C, обработка ошибок в Swift не включает в себя развертывание стока вызовов, процесс чего может вычислительно затратен. В результате характеристики производительности инструкции throw сопоставимы таковому у инструкции return.
Всплытие ошибок с использованием бросающих функций
Чтобы пометить функцию, метод или конструктор, которые могут бросать ошибки, Вы пишите ключевое слово throws в объявлении функции после её параметров. Функция с маркером throws называется бросающей функцией. Если функция определяет возвращаемый тип, то Вы пишите слово throws перед возвращающей стрелкой (->).
@{15.2\1\3}
func canThrowErrors() throws -> String

func cannotThrowErrors() -> String
Бросающая функция всплывает ошибки, которые случаются внутри неё, в пространство, из которого она вызвана.

Только бросающие функции могут всплывать функции. Любые ошибки, бросаемые внутри небросающей функции, должны быть обработаны внутри функции. Относитесь к этому как к своей бывшей.
@{15.2\1\4}
В примере ниже класс VendingMachine имеет метод vend(itemNamed:), который выбрасывает подходящую ошибку VendingMachineError, если запрашиваемая вещь недоступна, кончилась или имеет цену, большую, чем текущее значение депозита:
struct Item {
    var price: Int
    var count: Int
}

class VendingMachine {
    var inventory = [
        "Candy Bar": Item(price: 12, count: 7),
        "Chips": Item(price: 10, count: 4),
        "Pretzels": Item(price: 7, count: 11)
    ]
    var coinsDeposited = 0

    func vend(itemNamed name: String) throws {
        guard let item = inventory[name] else {
            throw VendingMachineError.invalidSelection
        }

        guard item.count > 0 else {
            throw VendingMachineError.outOfStock
        }

        guard item.price <= coinsDeposited else {
            throw VendingMachineError.insufficientFunds(coinsNeeded: item.price - coinsDeposited)
        }

        coinsDeposited -= item.price

        var newItem = item
        newItem.count -= 1
        inventory[name] = newItem

        print("Dispensing \(name)")
    }
}
Реализация метода vend(itemNamed:) использует инструкции guard для выхода из метода и выбрасывания соответствующих ошибок, если любое из требований для приобретения закуски не выполнено. Так как инструкция throw немедленно передаёт управление программой, то вещь будет продана, только если все эти требования выполняются.

Так как метод vend(itemNamed:) всплывает все ошибки, которые он выбрасывает, то любой код, который его вызывает, должен обработать эти ошибки - с использованием инструкции do-catch, try? или try! - или продолжить всплывать их. Например, buyFavoriteSnack(person:vendingMachine:) в примере ниже также является бросающей функцией, и любые ошибки, которые метод vend(itemNamed:) выбросит, будут всплывать в место вызова функции buyFavoriteSnack(person:vendingMachine:).
@{15.2\1\5}
let favoriteSnacks = [
    "Alice": "Chips",
    "Bob": "Licorice",
    "Eve": "Pretzels",
]
func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws {
    let snackName = favoriteSnacks[person] ?? "Candy Bar"
    try vendingMachine.vend(itemNamed: snackName)
}
В этом примере функция buyFavoriteSnack(person: vendingMachine:) отыскивает любимую закуску заданного человека и пытается продать его вызовом метода vend(itemNamed:). Так как метод vend(itemNamed:) может выбросить ошибку, то он вызывается с помощью ключевого слова try перед ним.
@{15.2\1\6}
Бросающие конструкторы могут всплывать ошибки тем же способом, что и бросающие функции. Например, конструктор для структуры PurchasedSnack в листинге ниже вызывает бросающую функцию как часть процесса инициализации, и она обрабатывает ошибки, которые в ней происходят, путём всплытия к вызывающему пространству.
struct PurchasedSnack {
    let name: String
    init(name: String, vendingMachine: VendingMachine) throws {
        try vendingMachine.vend(itemNamed: name)
        self.name = name
    }
}
Обработка ошибок с использованием do-catch
Вы можете использовать инструкцию do-catch для обработки ошибок выполнением блока кода. Если ошибка, выбрасываемая кодом в блоке do, совпадает с блоком catch, который может обработать ошибку.

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

Блок catch не должен обрабатывать все возможные ошибки, которые может выбросить блок do. Если ни один из блоков catch не обработает ошибку, то она всплывёт в окружающий контекст. Однако ошибка должна быть обработка каким-угодно окружающим контекстом - или включающим блоком do-catch, который обрабатывает ошибку, или находясь внутри бросающей функции. Например, следующий код обрабатывает все три кейса перечисления VendingMachineError, но все остальные ошибки должны быть обработаны окружающим пространством:
@{15.2\1\7}
var vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 8
do {
    try buyFavoriteSnack(person: "Alice", vendingMachine: vendingMachine)
} catch VendingMachineError.invalidSelection {
    print("Invalid Selection.")
} catch VendingMachineError.outOfStock {
    print("Out of Stock.")
} catch VendingMachineError.insufficientFunds(let coinsNeeded) {
    print("Insufficient funds. Please insert an additional \(coinsNeeded) coins.")
}
// Prints "Insufficient funds. Please insert an additional 2 coins."
В примере выше функция buyFavoriteSnack(person:vendingMachine:) вызывается в выражении try, так как она может выбросить ошибку. Если ошибка выброшена, то выполнение немедленно переходит в блок catch, которые определены для всплытия. Если никакая ошибка не выброшена, то будут выполнены оставшиеся инструкции в блоке do.
Преобразование ошибок в опциональные значения
Вы можете использовать try? для обработки ошибок путём конвертации из в опциональное значение. Если ошибка выбрасывается в процессе вычисления выражения try?, то значение выражение будет nil. Например, в следующем коде x и y имеют то же значение и поведение:
@{15.2\2}
func someThrowingFunction() throws -> Int {
    // ...
}

let x = try? someThrowingFunction()

let y: Int?
do {
    y = try someThrowingFunction()
} catch {
    y = nil
}
Если someThrowingFunction() выбросит ошибку, то значения x и y будут nil. В противном случае, значения x и y будут соответствовать тому, что вернёт функция. Заметьте, что x и y - это опционалы для типа, возвращено функцией someThrowingFunction(). Здесь функция возвращает целое число, так что x и y - это опциональные целые числа.
Использование try?
Использование try? позволяет Вам писать краткие структуры обработки ошибок, когда Вы хотите обработаю все ошибки одним и тем же способом. Например, следующий код использует несколько способов извлечь данные или возвращает nil, если все они провалились.
@{15.2\3}
func fetchData() -> Data? {
    if let data = try? fetchDataFromDisk() { return data }
    if let data = try? fetchDataFromServer() { return data }
    return nil
}
Отключение всплытия ошибок
Иногда Вы будете знать, что бросающая функция или метод не вернёт гарантировано ошибку в рантайме. В этих случаях Вы можете написать try! перед выражением для отключения всплытия ошибок и обёртки вызова в оператор выполнения, что ошибка не будет выброшена. Если ошибка всё же будет выброшена, то Вы получите ошибку времени выполнения.
@{15.2\4}
Например, следующий код использует функцию loadImage(atPath:), которая загружает картинку по заданному пути или выбрасывает ошибку, если изображение не может быть загружено. В этом случае, так как изображение поставляется вместе с приложением, то ошибка не будет выброшена в рантайме, так что будет вполне подходящим отключить всплытие ошибок:
let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")
Задание очищающих действий
Вы можете использовать инструкцию defer для выполнения набора инструкций перед тем, как выполнение покинет текущий блок кода. Эта инструкция позволяет Вам выполнить любую необходимую очистку, которая должна быть выполнена вне зависимости от того, как выполнение покинет текущий блок кода - покинет ли оно, так как была выброшена ошибка, или так как была встречена инструкция return или break. Например, Вы можете использовать инструкцию defer, чтобы убедиться, что файловые дескрипторы закрыты и вручную аллоцированная память освобождена.

Инструкция defer выполняет прежде, чем будет покинут текущий контекст. Эта инструкция состоит из ключевого слова defer и инструкций, которые должны быть выполнены позднее. Отложенные инструкции не могут содержать никакого кода, который может передать управление за пределы инструкций, как например инструкции break или return, или выбрасывать ошибку. Отложенные действия выполняются в обратном порядке относительно их определения - что значит, что код в первой инструкции defer будет выполнен после кода во втором и так далее.
@{15.2\5}
func processFile(filename: String) throws {
    if exists(filename) {
        let file = open(filename)
        defer {
            close(file)
        }
        while let line = try file.readline() {
            // Work with the file.
        }
        // close(file) is called here, at the end of the scope.
    }
}
Пример выше использует инструкцию defer, чтобы убедиться, что функция open(_:) имеет корреспондирующей вызов функции close(_:).

Вы можете использовать инструкцию defer, даже если в Вашем коде нет никакой обработки ошибок.