Swift 与 Objective-C

Objective-C nil 与 Java null 对比:为什么行为不同?

有一件事总会让刚接触 iOS 的开发者感到特别困惑。

4 分钟阅读
Objective-C nil 与 Java null 对比:为什么行为不同? 封面图

有一件事总会让刚接触 iOS 的开发者感到特别困惑。

在 Java 或 C++ 中,对 nil(null)对象进行操作会让应用立即崩溃。就是那个著名的 NullPointerException。

但在 Objective-C 中,即使向 nil 发送消息,也会静默结束。既不会崩溃,也不会报错。

刚开始接触时,你可能会想:“这不是 Bug 吗?”其实,这是语言有意这样设计的。

简单来说,在 Objective-C 中向 nil 发送消息不会发生任何事情,只会返回 0(或 nil)。

这不是放任错误不管,而是语言层面刻意设计的一种安全机制。

今天就来看看为什么会有这种行为,以及应该如何理解它。


向 nil 发送消息为什么不会崩溃?

关键在于 Objective-C 发送消息的方式。

当我们写下 [object doSomething] 这样的代码时,编译器会把它转换为名为 objc_msgSend(object, @selector(doSomething)) 的函数调用。

它不是直接调用方法,而是请求运行时:“请把这条消息传递给这个对象。”

objc_msgSend 函数一开始就会检查接收对象是否为 nil。

objc_msgSend 静默吞掉 nil 的位置
objc_msgSend 静默吞掉 nil 的位置

如果接收对象是 nil 呢?

它不会去查找方法,而是立即返回 0,然后静默结束。

也就是说,nil 就像是“吞掉消息的存在”。既然消息根本到不了任何地方,自然也不会崩溃。

// receiver如果是 nil就不查找方法,立即 0 返回
NSString *name = nil;
NSUInteger len = [name length];  // 无崩溃 len == 0

这和 NullPointerException 有什么区别?

这是很多人容易混淆的地方,所以我整理成了表格。

分类 Objective-C (nil) Java (null)
调用 null 上的方法 忽略并返回 0/nil 发生 NullPointerException
应用行为 不崩溃并继续执行 因异常而崩溃
处理方 运行时(objc_msgSend) JVM 异常处理

Java 一访问 null 引用,就会抛出异常,仿佛在说:“根本没有这个对象。”

而 Objective-C 运行时会把 nil 当作正常值处理。

所以 Java 开发者转到 Objective-C 后,通常会因为这个差异别扭一段时间。

返回值也会根据类型略有不同。对象类型返回 nil,数值类型返回 0,结构体则返回由 0 填充的值。


不崩溃就一定是好事吗?

说实话,这是把双刃剑。

优点很明显:不需要在代码各处堆满 nil 检查。

例如,数组为空并返回 nil 时,向它询问 count 也只会得到 0,因此流程不会中断。

但正是这一点也可能成为陷阱。

Bug 不会以崩溃的形式暴露出来,而是悄悄被掩盖。

当数据没有显示在界面上,排查很久后你常会发现,中途某个对象变成了 nil,导致它收到的所有消息都被忽略。

如果直接崩溃,可能很快就能找到;但由于它静默跳过,定位原因反而需要更久。

直接运行一次,就能快速确认它是否真的不会崩溃
直接运行一次,就能快速确认它是否真的不会崩溃

因此,养成区分 nil 是否可以流过某个位置,以及该位置是否必须有值的习惯非常重要。


实际开发中该如何处理?

下面整理几个我会采用的方法。

  1. 在重要分支中显式检查 nil(if (object == nil)
  2. 通过注释或名称标明可能返回 nil 的方法
  3. 怀疑出现意外的 nil 时,在开发阶段使用 NSAssert 将其拦截

转到 Swift 后,情况又不一样了。

Swift 通过 Optional 这一概念,直接在类型系统中明确 nil 的可能性。

可能为 nil 的值必须加上问号标记,展开使用时也会被强制安全处理。

换句话说,Swift 把 Objective-C 的“静默跳过”变成了“提前显式表达”。

与 Swift 并排比较,就能看出设计理念的差异
与 Swift 并排比较,就能看出设计理念的差异

Objective-C 向 nil 发送消息却不崩溃,不是 Bug,而是一种设计理念。

便利也会隐藏静默 Bug;理解“为什么不崩溃?”之后,反而能写出更稳健的代码。

如果你现在在 iOS 代码中遇到原因不明的空白界面,不妨检查一下中途是否有 nil 悄悄经过。一定会有所帮助。

延伸阅读