在Swift语言中的错误处理

195

我对Swift的了解不是很深,但有一件事情引起了我的注意,那就是Swift中没有异常。那么在Swift里如何处理错误呢?有人发现了与错误处理相关的内容吗?


1
我发现错误信息就像 Obj-C 一样:o - Arbitur
15
@Arbitur是老式的segfault方式吗? - peko
在Swift中创建了一个NSTimer,当我拼写错误的函数时,它崩溃并给出了一个错误,说找不到该方法 :) - Arbitur
3
您可以按照此文章中的说明为 Swift 添加 try-catch 支持:https://medium.com/@_willfalcon/adding-try-catch-to-swift-71ab27bcb5b8 - William Falcon
@peko 如何在Swift中处理段错误(segfault)?我认为目前还不可能,这使得一些错误无法恢复。 - Orlin Georgiev
13个回答

153

Swift 2 & 3

Swift 2有一些变化,引入了一个新的错误处理机制,该机制与异常有些类似但细节不同。

1. 指示可能发生错误

如果函数/方法想要表明它可能会抛出错误,它应该包含throws关键字,如下所示:

func summonDefaultDragon() throws -> Dragon

注意:该函数可以抛出任何实现ErrorType接口的类型的实例或者根本不会抛出。此声明仅说明该函数可能会抛出异常。

2. 调用可能抛出错误的函数

为了调用该函数,您需要使用try关键字,像这样:

try summonDefaultDragon()

通常应该像这样使用 do-catch 块

do {
    let dragon = try summonDefaultDragon() 
} catch DragonError.dragonIsMissing {
    // Some specific-case error-handling
} catch DragonError.notEnoughMana(let manaRequired) {
    // Other specific-case error-handlng
} catch {
    // Catch all error-handling
}
注意:捕获子句使用了 Swift 模式匹配的所有强大功能,因此您在此非常灵活。

如果您正在从一个被标记为 throws 关键字的函数中调用一个抛出异常的函数,则可以决定传播该错误:

func fulfill(quest: Quest) throws {
    let dragon = try summonDefaultDragon()
    quest.ride(dragon)
} 

或者,您可以使用try?调用throwing函数:

let dragonOrNil = try? summonDefaultDragon()

这种方式可以让你获得返回值或者nil,如果出现任何错误。使用这种方式,你不会得到错误对象。

这意味着你也可以将try?与其他有用的语句结合使用,例如:

if let dragon = try? summonDefaultDragon()
或者
guard let dragon = try? summonDefaultDragon() else { ... }

最后,你可以判断出错误实际上不会发生(例如因为你已经检查了先决条件),并使用 try! 关键字:

let dragon = try! summonDefaultDragon()

如果函数实际上抛出了错误,那么您的应用程序将会在运行时出现错误并终止。

3. 抛出错误

为了抛出一个错误,您可以像这样使用throw关键字。

throw DragonError.dragonIsMissing

你可以抛出任何符合 ErrorType 协议的东西。首选 NSError 符合此协议,但您可能想选择基于枚举的 ErrorType,它使您能够将多个相关错误分组,可能带有其他数据片段,例如:

enum MyError: Error { case networkError case serverError(String) case parsingError(lineNumber: Int) }
enum DragonError: ErrorType {
    case dragonIsMissing
    case notEnoughMana(requiredMana: Int)
    ...
}

Swift 2和3的错误处理机制与Java/C#/C++的异常处理有以下主要差异:

  • 语法略有不同:使用do-catch+try+defer来取代传统的try-catch-finally语法。
  • 相较于成功路径,异常处理通常在异常路径上会产生更高的执行时间开销。但这对于Swift 2.0的错误处理来说并非如此,其中成功路径和错误路径的开销大致相同。
  • 所有可能引发错误的代码必须声明,而异常可以从任何地方抛出。在Java中,“所有错误都是已检查异常”。然而,与Java不同的是,您不需要指定可能抛出的错误。
  • Swift异常不兼容ObjC异常。您的do-catch块将无法捕获任何NSException异常,反之亦然,您必须使用ObjC。
  • Swift异常与Cocoa NSError方法约定兼容,返回false(用于返回Bool的函数)或nil(用于返回AnyObject的函数),并传递NSErrorPointer以获取错误详细信息。

为了更轻松地处理错误,还有两个概念:

  • 使用defer关键字的延迟操作,它们允许您实现与Java/C#等中的finally块相同的效果。
  • 使用guard关键字的守卫语句,它允许您编写比普通错误检查/信号代码更少的if/else代码。

Swift 1

运行时错误:

对于处理运行时错误(如网络连接问题、数据解析、打开文件等),建议像在ObjC中一样使用NSError进行处理,因为Foundation、AppKit、UIKit等会以这种方式报告其错误。因此,这更多是框架相关而非语言相关的问题。

另一个常用模式是使用类似于AFNetworking中的成功/失败分离块:

var sessionManager = AFHTTPSessionManager(baseURL: NSURL(string: "yavin4.yavin.planets"))
sessionManager.HEAD("/api/destoryDeathStar", parameters: xwingSquad,
    success: { (NSURLSessionDataTask) -> Void in
        println("Success")
    },
    failure:{ (NSURLSessionDataTask, NSError) -> Void in
        println("Failure")
    })

尽管如此,NSError实例仍经常在失败块中被接收,描述了错误信息。

程序员错误:

对于程序员错误(例如数组元素越界、向函数调用传递无效参数等),您可以在ObjC中使用异常。Swift语言似乎没有任何语言支持异常(如throwcatch等关键字)。但是,正如文档所建议的那样,它运行在与ObjC相同的运行时上,因此您仍然可以像这样抛出NSExceptions

NSException(name: "SomeName", reason: "SomeReason", userInfo: nil).raise()

仅使用纯Swift无法捕获它们,不过您可以选择在ObjC代码中捕获异常。

问题在于您是否应该为程序员错误抛出异常,还是像Apple在语言指南中建议的那样使用断言。


20
使用Cocoa APIs(NSFileHandle)打开文件或网络连接可能会出现异常,需要捕获异常。在Swift中没有异常处理机制,因此需要使用Objective-C实现该部分程序或者使用BSD C APIs来完成工作(这两种方法都不是很好)。有关NSFileHandle.writeData的详细信息,请参阅文档:https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/Classes/NSFileHandle_Class/Reference/Reference.html#//apple_ref/occ/instm/NSFileHandle/writeData: - Matt Gallagher
5
没有异常处理意味着使用具有所有固有问题的两阶段对象构造方式。请参见http://www.stroustrup.com/except.pdf。 - Phil
2
fatalError(...)也是一样的。 - holex
8
尽管我很喜欢Swift,但我认为这是一个灾难性的选择。而且,已经尝到了一些后果的滋味,他们在这个省略上玩火,非常危险… - Rob
3
是的,现在很少使用检查异常,因为发现强制程序员捕获他们几乎无法从中恢复的异常会污染代码并违反单一责任原则。领域级别的类不想处理基础设施层面的异常。因此,现在更倾向于使用非检查异常,如果必要,感兴趣的方可以捕获它们。检查异常表示肯定可恢复,而非检查异常表示可能不可或不需要恢复。 - Jasper Blues
显示剩余2条评论

70
更新于2015年6月9日 - 非常重要
Swift 2.0引入了try、throw和catch关键字,其中最令人兴奋的是:
Swift会自动将产生错误的Objective-C方法转换为根据Swift本地错误处理功能抛出错误的方法。
注意:导入Swift时,消耗错误(例如委托方法或带有NSError对象参数的完成处理程序的方法)的方法不会成为抛出方法。
例子:(来自书中)
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *URL = [NSURL fileURLWithPath:@"/path/to/file"];
NSError *error = nil;
BOOL success = [fileManager removeItemAtURL:URL error:&error];
if (!success && error){
    NSLog(@"Error: %@", error.domain);
}

在Swift中的等效代码将会是:
let fileManager = NSFileManager.defaultManager()
let URL = NSURL.fileURLWithPath("path/to/file")
do {
    try fileManager.removeItemAtURL(URL)
} catch let error as NSError {
    print ("Error: \(error.domain)")
}

抛出错误:
*errorPtr = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCannotOpenFile userInfo: nil]

将自动传播给调用者:
throw NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotOpenFile, userInfo: nil)

从苹果图书《Swift编程语言》来看,似乎应该使用枚举来处理错误。
以下是书中的一个例子。
enum ServerResponse {
    case Result(String, String)
    case Error(String)
}

let success = ServerResponse.Result("6:00 am", "8:09 pm")
let failure = ServerResponse.Error("Out of cheese.")

switch success {
case let .Result(sunrise, sunset):
    let serverResponse = "Sunrise is at \(sunrise) and sunset is at \(sunset)."
case let .Error(error):
    let serverResponse = "Failure...  \(error)"
}

从:苹果公司。“Swift编程语言。”iBooks. https://itun.es/br/jEUH0.l 更新
来自苹果新闻书籍,“使用Swift与Cocoa和Objective-C”。使用Swift语言不会发生运行时异常,因此您不需要try-catch。相反,您可以使用可选链
以下是该书的一部分:
例如,在下面的代码清单中,由于NSDate对象上不存在length属性和characterAtIndex:方法,因此不执行第一行和第二行。 myLength常量被推断为可选Int,并设置为nil。您还可以使用if-let语句有条件地解包对象可能无法响应的方法的结果,如第三行所示。
let myLength = myObject.length?
let myChar = myObject.characterAtIndex?(5)
if let fifthCharacter = myObject.characterAtIndex(5) {
    println("Found \(fifthCharacter) at index 5")
}

摘自:苹果公司。“使用Swift与Cocoa和Objective-C。” iBooks。https://itun.es/br/1u3-0.l
这本书还鼓励你使用Objective-C中的Cocoa错误模式(NSError对象)。
在Swift中,错误报告遵循与Objective-C相同的模式,同时提供可选返回值的额外好处。在最简单的情况下,您可以从函数中返回一个Bool值来指示它是否成功。当您需要报告错误原因时,您可以向函数添加一个类型为NSErrorPointer的NSError输出参数。该类型大致相当于Objective-C的NSError **,具有额外的内存安全和可选类型。您可以使用前缀&运算符将对可选NSError类型的引用传递为NSErrorPointer对象,如下面的代码清单所示。
var writeError : NSError?
let written = myString.writeToFile(path, atomically: false,
    encoding: NSUTF8StringEncoding,
    error: &writeError)
if !written {
    if let error = writeError {
        println("write failure: \(error.localizedDescription)")
    }
}

摘自:苹果公司。“使用Swift与Cocoa和Objective-C。” iBooks。https://itun.es/br/1u3-0.l

对于最后一条语句,应该是:
do { try myString.writeToFile(path, atomically: true, encoding: NSUTF8StringEncoding) }catch let error as NSError { print(error) }
- LiangWang
1
@Jacky 是的,对于Swift 2.0来说是正确的。尽管这个答案是在Swift 2.0发布之前写的,但我已经更新了答案以展示在Swift 2.0中处理错误的新方法。我本来想让这种方式作为参考,但我会考虑更新整个答案,只使用Swift 2.0。 - Guilherme Torres Castro

13

Swift 中没有异常(Exceptions)的概念,这和 Objective-C 的处理方式相似。

在开发中,你可以使用 assert 来捕获任何可能出现的错误,并在进入生产前修复它们。

经典的 NSError 方法没有变化,你需要发送一个 NSErrorPointer,然后它会被填充。

简单示例:

var error: NSError?
var contents = NSFileManager.defaultManager().contentsOfDirectoryAtPath("/Users/leandros", error: &error)
if let error = error {
    println("An error occurred \(error)")
} else {
    println("Contents: \(contents)")
}

6
这引出了两个问题:当我们从Swift中调用的ObjC代码实际上抛出异常时会发生什么,以及NSError是否像在ObjC中那样是我们通用的错误对象? - Maciej Jastrzebski
1
Swift 中的初始化器不会或不能失败,这只是一种事实吗? - Phil
11
异常处理看起来相当混乱。 - Tash Pemhiwa
27
当可以直接崩溃时,谁还需要异常呢?或者在声明的所有函数中将NSError**作为参数传递?这样每个 f();g(); 都会变成 f(&err);if(err) return;g(&err);if(err) return; 在第一个月内,然后就变成了 f(nil);g(nil);hopeToGetHereAlive(); - hariseldon78
3
此答案已过时(Swift现在支持异常),并且有误(Objective-C确实支持异常)。 - Rog
显示剩余7条评论

12
推荐的“Swift方式”是:
func write(path: String)(#error: NSErrorPointer) -> Bool { // Useful to curry error parameter for retrying (see below)!
    return "Hello!".writeToFile(path, atomically: false, encoding: NSUTF8StringEncoding, error: error)
}

var writeError: NSError?
let written = write("~/Error1")(error: &writeError)
if !written {
    println("write failure 1: \(writeError!.localizedDescription)")
    // assert(false) // Terminate program
}

然而,我更喜欢使用try/catch,因为我觉得这样更容易理解,因为它将错误处理移动到了一个单独的块中,这种安排有时被称为“黄金路径”。幸运的是,你可以通过使用闭包来实现这一点:

TryBool {
    write("~/Error2")(error: $0) // The code to try
}.catch {
    println("write failure 2: \($0!.localizedDescription)") // Report failure
    // assert(false) // Terminate program
}

此外,添加重试功能也非常容易:

TryBool {
    write("~/Error3")(error: $0) // The code to try
}.retry {
    println("write failure 3 on try \($1 + 1): \($0!.localizedDescription)")
    return write("~/Error3r")  // The code to retry
}.catch {
    println("write failure 3 catch: \($0!.localizedDescription)") // Report failure
    // assert(false) // Terminate program
}

TryBool的列表如下:
class TryBool {
    typealias Tryee = NSErrorPointer -> Bool
    typealias Catchee = NSError? -> ()
    typealias Retryee = (NSError?, UInt) -> Tryee

    private var tryee: Tryee
    private var retries: UInt = 0
    private var retryee: Retryee?

    init(tryee: Tryee) {
        self.tryee = tryee
    }

    func retry(retries: UInt, retryee: Retryee) -> Self {
        self.retries = retries
        self.retryee = retryee
        return self
    }
    func retry(retryee: Retryee) -> Self {
        return self.retry(1, retryee)
    }
    func retry(retries: UInt) -> Self {
        // For some reason you can't write the body as "return retry(1, nil)", the compiler doesn't like the nil
        self.retries = retries
        retryee = nil
        return self
    }
    func retry() -> Self {
        return retry(1)
    }

    func catch(catchee: Catchee) {
        var error: NSError?
        for numRetries in 0...retries { // First try is retry 0
            error = nil
            let result = tryee(&error)
            if result {
                return
            } else if numRetries != retries {
                if let r = retryee {
                    tryee = r(error, numRetries)
                }
            }
        }
        catchee(error)
    }
}

您可以编写一个类似的类来测试返回的 Optional 值而不是 Bool 值:
class TryOptional<T> {
    typealias Tryee = NSErrorPointer -> T?
    typealias Catchee = NSError? -> T
    typealias Retryee = (NSError?, UInt) -> Tryee

    private var tryee: Tryee
    private var retries: UInt = 0
    private var retryee: Retryee?

    init(tryee: Tryee) {
        self.tryee = tryee
    }

    func retry(retries: UInt, retryee: Retryee) -> Self {
        self.retries = retries
        self.retryee = retryee
        return self
    }
    func retry(retryee: Retryee) -> Self {
        return retry(1, retryee)
    }
    func retry(retries: UInt) -> Self {
        // For some reason you can't write the body as "return retry(1, nil)", the compiler doesn't like the nil
        self.retries = retries
        retryee = nil
        return self
    }
    func retry() -> Self {
        return retry(1)
    }

    func catch(catchee: Catchee) -> T {
        var error: NSError?
        for numRetries in 0...retries {
            error = nil
            let result = tryee(&error)
            if let r = result {
                return r
            } else if numRetries != retries {
                if let r = retryee {
                    tryee = r(error, numRetries)
                }
            }
        }
        return catchee(error)
    }
}

TryOptional 版本强制使用非 Optional 返回类型,从而使后续编程更加容易,例如:“Swift Way”:

struct FailableInitializer {
    init?(_ id: Int, error: NSErrorPointer) {
        // Always fails in example
        if error != nil {
            error.memory = NSError(domain: "", code: id, userInfo: [:])
        }
        return nil
    }
    private init() {
        // Empty in example
    }
    static let fallback = FailableInitializer()
}

func failableInitializer(id: Int)(#error: NSErrorPointer) -> FailableInitializer? { // Curry for retry
    return FailableInitializer(id, error: error)
}

var failError: NSError?
var failure1Temp = failableInitializer(1)(error: &failError)
if failure1Temp == nil {
    println("failableInitializer failure code: \(failError!.code)")
    failure1Temp = FailableInitializer.fallback
}
let failure1 = failure1Temp! // Unwrap

使用TryOptional:

let failure2 = TryOptional {
    failableInitializer(2)(error: $0)
}.catch {
    println("failableInitializer failure code: \($0!.code)")
    return FailableInitializer.fallback
}

let failure3 = TryOptional {
    failableInitializer(3)(error: $0)
}.retry {
    println("failableInitializer failure, on try \($1 + 1), code: \($0!.code)")
    return failableInitializer(31)
}.catch {
    println("failableInitializer failure code: \($0!.code)")
    return FailableInitializer.fallback
}

自动解封注释。

8
编辑:虽然这个答案有效,但它只是Objective-C转换为Swift。随着Swift 2.0的变化,它已经过时了。Guilherme Torres Castro上面的答案是处理Swift中错误的首选方式的很好介绍。VOS
需要调用一个带有NSError参数的函数... 尽管需要一些琢磨,但我认为我已经弄清楚了。它看起来很丑陋,只是Objective-C版本的薄皮罢了。
var fooError : NSError ? = nil

let someObject = foo(aParam, error:&fooError)

// Check something was returned and look for an error if it wasn't.
if !someObject {
   if let error = fooError {
      // Handle error
      NSLog("This happened: \(error.localizedDescription)")
   }
} else {
   // Handle success
}`

编写一个接受错误参数的函数...

func foo(param:ParamObject, error: NSErrorPointer) -> SomeObject {

   // Do stuff...

   if somethingBadHasHappened {
      if error {
         error.memory = NSError(domain: domain, code: code, userInfo: [:])
      }
      return nil
   }

   // Do more stuff...
}

来源:https://developer.apple.com/library/prerelease/ios/documentation/swift/conceptual/buildingcocoaapps/AdoptingCocoaDesignPatterns.html请采用 Cocoa 设计模式Cocoa 是一种基于 Objective-C 的编程框架,它提供了许多设计模式来帮助你构建高效、可维护的应用程序。这些模式是经过时间考验的,并且已经被广泛使用和证明。在本文中,我们将介绍一些常见的 Cocoa 设计模式,以及如何在 Swift 中实现它们。无论你是新手还是有经验的开发人员,这些模式都可以帮助你更好地组织代码并提高应用程序的质量。 - Markus Rautopuro

6

这是一个关于Objective C的基本封装,它提供了try catch功能。 https://github.com/williamFalcon/SwiftTryCatch

使用方法如下:

SwiftTryCatch.try({ () -> Void in
        //try something
     }, catch: { (error) -> Void in
        //handle error
     }, finally: { () -> Void in
        //close resources
})

好主意。但是决定使用它的人必须记住,在try块中分配的对象在抛出异常时不会被释放。这可能会导致僵尸对象的问题,并且每次使用RAII都会受到影响(自动解锁,自动SQL提交,自动SQL回滚...)。也许C++可以通过某种形式的“runAtExit”来帮助我们? - hariseldon78
更新:我刚刚发现在clang中有一个标志可以启用在异常抛出时释放对象:-fobjc-arc-exceptions。我必须尝试一下它是否仍然适用于包装版本(我认为应该是这样的)。 - hariseldon78
如果您使用此选项,请注意代码大小会增加,因为编译器必须生成半异常安全的代码。另外:依赖这样的编译器特性可能不是最好的选择。异常仅用于程序员错误,因此在开发过程中打开编译器选项以节省一点内存并不值得。如果您的生产代码中有异常,您应该首先处理导致这些异常的问题。 - Christian Kienle
1
可能会出现你无法控制的情况。例如,以错误的格式解析json。 - William Falcon

4
如Guilherme Torres Castro所说,在Swift 2.0中,可以在编程中使用try、catch、do。
例如,在CoreData获取数据的方法中,不再需要将&error作为参数放入managedContext.executeFetchRequest(fetchRequest, error: &error)中,现在只需使用managedContext.executeFetchRequest(fetchRequest)并使用trycatch处理错误。详细信息请参阅Apple文档链接
do {
   let fetchedResults = try managedContext.executeFetchRequest(fetchRequest) as? [NSManagedObject]
   if let results = fetchedResults{
      people = results
   }
} catch {
   print("Could not fetch")
}

如果您已经下载了Xcode7 Beta,请尝试在“文档和API参考”中搜索“抛出错误”,并选择第一个显示的结果,它会给出有关此新语法可以完成的基本想法。然而,许多API的完整文档尚未发布。
更多高级的错误处理技术可以在“Swift新特性(2015年会话106 28m30s)”中找到。

3
这是关于Swift 2.0的更新答案。我希望能够拥有像Java一样功能丰富的错误处理模型。最终,他们宣布了一个好消息。在此处

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

例如:
func loadData() throws { }
func test() {
do {
    try loadData()
} catch {
    print(error)
}}

2
从Swift 2开始,正如其他人已经提到的那样,最好通过使用do/try/catch和ErrorType枚举来处理错误。这对于同步方法非常有效,但异步错误处理需要一些巧妙的方法。
本文提出了一个很好的解决方案:

https://jeremywsherman.com/blog/2015/06/17/using-swift-throws-with-completion-callbacks/

总结一下:
// create a typealias used in completion blocks, for cleaner code
typealias LoadDataResult = () throws -> NSData

// notice the reference to the typealias in the completionHandler
func loadData(someID: String, completionHandler: LoadDataResult -> Void)
    {
    completionHandler()
    }

那么,对上述方法的调用将如下所示:
self.loadData("someString",
    completionHandler:     
        { result: LoadDataResult in
        do
            {
            let data = try result()
            // success - go ahead and work with the data
            }
        catch
            {
            // failure - look at the error code and handle accordingly
            }
        })

这似乎比在异步函数中传递单独的 errorHandler 回调更加简洁,而在 Swift 2 之前,这就是如何处理的。

2

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