Быстрый синтаксис do-try-catch

162

Я попытаюсь понять новую вещь по обработке ошибок в swift 2. Вот что я сделал: я сначала объявил enum:

enum SandwichError: ErrorType {
    case NotMe
    case DoItYourself
}

А потом я объявил метод, который выдает ошибку (не исключение, ребята. Это ошибка.). Вот этот метод:

func makeMeSandwich(names: [String: String]) throws -> String {
    guard let sandwich = names["sandwich"] else {
        throw SandwichError.NotMe
    }

    return sandwich
}

Проблема с вызывающей стороны. Вот код, который вызывает этот метод:

let kitchen = ["sandwich": "ready", "breakfeast": "not ready"]

do {
    let sandwich = try makeMeSandwich(kitchen)
    print("i eat it \(sandwich)")
} catch SandwichError.NotMe {
    print("Not me error")
} catch SandwichError.DoItYourself {
    print("do it error")
}

После doстроки компилятор говорит Errors thrown from here are not handled because the enclosing catch is not exhaustive. Но, на мой взгляд, это исчерпывающее, потому что в SandwichErrorenum есть только два случая .

Для регулярных операторов переключения swift может понять, что он исчерпывающий, когда обрабатывается каждый случай.

Mustafa
источник
3
Вы не указываете тип ошибки, которую вы генерируете, поэтому Swift не может определить все возможные варианты
Farlei Heinen
Есть ли способ указать тип ошибки?
Мустафа
Я ничего не могу найти в новой версии книги Swift - только ключевое слово throws прямо сейчас
Farlei Heinen
У меня работает на детской площадке без ошибок и предупреждений.
Fogmeister
2
Похоже, что игровые площадки допускают doблоки на верхнем уровне, которые не являются исчерпывающими - если вы оберните do в функцию без броска, это вызовет ошибку.
Сэм

Ответы:

267

У модели обработки ошибок Swift 2 есть два важных момента: полнота и отказоустойчивость. Вместе они сводятся к вашему do/ catchзаявлению необходимости ловить все возможные ошибки, а не только те , которые вы знаете , что вы можете бросить.

Обратите внимание, что вы не объявляете, какие типы ошибок может генерировать функция, а только выдает ли она вообще. Это проблема типа ноль-одна-бесконечность: поскольку кто-то определяет функцию, которую будут использовать другие (в том числе и ваше будущее), вам не нужно заставлять каждого клиента вашей функции приспосабливаться к каждому изменению в реализации вашей программы. функция, в том числе какие ошибки он может выдать. Вы хотите, чтобы код, вызывающий вашу функцию, был устойчивым к таким изменениям.

Поскольку ваша функция не может сказать, какие ошибки она выдает (или может выдать в будущем), catchблоки, которые ее отлавливают, не знают, какие типы ошибок она может генерировать. Таким образом, в дополнение к обработке типов ошибок, о которых вы знаете, вам нужно обрабатывать те, которые вы не делаете, с помощью универсального catchоператора - таким образом, если ваша функция изменит набор ошибок, которые она выдает в будущем, вызывающие по-прежнему будут ловить ее ошибки.

do {
    let sandwich = try makeMeSandwich(kitchen)
    print("i eat it \(sandwich)")
} catch SandwichError.NotMe {
    print("Not me error")
} catch SandwichError.DoItYourself {
    print("do it error")
} catch let error {
    print(error.localizedDescription)
}

Но давайте не будем останавливаться на достигнутом. Подумайте еще об этой идее устойчивости. Как вы спроектировали свой бутерброд, вы должны описывать ошибки в каждом месте, где вы их используете. Это означает, что всякий раз, когда вы меняете набор ошибок, вы должны менять каждое место, где они используются ... не очень весело.

Идея определения ваших собственных типов ошибок состоит в том, чтобы позволить вам централизовать подобные вещи. Вы можете определить descriptionметод для ваших ошибок:

extension SandwichError: CustomStringConvertible {
    var description: String {
        switch self {
            case NotMe: return "Not me error"
            case DoItYourself: return "Try sudo"
        }
    }
}

И тогда ваш код обработки ошибок может попросить ваш тип ошибки описать себя - теперь каждое место, где вы обрабатываете ошибки, может использовать один и тот же код, а также обрабатывать возможные будущие случаи ошибок.

do {
    let sandwich = try makeMeSandwich(kitchen)
    print("i eat it \(sandwich)")
} catch let error as SandwichError {
    print(error.description)
} catch {
    print("i dunno")
}

Это также прокладывает путь для типов ошибок (или расширений для них) для поддержки других способов сообщения об ошибках - например, у вас может быть расширение для вашего типа ошибки, которое знает, как представить UIAlertControllerсообщение об ошибке для пользователя iOS.

rickster
источник
1
@rickster: Не могли бы вы воспроизвести ошибку компилятора? Исходный код компилируется без ошибок или предупреждений для меня. И если выдается неперехваченное исключение, программа прерывается с помощью error caught in main(). Так что, хотя все, что вы сказали, звучит разумно, я не могу воспроизвести это поведение.
Мартин Р
5
Любите, как вы разделили сообщения об ошибках в расширении. Действительно хороший способ сохранить ваш код в чистоте! Отличный пример!
Konrad77
Настоятельно рекомендуется избегать использования принудительного tryвыражения в производственном коде, так как это может вызвать ошибку времени выполнения и привести к сбою приложения
Otar
@ Хорошая мысль в целом, но это немного не по теме - ответ не касается использования (или не использования) try!. Также, возможно, существуют допустимые, «безопасные» варианты использования для различных «принудительных» операций в Swift (разворачивание, попытка и т. Д.) Даже для производственного кода - если с помощью предусловия или конфигурации вы надежно исключили возможность сбоя, это может более разумно закорачивать на мгновенный сбой, чем писать код обработки ошибок, который невозможно проверить.
Рикстер
Если вам все, что вам нужно, это отображать сообщение об ошибке, то размещение логики внутри SandwichErrorкласса имеет смысл. Однако, я подозреваю, что для большинства ошибок логика обработки ошибок не может быть настолько инкапсулирована. Это связано с тем, что обычно требуется знание контекста вызывающего абонента (либо восстановить, либо повторить попытку, либо сообщить о сбое в восходящем направлении и т. Д.). Другими словами, я подозреваю, что наиболее распространенным шаблоном должно быть сопоставление с конкретными типами ошибок.
максимум
29

Я подозреваю, что это просто еще не реализовано должным образом. Руководство по программированию Swift определенно подразумевает, что компилятор может выводить исчерпывающие совпадения «как оператор switch». В нем не упоминается необходимость генерала catch, чтобы быть исчерпывающим.

Вы также заметите, что ошибка находится в tryстроке, а не в конце блока, то есть в какой-то момент компилятор сможет определить, какой tryоператор в блоке имеет необработанные типы исключений.

Документация немного двусмысленная, хотя. Я пролистал видео «Что нового в Swift» и не смог найти никаких подсказок; Я буду продолжать пытаться

Обновить:

Теперь мы подошли к бета-версии 3 без намека на вывод ErrorType. Теперь я верю, что если это когда-либо планировалось (и я все еще думаю, что это было в какой-то момент), динамическая диспетчеризация расширений протокола, вероятно, убила его.

Бета 4 Обновление:

Xcode 7b4 добавил поддержку комментариев к документу Throws:, который «должен использоваться для документирования, какие ошибки могут быть сгенерированы и почему». Я предполагаю, что это по крайней мере обеспечивает некоторый механизм для сообщения об ошибках потребителям API. Кому нужна система типов, когда у вас есть документация!

Еще одно обновление:

Потратив некоторое время в надежде на автоматический ErrorTypeвывод и выяснив, какими будут ограничения для этой модели, я передумал - это то, что я надеюсь, Apple реализует вместо этого. По существу:

// allow us to do this:
func myFunction() throws -> Int

// or this:
func myFunction() throws CustomError -> Int

// but not this:
func myFunction() throws CustomErrorOne, CustomErrorTwo -> Int

Еще одно обновление

Обоснование обработки ошибок Apple теперь доступно здесь . Были также некоторые интересные обсуждения в списке рассылки swift-evolution . По сути, Джон МакКолл против типизированных ошибок, потому что он считает, что большинство библиотек в конечном итоге будет содержать общий случай ошибок, и что типизированные ошибки вряд ли добавят много кода, кроме шаблонного (он использовал термин «желательный блеф»). Крис Латтнер сказал, что он открыт для опечаток в Swift 3, если он может работать с моделью устойчивости.

Сэм
источник
Спасибо за ссылки. Однако Джон не убедил: «во многих библиотеках есть тип« другая ошибка »», это не означает, что всем нужен тип «другая ошибка».
Франклин Ю
Очевидный счетчик состоит в том, что нет простого способа узнать, какую ошибку выдает функция, до тех пор, пока она не заставит разработчика перехватить все ошибки и попытаться обработать их как можно лучше. Это довольно раздражает, если быть откровенным.
Уильям Т Фроггард
4

Свифт обеспокоен тем, что ваше заявление о кейсе не охватывает все случаи, чтобы его исправить, вам нужно создать случай по умолчанию:

do {
    let sandwich = try makeMeSandwich(kitchen)
    print("i eat it \(sandwich)")
} catch SandwichError.NotMe {
    print("Not me error")
} catch SandwichError.DoItYourself {
    print("do it error")
} catch Default {
    print("Another Error")
}
Icaro
источник
2
Но разве это не неловко? У меня есть только два случая, и все они перечислены в catchзаявлениях.
Мустафа
2
Сейчас самое подходящее время для запроса улучшения, который добавляет func method() throws(YourErrorEnum)или даже throws(YourEnum.Error1, .Error2, .Error3)знает, что может быть брошено
Матиас Баух
8
Команда компиляторов Swift на одной из сессий WWDC дала понять, что им не нужны педантичные списки всех возможных ошибок «как Java».
Сэм
4
Нет ошибки по умолчанию / по умолчанию; просто оставьте пустой улов {}, как отмечали другие
авторы
1
@Icaro Это не делает меня в безопасности; если я "добавлю новую запись в массив" в будущем, компилятор должен кричать на меня за то, что я не обновил все затронутые предложения catch.
Франклин Ю
3

Я также был разочарован отсутствием типа, который может выдавать функция, но теперь я получаю его благодаря @rickster, и я резюмирую это следующим образом: допустим, мы могли бы указать тип, который выдает функция, у нас будет что-то вроде этого:

enum MyError: ErrorType { case ErrorA, ErrorB }

func myFunctionThatThrows() throws MyError { ...throw .ErrorA...throw .ErrorB... }

do {
    try myFunctionThatThrows()
}
case .ErrorA { ... }
case .ErrorB { ... }

Проблема в том, что даже если мы ничего не изменим в myFunctionThatThrows, просто добавим случай ошибки в MyError:

enum MyError: ErrorType { case ErrorA, ErrorB, ErrorC }

мы облажались, потому что наш do / try / catch больше не является исчерпывающим, как и любое другое место, где мы вызывали функции, которые выбрасывают MyError

greg3z
источник
3
Не уверен, что я понимаю, почему ты облажался. Вы получите ошибку компилятора, чего вы хотите, верно? Вот что происходит с переключением операторов, если вы добавляете регистр enum.
Сэм
В некотором смысле мне казалось наиболее вероятным, что это произойдет с ошибками enums / do case, но это именно так, как это было бы в enums / switch, вы правы. Я все еще пытаюсь убедить себя, что выбор Apple не печатать то, что мы бросаем, является хорошим, но вы не поможете мне в этом! ^^
greg3z
Ручной ввод сгенерированных ошибок может привести к большой неразберихе в нетривиальных случаях. Типы представляют собой объединение всех возможных ошибок из всех операторов throw и try внутри функции. Если вы вручную сохраняете перечисления ошибок, это будет больно. А catch {}в нижней части каждого блока, возможно, еще хуже. Я надеюсь, что компилятор в конечном итоге выведет типы ошибок автоматически, но не смог подтвердить.
Сэм
Я согласен, что теоретически компилятор должен иметь возможность определять типы ошибок, которые выдает функция. Но я думаю, что для разработчика имеет смысл явно записать их для ясности. В нетривиальных случаях, о которых вы говорите, перечисление различных типов ошибок мне кажется вполне приемлемым: func f () выдает ErrorTypeA, ErrorTypeB {}
greg3z
Определенно, есть большая часть, отсутствующая в том, что нет механизма для передачи типов ошибок (кроме комментариев в документации). Однако команда Swift заявила, что не хочет иметь явных списков типов ошибок. Я уверен, что большинство людей, которые раньше имели дело с проверенными исключениями Java, согласятся 😀
Сэм
1
enum NumberError: Error {
  case NegativeNumber(number: Int)
  case ZeroNumber
  case OddNumber(number: Int)
}

extension NumberError: CustomStringConvertible {
         var description: String {
         switch self {
             case .NegativeNumber(let number):
                 return "Negative number \(number) is Passed."
             case .OddNumber(let number):
                return "Odd number \(number) is Passed."
             case .ZeroNumber:
                return "Zero is Passed."
      }
   }
}

 func validateEvenNumber(_ number: Int) throws ->Int {
     if number == 0 {
        throw NumberError.ZeroNumber
     } else if number < 0 {
        throw NumberError.NegativeNumber(number: number)
     } else if number % 2 == 1 {
         throw NumberError.OddNumber(number: number)
     }
    return number
}

Теперь подтвердите номер:

 do {
     let number = try validateEvenNumber(0)
     print("Valid Even Number: \(number)")
  } catch let error as NumberError {
     print(error.description)
  }
Йогендра Сингх
источник
-2

Создайте перечисление следующим образом:

//Error Handling in swift
enum spendingError : Error{
case minus
case limit
}

Создайте метод как:

 func calculateSpending(morningSpending:Double,eveningSpending:Double) throws ->Double{
if morningSpending < 0 || eveningSpending < 0{
    throw spendingError.minus
}
if (morningSpending + eveningSpending) > 100{
    throw spendingError.limit
}
return morningSpending + eveningSpending
}

Теперь проверьте, есть ошибка или нет, и обработайте ее:

do{
try calculateSpending(morningSpending: 60, eveningSpending: 50)
} catch spendingError.minus{
print("This is not possible...")
} catch spendingError.limit{
print("Limit reached...")
}
Мистер Джавед Мултани
источник
Близко, но не сигара. Попробуйте исправить интервал и создать enum camel case.
Алек