Spec-Zone.ru › Swift Language

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

Реагирование на ошибки и восстановление после них.

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

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

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

Примечание: Обработка ошибок в Swift взаимодействует с шаблонами обработки ошибок, использующими класс NSError в Cocoa и Objective-C. Дополнительную информацию об этом классе см. на странице Обработка ошибок Cocoa в Swift.

Представление и выбрасывание ошибок

В 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 = [
        "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:).

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 перед ним.

Инициализаторы, выбрасывающие ошибки, могут распространять ошибки аналогично функциям, выбрасывающим ошибки. Например, инициализатор структуры 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 <#expression#>
    <#statements#>
} catch <#pattern 1#> {
    <#statements#>
} catch <#pattern 2#> where <#condition#> {
    <#statements#>
} catch <#pattern 3#>, <#pattern 4#> where <#condition#> {
    <#statements#>
} catch {
    <#statements#>
}

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

Например, следующий код сопоставляется со всеми тремя случаями перечисления VendingMachineError.

var vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 8
do {
    try buyFavoriteSnack(person: "Alice", vendingMachine: vendingMachine)
    print("Success! Yum.")
} 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.")
} catch {
    print("Unexpected error: \(error).")
}
// Prints "Insufficient funds. Please insert an additional 2 coins."

В примере выше функция 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("Couldn't buy that from the vending machine.")
    }
}

do {
    try nourish(with: "Beet-Flavored Chips")
} catch {
    print("Unexpected non-vending-machine-related error: \(error)")
}
// Prints "Couldn't buy that from the vending machine."

В функции 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("Invalid selection, out of stock, or not enough money.")
    }
}

Функция eat(item:) перечисляет ошибки вендингового автомата для перехвата, и ее текст ошибки соответствует элементам в этом списке. Если выбрасываются любые из трех перечисленных ошибок, этот оператор catch обрабатывает их, выведя сообщение. Любые другие ошибки распространяются в окружающую область, включая любые ошибки вендингового автомата, которые могут быть добавлены позже.

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

Вы используете оператор 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(). Здесь функция возвращает целое число, поэтому 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")

Указание типа ошибки

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

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

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

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

  • В коде, который только распространяет ошибки, описанные универсальными параметрами, например, функции, принимающей аргумент-замыкание и распространяющей любые ошибки из этого замыкания. Для сравнения между распространением определенного типа ошибки и использованием rethrows см. <doc:Declarations#Rethrowing-Functions-and-Methods>.

Например, рассмотрите код, который суммирует рейтинги и использует следующий тип ошибки:

enum StatisticsError: Error {
    case noRatings
    case invalidRating(Int)
}

Чтобы указать, что функция генерирует только StatisticsError значения в качестве ошибок, вы пишете throws(StatisticsError) вместо только throws при объявлении функции. Этот синтаксис также называется типизированными бросаемыми ошибками, потому что вы пишете тип ошибки после throws в объявлении. Например, функция ниже генерирует StatisticsError значения в качестве ошибок.

func summarize(_ ratings: [Int]) throws(StatisticsError) {
    guard !ratings.isEmpty else { throw .noRatings }

    var counts = [1: 0, 2: 0, 3: 0]
    for rating in ratings {
        guard rating > 0 && rating <= 3 else { throw .invalidRating(rating) }
        counts[rating]! += 1
    }

    print("*", counts[1]!, "-- **", counts[2]!, "-- ***", counts[3]!)
}

В коде выше функция summarize(_:) суммирует список рейтингов в диапазоне от 1 до 3. Эта функция генерирует экземпляр StatisticsError, если входной данные некорректны. В обоих местах в коде выше, где генерируется ошибка, опускается тип ошибки, так как тип ошибки функции уже определён. Вы можете использовать сокращенную форму throw .noRatings вместо написания throw StatisticsError.noRatings при генерации ошибки в такой функции.

Когда вы пишете конкретный тип ошибки в начале функции, Swift проверяет, что вы не генерируете никаких других ошибок. Например, если вы попытаетесь использовать VendingMachineError из примеров ранее в этой главе в функции summarize(_:) выше, этот код выдаст ошибку во время компиляции.

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

func someThrowingFunction() -> throws {
    let ratings = [1, 2, 3, 2, 2, 1]
    try summarize(ratings)
}

В приведенном выше коде тип ошибки для someThrowingFunction() не указан, поэтому он генерирует any Error. Вы также можете написать тип ошибки явно как throws(any Error); приведенный ниже код эквивалентен предыдущему:

func someThrowingFunction() -> throws(any Error) {
    let ratings = [1, 2, 3, 2, 2, 1]
    try summarize(ratings)
}

В этом коде someThrowingFunction() распространяет любые ошибки, которые summarize(_:) генерирует. Ошибки из summarize(_:) всегда являются StatisticsError значениями, что также является допустимой ошибкой для someThrowingFunction().

Так же, как вы можете написать функцию, которая никогда не возвращает значение с типом возврата Never, вы можете написать функцию, которая никогда не генерирует ошибку с throws(Never):

func nonThrowingFunction() throws(Never) {
  // ...
}

Эта функция не может генерировать ошибки, потому что невозможно создать значение типа Never для генерации ошибки.

В дополнение к указанию типа ошибки функции, вы также можете указать конкретный тип ошибки для оператора do-catch. Например:

let ratings = []
do throws(StatisticsError) {
    try summarize(ratings)
} catch {
    switch error {
    case .noRatings:
        print("No ratings available")
    case .invalidRating(let rating):
        print("Invalid rating: \(rating)")
    }
}
// Prints "No ratings available"

В этом коде, написав do throws(StatisticsError), указывается, что оператор do-catch генерирует StatisticsError значения в качестве ошибок. Как и другие операторы do-catch, блок catch может обрабатывать все возможные ошибки или распространять необработанные ошибки для обработки в окружающем области. Этот код обрабатывает все ошибки, используя оператор switch с по одному случаю для каждого значения перечисления. Как и другие блоки catch, не имеющие шаблона, этот блок соответствует любой ошибке и связывает ошибку с локальной константой с именем error. Так как оператор do-catch генерирует StatisticsError значения, error является значением типа StatisticsError.

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

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

let ratings = []
do {
    try summarize(ratings)
} catch {
    switch error {
    case .noRatings:
        print("No ratings available")
    case .invalidRating(let rating):
        print("Invalid rating: \(rating)")
    }
}
// Prints "No ratings available"

Даже если блок do-catch выше не указывает тип генерируемой ошибки, Swift предполагает, что он генерирует StatisticsError. Вы можете явно написать throws(any Error), чтобы не допустить автоматического определения Swift типизированных бросаемых ошибок.

Указание действий по очистке

Вы используете оператор 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() {
            // Work with the file.
        }
        // close(file) is called here, at the end of the scope.
    }
}

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

Вы можете использовать оператор defer даже в тех случаях, когда код обработки ошибок не используется. Дополнительную информацию см. в <doc:ControlFlow#Deferred-Actions>.

This source file is part of the Swift.org open source project
Copyright © 2014 - 2025 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API