捕获 Objective-C 和 Swift 异常



我实际上没有问题。但是我在 Swift 中使用单个 catch-block 来处理 Swift 错误和NSException型 Objective-C 异常时遇到了一个问题,类似于这样:

do {
try object1.throwingObjectiveCMethod()
try object2.throwingSwiftMethod()
} catch {
print("Error:", error)
}

我找不到这个问题的答案,所以我想我会把它贴在这里,以防其他人可能会遇到它。

退一步说,我需要使用 Swift 中的一些旧的 Objective-C 库,这可能会抛出需要在 Swift 中捕获的 NSExceptions。

假设这个库看起来有点像这样:

#import <Foundation/Foundation.h>
@interface MyLibrary : NSObject
- (void)performRiskyTask;
@end
@implementation MyLibrary
- (void)performRiskyTask {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"Something went wrong"
userInfo:nil];
}
@end

现在,假设我尝试通过以下方式使用此库:

do {
let myLibrary = MyLibrary()
try myLibrary.performRiskyTask()
} catch {
print("Error caught:", error)
}

Swift 编译器已经让我知道try表达式中没有抛出函数,并且无法访问catch块。事实上,当我运行代码时,它会崩溃并出现运行时错误:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Something went wrong'
*** First throw call stack:
(
0   CoreFoundation                      0x00007ff8099861e3 __exceptionPreprocess + 242
1   libobjc.A.dylib                     0x00007ff8096e6c13 objc_exception_throw + 48
2   Exceptions                          0x0000000100003253 -[MyLibrary performRiskyTask] + 83
3   Exceptions                          0x00000001000035c2 main + 66
4   dyld                                0x000000010001951e start + 462
)
libc++abi: terminating with uncaught exception of type NSException

正如 Swift 文档中所述,只有NSError **模式被转换为 Swift 的 try-catch 模型。没有捕获 NSExceptions 的内置方法:

在 Swift 中,你可以从使用 Cocoa 的错误模式传递的错误中恢复,如上文Catch Errors中所述。但是,在 Swift 中没有安全的方法来从 Objective-C 异常中恢复。要处理 Objective-C 异常,请编写 Objective-C 代码,在异常到达任何 Swift 代码之前捕获异常。 (https://developer.apple.com/documentation/swift/cocoa_design_patterns/handling_cocoa_errors_in_swift)

正如另一个答案(请参阅此处)中指出的,可以使用以下通用异常处理程序解决此问题:

#import <Foundation/Foundation.h>
@interface ObjC : NSObject
+ (BOOL)catchException:(void (^)(void))tryBlock error:(NSError **)error;
@end
@implementation ObjC
+ (BOOL)catchException:(void (^)(void))tryBlock error:(NSError **)error {
@try {
tryBlock();
return YES;
} @catch (NSException *exception) {
if (error != NULL) {
*error = [NSError errorWithDomain:exception.name code:-1 userInfo:@{
NSUnderlyingErrorKey: exception,
NSLocalizedDescriptionKey: exception.reason,
@"CallStackSymbols": exception.callStackSymbols
}];
}
return NO;
}
}
@end

重写我的 Swift 代码(并在我的桥接标头中导入ObjC.h)......

do {
let myLibrary = MyLibrary()
try ObjC.catchException {
myLibrary.performRiskyTask()
}
} catch {
print("Error caught:", error)
}

...,现在正在捕获异常:

Error caught: Error Domain=NSInternalInconsistencyException Code=-1 "Something went wrong" UserInfo={CallStackSymbols=(
0   CoreFoundation                      0x00007ff8099861e3 __exceptionPreprocess + 242
1   libobjc.A.dylib                     0x00007ff8096e6c13 objc_exception_throw + 48
2   Exceptions                          0x0000000100002c33 -[MyLibrary performRiskyTask] + 83
3   Exceptions                          0x0000000100003350 $s10ExceptionsyycfU_ + 32
4   Exceptions                          0x00000001000033d8 $sIeg_IeyB_TR + 40
5   Exceptions                          0x0000000100002c9f +[ObjC catchException:error:] + 95
6   Exceptions                          0x0000000100003069 main + 265
7   dyld                                0x000000010001d51e start + 462
), NSLocalizedDescription=Something went wrong, NSUnderlyingError=Something went wrong}
Program ended with exit code: 0

所以这很好用。

但是现在假设,我的库调用嵌套在其他 Swift 函数中,这可能会引发其他 Swift 原生错误:

func performTasks() throws {
try performOtherTask()
useLibrary()
}
func performOtherTask() throws {
throw MyError.someError(message: "Some other error has occurred.")
}
func useLibrary() {
let myLibrary = MyLibrary()
myLibrary.performRiskyTask()
}
enum MyError: Error {
case someError(message: String)
}

当然,现在我可以用一个ObjC.catchException块来包围对我库的每一次调用,甚至可以围绕这个库构建一个 Swift 包装器。但相反,我想在一个位置捕获所有异常,而不必为每个方法调用编写额外的代码:

do {
try ObjC.catchException {
try performTasks()
}
} catch {
print("Error caught:", error)
}

但是,此代码无法编译,因为传递给ObjC.catchException方法的闭包不应引发:

Invalid conversion from throwing function of type '() throws -> ()' to non-throwing function type '() -> Void'

要解决此问题,请将void (^tryBlock)(void)替换为非转义void (^tryBlock)(NSError **)并将 Objective-C 方法标记为"针对 Swift 进行了改进"(类似于以下答案):

#import <Foundation/Foundation.h>
@interface ObjC : NSObject
+ (BOOL)catchException:(void (NS_NOESCAPE ^)(NSError **))tryBlock error:(NSError **)error NS_REFINED_FOR_SWIFT;
@end

然后将错误指针传递给tryBlock,并根据是否发生错误更改返回值:

@implementation ObjC
+ (BOOL)catchException:(void (NS_NOESCAPE ^)(NSError **))tryBlock error:(NSError **)error {
@try {
tryBlock(error);
return error == NULL || *error == nil;
} @catch (NSException *exception) {
if (error != NULL) {
*error = [NSError errorWithDomain:exception.name code:-1 userInfo:@{
NSUnderlyingErrorKey: exception,
NSLocalizedDescriptionKey: exception.reason,
@"CallStackSymbols": exception.callStackSymbols
}];
}
return NO;
}
}
@end

然后使用以下扩展"优化"此方法:

extension ObjC {
static func catchException(_ block: () throws -> Void) throws {
try __catchException { (errorPointer: NSErrorPointer) in
do {
try block()
} catch {
errorPointer?.pointee = error as NSError
}
}
}
}

现在,Swift 代码可以编译:

do {
try ObjC.catchException {
try performTasks()
}
print("No errors.")
} catch {
print("Error caught:", error)
}

它捕获了 Swift 错误:

Error caught: someError(message: "Some other error has occurred.")
Program ended with exit code: 0

同样,它仍然会捕获NSException

func performOtherTask() throws {
//throw MyError.someError(message: "Some other error has occurred.")
}
Error caught: Error Domain=NSInternalInconsistencyException Code=-1 "Something went wrong" UserInfo={CallStackSymbols=(
0   CoreFoundation                      0x00007ff8099861e3 __exceptionPreprocess + 242
1   libobjc.A.dylib                     0x00007ff8096e6c13 objc_exception_throw + 48
2   Exceptions                          0x0000000100002643 -[MyLibrary performRiskyTask] + 83
3   Exceptions                          0x00000001000030fa $s10Exceptions10useLibraryyyF + 58
4   Exceptions                          0x0000000100002cfa $s10Exceptions12performTasksyyKF + 42
5   Exceptions                          0x0000000100002c9f $s10ExceptionsyyKXEfU_ + 15
6   Exceptions                          0x00000001000031b8 $sSo4ObjCC10ExceptionsE14catchExceptionyyyyKXEKFZySAySo7NSErrorCSgGSgXEfU_ + 56
7   Exceptions                          0x000000010000339c $sSAySo7NSErrorCSgGSgIgy_AEIegy_TR + 12
8   Exceptions                          0x000000010000340c $sSAySo7NSErrorCSgGSgIegy_AEIyBy_TR + 28
9   Exceptions                          0x00000001000026b3 +[ObjC catchException:error:] + 99
10  Exceptions                          0x0000000100002e93 $sSo4ObjCC10ExceptionsE14catchExceptionyyyyKXEKFZ + 371
11  Exceptions                          0x00000001000029ce main + 46
12  dyld                                0x000000010001d51e start + 462
), NSLocalizedDescription=Something went wrong, NSUnderlyingError=Something went wrong}
Program ended with exit code: 0

而且,当然,如果根本没有抛出异常,它只会运行:

- (void)performRiskyTask {
//    @throw [NSException exceptionWithName:NSInternalInconsistencyException
//                                   reason:@"Something went wrong"
//                                 userInfo:nil];
}
No errors.
Program ended with exit code: 0

编辑:要支持抛出NSExceptions的非无效方法,请参阅这篇文章。

最新更新