Обработка ошибок в Swift: try/catch и не только — полное руководство
Обработка ошибок — одна из ключевых тем в разработке под iOS и macOS. В языке Swift реализована мощная и элегантная система управления сбоями, которая выходит далеко за рамки простого try/catch. Понимание этих механизмов позволяет писать надёжный, предсказуемый и безопасный код. В этой статье мы разберём все аспекты: от базового объявления ошибок до продвинутых техник с опциональными значениями и деинициализаторами.
Основы: протокол Error и ключевое слово throws
В Swift любая ошибка должна соответствовать протоколу Error. Обычно для этого используют перечисления (enum). Рассмотрим простой пример — проверка пароля:
enum PasswordError: Error {
case tooShort
case missingDigit
case missingUppercase
}
func validatePassword(_ password: String) throws {
guard password.count >= 8 else {
throw PasswordError.tooShort
}
guard password.rangeOfCharacter(from: .decimalDigits) != nil else {
throw PasswordError.missingDigit
}
guard password.rangeOfCharacter(from: .uppercaseLetters) != nil else {
throw PasswordError.missingUppercase
}
print("Пароль корректен")
}
Ключевое слово throws в объявлении функции сообщает компилятору, что выполнение может быть прервано ошибкой. Вызов такой функции требует специального синтаксиса.
Конструкция do-catch и ключевое слово try
Для обработки потенциальных ошибок используется блок do-catch с пометкой try перед вызовом:
do {
try validatePassword("Hello123")
print("Успех!")
} catch PasswordError.tooShort {
print("Ошибка: пароль слишком короткий")
} catch PasswordError.missingDigit {
print("Ошибка: в пароле нет цифры")
} catch PasswordError.missingUppercase {
print("Ошибка: в пароле нет заглавной буквы")
} catch {
print("Неизвестная ошибка: \(error)")
}
Обратите внимание на последний блок catch — он перехватывает любые неучтённые ранее ошибки. Это обязательное требование: компилятор Swift проверяет, что все возможные ошибки обработаны.
try? и try! — упрощённые варианты обработки
Иногда вам не нужен полный контроль над ошибками. Swift предлагает два сокращённых варианта:
- try? — преобразует ошибку в опциональное значение
nil. Если ошибки нет — возвращает результат, обёрнутый вOptional. - try! — утверждает, что ошибки не будет. Если она всё же возникнет, приложение аварийно завершится (runtime crash).
// try? — безопасное игнорирование ошибки
if let _ = try? validatePassword("weak") {
print("Пароль прошёл проверку")
} else {
print("Пароль не прошёл")
}
// try! — только если вы уверены на 100%
let result = try! validatePassword("StrongPass1") // Краш, если пароль невалиден
Используйте try! только в ситуациях, где ошибка теоретически невозможна (например, при загрузке ресурса, встроенного в бандл приложения).
Продвинутые техники: rethrows, defer и Result
Переброс ошибок (rethrows)
Ключевое слово rethrows применяется к функциям высшего порядка, которые принимают замыкание, способное выбрасывать ошибку. Функция сама не выбрасывает ошибку, но передаёт её из замыкания:
func processItems(_ items: [String], using validator: (String) throws -> Void) rethrows {
for item in items {
try validator(item)
}
}
// Использование
do {
try processItems(["Hello", "World123"], using: validatePassword)
} catch {
print("Ошибка обработки: \(error)")
}
Это позволяет писать гибкие функции-обёртки без дублирования кода обработки ошибок.
Очистка ресурсов с defer
Блок defer гарантирует выполнение кода при любом завершении функции — как при успехе, так и при ошибке. Идеально подходит для закрытия файлов, освобождения памяти или завершения сетевых сессий:
func readFile(at path: String) throws -> String {
let file = openFile(path) // Гипотетическая функция
defer {
closeFile(file) // Закроется в любом случае
print("Файл закрыт")
}
guard file.isValid else {
throw FileError.corrupted
}
return file.read()
}
Помните: блоки defer выполняются в обратном порядке их объявления.
Тип Result для асинхронных операций
Начиная с Swift 5, тип Result<Success, Failure> позволяет обрабатывать ошибки без исключений — особенно удобно в асинхронных функциях и замыканиях:
enum NetworkError: Error {
case badURL
case noData
}
func fetchUser(completion: (Result<String, NetworkError>) -> Void) {
let success = Bool.random()
if success {
completion(.success("User: John"))
} else {
completion(.failure(.noData))
}
}
// Использование
fetchUser { result in
switch result {
case .success(let user):
print(user)
case .failure(let error):
print("Ошибка: \(error)")
}
}
Result особенно полезен при работе с Combine, async/await или старыми callback-based API.
Лучшие практики и частые ошибки
- Не злоупотребляйте try! — это источник неожиданных крашей. Используйте только в unit-тестах или когда уверены в данных на 100%.
- Создавайте свои типы ошибок — используйте перечисления с ассоциированными значениями для передачи дополнительного контекста.
- Избегайте пустых catch-блоков — всегда логируйте или обрабатывайте ошибку, даже если просто пробрасываете её дальше.
- Используйте defer для ресурсов — это гарантирует корректную очистку даже при неожиданных ошибках.
- Для асинхронного кода предпочитайте Result или async throws (в современных версиях Swift) — это делает поток ошибок явным.
Заключение
Обработка ошибок