使用isKindOfClass是否安全:针对NSString实例来确定types?

从NSObject中的isKindOfClass:方法文档:

在类集群表示的对象上使用此方法时要小心。 由于类集群的性质,返回的对象可能并不总是您所期望的types。

然后文档继续给出一个例子,为什么你不应该问一个NSArray实例的以下内容:

// DO NOT DO THIS! if ([myArray isKindOfClass:[NSMutableArray class]]) { // Modify the object } 

现在给一个不同的用法的例子,假设我有一个NSObject的实例,我想确定是否有一个NSString或NSArray。

这两种types都是类集群 – 但从上面的文档看来,危险在于对isKindOfClass的回答:太肯定了(有时候当你真的没有可变数组时,回答YES),而是询问关于简单成员资格的问题一个集群仍然是有效的。

一个例子:

 NSObject *originalValue; // originalValue gets set to some instance if ( [originalValue isKindOfClass:[NSString class]] ) // Do something with string 

这个假设是否正确? 使用isKindOfClass:对类集群实例来确定成员资格真的很安全吗? 我特别感兴趣的是无处不在的NSString,NSArray和NSDictionary的答案,但我很想知道它是否可泛化。

文档中的警告使用NSMutableArray示例。 假设一些开发者使用NSMutableArray作为自己实现的一种新typesCoolFunctioningArray基类。 这个CoolFunctioningArraydevise是不可变的。 然而, isKindOfClass会返回YES isKindOfClass:[NSMutableArray class] ,这是真的,但是由devise是不是。

isKindOfClass:如果接收者某处inheritance自作为parameter passing的类,将返回YESisMemberOfClass:只有在接收者是仅作为parameter passing的类的实例(即不包括子类)时才会返回YES

通常是isKindOfClass:用来testing成员资格是不安全的。 如果一个实例为isKindOfClass:[NSString class]返回YES ,你只知道它将响应在NSString类中定义的所有方法,但是你不知道这些方法的实现可能会做什么。 有人可能有子types的NSString引发长度方法的exception。

我认为你可以使用isKindOfClass:进行这种testing,尤其是如果你正在使用你自己的代码,你(作为一个好的撒玛利亚人)devise的方式,它会以我们所有人的期望做出反应(例如NSString子类的长度方法返回它表示的string的长度,而不是引发exception)。 如果您使用了许多奇怪开发人员的外部库(例如CoolFunctioningArray的开发人员),则应谨慎使用isKindOfClass方法,最好使用isMemberOfClass:方法(可能多次testing成员身份一组类)。

让我们考虑下面的代码:

 if ([dict isKindOfClass:[NSMutableDictionary class]]) { [(NSMutableDictionary*)dict setObject:@1 forKey:@"1"]; } 

我想这个问题中引用的苹果笔记的真正目的是这个代码很容易导致崩溃。 而这里的问题在于与CoreFoundation的免费桥接。 让我们更详细地考虑4个变种:

 NSDictionary* dict = [NSDictionary new]; NSMutableDictionary* dict = [NSMutableDictionary new]; CFDictionaryRef dict = CFDictionaryCreate(kCFAllocatorDefault, NULL, NULL, 0, &kCFCopyStringDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFCopyStringDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); 

的确,在所有4种变体中,字典的真实类将是__NSCFDictionary。 所有4个变体都会通过第一个代码片段中的testing。 但是在两种情况下(NSDictionary和CFDictionaryRef声明),我们会用类似下面的log来崩溃:

*终止应用程序由于未捕获的exception'NSInternalInconsistencyException',原因:' – [__ NSCFDictionary setObject:forKey:]:mutating方法发送到不可变对象'

所以,事情越来越清楚。 有2例可以修改,2例不可以。 它依赖于创build函数,可能的可变性状态由__NSCFDictionary对象本身来跟踪。

但是,为什么我们使用相同的类为可变和不可变的对象? 可能的答案 – 因为C语言的限制(和CoreFoundation是一个C API,如我们所知)。 我们在C有什么问题? 可变和不可变字典types的声明应该反映如下:CFMutableDictionaryReftypes是CFDictionaryRef的子types。 但C没有这个机制。 但是我们希望能够在CFDictionary对象的函数中传递CFMutableDictionary对象,并且最好没有编译器发出恼人的警告。 我们要做什么?

让我们看看下面的CoreFoundationtypes声明:

 typedef const struct __CFDictionary * CFDictionaryRef; typedef struct __CFDictionary * CFMutableDictionaryRef; 

正如我们在这里可以看到的,CFDictionary和CFMutableDictionary由相同的types表示,只有const修饰符不同。 所以,这里开始麻烦。

这同样也适用于NSArray,但情况稍微复杂一点。 当你直接创buildNSArray或者NSMutableArray时,你会得到一些特殊的类(分别是__NSArrayI和__NSArrayM)。 对于这个类的崩溃没有转载。 但是当你创buildCFArray或者CFMutableArray时,事物保持不变。 所以要小心!

您正在阅读错误的警告。 它所说的仅仅是因为它在内部被表示为可变的东西,并不意味着你应该试图改变它,因为改变它可能会违反你和任何你得到数组的人之间的契约; 它可能会违反他们的假设,你不会改变它。

然而, isKindOfClass:给出的testing肯定仍然有效。 如果[something isKindOfClass:[NSMutableArray class]] ,那么它是一个NSMutableArray或其子类的实例,这几乎意味着它是可变的。

这是苹果文档中的一个奇怪的警告。 这就好像在说:“小心这个function,因为如果你把它和其他破解的代码一起使用的话,就不能工作。 但是你可以用任何东西来说。

如果devise遵循替代原则,那么KindOfClass将起作用。 ( 维基百科:Liskov替代原则 )

如果一些代码违反了原则,通过返回一个不响应addObject:和其他NSMutableArray方法的NSMutableArray子类的实例,那么这个代码有一个问题…除非它只是一个小规模的临时攻击…