在 Swift 中捕获 NSException

Posted

技术标签:

【中文标题】在 Swift 中捕获 NSException【英文标题】:Catching NSException in Swift 【发布时间】:2015-12-21 21:45:44 【问题描述】:

以下 Swift 代码引发 NSInvalidArgumentException 异常:

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

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

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

【问题讨论】:

不幸的是,您无法在 Swift 中捕获 Objective-C 异常,例如参见 ***.com/questions/24023112/…。人们可能会认为这是一个错误,即 NSTask 抛出异常而不是返回错误,您可以提交错误报告,但我怀疑 Apple 会更改 API。 感谢@MartinR。我认为这要么是 API 中的错误,要么 Swift 应该提供一种机制来捕获 ObjC 异常(或者两者都更好)......无论如何,我已经打开了一个错误(openradar.appspot.com/22837476),虽然我猜还有更多有同样问题的API方法 我的例子是NSPredicate(fromMetadataQueryString:)。这应该是一个init?,所以如果字符串不好,它可能打算返回nil,但实际上它只是因NSException而崩溃。 好问题!但是如果你自己扔NSException,那么很容易see how to create NSError(然后扔那个,而不是NSException 【参考方案1】:

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

【讨论】:

想通了!你需要在 tryBlock() 成功运行后返回一些东西,否则它总是会碰到 catch 块。我仔细检查了它是否仍然正确触发真正的异常。我将编辑您的答案以包含此更改。 像魅力一样工作。谢谢! 哇,这仍然是在 Swift 2.x 中捕获 NSException 的唯一方法吗?这在 3.0 中有所改变吗? 直到我在 @catch 块中添加了明确的 return 之后,它才对我有用;为此进行了编辑(并删除了不必要的let error,因为错误总是error的形式进入一般catch块) FWIW,在 Swift 3 中,您还可以将 __attribute__((noescape)) 添加到 tryBlock 的类型中,这样您就可以在使用 self 时调用方法,而无需显式包含 self。这使得完整的声明+ (BOOL)catchException:(__attribute__((noescape)) void(^)())tryBlock error:(__autoreleasing NSError **)error;【参考方案2】:

我的建议是创建一个 C 函数来捕获异常并返回 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 的快速简便的方法。

【讨论】:

【参考方案3】:

TL;DR:使用 Carthage 包含 https://github.com/eggheadgames/SwiftTryCatch 或 CocoaPods 包含 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 框架中然后运行测试,事情会变得有点有趣。我们遇到了找不到类函数的问题(错误:使用未解析的标识符)。因此,出于这个原因,并且只是一般的易用性,我们将其打包为 Carthage 库以供一般使用。

奇怪的是,我们可以在其他地方毫无问题地使用 Swift + ObjC 框架,只是框架的单元测试遇到了困难。

请求公关! (如果有一个组合 CocoaPod 和 Carthage 构建,并进行一些测试,那就太好了)。

【讨论】:

对于 STC,我建议将文件复制到您的项目中。我在尝试通过包管理器导入和更新 STC 时遇到了太多问题。它有大约 173 种不同的分叉。它们支持 Swift 1、Swift 2 和/或 Swift 3。它们支持 Carthage、CocoaPods 和/或 SPM。它们支持 ios、macOS、tvOS 和/或 watchOS。但是没有一个 fork 支持我需要的东西的特定组合(或者已经在一年内更新),我浪费了太多时间寻找。选择一个,将其复制到您的项目中,然后继续进行更有用的工作。【参考方案4】:

如 cmets 中所述,此 API 会针对其他可恢复的故障条件引发异常是一个错误。 File it, and request an NSError-based alternative. 大部分情况下,当前的情况已经过时了,因为 NSTask 可以追溯到 Apple 将异常仅用于程序员错误之前。

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

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

【讨论】:

我实际上在 9 月份就打开了一个错误(它出现在 cmets 中,我现在将其添加到问题本身)【参考方案5】:

该版本改进了上面的答案以返回实际的详细异常消息。

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

【讨论】:

【参考方案6】:

您无法在 Swift 中捕获 Objective-C 异常。但是,您可以通过制作一个 Objective-C 包装器来解决这个问题,然后将其导入 Swift。我已经完成了这项工作,并把它做成了一个可重用的 Swift Package Manager 包。只需在 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.

【讨论】:

以上是关于在 Swift 中捕获 NSException的主要内容,如果未能解决你的问题,请参考以下文章

在 Swift 中捕获原始图像类型

如何在 Swift 中捕获 NSKeyedUnarchiver“NSInvalidUnarchiveOperationException”?

使用 swift 3 在 iOS 中存储捕获的图像

swift没有捕获firebase firestore权限错误,请尝试捕获

如何在 swift 2.2 中捕获 NSUnknownKeyException?

Cocoa:在 Swift 中保存时捕获屏幕并缩放图像