Error/Exception 在 returns bool 的方法中处理

Error/Exception handling in a method that returns bool

在我的自定义框架中,我有一个如下所示的方法,它从字典中获取值并将其转换为 BOOL 和 returns 布尔值。

- (BOOL)getBoolValueForKey:(NSString *)key;

如果这个方法的调用者传递了一个不存在的键怎么办。我应该抛出一个自定义的 NSException 说密钥不存在(但在 objective c 中不推荐抛出异常)还是向该方法添加 NSError 参数,如下所示?

- (BOOL)getBoolValueForKey:(NSString *)key error:(NSError **)error; 

如果我使用 NSError,我将不得不 return 'NO' 这会产生误导,因为 'NO' 可以是任何有效键的有效值。

如果您希望将传递不存在的密钥作为程序员错误进行通信,即在运行时实际上不应该发生的事情,例如上游应该处理这种可能性,那么断言失败或 NSException 是这样做的方法。从 Exception Programming Guide:

引用 Apple 的文档

You should reserve the use of exceptions for programming or unexpected runtime errors such as out-of-bounds collection access, attempts to mutate immutable objects, sending an invalid message, and losing the connection to the window server. You usually take care of these sorts of errors with exceptions when an application is being created rather than at runtime.

如果您希望传达程序可以从中恢复/可以继续执行的运行时错误,那么添加错误指针是实现此目的的方法。

原则上使用 BOOL 作为 return 类型是可以的,即使出现非严重错误情况也是如此。但是,如果您打算与 Swift 中的此代码进行交互,则存在一些特殊情况:

  • 如果您通过 Swift 访问此 API,NO 总是意味着抛出错误,即使在您的 Objective-C 方法实现中您没有填充错误指针,即你需要一个 do / catch 并专门处理一个 nil 错误。
  • 相反的实际上也是有效的,即在成功的情况下可能会抛出错误(例如 NSXMLDocument 这样做是为了传达非关键验证错误)。据我所知,无法将此非严重错误信息传达给 Swift。

如果您确实打算使用 Swift 中的这个 API,我可能会将 BOOL 装箱到一个可为空的 NSNumber(在这种情况下,错误情况将为 nil,成功情况为 NO将是一个包含 NO 的 NSNumber。

我应该注意,对于可能失败的特定情况 setter,您应该遵循一些严格的约定,

此 API 由 NSUserDefaults 长期建立,应该是您设计 API:

的起点
- (BOOL)boolForKey:(NSString *)defaultName;

If a boolean value is associated with defaultName in the user defaults, that value is returned. Otherwise, NO is returned.

您应该避免创建不同的 API 来从密钥库中获取布尔值,除非您有充分的理由。在大多数 ObjC 接口中,获取不存在的键 returns nilnil 在布尔上下文中被解释为 NO

传统上,如果要区分 NOnil,则调用 objectForKey 检索 NSNumber 并检查 nil。同样,这是许多 Cocoa 密钥存储的行为,不应轻易更改。

但是,可能有充分的理由违反此预期模式(在这种情况下,您绝对应该在文档中仔细记录,因为这很令人惊讶)。在这种情况下,有几种完善的模式。

首先,您可以将获取未知密钥视为编程错误,您应该抛出异常,并期望程序很快因此而崩溃。为此创建新的异常类型是非常不寻常的(也是意想不到的)。你应该提出 NSInvalidArgumentException 正是为了这个问题而存在的。

其次,你可以通过正确使用get的方法来区分nilNO。您的方法以 get 开头,但它不应该。 get 表示 Cocoa 中的 "returns by reference",您可以这样使用它。像这样:

- (BOOL)getBool:(BOOL *)value forKey:(NSString *)key {
    id result = self.values[key];
    if (result) {
        if (value) {
            // NOTE: This throws an exception if result exists, but does not respond to 
            // boolValue. That's intentional, but you could also check for that and return 
            // NO in that case instead.
            *value = [result boolValue]; 
        }
        return YES;
    }

    return NO;
}

这需要一个指向布尔值的指针,如果该值可用,则将其填充,returns YES。如果该值不可用,则它 returns NO.

没有理由涉及NSError。这增加了复杂性而没有提供任何价值。即使你正在考虑Swift桥接,我也不会在这里使用NSError来获得throws。相反,您应该围绕此 returns Bool? 方法编写一个简单的 Swift 包装器。这是一种更强大的方法,并且在 Swift 方面更易于使用。

如果你想要所有这些

  1. 区分失败案例和成功案例
  2. 只有在成功时才使用 bool 值
  3. 失败时,调用者错误地认为returnvalue不是key的值

我建议做一个基于块的实现。您将有一个 successBlockerrorBlock 来清楚地分开。

调用者会这样调用方法

[self getBoolValueForKey:@"key" withSuccessBlock:^(BOOL value) {
    [self workWithKeyValue:value];
} andFailureBlock:^(NSError *error) {
    NSLog(@"error: %@", error.localizedFailureReason);
}];

和实施:

- (void)getBoolValueForKey:(NSString *)key withSuccessBlock:(void (^)(BOOL value))success andFailureBlock:(void (^)(NSError *error))failure {

    BOOL errorOccurred = ...

    if (errorOccurred) {
        // userInfo will change
        // if there are multiple failure conditions to distinguish between
        NSDictionary *userInfo = @{
                               NSLocalizedDescriptionKey: NSLocalizedString(@"Operation was unsuccessful.", nil),
                               NSLocalizedFailureReasonErrorKey: NSLocalizedString(@"The operation timed out.", nil),
                               NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Have you tried turning it off and on again?", nil)
                               };

        NSError *error = [NSError errorWithDomain:@"domain" code:999 userInfo:userInfo];
        failure(error);

        return;
    }

    BOOL boolValue = ...
    success(boolValue);
}

我们用这个

- (id) safeObjectForKey:(NSString*)key {
    id retVal = nil;

    if ([self objectForKey:key] != nil) {
        retVal = [self objectForKey:key];
    } else {
        ALog(@"*** Missing key exception prevented by safeObjectForKey");
    }

    return retVal;
}

头文件NSDictionary+OurExtensions.h

#import <Foundation/Foundation.h>
@interface NSDictionary (OurExtensions)
- (id) safeObjectForKey:(NSString*)key;
@end

在这种情况下,如果调用者传递的密钥不存在,我宁愿返回 NSInteger 并返回 01NSNotFound。 从这个方法的性质来看,应该是调用者判断来处理NSNorFound。如我所见,从方法的名称来看,返回错误对用户来说并不是很鼓舞人心。

您指出了 Apple 错误处理方法的主要弱点。

我们通过保证 NSError 在成功案例中是 nil 来处理这些情况,因此您实际检查错误:

if (error) { 
    // ... problem
    // handle error and/ or return        
}

因为这与 Apple 的错误处理相矛盾,其中 Error 永远不能保证是 nil,但在失败情况下保证不是 nil,受影响的方法必须是详细记录客户了解这种特殊行为。

这不是一个很好的解决方案,但这是我所知道的最好的解决方案。

(这是我们在 swift 中不必再处理的令人讨厌的事情之一)