Зачем это нужно
Многие функции могут не выполнить свою работу не из-за бага, а по вполне ожидаемой причине: пользователь ввёл не то, файла нет на диске, сервер ответил не тем, что вы ждали. До сих пор в таких случаях вы либо возвращали опционал (nil вместо результата), либо просто верили, что всё пройдёт гладко. У опционала есть слабое место: он говорит «не получилось», но не говорит почему. А «почему» студенту, пользователю или вам самим при отладке обычно и нужно в первую очередь.
Swift даёт для этого отдельный механизм — обработку ошибок. Функция явно предупреждает: «я умею падать», и вызывающий код обязан на это отреагировать. Причём падение здесь управляемое: программа не крашится, а передаёт значение-описание проблемы туда, где его готовы обработать.
Протокол Error
Ошибкой в Swift может быть любое значение, тип которого соответствует протоколу Error. У самого протокола нет обязательных требований — это просто маркер: «это значение можно бросить как ошибку». Чаще всего для ошибок заводят enum: у сбоя обычно есть несколько разных причин, а перечисление — ровно то, что нужно, чтобы их перечислить одним типом.
enum FileError: Error {
case notFound
case empty
}
Здесь FileError описывает две причины, по которым чтение файла может не задаться. Можно было бы использовать и struct, но enum читается яснее, когда причин несколько, и с ним удобно работать в switch.
throws и throw
Чтобы функция могла бросить ошибку, в её объявлении после списка параметров пишут ключевое слово throws. Внутри тела, когда что-то пошло не так, используют throw с конкретным значением ошибки.
func readContent(name: String) throws -> String {
if name.isEmpty {
throw FileError.notFound
}
return "содержимое файла \(name)"
}
Как только выполняется throw, функция немедленно прекращает работу: код после него не выполнится, а обычное значение из return вызывающая сторона не получит вовсе. Обратите внимание: throws не заменяет тип возврата, он идёт рядом с ним — функция либо вернёт String, либо бросит ошибку, третьего не дано.
do, try и catch
Вызвать бросающую функцию просто так нельзя — компилятор потребует пометить вызов словом try, а сам вызов обернуть в блок do с хотя бы одним catch после него.
do {
let content = try readContent(name: "notes.txt")
print(content)
} catch {
print("не получилось прочитать: \(error)")
}
try — это не про «попробовать и вернуть nil», как было с try? из соседних тем, а прямое указание: «эта строка может бросить ошибку, читайте дальше код обработки». Если ошибки не случилось, выполнение просто продолжается после do-блока. Если случилась — управление сразу прыгает в ближайший подходящий catch, а в переменную error кладётся брошенное значение.
Сопоставление конкретных ошибок
Один общий catch ловит вообще всё, но обычно на разные причины хочется реагировать по-разному. Для этого после catch можно указать конкретный случай — так же, как в switch.
do {
let content = try readContent(name: "")
print(content)
} catch FileError.notFound {
print("файл с таким именем не существует")
} catch FileError.empty {
print("файл есть, но он пустой")
} catch {
print("что-то ещё пошло не так: \(error)")
}
Swift проверяет блоки catch по порядку сверху вниз и выполняет первый, который подходит брошенному значению. Общий catch без условия работает как «на все остальные случаи» и обязателен, если конкретные catch не покрывают вообще все варианты ошибки.
Распространение ошибки вверх
Необязательно обрабатывать ошибку сразу же, в той же функции, где её могли бросить. Если функция сама помечена throws, ей разрешено вызывать другую бросающую функцию через try без do/catch — ошибка просто пролетит дальше, наружу, к тому, кто вызвал уже эту функцию.
func loadAndPrint(name: String) throws {
let content = try readContent(name: name)
print(content)
}
loadAndPrint не ловит ошибку из readContent — она сама помечена throws, поэтому ошибка просто передаётся выше по цепочке вызовов. Обрабатывать её будет тот, кто в конце концов вызовет loadAndPrint внутри do/catch. Так ошибку можно «поднять» через сколько угодно уровней функций, пока не найдётся код, готовый на неё содержательно отреагировать — как правило, это ближе к пользовательскому интерфейсу или к точке входа программы.
Частые ошибки
- Забыть
tryперед вызовом бросающей функции — компилятор сразу укажет на это, ошибку пропустить незаметно нельзя. - Забыть добавить
throwsв сигнатуру функции, которая вызывает другую бросающую функцию безdo/catch— компилятор потребует либо обработать ошибку на месте, либо объявить свою функцию бросающей. - Ловить всё одним общим
catchтам, где причины на самом деле разные и заслуживают разной реакции — студент теряет информацию, которую даёт конкретный тип ошибки. - Пытаться вернуть код ошибки числом или строкой вместо нормального типа, соответствующего
Error— тогда пропадают все преимущества проверки компилятором. - Писать
throwбез реального смысла ошибки, просто чтобы прервать функцию — для досрочного выхода без сбоя естьreturnиguard.
Резюме
- Любой тип, соответствующий протоколу
Error, можно бросить черезthrow; для набора причин удобнее всегоenum. - Функция, способная упасть, помечается
throwsв сигнатуре после списка параметров. - Вызов бросающей функции требует
tryи должен происходить либо внутриdo/catch, либо внутри другой функции, тоже помеченнойthrows. catchможно уточнять конкретным случаем ошибки, они проверяются по порядку, а общийcatchбез условия подхватывает всё остальное.- Необработанная ошибка распространяется вверх по цепочке вызовов, пока не найдётся подходящий
catch.
Проверьте себя
4 вопроса
Проверка возраста
Напишите функцию parseAge(_ input: String) throws -> Int, которая разбирает возраст из строки и бросает ошибку, если строка некорректна.
Тип ошибки AgeError уже объявлен в заготовке, менять его не нужно. Правила:
- если строка пустая — бросить
AgeError.empty; - если строку нельзя превратить в целое число — бросить
AgeError.notANumber; - если число отрицательное — бросить
AgeError.negative; - иначе вернуть это число.
Стоимость заказа с распространением ошибки
В заготовке уже есть тип ошибки OrderError и готовая функция checkQuantity(_:available:), которая проверяет количество товара и бросает ошибку, если оно некорректно. Не меняйте их.
Напишите функцию totalPrice(quantity: Int, pricePerItem: Int, available: Int) throws -> Int, которая:
- вызывает
checkQuantity(quantity, available: available)и НЕ ловит её ошибку сама, а даёт ей распространиться выше (функция уже помеченаthrows); - если проверка прошла без ошибки — возвращает
quantity * pricePerItem.