捕获 Objective-C 和 Swift 异常

Catching Both Objective-C and Swift Exceptions

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

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

我找不到这个问题的答案,所以我想我会 post 在这里,以防其他人 运行 进入它。

退一步说,我需要在 Swift 中使用一些旧的 Objective-C 库,这可能会抛出需要在 Swift.[=33= 中捕获的 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 ** 模式被 t运行 指定为 Swift 的 try-catch 模式。没有 built-in 方法来捕获 NSExceptions:

In Swift, you can recover from errors passed using Cocoa’s error pattern, as described above in Catch Errors. However, there’s no safe way to recover from Objective-C exceptions in Swift. To handle Objective-C exceptions, write Objective-C code that catches exceptions before they reach any Swift code. (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 代码(并在我的桥接 header 中导入 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-native 错误:

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) 替换为 non-escaping 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并根据是否发生错误更改return值:

@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 的 non-void 方法,请参阅 this post