Передача и обработка ошибок (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
. Для этого необходимо выполнить несколько шагов:
- Создание перечисления, которое представляет типы ошибок.
- Создание пробрасывающей функции, используя ключевое слово throws.
- Вызов функции, используя ключевое слово try.
- Оборачивание кода с помощью оператора 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!
для загрузки базы данных в память, потому что, если база данных повреждена, приложение все равно не сможет быть использовано.
Еще полезные ссылки
Также информацию по передаче и обработки ошибок можно получить на странице официальной документации.
Ссылки на официальную документацию: