1. do/catch: зачем нужен и как устроен
Сначала может показаться, что try — это и есть обработка: поставил try, и всё стало безопасно. Но try — это скорее табличка «Осторожно, тут может быть ошибка», а не спасательная служба. do/catch — это как раз спасательная служба: он говорит Swift, что в этом месте мы готовы принять решение, что делать при неудаче.
Представьте бытовую аналогию. try — это как прочитать на коробке «может содержать следы орехов». А do/catch — это момент, когда вы решаете: «Окей, у меня аллергия — значит, я не ем это, а беру другую коробку». Без решения предупреждение бесполезно.
В Swift логика такая: мы вызываем throwing‑функцию с try, а затем либо продолжаем выполнение (если всё хорошо), либо перепрыгиваем в catch (если была ошибка).
Наглядно это можно представить так:
flowchart TD
A["do { ... }"] --> B[try вызвать throwing-функцию]
B -->|успех| C[продолжаем код внутри do]
B -->|ошибка| D[прыжок в catch]
D --> E[реакция: сообщение / другой путь / остановка сценария]
Базовый синтаксис do/catch
Когда вы впервые пишете do/catch, хочется сделать его “на всякий случай” и забыть. Но его сила не в том, что он «ловит всё», а в том, что он явно фиксирует точку ответственности: вот здесь мы готовы обработать проблему.
Минимальная форма выглядит так: внутри do пишем код с try, а в catch — реакцию. Важно: в catch у нас есть переменная error (типа Error), чтобы хотя бы что-то вывести и не потерять причину.
import Foundation
enum MathError: Error { case divisionByZero }
func divide(_ a: Int, by b: Int) throws -> Int {
guard b != 0 else { throw MathError.divisionByZero }
return a / b
}
do {
let x = try divide(10, by: 2)
print("Ответ: \(x)") // Ответ: 5
} catch {
print("Ошибка: \(error)")
}
Обратите внимание на психологический эффект: мы перестали надеяться на лучшее и начали писать код так, будто ошибки — это нормальная часть жизни. Это уже шаг к зрелому стилю, даже если вы пока пишете учебные программы.
Несколько catch: порядок важнее, чем кажется
Один catch — это нормально, но часто хочется реагировать по-разному на разные причины. И Swift это позволяет: можно написать несколько catch, которые проверяются сверху вниз, как else if.
Тут есть классическое правило, почти как в жизни: «Сначала разбираемся с понятными и ожидаемыми проблемами, и только в конце — “ну вообще всё остальное”».
import Foundation
enum MathError: Error { case divisionByZero }
func divide(_ a: Int, by b: Int) throws -> Int {
guard b != 0 else { throw MathError.divisionByZero }
return a / b
}
do {
print(try divide(10, by: 0))
} catch MathError.divisionByZero {
print("Нельзя делить на ноль") // Нельзя делить на ноль
} catch {
print("Неожиданная ошибка: \(error)")
}
Здесь первый catch ловит конкретный кейс, а второй — «страховка». Если поменять их местами, общий catch перехватит всё, и конкретный станет бесполезным (а иногда компилятор ещё и предупредит, что код недостижим).
Чтобы закрепить идею, вот маленькая таблица:
| Что написано | Что это означает на практике |
|---|---|
|
Ловим только конкретный кейс конкретной ошибки |
|
Ловим любой кейс SomeError, дальше разбираем внутри |
|
Ловим вообще всё, что долетело сюда |
Ветвление по типам ошибок: «это моя ошибка или чужая?»
В больших программах (и даже в учебных CLI) часто встречается ситуация: у нас есть свои ошибки (например, ошибки ввода), и есть «какие-то другие» (которые мы пока не планировали). В таком случае удобно сначала попытаться распознать «свои» ошибки по типу, а остальные отдать в общий обработчик.
Для этого есть знакомый приём: catch let error as SomeError.
import Foundation
enum InputError: Error { case empty }
func requireNonEmpty(_ text: String) throws -> String {
guard !text.isEmpty else { throw InputError.empty }
return text
}
do {
_ = try requireNonEmpty("")
print("OK")
} catch let e as InputError {
print("Ошибка ввода: \(e)") // Ошибка ввода: empty
} catch {
print("Другая ошибка: \(error)")
}
Почему это полезно? Потому что на уровне «ошибка ввода» мы обычно хотим понятное сообщение пользователю, а на уровне «другая ошибка» — хотя бы не потерять информацию и не сделать вид, что всё хорошо.
Pattern matching в catch: достаём associated values
Сильная сторона do/catch в Swift — в том, что catch умеет pattern matching почти как switch. То есть если ваша ошибка хранит контекст через associated values, вы можете извлечь эти значения прямо в catch.
Это особенно удобно для валидации: «пришло значение X, но ожидали диапазон Y…Z».
import Foundation
enum YearError: Error {
case notANumber(input: String)
case outOfRange(value: Int, min: Int, max: Int)
}
func parseYear(_ text: String) throws -> Int {
guard let value = Int(text) else { throw YearError.notANumber(input: text) }
guard (1900...2100).contains(value) else {
throw YearError.outOfRange(value: value, min: 1900, max: 2100)
}
return value
}
do {
print(try parseYear("3024"))
} catch YearError.notANumber(let input) {
print("Год должен быть числом, а не '\(input)'")
} catch YearError.outOfRange(let value, let min, let max) {
print("Год \(value) вне диапазона \(min)...\(max)") // Год 3024 вне диапазона 1900...2100
}
Обратите внимание: мы не делали никаких дополнительных switch внутри catch. Мы сразу поймали конкретную причину и сразу же извлекли данные.
Один catch на несколько кейсов
Иногда разные причины ошибки должны приводить к одинаковой реакции. Например, «пустая строка» и «строка из пробелов» — для пользователя это почти одно и то же: «вы ничего не ввели».
В Swift можно написать один catch на несколько паттернов через запятую.
import Foundation
enum InputError: Error { case empty, whitespaceOnly }
func normalizeName(_ text: String) throws -> String {
if text.isEmpty { throw InputError.empty }
if text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
throw InputError.whitespaceOnly
}
return text
}
do {
_ = try normalizeName(" ")
} catch InputError.empty, InputError.whitespaceOnly {
print("Имя не должно быть пустым") // Имя не должно быть пустым
}
Это выглядит компактно и читаемо: вы показываете, что причины разные, но политика реакции — одинаковая.
2. Пример: усиливаем консольный “мини‑LibraryCLI”
Сценарий: добавление книги с понятными сообщениями
Сейчас сделаем важный шаг от «абстрактных примеров» к ощущению, что do/catch реально делает программу удобнее. Мы напишем маленький сценарий: добавление книги в память. Никаких сложных парсеров команд, только простой диалог: запросили название, запросили год, попытались создать книгу.
Сначала опишем модель и ошибки. Ошибки разделим на две группы: ошибки ввода строки и ошибки года. Это позволит красиво ветвиться в catch.
import Foundation
struct Book {
let title: String
let year: Int
}
enum TitleError: Error { case empty }
enum YearError: Error { case notANumber(input: String), outOfRange(Int) }
Теперь напишем функции‑валидаторы. Заметьте стиль: каждая функция либо возвращает корректное значение, либо бросает ошибку. Никаких “пустых строк как валидных данных”.
import Foundation
func parseTitle(_ text: String) throws -> String {
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { throw TitleError.empty }
return trimmed
}
func parseYear(_ text: String) throws -> Int {
guard let y = Int(text) else { throw YearError.notANumber(input: text) }
guard (1450...2100).contains(y) else { throw YearError.outOfRange(y) }
return y
}
А теперь самое интересное: точка, где мы реагируем. Мы попробуем создать Book, и если что-то пошло не так — покажем человеку нормальное сообщение и не уроним программу.
import Foundation
func readLineOrEmpty() -> String { readLine() ?? "" }
var books: [Book] = []
print("Название книги:")
let titleText = readLineOrEmpty()
print("Год издания:")
let yearText = readLineOrEmpty()
do {
let title = try parseTitle(titleText)
let year = try parseYear(yearText)
books.append(Book(title: title, year: year))
print("Добавлено: \(title) (\(year))")
} catch TitleError.empty {
print("Ошибка: название не должно быть пустым")
} catch YearError.notANumber(let input) {
print("Ошибка: год '\(input)' не является числом")
} catch YearError.outOfRange(let value) {
print("Ошибка: год \(value) вне допустимого диапазона")
} catch {
print("Неожиданная ошибка: \(error)")
}
Здесь видно главное: do/catch превращает «сломалось» в «мы предусмотрели». С точки зрения пользователя это разница между «программа упала» и «программа объяснила, что не так».
do/catch внутри цикла: ошибка как конец попытки, а не конец программы
В реальных консольных сценариях вы редко хотите, чтобы ошибка ввода завершала программу. Обычно ошибка означает: «эта попытка не удалась, давай попробуем ещё раз». И do/catch идеально ложится на цикл: каждая итерация — это попытка, а catch — реакция, после которой можно продолжать.
Сделаем мини‑цикл: пользователь вводит книги, пока не напишет exit. Обратите внимание: мы обрабатываем ошибки локально и продолжаем цикл, не «пробрасывая» их куда-то дальше.
import Foundation
var books: [Book] = []
while true {
print("Введите title,year или exit:")
let line = readLine() ?? ""
if line == "exit" { break }
do {
let parts = line.split(separator: ",", maxSplits: 1).map(String.init)
let title = try parseTitle(parts.first ?? "")
let year = try parseYear(parts.count > 1 ? parts[1] : "")
books.append(Book(title: title, year: year))
print("OK, книг: \(books.count)") // например: OK, книг: 1
} catch {
print("Не удалось добавить книгу: \(error)")
}
}
Да, вывод в catch пока грубоватый: он печатает error как есть. Но это уже огромный прогресс по сравнению с падением программы. А «красивые сообщения» мы как раз делали выше через конкретные catch.
3. Типичные ошибки при использовании do/catch
Ошибка №1: общий catch ставят первым, а конкретные — ниже.
Такой код выглядит логично на глаз («сначала обработаем всё, а потом частные случаи»), но работает наоборот: первый общий catch перехватывает любые ошибки, и специфические ветки становятся недостижимыми. Держите в голове правило: сверху — самое конкретное, снизу — самый общий «пылесос».
Ошибка №2: ловят ошибку и «молчат» — пустой catch { }.
Иногда хочется «чтобы компилировалось» и просто написать пустой catch. Это почти всегда превращается в загадочные баги: что-то не происходит, но почему — непонятно. Если уж ловите ошибку, дайте хотя бы минимальную реакцию: сообщение пользователю, лог в консоль, прекращение сценария.
Ошибка №3: в catch печатают одно и то же «что-то пошло не так» для всех причин.
Технически это обработка, но по смыслу — потеря информации. Если вы уже проектировали типизированные ошибки с кейсами и контекстом, используйте это богатство: делайте разные catch или pattern matching по associated values, чтобы сообщения были конкретными.
Ошибка №4: пытаются «ветвиться по строке ошибки», а не по типу/кейсу.
Новички иногда делают сравнения вроде "\(error)".contains("empty"). Это хрупко и нечестно: строка — не контракт. Контракт — это тип ошибки и её кейс. В Swift у вас для этого есть catch SomeError.someCase и catch let e as SomeError, и это намного надёжнее.
Ошибка №5: пишут огромный do на полфайла.
do/catch хорошо читается, когда в do лежит один понятный сценарий: «попробовать сделать X». Если вы запихнули туда 200 строк, то непонятно, какая именно из них бросила ошибку, и обработка становится мутной. В таких случаях полезнее дробить: несколько небольших do/catch вокруг самостоятельных шагов, либо вынос шагов в функции (но без фанатизма).
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ