Swift异常与异常处理

4

浏览了论坛和Swift文档(我承认没有完全阅读),似乎在Swift中,我们鼓励编写更加安全的代码,而不是使用try-catch机制来处理异常。 针对这一点,我有一个关于示例API的问题,并且希望学习如何更加安全地处理这种情况:

例如,我可以使用NSDecimalNumberHandler创建以下类:

class MathWhiz {

    init() {
    let defaultBehavior: NSDecimalNumberHandler =
    NSDecimalNumberHandler.defaultDecimalNumberHandler()
    }
    func add(op1: String, op2: String) ->NSDecimalNumber {
        return NSDecimalNumber.decimalNumberWithString(op1).decimalNumberByAdding(NSDecimalNumber.decimalNumberWithString(op2))
    }
}

如果我使用以下代码,则会得到一个数字:
let brain = MathWhiz()
brain.add("1", op2: "1e127")

然而,如果我引起了溢出异常:

brain.add("1", op2: "1e128")

我将按预期崩溃该程序。

因此,我的问题是,API引发异常,但我在这里没有处理它们。有其他人发帖指出Swift没有异常处理功能,但是本问题寻求一种优雅的方式来处理这个问题,以符合语言创作者的想法。 是否有一种推荐的方法来处理这个问题,而不必编写自己的代码来检查溢出、下溢、精度丢失等?我想让NSDecimalNumberHandler为我执行这些操作。

3个回答

6

如果您正在设计一个 Swift 函数(或方法),那么在处理错误时至少有 3 种选择:

选择 1:返回 Optional 类型

如果您的函数可能失败,并且经常发生这种情况,那么考虑返回一个 optional 类型变量。例如,在您的情况下,您的方法 add 可以返回一个 NSDecimalNumber? 而不是普通的 NSDecimalNumber。在这种情况下,您的方法将检查可能出现的所有问题,并在这些情况下返回 nil。溢出和下溢都会返回 nil,而其他所有情况都会返回一个 NSDecimalNumber。调用者必须检查并解包可选的 NSDecimalNumber,如下所示:

let brain = MathWhiz()
if let sum = brain.add("1", op2: "1e127") {
    println("the result was \(sum)")
} else
    println("something went wrong with MathWhiz add")
}

选择2:返回枚举类型

如果你想要返回更多关于出现错误的信息,你可以创建一个枚举类型,为每一个错误及成功分别设置一个值。例如:

enum MathWhizResult {
    case Overflow
    case Underflow
    case Success(NSDecimalNumber)
}

接着,需要定义add函数来返回MathWhizResult

func add(op1: String, op2: String) -> MathWhizResult

在出现错误的情况下,add 会返回 .Overflow.Underflow。在成功的情况下,add 返回 Success(result)。调用者必须检查枚举并解包结果。可以使用 switch 来实现此操作:
switch (brain.add("1", op2: "1e128")) {
case .Overflow
    println("darn, it overflowed")
case .Underflow
    println("underflow condition happened")
case .Success(let answer)
    println("the result was \(answer)"
}

选择3: 不显式处理错误

在前两个选项中解包结果可能会造成太多的开销,尤其是对于一个非常罕见的错误。你可以选择只返回结果,并让调用者处理可能存在的下溢或上溢条件。在这种情况下,他们必须在调用add之前自行检查这些条件。好处是,如果他们知道它们的程序永远不会导致下溢或上溢(例如,因为他们处理单个数字),那么就不需要解包结果。


我创建了一个小应用程序来演示如何使用NSDecimalNumbers实现此功能。我在Xcode中创建了一个Single View Application。在StoryBoard中的ViewController中,我添加了3个TextField(分别用于操作数1、操作数2和结果)和一个我标记为“+”的Button

ViewController.swift

import UIKit

class ViewController: UIViewController {
    @IBOutlet var operand1 : UITextField!
    @IBOutlet var operand2 : UITextField!
    @IBOutlet var result   : UITextField!

    var brain = MathWhiz()

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    @IBAction func addButton(sender : UIButton) {
        var op1 = operand1.text
        var op2 = operand2.text

        // Perform the add with the contents of the operand fields.
        // Print the answer, or "No Result" if add returns nil.
        if let answer = brain.add(op1, op2: op2)?.description {
            result.text = answer
        } else {
            result.text = "No Result"
        }
    }
}

MathWhiz.swift

import UIKit

// Declare that we implement NSDecimalNumberBehaviors so that we can handle
// exceptions without them being raised.
class MathWhiz: NSDecimalNumberBehaviors {
    var badException = false

    // Required function of NSDecimalNumberBehaviors protocol
    func roundingMode() -> NSRoundingMode {
        return .RoundPlain
    }

    // Required function of NSDecimalNumberBehaviors protocol
    func scale() -> CShort {
        return CShort(NSDecimalNoScale)
    }

    // Required function of NSDecimalNumberBehaviors protocol
    // Here we process the exceptions
    func exceptionDuringOperation(operation: Selector, error: NSCalculationError, leftOperand: NSDecimalNumber, rightOperand: NSDecimalNumber) -> NSDecimalNumber? {
        var errorstr = ""

        switch(error) {
        case .NoError:
            errorstr = "NoError"
        case .LossOfPrecision:
            errorstr = "LossOfPrecision"
        case .Underflow:
            errorstr = "Underflow"
            badException = true
        case .Overflow:
            errorstr = "Overflow"
            badException = true
        case .DivideByZero:
            errorstr = "DivideByZero"
            badException = true
        }
        println("Exception called for operation \(operation) -> \(errorstr)")

        return nil
    }

    // Add two numbers represented by the strings op1 and op2.  Return nil
    // if a bad exception occurs.
    func add(op1: String, op2: String) -> NSDecimalNumber? {
        let dn1 = NSDecimalNumber(string: op1)
        let dn2 = NSDecimalNumber(string: op2)

        // Init badException to false.  It will be set to true if an
        // overflow, underflow, or divide by zero exception occur.
        badException = false

        // Add the NSDecimalNumbers, passing ourselves as the implementor
        // of the NSDecimalNumbersBehaviors protocol.
        let dn3 = dn1.decimalNumberByAdding(dn2, withBehavior: self)

        // Return nil if a bad exception happened, otherwise return the result
        // of the add.
        return badException ? nil : dn3
    }
}

在#1和#2中,如何在返回nil之前检查错误(在add函数中)?这似乎让我回到了圆圈的起点,我想要检查溢出,但不包括随之而来的崩溃(在Swift中是这样的)。 - H_H
a + b > maxint 时会发生溢出。将 b 从两边减去,得到 a > maxint - b。因此,在这种情况下返回 nil。这是针对整数的,但你可以理解这个概念。 - vacawama
谢谢。我注意到NSDecimalNumber类有一个名为NSDecimalNumberExactnessException的变量,如果存在精度丢失错误,则会抛出异常。不确定询问如何完成是否超出界限? - H_H
看起来你可以定义一个符合 NSDecimalNumberBehaviors 协议并实现 exceptionDuringOperation() 方法的类,并使用 NSDecimalNumber.setDefaultBehavior 将其设置为 NSDecimalNumber 类的默认行为。这样,如果发生异常,就会调用你的 exceptionsDuringOperation() 方法。然后你返回 nil。 - vacawama
谢谢你的出色回答! - Dominique Vial
显示剩余8条评论

1

Swift 2.0更新答案

正如您所提到的,现在Swift 2.0支持使用trythrowcatch关键字。

这里是官方公告。

错误处理模型:Swift 2.0中的新错误处理模型会立即感觉自然,使用熟悉的try、throw和catch关键字。最重要的是,它被设计为与Apple SDK和NSError完美配合。实际上,NSError符合Swift的ErrorType。您一定会想观看WWDC会议上关于Swift的新功能的更多信息。

e.g

func loadData() throws { }

func test() {
    do {
        try loadData()
    } catch {
        print(error)
    }
}

1

好的,您正在使用ObjC API。因此只需在ObjC中处理异常。编写一个ObjC类来处理异常并返回值,以供Swift代码使用。

一种可能性是将MathWhiz编写为ObjC类,并返回一个inout NSError参数(即像Core Data一样采用**NSError方式),并在遇到可恢复错误时填充正确的值。然后,您可以从NSDecimalNumber中捕获异常并将其转换为NSError值。

您还可以编写一个完整的NSDecimalNumber包装器,以供Swift使用,然后在Swift代码中使用它代替NSDecimalNumber。也许您可以重载operator +及其兄弟的这个类,然后解决如何表示各种可能的错误而不使用异常。


也许最好的方法是使用我已经编写的Objective-C版本,并学习Core Data以实现您提到的参数和返回值。苹果表示他们支持Swift中的所有当前API,但我不确定他们是否清楚地说明了我们应该如何处理来自他们的Objective-C API的异常,如果我们不使用Objective-C(上面的示例是使用Swift进行比较的练习,我想看看我能多容易地将我的工作转换为Swift)。谢谢。 - H_H
是的,我不确定他们是否充分回答了那个问题。我猜我们会看到Swift等价的API,而这些API不使用异常。 - anisoptera

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接