在 Objective-C 中运行这段代码,结果相当出人意料。
NSArray *array = [NSArray arrayWithObjects:@1, @2, nil];
NSLog(@"%@", [array class]);
// 输出: __NSArrayI
明明创建的是 NSArray,类名却是__NSArrayI。空数组可能是__NSArray0,只有一个元素时也可能是__NSSingleObjectArrayI。
这不是 bug,而是**类簇(Class Cluster)**模式。
NSArray、NSString、NSNumber、NSDictionary 全都是抽象类。
实际实例会根据情况创建为隐藏的子类。
这是 Effective Objective-C 2.0 第 9 项讨论的内容,只要使用 Foundation 就无法绕开。
先看核心要点(3 点)
- NSArray 等 Foundation 集合只是提供公开接口的抽象类。
- 初始化器会选择并返回私有子类的实例(属于工厂模式的一种)。
- 因此,
[array class]比较很危险,子类化也有严格规则。
为什么要这样设计?
即使是一个数组,也有多种优化策略。
- 0 个元素的不变数组:无需重复创建,一个单例就够了
- 1~2 个元素:将指针内嵌到结构体中更快
- 大型可变数组:需要带有重新分配策略的独立实现
Foundation 没有把这些逻辑用 if 全塞进 NSArray 的一个实现中,而是为不同情况准备专用子类,再由工厂方法选择合适的实现返回。使用方只需了解 NSArray 接口,内部实现则可随 OS 版本自由替换。
实际上,[NSArray alloc]返回的是一个名为__NSPlaceholderArray 的临时对象,后续 init 系列调用会将其变成真正的实现。虽然我们习惯把 alloc 和 init 连用,但在类簇中,这两个阶段实际上会经过不同的对象。
实践中容易踩的两个坑
坑 1:直接比较类
// 危险代码
if ([object class] == [NSArray class]) {
// 绝不会进入这里,因为实际类是 __NSArrayI 等.
}
// 正确代码
if ([object isKindOfClass:[NSArray class]]) {
// 覆盖整个类簇进行判断
}
类簇成员都是公开类的子类,因此应使用 isKindOfClass: 判断所属体系,而不是进行相等比较。
坑 2:草率地进行子类化
看起来只要继承 NSArray 并重写一个方法,但这样的子类连存储空间都不会继承,因为抽象类没有存储空间。要正确加入类簇,必须遵守相关规则。
- 自行提供存储空间(通常是在内部持有真正的 NSArray)
- 实现所有指定的原始方法(NSArray 中是 count 和 objectAtIndex:)
其余方法都建立在这些原始方法之上,因此只需实现两个方法,就能启用类簇的全部功能。不过 Effective Objective-C 的建议很明确:大多数情况下,组合(持有 NSArray 的包装类)优于继承。
为什么 Swift 开发者也无法置身事外
Swift 的 Array 是结构体,看起来与此模式无关,但有两个交集。
第一是桥接。Objective-C API 传来的 NSArray 转为 Swift Array 时,背后仍有_ContiguousArrayStorage 或__NSArrayI 等实际实现。追查桥接性能问题时,最终都会遇到这些类簇实现。
第二是这个思想延续到了 Swift。“公开类型只暴露接口,实际实现由隐藏类型负责”的理念,延伸为 Swift 的 AnySequence 等类型擦除包装器,以及some关键字(不透明类型)。返回类型只有一个,但实际类型只有编译器和实现部分知道,这与类簇属于同一脉络。
总结
- NSArray、NSString、NSNumber、NSDictionary 是抽象类,实际实例是隐藏的子类
- 类型判断必须使用 isKindOfClass:,不要比较类是否相等
- 类簇子类化必须实现原始方法,通常组合更合适
- 分离接口与实现的思想,延续为 Swift 的不透明类型这一经典设计
即使在调试器中遇到带有陌生前缀__NS的类,也不必再慌张。那只是 Foundation 近 30 年来不断打磨的优化,如今显现在你眼前。

