有一件事,總是讓剛接觸 iOS 的開發者非常困惑。
在 Java 或 C++ 中,對 nil(null)物件做任何操作,App 都會立即崩潰。就是那個有名的 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 |
| App 行為 | 不崩潰並繼續執行 | 因例外而崩潰 |
| 處理者 | 執行階段(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 的值必須加上問號標示,展開使用時也會強制安全處理。
換句話說,Objective-C 的「安靜略過」在 Swift 中變成了「事先揭露」。
Objective-C 對 nil 訊息不崩潰不是 Bug,而是一種設計理念。
便利的同時也會隱藏安靜的 Bug;理解「為什麼不會崩潰?」之後,反而能寫出更穩健的程式碼。
如果你現在在 iOS 程式碼中遇到原因不明的空白畫面,不妨檢查一下中途是否有 nil 悄悄流過。一定會有所幫助。

