Сколько раз мы всматривались в этот код:
do {
try writeEverythingToDisk()
} catch let error {
// ???
}
или в этот:
switch result {
case .failure(let error):
// ???
}
и задавали себе вопрос: “Как же мне выудить из этой ошибки информацию?”
Проблема в том, что ошибка, вероятно, содержит много информации, которая могла бы нам помочь. Но получение этой информации часто дело не простое.
Чтобы понять причину этого, давайте рассмотрим имеющиеся в нашем распоряжении способы прикрепления информации к ошибкам.
Новое: LocalizedError
В языке Swift мы передаем ошибки в соответствии с протоколом Error. Протокол LocalizedError его наследует, расширяя его некоторыми полезными свойствами:
- errorDescription
- failureReason
- recoverySuggestion
Соответствие протоколу LocalizedError вместо протокола Error (и обеспечение реализации этих новых свойств), позволяет нам дополнить нашу ошибку множеством полезной информации, которая может быть передана во время исполнения программы (интернет-журнал NSHipster рассматривает этот вопрос более подробно):
enum MyError: LocalizedError {
case badReference
var errorDescription: String? {
switch self {
case .badReference:
return "The reference was bad."
}
}
var failureReason: String? {
switch self {
case .badReference:
return "Bad Reference"
}
}
var recoverySuggestion: String? {
switch self {
case .badReference:
return "Try using a good one."
}
}
}
Старое: userInfo
Хорошо знакомый класс NSError содержит свойство - словарь userInfo, который мы можем заполнить всем, чем захотим. Но также, этот словарь содержит несколько заранее определённых ключей:
- NSLocalizedDescriptionKey
- NSLocalizedFailureReasonErrorKey
- NSLocalizedRecoverySuggestionErrorKey
Можно заметить, что их названия очень похожи на свойства LocalizedError. И, фактически, они играют аналогичную роль:
let info = [
NSLocalizedDescriptionKey:
"The reference was bad.",
NSLocalizedFailureReasonErrorKey:
"Bad Reference",
NSLocalizedRecoverySuggestionErrorKey:
"Try using a good one."
]
let badReferenceNSError = NSError(
domain: "ReferenceDomain",
code: 42,
userInfo: info
)
Это выглядит, как будто LocalizedError и NSError должны быть в основном равнозначны, верно? Что ж, в этом-то и заключается основная проблема.
Старое встречается с новым
Дело в том, что класс NSError соответствует протоколу Error, но не протоколу LocalizedError. Иными словами:
badReferenceNSError is NSError //> true
badReferenceNSError is Error //> true
badReferenceNSError is LocalizedError //> false
Это значит, что если мы попытаемся извлечь информацию из любой произвольной ошибки привычным способом, то это сработает должным образом только для Error и LocalizedError, но для NSError будет отражено только значение свойства localizedDescription:
// The obvious way that doesn’t work:
func log(error: Error) {
print(error.localizedDescription)
if let localized = error as? LocalizedError {
print(localized.failureReason)
print(localized.recoverySuggestion)
}
}
log(error: MyError.badReference)
//> The reference was bad.
//> Bad Reference
//> Try using a good one.
log(error: badReferenceNSError)
//> The reference was bad.
Это довольно неприятно, потому как известно, что наш объект класса NSError содержит в себе информацию о причине сбоя и предложение по исправлению ошибки, прописанные в его словаре userInfo. И это, по какой-то причине, не отображается через соответствие LocalizedError.
Новое становится старым
В этом месте мы можем впасть в отчаяние, мысленно представляя себе множество операторов switch, пытающихся отсортировать по типам и по наличию различные свойства словаря userInfo. Но не бойтесь! Есть несложное решение. Просто оно не совсем очевидно.
Обратите внимание, что в классе NSError определены удобные методы для извлечения локализованного описания, причины сбоя и предложения по восстановлению в свойстве userInfo:
badReferenceNSError.localizedDescription
//> "The reference was bad."
badReferenceNSError.localizedFailureReason
//> "Bad Reference"
badReferenceNSError.localizedRecoverySuggestion
//> "Try using a good one."
Они отлично подходят для обработки NSError, но не помогают нам извлечь эти значения из LocalizedError... или это так?
Оказывается, протокол языка Swift Error соединён компилятором с классом NSError. Это означает, что мы можем превратить Error в NSError с помощью простого приведения типа:
let bridgedError: NSError
bridgedError = MyError.badReference as NSError
Но ещё больше впечатляет то, что когда мы производим приведение LocalizedError этим способом, то мост срабатывает правильно и подключает localizedDescription, localizedFailureReason и localizedRecoverySuggestion, указывая на соответствующие значения!
Поэтому, если мы хотим, чтобы согласованный интерфейс извлекал локализованную информацию из Error, LocalizedError и NSError, нам просто нужно не долго думая привести всё к NSError:
func log(error: Error) {
let bridge = error as NSError
print(bridge.localizedDescription)
print(bridge.localizedFailureReason)
print(bridge.localizedRecoverySuggestion)
}
log(error: MyError.badReference)
//> The reference was bad.
//> Bad Reference
//> Try using a good one.
log(error: badReferenceNSError)
//> The reference was bad.
//> Bad Reference
//> Try using a good one.
Готово!