obo.dev

Передача и обработка ошибок (Error Handling)

05 Dec 2022

Передача и обработка ошибок (Error Handling)

Мы используем обработку ошибок с помощью do-try-catch для реагирования на исправимые ошибки. Это дает нам больший контроль над различными ошибочными сценариями, которые могут возникнуть в нашем коде. Например, при вводе неправильного имени пользователя или пароля.

Зачем нужно отслеживать ошибки?

В приложении некоторые ошибки могут быть результатом ошибок программиста. Когда приложение вылетает с сообщением «Index out of bounds», вероятно, где-то допущена ошибка. И точно так же, когда принудительно извлекается опциональное значение, которое имеет значение nil, приложение падает.

В практической разработке iOS некоторые ошибки являются частью работы приложения. Например, сообщение «Недостаточно средств» при попытке оплаты с помощью карты.

Ошибки такого рода исправимы. Их можно обработать и отреагировать соответствующим образом. К примеру:

  • при попытке снять деньги в банкомате отображается «Неверный PIN-код»;
  • автомобиль показывает индикатор низкого уровня топлива при попытке запустить двигатель;
  • попытка аутентификации для API возвращает «Неверное имя пользователя / пароль».

Можно работать с этими ошибками, отобразив сообщение с предупреждением или сделав что-то еще. Например:

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

Причины ошибок в Swift

Ошибка может возникнуть по многим причинам. Некоторые из них:

  • Неверный пользовательский ввод;
  • Сбой устройства;
  • Потеря сетевого соединения;
  • Физические ограничения (недостаточно памяти на диске);
  • Ошибки кода;
  • Открытие недоступного файла.

Поскольку ошибки ненормально прерывают выполнение программы, то важно обрабатывать такие ошибки.

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

Ошибка (исключение) — это неожиданное событие, возникающее во время выполнения программы.

Например:

var numerator = 10
var denominator = 0

// try to divide a number by 0
var result = numerator / denominator // error code

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

Шаги для обработки ошибок в Swift

Swift поддерживает обработку ошибок с помощью блока кода do-try-catch. Для этого необходимо выполнить несколько шагов:

  1. Создание перечисления, которое представляет типы ошибок.
  2. Создание пробрасывающей функции, используя ключевое слово throws.
  3. Вызов функции, используя ключевое слово try.
  4. Оборачивание кода с помощью оператора try в блок do {...} и добавление блока catch {...} для обработки всех ошибок.

1. Создание перечисления, которое представляет типы ошибок.

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

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

Посмотрим пример:

enum DivisionError: Error {
    case dividedByZero
}

В этом примере было создано перечисление с именем DivisionError со значением dividedByZero.

Поскольку DivisionError соответствует протоколу Error, теперь можно выдать значение ошибки из перечисления.

2. Создание пробрасывающей функции

Чтобы выбрасывать ошибки из функции, необходимо создать функцию пробрасывания, используя ключевое слово throws.

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

Например:

// create throwing function using throws keyword
func division(numerator: Int, denominator: Int) throws {

    // throw error if divide by 0
    if denominator == 0 {
        throw DivisionError.dividedByZero
    }

    ...     
}

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

Функция выбрасывает значение dividedByZero из перечисления DivisionError, если выполнится условие denominator == 0.

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

Примечание. Ключевое слово throw имеет тот же эффект, что и ключевое слово return.

Ключевое слово return возвращает некоторое значение из функции, тогда как throw останавливает выполнение функции и возвращает значение ошибки вызывающей функции.

Ключевое слово throw, как и ключевое слово return, относится к Операторам передачи управления (Control Transfer Statements).

Про остальные Операторы передачи управления можно узнать в статье “Управление потоком” (см. ссылку внизу).

3. Вызов функции с использованием ключевого слова “try”

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

Например:

// call throwing function using try keyword
try division(numerator: 10, denominator: 0)

Однако, процесс обработки ошибок еще не завершен.

Если запустить программу сейчас, то получим сообщение об ошибке: “An error was thrown and was not caught” (“Произошла ошибка, которая не была обнаружена”).

Итак, чтобы выловить выброшенную ошибку, необходимо использовать оператор do-catch.

4. Обработка ошибок с помощью оператора “do-catch”

В Swift код try оборачивается в блок do и добавляется блок catch для обработки всех ошибок.

Например:

do {
  try division(numerator: 10, denominator: 0)
  ...
} catch DivisionError.dividedByZero {
  // statement
}

Здесь вызывается функция division() в блоке do {...}, которая может выбрасывать ошибки. Также был добавлен блок catch {...} для перехвата ошибки в случае, если функция выдаст её.

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

Пример обработки ошибок

// create an enum with error values
enum DivisionError: Error {
    case dividedByZero
}

// create a throwing function using throws keyword
func division(numerator: Int, denominator: Int) throws {

    // throw error if divide by 0
    if denominator == 0 {
        throw DivisionError.dividedByZero
    } else {
        let result = numerator / denominator
        print(result)
    }
}

// call throwing function from do block
do {
    try division(numerator: 10, denominator: 0)
    print("Valid Division")
}
// catch error if function throws an error
catch DivisionError.dividedByZero {
    print("Error: Denominator cannot be 0")
}

// Output
// Error: Denominator cannot be 0

В приведенном выше примере:

  • DivisionError - это перечисление;
  • division() — это пробрасывающая функция;
  • оператор do-catch обрабатывает ошибку.

Здесь использовали try для передачи значения в пробрасывающую функцию

try division(numerator: 10, denominator: 0)

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

Если состояние ошибки (denominator == 0):

  • встречается — пробрасывающая функция division() выдает ошибку (из перечисления DivisionError), которую ловит блок catch;
  • не встречается — выполняется оператор else внутри вызывающей функции и функция печати внутри блока do.

Тот же пример, только в вызове уже не будет ошибки:

// call throwing function from do block
do {
    try division(numerator: 10, denominator: 2)
    print("Valid Division")
}
// catch error if function throws an error
catch DivisionError.dividedByZero {
    print("Error: Denominator cannot be 0")
}

// Output
// 5
// Valid Division

Таким образом, функции, которые выбрасывают ошибки, следует вызывать в блоке do {...} и дополнительно прописывать блок catch {...}, который будет их отлавливать и обрабатывать.

Еще один пример.

Представьте, что мы пытаемся запустить ракету:

// create an enum with error values
enum RocketError: Error {
    case insufficientFuel
    case insufficientAstronauts(needed: Int)
    case unknownError
}

// create a throwing function using throws keyword
func igniteRockets(fuel: Int, astronauts: Int) throws {
    if fuel < 1000 {
        throw RocketError.insufficientFuel
    }
    else if astronauts < 3 {
        throw RocketError.insufficientAstronauts(needed: 3)
    }
 
    // Rocket launch
    print("3... 2... 1... Start!")
}

// call throwing function from do block
do {
    try igniteRockets(fuel: 5000, astronauts: 1)    
} catch { // catch error if function throws an error
    print(error)
}

// Output
// insufficientAstronauts(needed: 3)

Функция igniteRockets(fuel:astronauts:) будет запускать ракеты, только если значение fuel больше или равно 1000 и если на борту есть как минимум 3 астронавта.

Функция igniteRockets(…) также помечается ключевым словом throws. Это ключевое слово указывает, что ошибки должны быть обработаны.

В приведенном выше коде используется тип ошибки RocketError:

enum RocketError: Error {
    case insufficientFuel
    case insufficientAstronauts(needed: Int)
    case unknownError
}

Данное перечисление определяет три типа ошибок:

  • .insufficientFuel,
  • .insufficientAstronauts(needed)
  • .unknownError.

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

Возьмем, например, ошибку insufficientAstronauts(needed:). Когда выдается эта ошибка, то можно использовать аргумент needed:, который указывает, сколько астронавтов необходимо для успешного запуска ракеты.

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

Обработка ошибок в Swift осуществляется с помощью блока кода do-try-catch:

do {
    try igniteRockets(fuel: 5000, astronauts: 1)    
} catch {
    print(error)
}

Обработка ошибок имеет три аспекта:

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

Если функция выдает ошибку, то вызывается блок catch. В данном случае, это выводит ошибку на консоль.

Также можно реагировать на ошибки в индивидуальном порядке:

do {
    try igniteRockets(fuel: 5000, astronauts: 1)    
} catch RocketError.insufficientFuel {
    print("Ракете нужно больше топлива!")
} catch RocketError.insufficientAstronauts(let needed) {
    print("Нужно как минимум \(needed) астронавта...")
}

Вышеуказанные блоки catch вызываются на основании отдельных перечислений RocketError.

Можно напрямую получить доступ к связанным значениям перечислений, таких как needed. Также можно использовать выражения с шаблоном where, чтобы получить больший контроль над ошибочным сценарием.

В этом случае код будет выглядеть так:

// create an enum with error values
enum RocketError: Error {
    case insufficientFuel
    case insufficientAstronauts(needed: Int)
    case unknownError
}

// create a throwing function using throws keyword
func igniteRockets(fuel: Int, astronauts: Int) throws {
    if fuel < 1000 {
        throw RocketError.insufficientFuel
    }
    else if astronauts < 3 {
        throw RocketError.insufficientAstronauts(needed: 3)
    }
 
    // Rocket launch
    print("3... 2... 1... Start!")
}

// call throwing function from do block
do {
    try igniteRockets(fuel: 5000, astronauts: 1)    
} catch RocketError.insufficientFuel {
    print("Ракете нужно больше топлива!")
} catch RocketError.insufficientAstronauts(let needed) {
    print("Нужно как минимум \(needed) астронавта...")
}

// Output
// Нужно как минимум 3 астронавта...

В этом примере не просто распечаталась ошибка, а вывелось в консоль более детальное сообщение об ошибке: “Нужно как минимум 3 астронавта…”.

Преобразование ошибок в опционалы с помощью “try?”

Целью обработки ошибок является явное определение того, что происходит при возникновении ошибки. Это позволяет реагировать определенным образом на ошибки.

В некоторых случаях, нас не волнует сама ошибка. Нам просто нужно получить значение из функции. И если возникает ошибка, мы можем возвратить nil. Эта возможность доступна с помощью ключевого слова try?. Когда используется оператор try?, не нужно использовать полный блок кода do-try-catch.

let result = try? calculateValue(for: 42)

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

Возможны два сценария:

  • Функция не выдает ошибку и возвращает значение, которое присваивается константе result;
  • Функция выдает ошибку и не возвращает значение. Это означает, что константе result присваивается nil.

Обработка ошибок с помощью try? означает, что можно воспользоваться синтаксисом, специфичным для опционалов, таких как объединение по nil (Nil Coalescing) и опциональное связывание (Optional binding):

if let result = try? calculateValue(for: 99) {
    // some code 
}
 
let result = try? calculateValue(for: 123) ?? 101

Отключение обработки ошибок

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

В этом случае мы можем написать try! во время вызова функции, чтобы отключить обработку ошибок.

Например:

enum DivisionError: Error {
    case dividedByZero
}

func division(numerator: Int, denominator: Int) throws {
    if denominator == 0 {
        throw DivisionError.dividedByZero
    } else {
        let result = numerator / denominator
        print("Result:", result)
    }
}

// disable error handling
try! division(numerator: 10, denominator: 5)

// Output
// Result: 2

В приведенном выше примере был использован оператор try! во время вызова функции, чтобы отключить обработку ошибок:

try! division(numerator: 10, denominator: 5)

В этом примере, поскольку знаменателю было присвоено значение 5, мы знаем, что программа не выдаст ошибку. Поэтому мы отключили обработку ошибок.

Также обратите внимание, что когда используется оператор try!, то не нужно использовать оператор do-catch.

Примечание. Если используется try! и есть ошибка, то тогда приложение просто вылетит.

Подытожим.

Можно полностью отключить обработку ошибок с помощью try!. Ключевое слово try! используется для принудительного извлечения опционального значения.

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

  • Используйте try!, когда на 100% уверенны, что ошибка не возникнет;
  • Используйте, try!, когда невозможно продолжить выполнение кода.

Представьте, что пишите приложение, в которое встроен файл базы данных. Функция загрузки базы данных выдает ошибку, когда база данных повреждена. Можно использовать try! для загрузки базы данных в память, потому что, если база данных повреждена, приложение все равно не сможет быть использовано.


Еще полезные ссылки

Также информацию по передаче и обработки ошибок можно получить на странице официальной документации.

Ссылки на официальную документацию: