Link Search Menu Expand Document

Обробка помилок

Обробка помилок – це процес відповіді та відновлення з помилкових умов у програмі. Swift надає першокласну підтримку викидування, перехоплення, поширення та маніпулювання відновлюваними помилками під час виконання програми.

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

Як приклад, можна розглянути задачу читання та обробки інформації з файлу на диску. Є багато причин невдалого виконання цієї задачі, включаючи відсутність файлу на диску по вказаному шляху, відсутність дозволу на читання цього файлу, або файл закодовано в несумісному форматі. Можливість відрізнити ці ситуації дозволяють програмі розв’язати деякі з цих помилок та передати користувачу змістовне повідомлення, якщо якусь помилку розв’язати неможливо.

Примітка

Обробка помилок у Swift взаємодіє з шаблонами обробки помилок що використовують клас NSError у Cocoa та Objective-C. Детальніше с цим класом можна ознайомитись у розділі Обробка Помилок у книзі Using Swift with Cocoa and Objective-C (Swift 3.0.1).

Представлення та викидання помилок

У Swift помилки представляються значеннями типів, що підпорядковуються протоколу Error. Цей порожній протокол вказує на те, що тип можна використовувати для обробки помилок.

Перечислення у Swift найбільш вдало підходять для моделювання групи пов’язаних помилкових умов, при цьому їх асоційовані значення дозволяють передати додаткову інформацію про природу помилки. Наприклад, ось як можна представити помилкові умови для роботи автомату зі снеками всередині гри:

enum VendingMachineError: Error {
    case invalidSelection
    case insufficientFunds(coinsNeeded: Int)
    case outOfStock
}

Викидання помилки дозволяє вам чітко означити, сталось щось неочікуване, і нормальний сценарій виконання програми не може бути продовжено. Помилки викидаються за допомогою інструкції throw. Наприклад, наступний код викидає помилку, котра означає, що для роботи автомату зі снеками не вистачає п’яти монет:

throw VendingMachineError.insufficientFunds(coinsNeeded: 5)

Обробка помилок

Коли викидається помилка, довколишній шматок коду повинен взяти відповідальність за її обробку – наприклад, виправляючи проблему, пробуючи альтернативний підхід або просто інформуючи користувача про помилку.

У мові Swift є чотири способи обробки помилок. Можна передати помилку далі з поточної функції до коду, що викликає цю функцію, обробити помилку за допомогою інструкції do-catch, обробити помилку як опціональне значення, або допустити, що на практиці дана помилка неможлива, Кожен з цих підходів описаний у даному розділі нижче.

Коли функція викидає помилку, вона міняє потік виконання програми, і тому важливо мати можливість швидко ідентифікувати місця в коді, що можуть викидати помилки. Для ідентифікації таких місць, використовують ключове слово try (або його варіації: try? та try!) перед частиною коду, що викликає функцію, метод чи ініціалізатор, котрий може викинути помилку. Дані ключові слова описані в поточному розділі нижче.

Примітка

Обробка помилок у Swift нагадує обробку виключень в інших мовах, де також використовуються ключові слова try, catch та throw. На відміну від обробки помилок у багатьох мовах, включаючи Objective-C, обробка помилок у Swift не включає розгортання стеку викликів, процесу, що може бути обчислювально затратним. Характеристики швидкодії інструкції throw є сумірними з такими ж характеристиками інструкції return.

Поширення помилок за допомогою функцій, що викидають помилки

Щоб позначити, що функція, метод чи ініціалізатор може викинути помилку, використовують ключове слово throws в їх оголошенні, після списку параметрів. Функцію, котру позначено ключовим словом throws, називають функцією, що викидає помилку. Якщо функція визначає тип, що повертається, ключове слово throws записується перед стрілкою повернення (->).

func canThrowErrors() throws -> String

func cannotThrowErrors() -> String

Функція, що викидає помилку, поширює помилку, котру було викинено всередині її контексту, у контекст, в якому викликано її саму.

Примітка

Тільки функці, що викидають помилку, можуть поширювати помилки. Будь-яка помилка, що викидається всередині функції, що не викидає помилку, повинна бути оброблена всередині функції.

У прикладі нижче визначено клас VendingMachine, що модулює автомат зі снеками та має метод vend(itemNamed:), що викидає помилку типу VendingMachineError у випадках, коли запитаний продукт недоступний, або закінчився, або якщо його вартість перевищує поточний депозит:

struct Item {
    var price: Int
    var count: Int
}

class VendingMachine {
    var inventory = [
        "Артек": Item(price: 12, count: 7),
        "Контік": Item(price: 10, count: 4),
        "Чіпси \"Люкс\"": 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("Видача: \(name)")
    }
}

Реалізація методу vend(itemNamed:) використовує інструкції guard для раннього виходу з методу із викиданням відповідних помилок, якщо певні вимоги для покупки снеку не виконуються. Оскільки інструкція throw одразу перериває виконання поточної функції, продукт буди видано лише за виконання усіх необхідних вимог.

Оскільки метод vend(itemNamed:) поширює усі помилки, котрі у ньому викидаються, будь-який код, що викликає цей метод, повинен або якось обробити ці помилки (шляхом використання інструкцій do-catch, або try?, чи try!), або продовжити поширювати ці помилки. Наприклад, функцію buyFavoriteSnack(person:vendingMachine:) у прикладі нижче також визначено як функцію, що викидає помилки, і тому усі помилки, що викидає метод vend(itemNamed:), будуть перекинуті далі до точки виклику функції buyFavoriteSnack(person:vendingMachine:).

let favoriteSnacks = [
    "Лана": "Контік",
    "Яніна": "Чіпси \"Люкс\"",
    "Уляна": "Артек",
]
func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws {
    let snackName = favoriteSnacks[person] ?? "Артек"
    try vendingMachine.vend(itemNamed: snackName)
}

У цьому прикладі, функція buyFavoriteSnack(person: vendingMachine:)шукає улюблений снек заданої особи, і пробує придбати його, викликавши метод vend(itemNamed:). Оскільки метод vend(itemNamed:) може викинути помилку, перед його викликом йде ключове слово try.

Ініціалізатори, що викидають помилки, поширюють помилки у той же спосіб, що й аналогічні функції. Наприклад, ініціалізатор структури 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, в котрому буде оброблено цю помилку.

Ось так виглядає загальна форма інструкції do-catch:

do {
    try <#вираз#>
    <#інструкції#>
} catch <#шаблон 1#> {
    <#інструкції#>
} catch <#шаблон 2#> where <#умова#> {
    <#інструкції#>
} catch <#шаблон 3#>, <#шаблон 4#> where <#умова#> {
    <#інструкції#>
} catch {
    <#інструкції#>
}

Шаблон після ключового слова catch позначає види помилок, котрі даний блок може обробити. Якщо блок catch не має шаблону, він оброблюватиме будь-які помилки, та прив’язуватиме помилку до локальної константи на ім’я error. Детальніше зі співпадінням із шаблонами можна ознайомитись у розділі Шаблони.

Наприклад, у коді нижче помилка звіряється з усіма трьома елементами перечислення VendingMachineError.

var vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 8
do {
    try buyFavoriteSnack(person: "Лана", vendingMachine: vendingMachine)
    print("Успіх! Ням.")
} catch VendingMachineError.invalidSelection {
    print("Неправильний вибір.")
} catch VendingMachineError.outOfStock {
    print("Закінчились.")
} catch VendingMachineError.insufficientFunds(let coinsNeeded) {
    print("Недостаньо коштів. Будь ласка, вставте додатково \(coinsNeeded) монет(и).")
} catch {
    print("Неочікувана помилка: \(error).")
}
// Надрукує "Недостаньо коштів. Будь ласка, вставте додатково 2 монет(и)."

У прикладі вище, функція buyFavoriteSnack(person:vendingMachine:) викликається через вираз try, оскільки вона може викинути помилку. Якщо викидається помилка, виконання одразу переноситься до пунктів catch, які почергово звіряють помилку зі своїми шаблонами та зупиняються при першому збігу. Якщо жоден із шаблонів не збігається, помилка обробляється останнім пунктом catch, і прив’язується до локальної константи error. Якщо не викидається жодної помилки, виконується решта інструкцій усередині інструкції do.

Блоки catch не обов’язково повинні обробляти будь-яку можливу помилку, що може викинути блок do. Якщо жоден з блоків catch не оброблює помилку, її можна поширити до контексту вище. Однак, помилку потрібно обов’язково обробити в якомусь із контекстів вище. У функції, що не викидає помилок, внутрішня інструкція do-catch повинна обробити всі помилки. У функції, що викидає помилку, усі помилки мають бути оброблені або у внутрішній інструкції do-catch, або у місці виклику цієї функції. Якщо помилка поширюється до найвищого контексту без обробки, ви отримаєте помилку часу виконання.

Наприклад, код вище можна написати так, що всі помилки, котрі не є екземплярами VendingMachineError, оброблятимуться у контексті вище:

func nourish(with item: String) throws {
    do {
        try vendingMachine.vend(itemNamed: item)
    } catch is VendingMachineError {
        print("Неможливо купити це у автоматі зі снеками.")
    }
}

do {
    try nourish(with: "Чіпси зі смаком буряка")
} catch {
    print("Неочікувана не пов'язана з автоматом зі снеками помилка: \(error)")
}
// Надрукує "Неможливо купити це у автоматі зі снеками."

У функції nourish(with:), якщо виклик функції vend(itemNamed:) викидує помилку, котра є елементом перечислення VendingMachineError, функція nourish(with:) обробляє цю помилку, друкуючи відповідне повідомлення. В інших випадках функція nourish(with:) прокидує помилку до місця свого виклику. Після цього помилку буде відловлено у загальному пункті catch наприкінці цього прикладу.

Відловити декілька пов’язаних помилок можна також іншим способом: просто перерахувавши їх після catch, розділяючи комами. Наприклад:

func eat(item: String) throws {
    do {
        try vendingMachine.vend(itemNamed: item)
    } catch VendingMachineError.invalidSelection, VendingMachineError.insufficientFunds, VendingMachineError.outOfStock {
        print("Неправильний вибір, закінчились, або недостаньо коштів.")
    }
}

У функції eat(item:) перераховуються всі помилки VendingMachineError, які слід обробити у ній, і текст її повідомлення про помилку відповідає цим помилкам. Якщо буде викинуто будь-яку з трьох перечислених помилок, цей пункт catch обробить їх, надрукувавши повідомлення. Будь-які інші помилки будуть поширені до навколишнього контексту, включно з елементами перечислення VendingMachineError, котрі можуть бути додані колись у майбутньому.

Перетворення помилок на опціональні значення

Ключове слово try? дозволяє обробити помилку, перетворивши її на опціональне значення. Якщо під час виконання буде викинуто помилку, результатом виразу буде nil. Наприклад, у наступному коді x та y мають однакові значення та поведінку:

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? дозволяє лаконічно записувати обробку кількох різних помилок одним способом. Наприклад, код нижче використовує кілька різних підходів отримання даних, або повертає nil у випадку, якщо всі вони виявились невдалими.

func fetchData() -> Data? {
    if let data = try? fetchDataFromDisk() { return data }
    if let data = try? fetchDataFromServer() { return data }
    return nil
}

Відключення поширення помилок

Іноді ми буваємо впевнені у тому, що функція чи метод, котрі викидають помилки, фактично не будуть їх викидувати під час виконання. В таких випадках можна відключити поширення помилок, шляхом запису інструкції try! перед виразом, що викидає помилку, і таким чином загорнути виклик у перевірку часу виконання, що помилку не буде викинуто. Якщо помилку таки буде викинуто, ми отримаємо помилку часу виконання.

Наприклад, наступний код використовує функцію loadImage(atPath:), котра завантажує зображення по заданому шляху, або викидує помилку, якщо задане зображення не може бути завантаженим. В цьому випадку, оскільки зображення поставляється разом із додатком, ми можемо бути впевнені у тому, що під час виконання не буде викидатись жодних помилок, і тому доречним буде відключити поширення помилок.

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

Визначення дій очистки

Інструкція defer дозволяє виконати набір інструкцій безпосередньо перед завершенням виконання поточного блоку коду. Ця інструкція дозволяє зробити необхідні дії по очищенню, котрі повинні бути виконані незалежно від того, як завершиться поточний блок коду – внаслідок викинутої помилки, чи інструкції на кшталт return чи break. Наприклад, можна використовувати інструкцію defer для того, щоб гарантувати, що відкритий раніше файл буде закрито, а виділену вручну пам’ять – вивільнено.

Інструкція defer буквально відкладає виконання свого коду до виходу із поточного контексту. Дана інструкція складається із ключового слова defer та інструкцій, виконання яких необхідно відкласти для пізнішого виконання. Відкладені інструкції не можуть містити жодного коду, що передає контроль до іншого блоку коду, як то інструкції break чи return або викидування помилки. Якщо у блоці коду є кілька інструкцій defer, вони виконуються у зворотньому порядку відносно їх появи у коді: інструкції першого defer виконуються після виконання інструкції другого defer, і так далі.

func processFile(filename: String) throws {
    if exists(filename) {
        let file = open(filename)
        defer {
            close(file)
        }
        while let line = try file.readline() {
            // Робота з файлом.
        }
        // close(file) викликається тут, у кінці контексту.
    }
}

У прикладі вище інструкція defer використовується для того, щоб гарантувати, що після кожного виклику функції open(_:) буде викликано відповідну функцію close(_:).

Примітка

Інструкцію defer можна використовувати навіть тоді, коли поруч немає ніякої обробки помилок.