我实际上没有问题。但是我在 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的非无效方法,请参阅这篇文章。