在 Swift 中捕获 NSException

Catching NSException in Swift

Swift 中的以下代码引发 NSInvalidArgumentException 异常:

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

如何捕获异常?据我了解,Swift 中的 try/catch 是针对 Swift 内抛出的错误,而不是针对从 NSTask(我猜是用 ObjC 编写的)等对象引发的 NSExceptions。我是 Swift 的新手,所以我可能遗漏了一些明显的东西...

编辑:这里是错误的雷达(专门针对 NSTask):openradar.appspot.com/22837476

我的建议是制作一个 C 函数来捕获异常,return 改为 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 的快速简便的方法。

TL;DR: 使用 Carthage 包含 https://github.com/eggheadgames/SwiftTryCatch or CocoaPods to include https://github.com/ravero/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 框架中,然后 运行 测试。我们遇到了未找到 class 函数的问题( 错误:使用未解析的标识符 )。因此,出于这个原因,以及一般的易用性,我们将其打包为 Carthage 库以供一般使用。

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

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

如评论中所述,此 API 为 otherwise-recoverable 失败条件抛出异常是一个错误。 File it, and request an NSError-based alternative. 目前的情况大多是不合时宜的,因为 NSTask 可以追溯到 Apple 标准化仅针对程序员错误的异常之前。

与此同时,虽然您 可以 使用其他答案中的一种机制来捕获 ObjC 中的异常并将它们传递给 Swift,但请注意这样做不是很安全。 ObjC(和 C++)异常背后的 stack-unwinding 机制很脆弱,并且从根本上与 ARC 不兼容。这就是 Apple 仅针对程序员错误使用异常的部分原因——这个想法是您可以(至少在理论上)在开发过程中整理出应用程序中的所有异常情况,并且不会在生产代码中出现异常。 (Swift 错误或 NSErrors,另一方面,可以指示可恢复的情境或用户错误。)

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

这是一些代码,将 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"

您无法在 Swift 中捕获 Objective-C 异常。但是,您可以通过制作一个 Objective-C 包装器然后导入到 Swift 来解决这个问题。我已经完成了这项工作并使它成为可重复使用的 Swift 包管理器包。只需在 Xcode 中添加 this package 然后像这样使用它:

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

版本将上面的答案改进为 return 实际的详细异常消息。

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

之前:

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