Swift do-try-catch语法


162

我尝试理解Swift 2中的新错误处理方法。这是我做的:我首先声明了一个错误枚举:

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。但是我认为这是详尽无遗的,因为SandwichError枚举中只有两种情况。

对于常规的switch语句,swift可以理解,在处理每种情况时,它都是详尽的。


3
您无需指定抛出的错误类型,因此Swift无法确定所有可能的选项
Farlei Heinen 2015年

有没有办法指定错误的类型?
mustafa

我在新版Swift书中找不到任何内容-目前只有throws关键字
Farlei Heinen 2015年

在没有错误或警告的操场上为我工作。
Fogmeister 2015年

2
操场上似乎允许do顶层的块不是穷尽的-如果将do封装为非抛出函数,则会生成错误。
2015年

Answers:


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")
}

这也为错误类型(或其扩展)支持其他报告错误的方式铺平了道路-例如,您可以在错误类型上拥有扩展名,该扩展名知道如何向UIAlertControlleriOS用户报告错误。


1
@rickster:您真的可以重现编译器错误吗?原始代码对我来说没有错误或警告。并且,如果引发了未捕获的异常,则程序将以error caught in main().- 终止。因此,尽管您所说的一切听起来很合理,但我无法重现该行为。
马丁·R

5
喜欢如何在扩展程序中分隔错误消息。保持代码整洁的好方法!很好的例子!
Konrad77

强烈建议您避免try在生产代码中使用强制表达式,因为它可能导致运行时错误并导致应用程序崩溃
Otar,

@Otar总体上有很好的想法,但这有点不合时宜-答案不能解决使用(或不使用)的问题try!。此外,即使对于生产代码,Swift中各种“强制”操作(展开,尝试等)也可能存在有效的“安全”用例,如果通过前提条件或配置可靠地消除了失败的可能性,则可以与编写无法测试的错误处理代码相比,使短路到即时故障更为合理。
rickster

如果您只需要显示错误消息,则将该逻辑放在SandwichError类中是有意义的。但是,我怀疑对于大多数错误,错误处理逻辑不能如此封装。这是因为它通常需要了解调用方的上下文(无论是恢复,重试还是在上游报告失败等)。换句话说,我怀疑最常见的模式还是必须与特定的错误类型匹配。
最多

29

我怀疑这只是尚未正确实施。《Swift编程指南》绝对似乎暗示编译器可以“像switch语句”推断出详尽的匹配项。它并没有提到需要一个通用catch的才能穷举。

您还会注意到错误try在行上,而不是代码块的末尾,即在某个时候,编译器将能够查明代码try块中的哪个语句具有未处理的异常类型。

但是文档有点含糊。我浏览了“ Swift的新功能”视频,找不到任何线索。我会继续努力。

更新:

现在,我们可以使用Beta 3,而不会提示ErrorType推断。我现在相信,如果曾经计划过(并且我仍然认为是在某个时候),那么协议扩展的动态调度可能会杀死它。

Beta 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的错误处理原理现在可以在此处获得快速发展邮件列表上也进行了一些有趣的讨论。本质上,John McCall反对键入错误,因为他认为大多数库无论如何都会最终包含一个通用错误情况,而且除了样板外,键入错误不太可能给代码增加太多(他使用了“期望虚张声势”一词)。克里斯·拉特纳(Chris Lattner)表示,如果它可以与弹性模型一起使用,他愿意接受Swift 3中的键入错误。


感谢您的链接。约翰没有说服:“许多库都包含“其他错误”类型”并不意味着每个人都需要“其他错误”类型。
富兰克林于

明显的反响是,没有一种简单的方法可以知道函数将抛出哪种错误,直到它抛出,这迫使开发人员捕获所有错误并尝试尽可能地处理它们。坦率地说,这很烦人。
威廉·T·弗罗加德

4

Swift担心您的case语句不能涵盖所有情况,要解决该问题,您需要创建一个默认case:

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")
}

2
但这不是很尴尬吗?我只有两种情况,所有情况都在catch声明中列出。
mustafa

2
现在是增强请求的好时机,它可以添加func method() throws(YourErrorEnum),或者甚至throws(YourEnum.Error1, .Error2, .Error3)使您知道可以抛出的内容
Matthias Bauch 2015年

8
WWDC会议之一上的Swift编译器团队明确表示,他们不想要所有可能的错误(如Java)的书呆子列表。
2015年

4
没有默认/默认错误;就像其他海报所指出的那样,留个空白{}
Opus1217

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的函数的任何其他地方


3
不知道我为什么要搞砸你。您会得到一个编译器错误,这是您想要的,对吗?如果添加枚举大小写,这就是切换语句的过程。
2015年

从某种意义上说,在我看来,这种情况最有可能发生在枚举错误/执行错误的情况下,但这恰好与枚举/切换错误一起发生,这是对的。我仍在努力使自己相信,苹果选择不键入我们抛出的内容是个好选择,但您对此并没有帮助!^^
greg3z 2015年

在非平凡的情况下,手动键入引发的错误最终会变成一团糟。类型是函数中所有throw和try语句中所有可能错误的并集。如果您手动维护错误枚举,这将很痛苦。catch {}但是,每个区块底部的A 可能会更糟。我希望编译器最终会在可以的地方自动推断错误类型,但我无法确认。
2015年

我同意编译器在理论上应该能够推断函数引发的错误类型。但是我认为对于开发人员来说,为清楚起见将它们明确写下来也是有意义的。在您谈论的非平凡案例中,列出不同的错误类型对我来说似乎可以:func f()抛出ErrorTypeA,ErrorTypeB {}
greg3z

肯定有很大一部分缺失,因为没有机制可以传达错误类型(文档注释除外)。但是,Swift团队表示他们不希望明确列出错误类型。我敢肯定,过去处理Java检查异常的大多数人都会同意😀–
Sam Sam

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...")
}

关闭但没有雪茄。尝试固定间距并制成枚举骆驼壳。
亚历克
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.