Objective C:如何检查变量是否为NSArray或NSMutableArray

22

如何检查一个变量是NSArray还是NSMutableArray?


是的,你可以这样做(请参见下面的答案),但这通常是不好的实践,如果可以避免就避免。通常,你自己创建集合,并且你应该事先知道一个集合是否可变。尝试通过设计来解决你的问题。 - Motti Shneor
8个回答

32

注意,自此回答编写以来,NS{,Mutable}Array的实现已更改。 因此,isKindOfClass:现在可以使用。关于在哪些平台上以及何时更改,我不清楚。

在官方文档确认安全之前,我强烈建议不要编写尝试检测集合是可变还是不可变的代码。即使这样做是安全的,这种设计模式几乎总是表明存在严重的设计缺陷。

(对于那些有权限的人)已提交 rdar://10355515 请求澄清。


考虑:

int main (int argc, const char * argv[]) {
    NSArray *array = [NSArray arrayWithObjects: [NSObject new], nil];
    NSMutableArray *mutableArray = [NSMutableArray arrayWithObjects: [NSObject new], nil];

    NSLog(@"array's class: %@", NSStringFromClass([array class]));
    NSLog(@"mutableArray's class: %@", NSStringFromClass([mutableArray class]));

    NSLog(@"array responds to addObject: %@", 
          [array respondsToSelector: @selector(addObject:)] ? @"YES" : @"NO");

    return 0;
}

(我使用非空数组,因为空的NSArray经常出现,Cocoa为了优化提供了单个共享实例。)

array's class: NSCFArray
mutableArray's class: NSCFArray
array responds to addObject: YES

也就是说,既不能使用-isKindOfClass:,也不能检查addObject:方法的实现来区分NSArray和NSMutableArray。

简而言之,您无法区分NSArray和NSMutableArray的区别。这是故意设计的行为。这也适用于NSString、NSDictionary和NSSet(它们都有一个可变的子类)。

这可能会让人感到惊讶。但事实上,需要检查是否可变的设计模式很难使用且具有重大开销。

例如,如果测试可变性是一种常见的模式,那么Cocoa中返回NSArray实例的所有方法都必须实际返回NSArray实例,并且永远不会返回对内部NSMutableArray的引用,因为这可能导致结果不可预测。


这很糟糕。如果您尝试向不可变数组添加对象,至少应该发生一些事情。 - nils
如果您尝试将一个对象添加到不可变数组中,将会抛出一个异常。 - bbum
非常抱歉,但您的回答完全是错误的。请看下面我的答案,使用相同的测试-只是更好的输出格式,并适用于数组和字典。 - Motti Shneor
@MottiShneor,您读了我在一开始写的更新内容吗(几年前)?实现已经改变了(几年前)。仍然依赖于-isKindOfClass:是依赖于实现细节。除非文档更新明确说明这样做是API合同的一部分,否则它是不安全的,任何目前可以工作的代码也不能保证三年后仍能工作。 - bbum
我已经阅读了它,但这些Cocoa类比iOS甚至MacOS-X早20年。在Cocoa中,可变性测试不是实现细节,而是深层设计考虑。虽然为可变性编写条件代码没有太多意义(因为您自己创建集合并应该根据设计知道它们的可变性),但确实存在某些情况(例如实现真正的深度可变副本或编写自定义序列化机制)需要此类API。作为动态语言,Obj-C鼓励探测对象、类、协议和选择器作为正常和良好行为的一部分。 - Motti Shneor
显示剩余6条评论

6
我会按照以下步骤进行-
if([unkownArray isKindOfClass:[NSMutableArray class]]){
  // This is a nsmutable array
}
else if ([unkownArray isKindOfClass:[NSArray class]]){
 // This is a nsarray
}

这应该有效,并且我自己也测试过了。但是我担心这里所说的: https://dev59.com/33I-5IYBdhLWcg3wkJMA#1789958 ... - Ferran Maylinch

6

不太好但技术上准确的建议...

唯一的方法是在@try/@catch块中调用[unknownArray addObject:someObject],并捕获如果unknownArray是不可变的将抛出的NSInternalInconsistencyException异常(实际异常可能是方法未实现或类是不可变异常)。

好的建议...

简短的答案是永远不要尝试查看不可变对象是否内部可变。

禁止查看不可变对象的可变性的原因是为了支持像这样工作的类的方法:

- (NSArray *)internalObjects
{
    return myInternalObjects;
}

对象myInternalObjects可能是可变的,但是该类中的这个方法指出: 不要改变我返回给你的内容。这样做可能会带来严重的危险。如果该类允许您更改数组,则它将具有不同的访问器或修改器方法。
如果您有一个需要对myInternalObjects变量进行可变访问的友好类,则声明一个特殊的适配器类别,只有友好类导入其中,并使用如下方法:
- (NSMutableArray *)mutableInternalObjectsArray;

这将允许你假设这个朋友足够聪明,不会违反特殊规则,以便拥有所需的访问权限,但不会在更广泛的范围内暴露可变性。

1
如果你想要一个可变数组,那就自己创建一个吧:
NSArray *someArray = /* obtain from somewhere, could be mutable, could be immutable */
NSMutableArray *mutableVersion = [someArray mutableCopy]; // definitely mutable

// later

[mutableVersion release];

在很少的情况下检查内部类型是一个好主意(其他答案已经涵盖了这些情况)。如果你想要一个可变数组,不要检查现有数组是否可变,只需自己创建一个,以确保它是可变的。


但是人们想要检查数组的可变性的一点是,此时进行的更改会被拥有同一数组的其他人发现...创建自己的可变副本意味着您可以看到更改,但没有人可以。然而,我同意 bbum 的观点,任何必须提出这个问题的设计都有很大的问题...您的数据流应该期望可变或不可变数据,而不是有时修改的混合数据。 - Kendall Helmstetter Gelner
是的!在调用 NSJSONSerialization writeJSONObject:toStream:options:error: 时,您必须了解对象的可变性。如果您将一个可变字典/数组传递给它而没有指定 NSJSONReadingMutableContainers 选项,这种不一致可能会导致 *** Collection <__NSDictionaryM: 0xabcdef> was mutated while being enumerated 异常。在我的情况下,我们有一个通用的 writeDictionary:(NSDictionary *)dict asJSONToFile:(NSString *)filePath 实用方法。为了保证一致性,唯一的方法是始终创建一个可变副本,并始终设置 NSJSONReadingMutableContainers - Goffredo
@Goffredo,我认为你是错的。你的异常可能是由其他代码引起的,在另一个线程中运行,尝试在你序列化它时改变了集合。NSJSONSerialization永远不会改变它接收到的用于序列化输出的对象。只有在将JSON数据字符串读入NS集合时,它才会改变任何东西。 - Motti Shneor

1

使用 ...

[Array isKindOfClass:[NSMutableArray class]]

[Array isKindOfClass:[NSArray class]]

这将正常工作。


0
这是我的小测试代码,下面是它的结果。它演示了验证Foundation集合对象可变性的技术。
请注意,可变版本(例如NSMutableArray)继承自不可变版本(NSArray),因此测试类需要注意。
此外,仅测试可变方法的实现似乎可以正常工作,但可能比测试类慢一些。
无论如何 - 避免使用@try@catch尝试改变不可变对象,根据Apple文档,这些不应在部署代码中使用-因为异常会导致堆栈展开,这特别昂贵,并且阻止和无效很多优化。
id arr1 = [NSArray arrayWithObjects:@"Motti",@"name",@55, @"age", nil];
id arr2 = [NSMutableArray arrayWithObjects:@"Motti",@"name",@55, @"age", nil];

NSLog (@"arr1 isKindOfClass NSArray:%@, NSMutableArray:%@", [arr1 isKindOfClass:[NSArray class]] ? @"YES" : @"NO", [arr1 isKindOfClass:[NSMutableArray class]] ? @"YES" : @"NO");
NSLog (@"arr2 isKindOfClass NSArray:%@, NSMutableArray:%@", [arr2 isKindOfClass:[NSArray class]] ? @"YES" : @"NO", [arr2 isKindOfClass:[NSMutableArray class]] ? @"YES" : @"NO");

NSLog (@"arr1 implements insertObject:atIndex:%@", [arr1 respondsToSelector: @selector(insertObject:atIndex:)] ? @"YES" : @"NO");
NSLog (@"arr2 implements insertObject:atIndex:%@", [arr2 respondsToSelector: @selector(insertObject:atIndex:)] ? @"YES" : @"NO");

id dict1 = [NSDictionary dictionaryWithObjectsAndKeys:@"Motti",@"name",@55, @"age", nil];
id dict2 = [NSMutableDictionary dictionaryWithObjectsAndKeys:@"Motti",@"name",@55, @"age", nil];

NSLog (@"dict1 isKindOfClass NSDictoinary:%@, NSMutableDictionary:%@", [dict1 isKindOfClass:[NSDictionary class]] ? @"YES" : @"NO", [dict1 isKindOfClass:[NSMutableDictionary class]] ? @"YES" : @"NO");
NSLog (@"dict2 isKindOfClass NSDictoinary:%@, NSMutableDictionary:%@", [dict2 isKindOfClass:[NSDictionary class]] ? @"YES" : @"NO", [dict2 isKindOfClass:[NSMutableDictionary class]] ? @"YES" : @"NO");

NSLog (@"dict1 implements setObject:forKey:%@", [dict1 respondsToSelector: @selector(setObject:forKey:)] ? @"YES" : @"NO");
NSLog (@"dict2 implements setObject:forKey:%@", [dict2 respondsToSelector: @selector(setObject:forKey:)] ? @"YES" : @"NO");

并且结果打印出来:

14:30:42.2683 Test[381:286] arr1 isKindOfClass NSArray:YES, NSMutableArray:NO
14:30:42.2683 Test[381:286] arr2 isKindOfClass NSArray:YES, NSMutableArray:YES
14:30:42.2684 Test[381:286] arr1 implements insertObject:atIndex:NO
14:30:42.2684 Test[381:286] arr2 implements insertObject:atIndex:YES
14:30:42.2684 Test[381:286] dict1 isKindOfClass NSDictoinary:YES, NSMutableDictionary:NO
14:30:42.2685 Test[381:286] dict2 isKindOfClass NSDictoinary:YES, NSMutableDictionary:YES
14:30:42.2686 Test[381:286] dict1 implements setObject:forKey:NO
14:30:42.2687 Test[381:286] dict2 implements setObject:forKey:YES

0

请参见NSObject的-class方法:

NSLog(@"myUnknownArray is of type: %@", [myUnknownArray class]);

您还可以使用 +class 方法直接进行更多检查:

BOOL isMutableArray = [myUnknownArray isKindOfClass:[NSMutableArray class]];

3
由于Foundation的实现更改,过去版本可能无法运行,而一些当前版本可能可以运行。 - bbum
阅读文档 - Apple 明确表示不要这样做(NSObject类描述,位于isKindOfClass下)。 - David H
如果对象的类型是 NSMutableArray,则 [object isKindOfClass:[NSArray class]] 也会返回 YES。 @Alexander @Alex - Burhanuddin Sunelwala

-1

你必须使用:

[yourArray isKindOf: [NSArray class]]

由于您的数组可能不是NSArray而是某些其他低级类型,因此对类的简单比较可能会失败。您还可以检查数组是否响应您需要的方法。

[yourArray respondsToSelector: @selector(addObject:)]

@bbum,在哪些经验情况下(在iOS上,因为这个问题最初标记为iPhone)会使isKindOfClass不起作用? - Dan Rosenstark
当回答这个问题时,它不起作用,可能是由于基础集合类的实现更改的副作用,在某些版本中可能会起作用。 - bbum
@bbum 基础框架数组类有哪些内容?是我编译时使用的 SDK,还是我运行已编译代码的设备?换句话说,如果现在使用特定的 SDK 进行编译可以正常工作,那么只有当 SDK 更改时才会出现问题吗?或者它可能在此期间的任何时间出现问题? - Dan Rosenstark
2
@yar 实现已随系统一同交付。即,如果您依赖于该行为并且更新更改了该行为,则实现可能会中断。因此需要文档记录。 - bbum
感谢 @bbum,您总是很有启发性。 - Dan Rosenstark

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