在Objective-C中,检查方法参数的最佳方式是什么?

6

在编写方法或函数时,检查输入参数以响应任何可能的失败场景是一个好习惯。

例如:

-(void)insertNameInDictionary:(NSString*)nameString
{
    [myDictionary setObject:nameString forKey:@"Name"];
}

这看起来还不错,但如果nameStringnil,应用程序将崩溃。因此,我们可以检查它是否为nil,对吧?我们还可以检查它是否为NSString而不是NSNumber,或者它是否响应我们的方法需要调用的方法。
那么我的问题是:哪种方式最完整、最优雅地检查这些参数?

1
你想要达成什么目标?如果名称为空,应该做出什么适当的响应? - Hot Licks
nameString上放置nil检查或进行@try...@catch()块并在那里处理异常有什么问题?你可以认为你所有的参数都需要具有“完整和优雅”的验证检查。这似乎意味着你永远不会信任你的输入。我认为,当它们是程序外部的输入时,你需要以“完整和优雅”的方式防范你的输入,并专注于编写“完整和优雅”的业务逻辑,以确保你一开始就不使用错误的数据调用这些方法。 - Tim Reddy
@HotLicks 我想要实现一个适当的输入验证,以防止失败。检查是否为nil和检查类别,可以防止我的代码失败,并且 (@T Reddy) 防止我的代码在外部代码中导致故障。所以我的问题是,还有其他重要的NSString或其他类别的检查吗?还有更优雅的方法吗?(系统是否提供了工具?或者有什么指南?) - LuisEspinoza
你打算如何处理错误的值?为什么要责备调用者这么重要呢?(如果你正在编写一个将要销售的API,这是一个合理的关注点,但如果你是自己的客户端或在小团队中工作,这并不是很重要的事情。) - Hot Licks
1
但是如果忽略了错误的值,那么这个 bug 就会被忽略掉。最好崩溃。 - Hot Licks
显示剩余3条评论
5个回答

25

有多种方法可以实现这种保护:

  • NSParameterAssert
  • __attribute__((nonnull))
  • 和一个简单的 if 测试。

[编辑] 自从最新的 Xcode 版本(Xcode 6)以来,苹果添加了nullability annotations,这是另一种更好的方法来表达参数是否可以为nil / null。您应该迁移到该标注而不是使用__attribute__((nonnull)),使您的API更易读。


详细说明如下:

  1. NSParameterAssert是苹果专用的宏,检查方法参数的条件,如果失败则抛出专用异常。

    • 这只是在运行时进行的保护
    • 此宏仍然认为将nil作为参数传递是程序/设计错误,因为根据应用程序工作流程,它通常不会发生(例如,由于其他条件保证参数永远不会是nil),如果出现了问题,则说明确实出了问题。
    • 它仍然抛出异常(如果需要,您的调用代码可以使用@try/@catch),但它具有更明确的优点(告诉参数预期不是nil而不是以难以理解的调用堆栈/消息崩溃)。
  2. 如果您想允许代码使用nil调用您的方法,但在这种情况下什么也不做,那么您只需在函数/方法开头写if (!param) return

    • 这认为传递nil不是程序/设计错误,因此由于应用程序的工作流程可能会出现,因此这是一个可接受的情况,不应该导致崩溃。
  3. 较少人知道,有GCC / LLVM属性__attribute__((nonnull))专门用于告诉编译器某些函数/方法的参数预计是非空的。这样,如果编译器可以在编译时检测到您尝试使用nil/NULL参数调用您的方法/函数(例如直接调用 insertNameInDitionary:nil 而不是使用变量,在编译时尚不能确定其值),它将立即发出编译错误,让您尽快修复它。

  4. [编辑]自从最新的Xcode 6以来,您可以(并且应该)使用nullability annotations而不是__attribute__((nonnull))。请参阅苹果博客文章中的示例。


因此,总之:

如果您想标记您的方法期望参数为非nil,从而表明用nil调用它是逻辑错误,您应该执行以下操作:

- (void)insertNameInDictionary:(NSString*)nameString __attribute__((nonnull))
{
    // __attribute__((nonnull)) allows to check obvious cases (directly passing nil) at compile time
    NSParameterAssert(nameString); // NSParameterAssert allows to check other cases (passing a variable that may or may not be nil) at runtime
    [myDictionary setObject:nameString forKey:@"Name"];
}

如果你认为使用nil调用你的方法是可能发生且可以接受的,并且你只是想避免在这些情况下崩溃,那么可以像这样做:

-(BOOL)insertNameInDictionary:(NSString*)nameString
{
    if (nameString == nil) return NO;
    [myDictionary setObject:nameString forKey:@"Name"];
    return YES;
}

如果您认为您应该能够在字典中插入一个 nil 对象,那么您可以在这种情况下将 nil 值转换为 NSNull,以便插入专门用于此类用途的 NSNull 单例(我使用了短形式的三元运算符 ?: 使代码更加简洁):

-(void)insertNameInDictionary:(NSString*)nameString
{
    [myDictionary setObject:nameString?:[NSNull null] forKey:@"Name"];
}

最后一种情况,如果你想在这个特定的例子中将nil传递进去, 这将会从myDictionary移除Name。你可以简单地进行一个if测试,如果nameStringnil,则调用removeObjectForKey:@"Name",如果不是,则调用setObject:forKey:...或者你可以使用KVC和通用的setValue:forKey:(KVC方法,与NSDictionary无关,因此不能与setObject:forKey:混淆),在NSDictionary的情况下完全具有相同的行为(如果我们将nil作为值参数传递,则会从字典中删除该键)。


1
请注意,自此回答以来,苹果已经在ObjC语言中添加了“nullability annotations”,因此不应再使用__attribute__((nonnull)),而应该使用nullablenonnull注释其参数,并使用NS_ASSUME_NONNULL_BEGIN + NS_ASSUME_NONNULL_END区域审计宏。有关更多信息,请参见https://developer.apple.com/swift/blog/?id=25。 - AliSoftware

2
- (void)insertNameInDictionary:(NSString *)nameString
{
    if ([nameString isKindOfClass:[NSString class]]) {
        [myDictionary setObject:nameString forKey:@"Name"];
    }
}

是的,应该是这样的。我已经修复了它。 - Duncan C

2

苹果建议使用NSAssert来实现这个功能:

NSAssert(nameString, @"nil nameString is not allowed");

这个断言会终止你的程序,并产生一个错误消息来解释发生了什么。

上面的代码中,!= nil 部分是隐含的,因为Objective-C允许。你可以明确地表达它以获得更好的可读性:

NSAssert(nameString != nil, @"nil nameString is not allowed");

断言不仅限于检查 nil,因为它们可以采用任意复杂的条件。您可以检查参数是否符合预期类型,是否响应特定选择器等等。在发布代码中可以禁用断言以节省CPU周期和电池。


1
我看到这里有几个问题。NSMutableDictionary没有"setObject"方法。您需要指定一个键。
-(void)insertNameInDictionary:(NSString*)nameString
{
    if(nameString)
        [myDictionary setObject:nameString forKey: @"someKey"];
}

在我的示例中,您还可以通过"if(nameString)" 来进行一个空值检查。如果您想要一个空字符串,请使用@""而不是nil


是的,我明白了。你不能将一个nil对象放入键中。你需要使用空字符串(@"")或NSNull或一些Objective-C对象,而不是nil。此外,NSDictionary需要是NSMutableDictionary - Michael Dautermann
这并不确认它是正确的类型还是错误的类型。因为有人总是可以传入一个不是字符串的东西,而且提问者也问到了这一点。我下面的答案首先验证它是一个字符串,同时确保它不是空值。 - Gavin
很好的回答,@Gavin。对你的回答点个赞。我的回答只是检查输入是否为空。我可以编辑我的回答来进行类型检查和空值检查,但我认为Luis现在知道该怎么做了。 - Michael Dautermann
是的,也许我没有正确表达我的问题。我正在寻找一种检查输入的方法。输入可以是NSString、NSArray或任何其他对象。 - LuisEspinoza
那么,Gavin的回答是对你的问题最好的答案,Luis。 - Michael Dautermann

1
这完全取决于你想要发生什么,如果你希望在值为nil时移除KV对,则可以使用setValue:forKey:

例如:

-(void)insertNameInDictionary:(NSString*)nameString
{
    [myDictionary setValue:nameString forKey:@"Name"];
}

如果您想存储表示空值的值,可以使用NSNull

-(void)insertNameInDictionary:(NSString*)nameString
{
    [myDictionary setObject:nameString? :[NSNull null] forKey:@"Name"];
}

-(NSString *)nameInDictionary
{
    NSString * retVal = [myDictionary objectForKey:@"Name"];
    return (retVal==[NSNull null]) ? nil : retVal;
}

或者你可能只是想记录它。
-(void)insertNameInDictionary:(NSString*)nameString
{
    if(!nameString)
    {
        NSLog(@"expected nameString to be not null... silly me %s",__PRETTY_FUNCTION__);
        return;
    }
    [myDictionary setObject:nameString forKey:@"Name"];
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接