在 Swift 中捕捉 NSException

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

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

如何捕捉异常?据我所知,Swift 中的 try/catch 是针对 Swift 中抛出的错误,而不是针对 NSTask 这样的对象引发的 NSException (我猜这是用 ObjecC 编写的)。我是斯威夫特的新手所以可能我漏掉了一些明显的东西。

编辑 : 这里有一个针对 bug 的雷达(特别针对 NSTask) : < a href = “ http://openradar.appspot.com/22837476”rel = “ norefrer”> openradar.appspot.com/22837476

32408 次浏览

What I suggest is to make an C function that will catch the exception and return a NSError instead. And then, use this function.

The function could look like this:

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

And with a little bridging help, you'll just have to call:

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

Note: I didn't really test it, I couldn't find a quick and easy way to have Objective-C and Swift in a Playground.

TL;DR: Use Carthage to include https://github.com/eggheadgames/SwiftTryCatch or CocoaPods to include https://github.com/ravero/SwiftTryCatch.

Then you can use code like this without fear it will crash your app:

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

The accepted answer by @BPCorp works as intended, but as we discovered, things get a little interesting if you try to incorporate this Objective C code into a majority Swift framework and then run tests. We had problems with the class function not being found (Error: Use of unresolved identifier). So, for that reason, and just general ease of use, we packaged it up as a Carthage library for general use.

Strangely, we could use the Swift + ObjC framework elsewhere with no problems, it was just the unit tests for the framework that were struggling.

PRs requested! (It would be nice to have it a combo CocoaPod & Carthage build, as well as have some tests).

As noted in comments, that this API throws exceptions for otherwise-recoverable failure conditions is a bug. File it, and request an NSError-based alternative. Mostly the current state of affairs is an anachronism, as NSTask dates to back before Apple standardized on having exceptions be for programmer errors only.

In the meantime, while you could use one of the mechanisms from other answers to catch exceptions in ObjC and pass them to Swift, be aware that doing so isn't very safe. The stack-unwinding mechanism behind ObjC (and C++) exceptions is fragile and fundamentally incompatible with ARC. This is part of why Apple uses exceptions only for programmer errors — the idea being that you can (theoretically, at least) sort out all the exception cases in your app during development, and have no exceptions occurring in your production code. (Swift errors or NSErrors, on the other hand, can indicate recoverable situational or user errors.)

The safer solution is to foresee the likely conditions that could cause an API to throw exceptions and handle them before calling the API. If you're indexing into an NSArray, check its count first. If you're setting the launchPath on an NSTask to something that might not exist or might not be executable, use NSFileManager to check that before you launch the task.

Here is some code, that converts NSExceptions to Swift 2 errors.

Now you can use

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

Don't forget to add this to your "*-Bridging-Header.h":

#import "ObjC.h"

You cannot catch an Objective-C exception in Swift. However, you can work around that by making an Objective-C wrapper that you then import into Swift. I have done that work and made it a reusable Swift Package Manager package. Just add this package in Xcode and then use it like this:

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

The version improves the answer above to return the actual detailed exception message.

@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

Example usage:

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

After:

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