Swift 与 Objective-C

彻底掌握 Objective-C 的 id 类型(动态类型与鸭子类型总结)

刚开始学习 Objective-C 时,很多人都会在 id 这个类型面前停顿片刻。

4 分钟阅读
彻底掌握 Objective-C 的 id 类型(动态类型与鸭子类型总结) 封面图

刚开始学习 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指针。

id 的本质:原来只是一个包含 isa 的指针
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 时最常见的画面
这段红色日志,就是错误使用 id 时最常见的画面

总结来说,id是象征 Objective-C 动态特性的类型,而动态类型和鸭子类型都体现了“根据行为而不是类来判断对象”的理念。

刚开始可能觉得陌生又危险,但理解这一原理后,Cocoa 框架的设计会清晰得多。

希望今天让你困惑的id,现在已经没那么令人无从下手了。祝你愉快地学习 Objective-C!