Error-handling на русском

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

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

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

Заметка

Обработка ошибок в Swift перекликается с шаблонами обработки ошибок, которые используются в классе NSError  в Cocoa и Objective-C.

Отображение и выбрасывание ошибок

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

Перечисления в Swift особенно хорошо подходят для группировки схожих между собой условий возникновения ошибок и соответствующих им значениaями, что позволяет получить дополнительную информацию о природе самой ошибке. Например, вот как отображаются условия ошибки работы торгового автомата (vending machaine) внутри игры:

enum VendingMachineError: ErrorType {

  case InvalidSelection

  case InsufficientFunds(coinsNeeded: Int)

  case OutOfStock

}

Выбрасывание ошибки позволяет указать, что произошло что-то неожиданное и обычное выполнение программы не может продолжаться. Для того чтобы «выбросить» ошибку, вы используете оператор throw. Например, следующий код выбрасывает ошибку, указывая, что пять дополнительных монет нужны торговому автомату:

throw VendingMachineError.InsufficientFunds(coinsNeeded: 5)

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

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

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

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

Заметка

Обработка ошибок в Swift напоминает обработку исключений (exceptions) в других языках, с использованием ключевых слов try, catch и throw. В отличие от обработки исключений во многих языках, в том числе и в Objective-C- обработка ошибок в Swift не включает разворачивание стека вызовов, то есть процесса, который может быть дорогим в вычислительном отношении. Таким образом, производительные характеристики оператора throw сопоставимы с характеристиками оператора return.

Передача ошибки с помощью функции throw

Чтобы указать, что функция, метод или инициализатор могут выбросить ошибку, вам нужно написать ключевое слово throws в реализации функции после ее параметров. Функция, отмеченная throws называется выбрасывающий функцией (throw function). Если у функции установлен возвращаемый тип, то вы пишете ключевое слово throws перед стрелкой возврата (->).

func canThrowErrors() throws -> String

func cannotThrowErrors() -> String

Функция throw передает ошибки, которые возникают внутри нее в область вызова этой функции.

Заметка

Только функция throw может передавать ошибки. Любые ошибки, выброшенные внутри nonthrow функции, должны быть обработаны внутри самой функции.

В приведенном ниже примере 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 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
    inventory[name] = item
    dispenseSnack(name)
  }
}

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

Поскольку vend(itemNamed:) метод передает все ошибки, которые он выбрасывает, вызывающему его коду, то они должны быть обработаны напрямую, используя оператор do-catch, try? или try!, или должны быть переданы дальше. Например, buyFavoriteSnack(_:vendingMachine:) в примере ниже- это тоже функция throw, и любые ошибки, которые выбрасывает метод  vend(itemNamed:),будут переноситься до точки, где будет вызываться функция buyFavoriteSnack(_:vendingMachine:).

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(_:vendingMachine:) подбирает любимые закуски данного человека и пытается их купить, вызывая vend(itemNamed:) метод. Поскольку метод vend(itemNamed:) может выбросить ошибку, он вызывается с ключевым словом try перед ним.

Обработка ошибок с использование do-catch

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

Вот общий вид условия do-catch:

do {
    try expression
    statements
} catch pattern 1 {
    statements
} catch pattern 2 where condition {
    statements
}

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

Условие catch не должно обрабатывать всевозможные ошибки, которые могут быть выброшены в условии do. Если ни одно из условий catch не обрабатывает ошибку, то ошибка переносится в окружающую область. Тем не менее, ошибка должна обрабатываться либо окружающей областью, либо заключением в условие do-catch, обрабатывающим ошибку или находиться внутри выбрасывающей функции. Например, следующий код обрабатывает все три случая в перечислении VendingMachineError, но все другие ошибки должны быть обработаны окружающей областью:

var vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 8
do {
    try buyFavoriteSnack("Alice", vendingMachine: vendingMachine)
} catch VendingMachineError.InvalidSelection {
    print("Invalid Selection.")
} catch VendingMachineError.OutOfStock {
    print("Out of Stock.")
} catch VendingMachineError.InsufficientFunds(let coinsNeeded) {
    print("Не достаточно средств. Пожалуйста внесите еще \(coinsNeeded) монет(ы).")
}
// prints "Не достаточно средств. Пожалуйста внесите еще 2 монет(ы)."

В приведенном выше примере, buyFavoriteSnack(_:vendingMachine:) функция вызывается в выражении try, потому что она может выбросить ошибку. Если выбрасывается ошибка, выполнение немедленно переносится в условия catch, которые принимают решение о продолжении передачи ошибки. Если ошибка не выбрасывается, остальные операторы do выполняются.

Преобразование ошибок в опциональные значения

Вы можете использовать try? для обработки ошибки, преобразовав ее в опциональное значение. Если ошибка выбрасывается при условии 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(). Здесь функция возвращается как Int, поэтому x и y имеют опциональнымй Int (Int?). Использование try? позволяет написать краткий код обработки ошибок, если вы хотите обрабатывать все ошибки таким же образом. Например, следующий код использует несколько попыток для извлечения данных или возвращает nil, если попытки неудачные.

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

Запрет на передачу ошибок

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

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

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

Установка действий по очистке (Cleanup)

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

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

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) вызывается здесь, в конце зоны видимости.
    }
}

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

Заметка

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