Значения локализованных ошибок в практике Swift

08 ноября 2021

Сколько раз мы всматривались в этот код:

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.

Готово!

Оригинал статьи

Содержание