有一件事总会让刚接触 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。
如果接收对象是 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 是否可以流过某个位置,以及该位置是否必须有值的习惯非常重要。
实际开发中该如何处理?
下面整理几个我会采用的方法。
- 在重要分支中显式检查 nil(
if (object == nil)) - 通过注释或名称标明可能返回 nil 的方法
- 怀疑出现意外的 nil 时,在开发阶段使用
NSAssert将其拦截
转到 Swift 后,情况又不一样了。
Swift 通过 Optional 这一概念,直接在类型系统中明确 nil 的可能性。
可能为 nil 的值必须加上问号标记,展开使用时也会被强制安全处理。
换句话说,Swift 把 Objective-C 的“静默跳过”变成了“提前显式表达”。
Objective-C 向 nil 发送消息却不崩溃,不是 Bug,而是一种设计理念。
便利也会隐藏静默 Bug;理解“为什么不崩溃?”之后,反而能写出更稳健的代码。
如果你现在在 iOS 代码中遇到原因不明的空白界面,不妨检查一下中途是否有 nil 悄悄经过。一定会有所帮助。

