Swift语言中的错误处理


190

我对Swift的了解还不够多,但是我注意到的一件事是没有例外。那么如何在Swift中进行错误处理呢?有没有人发现任何与错误处理相关的信息?


1
我发现错误消息就像Obj-C一样:o
Arbitur 2014年

13
@Arbitur的旧segfault好方法?
peko 2014年

在Swift中创建了一个NSTimer,当我拼写错误的函数崩溃了,并给了我一个错误,提示它找不到方法:)
Arbitur 2014年

3
您可以按照本文中的说明添加对Swift的try-catch支持:medium.com/@
William Falcon

@peko您如何在Swift中处理段错误?我认为目前尚不可能,这使一些错误无法恢复
Orlin Georgiev

Answers:


148

斯威夫特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
}

注意:catch子句使用Swift模式匹配的所有强大功能,因此您在这里非常灵活。

如果您正在从本身用throws关键字标记的函数中调用throwing函数,则可以决定传播错误:

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 DragonError: ErrorType {
    case dragonIsMissing
    case notEnoughMana(requiredMana: Int)
    ...
}

新的Swift 2和3错误机制与Java / C#/ C ++样式异常之间的主要区别如下:

  • 语法是有点不同:do-catch+ try+ deferVS传统的try-catch-finally语法。
  • 异常处理通常会导致异常路径中的执行时间比成功路径中的执行时间长得多。Swift 2.0错误并非如此,成功路径和错误路径的成本大致相同。
  • 必须声明所有错误抛出代码,而异常可能已经从任何地方抛出。所有错误都是Java术语中的“检查的异常”。但是,与Java相比,您没有指定可能引发的错误。
  • Swift异常与ObjC异常不兼容。您的do-catch块将不会捕获任何NSException,反之亦然,因为您必须使用ObjC。
  • Swift异常与NSError返回false(对于Bool返回函数)或nil(对于AnyObject返回函数)并NSErrorPointer带有错误详细信息传递的Cocoa 方法约定兼容。

作为减轻错误处理的额外语法糖,还有两个概念

  • 延迟的动作(使用 defer关键字),使您可以实现与Java / C#/ etc中的finally块相同的效果
  • 保护语句(使用guard关键字),使您编写的if / else代码比普通的错误检查/信号代码少。

斯威夫特1

运行时错误:

正如Leandros建议的那样处理运行时错误(例如网络连接问题,解析数据,打开文件等),您应该NSError像在ObjC中那样使用,因为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中使用了异常。雨燕语言似乎并未有任何异常的语言支持(如throwcatch等关键字)。但是,正如文档所示,它与ObjC在同一运行时上运行,因此您仍然可以这样抛出NSExceptions

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

尽管您可能选择在ObjC代码中捕获异常,但是您无法在纯Swift中捕获它们。

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


20
使用Cocoa API(NSFileHandle)的“网络连接问题”和“打开文件”会引发需要捕获的异常。在Swift中没有例外,您需要在Objective-C中实现程序的这一部分,或者使用BSD C API执行所有工作(两者都是较差的解决方法)。见NSFileHandle.writeData文档的详细... developer.apple.com/library/ios/documentation/Cocoa/Reference/...
马特·加拉格尔

5
同样,没有异常处理意味着具有所有固有问题的两阶段对象构造。参见stroustrup.com/except.pdf
2014年

2
fatalError(...)是一样的好。
Holex 2014年

8
正如我喜欢Swift一样,我认为这是一个灾难性的选择,并且已经尝到了一些后果,因此他们正在疏忽大意……
Rob

2
是的,现在很少使用检查异常,因为发现强迫程序员捕获异常,他们几乎没有希望以单责任原则破坏的方式从污染代码中恢复。域级别的类不需要处理基础结构层异常。因此,现在倾向于使用非检查异常,如果合适的话,有兴趣的一方可以捕获它们。。已检查=绝对可恢复。未选中=不可恢复。
贾斯珀·布鲁斯

69

2015年6月9日更新-非常重要

雨燕2.0配备trythrow以及catch关键字和最令人兴奋的是:

Swift自动将产生错误的Objective-C方法转换为根据Swift的本机错误处理功能抛出错误的方法。

注意:消耗错误的方法(例如委托方法或采用带有NSError对象参数的完成处理程序的方法)不会成为由Swift导入时抛出的方法。

摘自:苹果公司。“将Swift与Cocoa和Objective-C结合使用(Swift 2预发行版)。” iBooks。

示例:(摘自本书)

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

快速等效为:

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)

从Apple书籍《 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)"
}

来自:苹果公司。“快速编程语言”。iBooks。https://itun.es/br/jEUH0.l

更新资料

从Apple新闻书中获得“将Swift与Cocoa和Objective-C结合使用”。使用快速语言不会发生运行时异常,因此这就是为什么您没有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")
}

摘录自:Apple Inc.“将Swift与Cocoa和Objective-C结合使用。” iBooks。https://itun.es/br/1u3-0.l


并且这些书还鼓励您使用Objective-C(NSError对象)中的可可错误模式

Swift中的错误报告遵循与Objective-C中相同的模式,并具有提供可选返回值的额外好处。在最简单的情况下,您从函数返回一个Bool值以指示其是否成功。当需要报告错误原因时,可以向函数添加NSErrorPointer类型的NSError out参数。此类型与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)")
    }
}

摘录自:Apple Inc.“将Swift与Cocoa和Objective-C结合使用。” iBooks。https://itun.es/br/1u3-0.l


对于最后一条语句,应为:do {试试myString.writeToFile(path,原子上:true,编码:NSUTF8StringEncoding)}将let错误捕获为NSError {print(error)}
Jacky

1
@Jacky是的,对swift 2.0来说确实如此,尽管此答案是在swift 2.0发行之前编写的,但我更新了答案以显示处理swift 2.0中错误的新方式。我一直在考虑让这种方式作为参考,但我将考虑将整个答案更新为仅使用Swift 2.0
Guilherme Torres Castro 2015年

12

Swift中没有异常,类似于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中那样是我们的通用错误对象?
MDJ 2014年

1
初始化失败或失败的只是Swift的生活事实吗?
Phil

11
异常处理看起来很脏
Tash Pemhiwa 2014年

27
是的,当您可能崩溃时,谁需要例外?还是在您声明的所有函数中将NSError **作为参数?这样,每个月都会f();g();变成f(&err);if(err) return;g(&err);if(err) return;第一个月,然后就变成了f(nil);g(nil);hopeToGetHereAlive();
hariseldon78 2014年

2
这个答案既过时了(Swift现在支持例外)也错了(Objective-C确实支持例外。)
Rog

11

推荐的“快速方式”是:

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版本强制使用非可选的返回类型,该类型使后续编程更加容易,例如,“快速方式:

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
}

注意自动展开。


7

编辑:尽管此答案有效,但这仅是音译为Swift的Objective-C。Swift 2.0中的更改已使其过时了。Guilherme Torres Castro的上述回答很好地介绍了Swift中处理错误的首选方法。VOS

需要花点时间才能弄清楚,但我想我很怀疑。虽然看起来很丑。没有比Objective-C版本更薄的皮肤了。

调用带有NSError参数的函数...

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


5

目标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-commit,自动sql-rollback ...)。也许C ++可以通过某种形式的“ runAtExit”帮助我们?
hariseldon78 2014年

更新:我刚刚发现在clang中有一个标志,可以在异常抛出时释放对象:-fobjc-arc-exceptions。我必须尝试它是否仍可以与包装版本一起使用(我认为应该如此)
hariseldon78 2014年

如果使用此选项,请注意代码大小会增加,因为编译器必须生成半异常安全代码。另外:依靠这种编译器功能可能不是最好的主意。异常仅适用于程序员错误,因此仅在开发过程中尝试使用该编译器选项以节省一点内存是不值得的。如果生产代码中有异常,则应首先处理导致这些异常的事情。
Christian Kienle 2014年

1
可能存在无法控制的情况。例如,以错误的格式解析json。
威廉·福尔肯

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

3

作为吉列尔梅托雷斯卡斯特罗说,在雨燕2.0, ,,trycatchdo可以在编程中使用。

例如,在CoreData获取数据的方法,而不是放&error作为参数进入managedContext.executeFetchRequest(fetchRequest, error: &error),现在我们只需要使用使用managedContext.executeFetchRequest(fetchRequest),然后用处理错误trycatch苹果文档链接

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 Session 106 28m30s)




1

从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之前如何处理的。


0

我所看到的是,由于设备的性质,您不想向用户抛出一堆神秘的错误处理消息。这就是为什么大多数函数返回可选值,然后您只需编写代码即可忽略可选值的原因。如果函数返回nil表示失败,则可以弹出一条消息或其他任何内容。


1
返回nil不会返回有关错误性质的信息。如果在发生错误时返回错误对象,则程序员可以根据错误选择忽略,处理,让其冒泡或“弹出消息或执行任何操作”。知识就是力量。
文斯·奥沙利文
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.