Книга: Swift. Основы разработки приложений под iOS, iPadOS и macOS. 5-е изд. дополненное и переработанное
Назад: 35. Универсальные шаблоны
Дальше: 37. Нетривиальное использование операторов

36. Обработка ошибок

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

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

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

36.1. Выбрасывание ошибок

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

Не стоит создавать одно перечисление на все случаи жизни. Группируйте возможные ошибки по их смыслу в различных перечислениях.

В следующем примере объявляется тип данных, который описывает ошибки в работе торгового автомата по продаже еды (листинг 36.1).

Листинг 36.1

enum VendingMachineError: Error {

    case InvalidSelection

    case InsufficientFunds(coinsNeeded: Int)

    case OutOfStock

}

Каждый из членов перечисления указывает на отдельный тип ошибки:

• неправильный выбор;

• нехватка средств;

• отсутствие выбранного товара.

Ошибка позволяет показать, что произошла какая-то нестандартная ситуация и обычное выполнение программы не может продолжаться. Процесс появления ошибки называется выбрасыванием ошибки. Для того чтобы выбросить ошибку, необходимо воспользоваться оператором throw. Так, следующий код при попытке совершить покупку выбрасывает ошибку о недостатке пяти монет (листинг 36.2).

Листинг 36.2

throw VendingMachineError.InsufficientFunds(coinsNeeded: 5)

36.2. Обработка ошибок

Сам по себе выброс ошибки не приносит каких-либо результатов. Выброшенную ошибку необходимо перехватить и корректно обработать. В Swift существует четыре способа обработки ошибок:

• передача ошибки;

• обработка ошибки оператором do-catch;

• преобразование ошибки в опционал;

• запрет на выброс ошибки.

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

Теперь разберем каждый из способов обработки ошибок.

Передача ошибки

При передаче ошибки блок кода (функция, метод или инициализатор), ставший источником ошибки, самостоятельно не обрабатывает ее, а передает выше в код, который вызвал данный блок кода.

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

В листинге 36.3 приведен пример объявления двух функций, которые передают возникающие в них ошибки выше.

Листинг 36.3

func anotherFunc() throws {

    // тело функции

    var value = try someFunc()

    // ...

}

func someFunc() throws -> String{

    // тело функции

    try anotherFunc()

    // ...

}

try someFunc()

Функция someFunc() возвращает значение типа String, поэтому ключевое слово throws указывается перед типом возвращаемого значения.

Функция anotherFunc() в своем теле самостоятельно не выбрасывает ошибки, она может лишь перехватить ошибку, выброшенную функцией anotherFunc(). Для того чтобы перехватить ошибку, выброшенную внутри блока кода, необходимо осуществлять вызов с помощью упомянутого ранее оператора try. Благодаря ему функция anotherFunc() сможет отреагировать на возникшую ошибку так, будто она сама является ее источником. А так как эта функция также помечена ключевым словом throws, она просто передаст ошибку в вызвавший ее код.

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

Рассмотрим пример из листинга 36.4.

Листинг 36.4

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 dispenseSnack(snack: String) {

        print("Dispensing \(snack)")

    }

    func vend(itemNamed name: String) throws {

        guard var 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

        item.count -= 1

        inventory[name] = item

        dispenseSnack(snack: name)

    }

}

Структура Item описывает одно наименование продукта из автомата по продаже еды. Класс VendingMachine описывает непосредственно сам аппарат. Его свойство inventory является словарем, содержащим информацию о наличии определенных товаров. Свойство coinsDeposited указывает на количество внесенных в аппарат монет. Метод dispenseSnack(snack:) сообщает о том, что аппарат выдает некий товар. Метод vend(itemNamed:) непосредственно обслуживает покупку товара через аппарат.

При определенных условиях (запрошенный товар недоступен, его нет в наличии или количества внесенных монет недостаточно для покупки) метод vend(itemNamed:) может выбросить ошибку, соответствующую перечислению VendingMachineError. Сама реализация метода использует оператор guard для реализации преждевременного выхода с помощью оператора throw. Оператор throw мгновенно изменяет ход работы программы, в результе выбранный продукт может быть куплен только в том случае, если все условия покупки выполняются.

Так как метод vend(itemNamed:) передает все возникающие в нем ошибки вызывающему его коду, то необходимо выполнить дальнейшую обработку ошибок с помощью оператора try или do-catch.

Реализуем функцию, которая в автоматическом режиме пытается приобрести какой-либо товар (листинг 36.5). В данном примере словарь favoriteSnacks содержит указатель на любимое блюдо каждого из трех человек.

Листинг 36.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:), для передачи ошибки выше необходимо использовать операторы throws и try.

Оператор do-catch

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

Синтаксис

    do {

        try имяВызываемого Блока

    } catch шаблон1 {

        // код...

    } catch шаблон2 {

        // код...

    }

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

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

Вы можете использовать ключевое слово where в шаблонах условий.

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

Используем оператор do-catch, чтобы перехватить и обработать возможные ошибки (листинг 36.6).

Листинг 36.6

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("Недостаточно средств. Пожалуйста, внесите еще \(coinsNeeded)

           монет(ы).")

}

// выводит "Недостаточно средств. Пожалуйста, внесите еще 2 монет(ы)."

В приведенном примере функция buyFavoriteSnack(person:vendingMachine:) вызывается в блоке do. Поскольку внесенной суммы монет не хватает для покупки любимой сладости покупателя Alice, возвращается ошибка и выводится соответствующее этой ошибке сообщение.

Преобразование ошибки в опционал

Для преобразования выброшенной ошибки в опциональное значение используется оператор try, а точнее, его форма try?. Если в этом случае выбрасывается ошибка, то значение выражения вычисляется как nil.

Рассмотрим пример из листинга 36.7.

Листинг 36.7

func someThrowingFunction() throws -> Int {

    // ...

}

let x = try? someThrowingFunction()

Если функция someThrowingFunction() выбросит ошибку, то в константе x окажется значение nil.

Запрет на выброс ошибки

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

Рассмотрим пример из листинга 36.8.

Листинг 36.8

let photo = try! loadImage("./Resources/John Appleseed.jpg")

Функция loadImage(_:) производит загрузку локального изображения, а в случае его отсутствия выбрасывает ошибку. Так как указанное в ней изображение является частью разрабатываемой вами программы и гарантированно находится по указанному адресу, с помощью оператора try! целесообразно отключить режим передачи ошибки.

Будьте внимательны: если при запрете передачи ошибки блок кода все же выбросит ее, то ваша программа экстренно завершится.

36.3. Отложенные действия по очистке

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

Отложенные действия выполняются в обратном порядке, то есть вначале выполняется блок последнего оператора defer, затем предпоследнего и т.д.

Рассмотрим пример использования блока отложенных действий (листинг 36.9).

Листинг 36.9

func processFile(filename: String) throws {

    if exists(filename) {

        let file = open(filename)

        defer {

            close(file)

        }

        while let line = try file.readline() {

            // работа с файлом.

        }

    }

}

В данном примере оператор defer просто обеспечивает закрытие открытого ранее файла.

Назад: 35. Универсальные шаблоны
Дальше: 37. Нетривиальное использование операторов