在Swift中捕获NSException

90

下面这段 Swift 代码会产生 NSInvalidArgumentException 异常:

task = NSTask()
task.launchPath = "/SomeWrongPath"
task.launch()

我该如何捕获异常?据我所知,Swift中的try/catch是用于捕获Swift内部抛出的错误,而不是来自像NSTask这样的对象引发的NSException(我猜它是用ObjC编写的)。我刚开始接触Swift,可能我错过了一些显而易见的东西...

编辑:这是一个有关这个Bug的radar链接(特别是针对NSTask): openradar.appspot.com/22837476


很遗憾,在Swift中无法捕获Objective-C异常,例如https://dev59.com/W2Ag5IYBdhLWcg3wDHbS。有人可能认为NSTask抛出异常而不是返回错误是一个错误,你可以提交一个错误报告,但我怀疑苹果公司会改变API。 - Martin R
感谢@MartinR。我认为这可能是API中的一个错误,或者Swift应该提供一种机制来捕获ObjC异常(或更好的两者都有)......无论如何,我已经提交了一个错误报告(https://openradar.appspot.com/22837476),尽管我猜测还有许多其他具有相同问题的API方法。 - silyevsk
对我来说,这个例子是 NSPredicate(fromMetadataQueryString:)。这应该是一个 init?,所以如果字符串有误,则可能会返回 nil,但实际上它只会崩溃并抛出 NSException。 - matt
好问题!但是如果你自己抛出 NSException,那么很容易查看如何创建 NSError(并抛出它,而不是 NSException)。 - Top-Master
6个回答

156

这里有一段代码,可以将NSExceptions转换为Swift 2错误。

现在您可以使用

do {
    try ObjC.catchException {

       /* calls that might throw an NSException */
    }
}
catch {
    print("An error ocurred: \(error)")
}

ObjC.h:

#import <Foundation/Foundation.h>

@interface ObjC : NSObject

+ (BOOL)catchException:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error;

@end

ObjC.m

#import "ObjC.h"

@implementation ObjC 

+ (BOOL)catchException:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error {
    @try {
        tryBlock();
        return YES;
    }
    @catch (NSException *exception) {
        *error = [[NSError alloc] initWithDomain:exception.name code:0 userInfo:exception.userInfo];
        return NO;
    }
}

@end

别忘了把这个加入到你的 "*-Bridging-Header.h" 文件中:

#import "ObjC.h"

1
搞定了!在tryBlock()成功运行后,你需要返回一些东西,否则它总是会触发catch块。我仔细检查了一下,它仍然可以正确地处理真正的异常。我将编辑您的答案以包括此更改。 - Ben Baron
4
在Swift 2.x中,这仍然是捕获NSException的唯一方法吗?在3.0中是否有更改? - valheru
4
直到我在@catch块中加入明确的return语句才起作用;已经进行了编辑(还删除了不必要的let error,因为错误总是作为error进入通用的catch块)。 - matt
3
值得一提的是,在Swift 3中,您还可以将tryBlock的类型添加__attribute__((noescape)),以便在使用它时调用self上的方法而无需明确包含self。这使完整声明为+ (BOOL)catchException:(__attribute__((noescape)) void(^)())tryBlock error:(__autoreleasing NSError **)error; - tgaul
1
好主意,已点赞。我进行了一些小的改进:1)将异常作为catchException()的返回值返回给Swift,以便它可以检查异常类或任何属性,如原因,包括NSException子类中的自定义属性。不要将其转换为NSError。2)捕获id,以防有人抛出不派生自NSException的异常。 - Kartick Vaddadi
显示剩余8条评论

14

我建议编写一个 C 函数来捕获异常并返回 NSError,然后使用这个函数。

这个函数可能看起来像这样:

NSError *tryCatch(void(^tryBlock)(), NSError *(^convertNSException)(NSException *))
{
    NSError *error = nil;
    @try {
        tryBlock();
    }
    @catch (NSException *exception) {
        error = convertNSException(exception);
    }
    @finally {
        return error;
    }
}

通过一点桥梁帮助,你只需要打电话就行:

if let error = tryCatch(task.launch, myConvertFunction) {
    print("An exception happened!", error.localizedDescription)
    // Do stuff
}
// Continue task

注意:我没有真正测试过它,我找不到在Playground中快速简便地同时使用Objective-C和Swift的方法。


9

TL;DR:使用Carthage来包含https://github.com/eggheadgames/SwiftTryCatch或者使用CocoaPods来包含https://github.com/williamFalcon/SwiftTryCatch

然后你可以使用如下代码而不用担心它会崩溃你的应用:

import Foundation
import SwiftTryCatch

class SafeArchiver {

    class func unarchiveObjectWithFile(filename: String) -> AnyObject? {

        var data : AnyObject? = nil

        if NSFileManager.defaultManager().fileExistsAtPath(filename) {
            SwiftTryCatch.tryBlock({
                data = NSKeyedUnarchiver.unarchiveObjectWithFile(filename)
                }, catchBlock: { (error) in
                    Logger.logException("SafeArchiver.unarchiveObjectWithFile")
                }, finallyBlock: {
            })
        }
        return data
    }

    class func archiveRootObject(data: AnyObject, toFile : String) -> Bool {
        var result: Bool = false

        SwiftTryCatch.tryBlock({
            result =  NSKeyedArchiver.archiveRootObject(data, toFile: toFile)
            }, catchBlock: { (error) in
                Logger.logException("SafeArchiver.archiveRootObject")
            }, finallyBlock: {
        })
        return result
    }
}

@BPCorp的回答已经实现了预期的功能,但是我们发现,如果您尝试将此Objective C代码合并到大多数Swift框架中并运行测试,则情况会变得有些有趣。我们遇到了找不到类函数的问题 (错误:使用未解决的标识符)。因此,基于通用性和使用便利性的考虑,我们将其打包为Carthage库供大家使用。

奇怪的是,我们可以在其他地方使用Swift + ObjC框架而没有任何问题,只是该框架的单元测试出现了问题。

请求提交PR! (最好将其作为CocoaPod和Carthage构建的组合,并进行一些测试)。


5
对于STC,我建议将文件直接复制到您的项目中。尝试通过软件包管理器导入和更新STC会遇到很多问题。有大约173个不同的分支。它们支持Swift 1、Swift 2和/或Swift 3。他们支持Carthage、CocoaPods和/或SPM。它们支持iOS、macOS、tvOS和/或watchOS。但是没有一个分支支持我需要的特定组合(或已经一年没有更新),我浪费了太多时间去寻找。选择一个,将其复制到您的项目中,并开始更有用的工作。 - Ssswift

4

如评论中所述,这个API在可恢复的故障条件下抛出异常是一个错误。请报告它,并请求基于NSError的替代方案。目前的情况大多数是过时的,因为NSTask是在Apple统一将异常仅用于程序员错误之前的产物。

同时,虽然您可以使用其他答案中的机制来捕获ObjC中的异常并将它们传递给Swift,但要注意这样做并不安全。 ObjC(和C ++)异常背后的堆栈展开机制是脆弱的,并且与ARC根本不兼容。这也是为什么Apple仅将异常用于程序员错误的部分原因 - 这样做的想法是你可以(至少在理论上)在开发期间解决应用程序中的所有异常情况,并且在生产代码中没有任何异常发生。(另一方面,Swift错误或NSError可以指示可恢复的情境或用户错误。)

更安全的解决方案是预见可能导致API抛出异常的场景,并在调用API之前处理它们。如果正在索引NSArray,请先检查其count。如果正在将launchPath设置为某些可能不存在或可能不可执行的内容,请在启动任务之前使用NSFileManager进行检查。


我实际上在九月份就打开了一个错误报告(它出现在评论中,我现在将其添加到问题本身中)。 - silyevsk

3
该版本改进了上面的答案,返回实际详细的异常信息。
@implementation ObjC

+ (BOOL)tryExecute:(nonnull void(NS_NOESCAPE^)(void))tryBlock error:(__autoreleasing NSError * _Nullable * _Nullable)error {
   @try {
      tryBlock();
      return YES;
   }
   @catch (NSException *exception) {
      NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] init];
      if (exception.userInfo != NULL) {
         userInfo = [[NSMutableDictionary alloc] initWithDictionary:exception.userInfo];
      }
      if (exception.reason != nil) {
         if (![userInfo.allKeys containsObject:NSLocalizedFailureReasonErrorKey]) {
            [userInfo setObject:exception.reason forKey:NSLocalizedFailureReasonErrorKey];
         }
      }
      *error = [[NSError alloc] initWithDomain:exception.name code:0 userInfo:userInfo];
      return NO;
   }
}

@end

示例用法:

      let c = NSColor(calibratedWhite: 0.5, alpha: 1)
      var r: CGFloat = 0
      var g: CGFloat = 0
      var b: CGFloat = 0
      var a: CGFloat = 0
      do {
         try ObjC.tryExecute {
            c.getRed(&r, green: &g, blue: &b, alpha: &a)
         }
      } catch {
         print(error)
      }

Before:

Error Domain=NSInvalidArgumentException Code=0 "(null)"

之后:

Error Domain=NSInvalidArgumentException Code=0 
"*** -getRed:green:blue:alpha: not valid for the NSColor NSCalibratedWhiteColorSpace 0.5 1; need to first convert colorspace." 
UserInfo={NSLocalizedFailureReason=*** -getRed:green:blue:alpha: not valid for the NSColor NSCalibratedWhiteColorSpace 0.5 1; need to first convert colorspace.}

从概念上讲,您将错误声明为 _Nullable,但是然后您使用 *error。您应该测试空错误,或者将其声明为 _Nonnull - Cœur
“上面的答案” - 哪个上面的答案?根据您对页面的排序方式,可能会有很多或没有。最好添加一个指向您所提到的答案的链接。 - SymbolixAU

2

在 Swift 中,你不能捕获 Objective-C 异常。不过,你可以通过创建一个 Objective-C 封装器并将其导入到 Swift 中来解决这个问题。我已经完成了这项工作,并将其打包成可重用的 Swift Package Manager 包。只需在 Xcode 中添加 此包,然后像这样使用它:

import Foundation
import ExceptionCatcher

final class Foo: NSObject {}

do {
    let value = try ExceptionCatcher.catch {
        return Foo().value(forKey: "nope")
    }

    print("Value:", value)
} catch {
    print("Error:", error.localizedDescription)
    //=> Error: [valueForUndefinedKey:]: this class is not key value coding-compliant for the key nope.
}

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