Swift 與 Objective-C

Objective-C nil 與 Java null 的比較:為什麼行為不同?

有一件事,總是讓剛接觸 iOS 的開發者非常困惑。

閱讀 4 分鐘
Objective-C nil 與 Java null 的比較:為什麼行為不同? 封面圖

有一件事,總是讓剛接觸 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。

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
App 行為 不崩潰並繼續執行 因例外而崩潰
處理者 執行階段(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 的值必須加上問號標示,展開使用時也會強制安全處理。

換句話說,Objective-C 的「安靜略過」在 Swift 中變成了「事先揭露」。

和 Swift 並列比較,就能看出設計理念的差異
和 Swift 並列比較,就能看出設計理念的差異

Objective-C 對 nil 訊息不崩潰不是 Bug,而是一種設計理念。

便利的同時也會隱藏安靜的 Bug;理解「為什麼不會崩潰?」之後,反而能寫出更穩健的程式碼。

如果你現在在 iOS 程式碼中遇到原因不明的空白畫面,不妨檢查一下中途是否有 nil 悄悄流過。一定會有所幫助。

延伸閱讀