【问题标题】:Catching NSException in Swift在 Swift 中捕获 NSException
【发布时间】: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 异常,例如参见 stackoverflow.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

标签: swift cocoa-touch exception foundation 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.
      }
      

      【讨论】:

        猜你喜欢
        • 2014-06-30
        • 1970-01-01
        • 2016-11-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-06-04
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多