刚开始学习 Objective-C 时,很多人都会在id这个类型面前停顿片刻。
你可能会疑惑:“这到底是什么类型?为什么放入任意对象都能通过编译?”
先说结论,id是“可以指向任意对象的万能指针”,也是 Objective-C 动态类型和鸭子类型的核心基础。
本文将逐一说明id类型的确切含义、动态类型和鸭子类型在实际代码中的运行方式,以及何时使用、何时需要谨慎。
id 类型到底是什么?
简单来说,id就是“指向任意 Objective-C 对象的指针”。
无需指定NSString *、NSArray *这样的具体类,也可以存储任意对象。
其实,id的本质比想象中简单,内部定义如下。
// objc.h 内部定义(摘要)
typedef struct objc_object {
Class isa; // 指示该对象所属类的指针
} *id;
也就是说,id是指向一个结构体的指针,而该结构体包含一个isa指针。
这个isa就像一张标签,在运行时告诉系统:“我是某个类的实例。”
有趣的是,id本身已经是指针,所以写作id obj,不会像id *obj那样再加星号。
这点很容易和NSString *str混淆。
动态类型就是这样运行的
动态类型(dynamic typing)是指“在运行时而不是编译时判断对象的实际类型”。
id中对象的实际类,会在程序运行时通过查看isa指针来确定。
因此可以写出这样的代码。
id obj = @"Hello"; // 现在是 NSString
NSLog(@"%@", [obj class]); // 输出: __NSCFConstantString
obj = @[@1, @2, @3]; // 现在改成 NSArray
NSLog(@"%@", [obj class]); // 输出: __NSArrayI
同一个变量obj会从字符串变成数组。
编译器不会阻止这一点,因为实际类型的判断被推迟到了运行时。
发送消息时也是如此。调用[obj length]时,编译器无法知道obj是否会响应length。
运行时会在当下检查obj的类,找到对应方法并执行。这称为消息分发。
两者对比如下。
- 静态类型:编译器预先确定并检查类型
- 动态类型:运行期间查看实际对象后决定
在 Objective-C 中,
id和动态类型体现了“先放进去,真正是什么等运行时再问”的理念。
鸭子类型的真正含义是什么?
这里是很多人容易误解的地方:把鸭子类型笼统地理解为“不检查类型”。
鸭子类型原本的说法是:
“如果它走路像鸭子,叫声像鸭子,那它就是鸭子。”
重点不在于对象属于哪个类,而在于“它能否响应那条消息”。
在 Objective-C 中,可以通过respondsToSelector:直接确认这一点。
// 无论类是什么,只要响应此方法就调用
if ([obj respondsToSelector:@selector(quack)]) {
[obj quack]; // 如果能像鸭子一样叫,就按鸭子处理
}
obj属于Duck类还是Robot类,都没有关系。
只要响应quack这条消息,对我们的代码来说它就是“鸭子”。
这与基于继承的多态有所不同。
即使没有继承同一个父类,彼此无关的类只要拥有相同的方法名,也可以用同样的方式处理。
正是这种灵活性,为代理模式、target-action 等 Cocoa 核心设计奠定了基础。
既方便又危险的 id:什么时候需要小心?
id越自由,付出的代价也越大。
由于编译器几乎不进行类型检查,发送无法响应的消息虽然能通过编译,却会在运行时导致应用崩溃。
这就是著名的unrecognized selector sent to instance崩溃。
所以在实际工作中会这样使用。
- 类型明确时,指定具体类(例如
NSString *),不要使用id - 必须使用
id时,在调用前通过respondsToSelector:进行防护 - 定义代理协议,记录“响应这条消息”这一约定
如今引入了instancetype以及NSArray<NSString *> *等泛型,因此滥用id的情况大幅减少。
即便如此,在框架底层和处理运行时的代码中,id依然像心脏一样跳动。
总结来说,id是象征 Objective-C 动态特性的类型,而动态类型和鸭子类型都体现了“根据行为而不是类来判断对象”的理念。
刚开始可能觉得陌生又危险,但理解这一原理后,Cocoa 框架的设计会清晰得多。
希望今天让你困惑的id,现在已经没那么令人无从下手了。祝你愉快地学习 Objective-C!

