如何評價"Null reference - my billion-dollar mistake"?

Tony Hoare introduced Null references in ALGOL W back in 1965 「simply because it was so easy to implement」, says Mr. Hoare. He talks about that decision considering it 「my billion-dollar mistake」.

Null References: The Billion Dollar Mistake

如果沒有Null reference, 編程是不是會更加容易點?通過引入Null才解決的問題又該怎麼解決呢?


就我理解null的問題主要有以下幾個:

1。雙重含義。null可以理解為"空",也可以理解為"無效"的。所以你拿著一個會返回null的函數,如果對系統的實現沒有深刻的理解,你會難以確定是否應該立刻處理掉(無效值),比如說賦個默認值或是拋異常,還是把null值繼續往後面傳遞(合法的空值)。這時一般人的選擇是繼續往下傳,畢竟如果在現場處理,你就有責任搞清楚後續程序的具體實現與意圖,而這與你手上的任務往往沒有直接關係。

2。由於1,往往導致爆NPE的位置與實際出問題的位置相隔十萬八千里。而你在處理NPE時,首要任務還是需要搞清楚這個null是無效值還是有效空值。還多了一種情況,有可能是上游的程序員腦抽忘了賦值,也可能是錯誤地進入了不會為其賦值的分支。也就是說,你還得搞清楚這是"故意的無效值"還是"無意的無效值"。

3。由於存在"無意的無效值"這種可能,你在檢查上游邏輯時,甚至不能完全相信程序來倒推當時的意圖。因為既然出現了"無意的無效值",說明該程序員沒有正確地用程序實現其意圖。舉個栗子,你發現上遊程序進入了一個沒有為變數賦初值的分支,你會難以判斷是程序走錯了分支,還是雖然進入了正確的分支,但該分支的實現忘了為其賦初值。

所以如果你希望找到最合理的修複位置,你就必須去問原來的程序員本人(如果他還記得)或者搞清楚該位置的原始需求(如果能找到文檔)。否則你就無法判定你的修復會不會是"兩個錯誤相加得到一個正確"。

由於以上幾點,導致合理解決NPE需要付出大量的時間。以至於有些程序員(例如我自己)看到NPE就有生理反應(具體表現為頭痛胸悶胃抽筋),所以我在團隊里一貫要求不要在程序中主動拋出NPE,你根據實際情況拋個IllegalStateException或者IllegalArgumentException會沒那麼嚇人。就算一定要拋(畢竟這種東西我不能強制),一定要傳個好看的message,不能拋空message的NPE出來嚇人。這點是硬性規定,否則過不了代碼審查。

目前在語言層面(用nullable標註來注釋介面屬於IDE層面)的null替代方案有:

1。函數內對於無效值盡量拋異常。特別地,在Java里應該使用專門的自定義Checked Exception。不過這種方案,對於經常出現無效值且較常用(有性能需求或在代碼中經常使用)的函數並不適用。

2。對於自身可取空值的類型,比如說集合類型,盡量使用類型定義的空值而非null。《Effective Java》中有相關條目

3。開發中在期待非null值的模塊入口處應儘快進行非null判斷,避免非法的null值進一步傳遞。內部介面則可採用nullable標註註明,也可採用以下第4點的方案。性能允許且沒有並發訪問需求的話,對取值有不同要求的多個的模塊之間不要共享Context。比如說我非常反對把HttpServletRequest直接往model里傳(包括進行簡單封裝後再傳),模塊應定義符合自己需求的Context對象,並設置校驗規則或者使用下面的Optional裝箱,在模塊入口處拷貝Context值時進行校驗。

4。使用專門的Optional對象對可為null的變數進行裝箱。這類Optional對象必須拆箱後才能參與運算,因而拆箱步驟就提醒使用者必須處理null值的情況。在開發體驗上,可以看作是把null值在變數粒度上轉化成了Checked Exception。

可參考的實現有:

a) Scala的Option

b) Guava的Optional

c) Java8的Optional


我認為函數式語言裡面的解決方案是比較好的,為此,我還專門為C#做了一個struct 容器,可以直接把變數丟進去,但是不能輕易取出。

我為它做了幾個介面函數:

1、通過匿名函數可以利用容器裡面的變數做事情(當變數為空時不執行),調用方法:

容器.Do(x=&> doSomething(x) )

2、Linq調用的介面(Select、Where、SelectMany),譬如,使用時可以:

from a in 容器1
from b in 容器2
select doSomething(a, b)

3、取出變數的函數,要求輸入參數:當變數為null時,給出默認值或者產生新值的函數。

4、判斷變數是否為空的函數。

有了這四個功能,已經滿足了所有場景的要求,而且大大減輕了null值檢查的腦力負擔。


Data.Maybe

Data.Either

try(), try() again in Rails

Optional (Java Platform SE 8 )


最近在關注GitHub上C# 7的提案,優先順序最高的一條就是關於Non-Nullable reference type的

Proposal for non-nullable references (and safe nullable references) · Issue #227 · dotnet/roslyn · GitHub

大意是準備為reference type引入T!(Non-Nullable), T?(Nullable)這樣的語法糖, 在編譯期檢測違反non-nullable的代碼。

C#之父幾年前在訪談中就談到這個Non-nullable reference type了

For example, in the type system we do not have separation between value and reference types and nullability of types. This may sound a little wonky or a little technical, but in C# reference types can be null, such as strings, but value types cannot be null. It sure would be nice to have had non-nullable reference types, so you could declare that 『this string can never be null, and I want you compiler to check that I can never hit a null pointer here』.

50% of the bugs that people run into today, coding with C# in our platform, and the same is true of Java for that matter, are probably null reference exceptions. If we had had a stronger type system that would allow you to say that 『this parameter may never be null, and you compiler please check that at every call, by doing static analysis of the code』. Then we could have stamped out classes of bugs

而相關的提案還有C#語言層面的code contract(感覺都是spec#從微軟研究院走出來了)

Proposal: Method Contracts · Issue #119 · dotnet/roslyn · GitHub

而CoreFx也有人提議添加Option&,跟Java8的Optional差不多.也有叫Maybe的。


其實不管有沒有null,編程都不會更容易一點。

有null只是讓寫程序看起來更容易一點,因為程序員在腦子沒想清楚的時候可以隨便丟個null出去就可以交差了。

就像如果我們不要整形和浮點型,全部用有理數類型來編程,編程看起來會簡單很多一樣。

順便說一句,NRE這個異常一般來說在正式的項目開發中是禁止出現的異常,和SOE(爆棧)異常一樣,屬於禁止出現的異常。


語言沒有null 也會有人自己定義null


還是F#/Scala/Swift的Option比較合理和優雅。


我以前曾經做過一個實驗,如果一個類型是class或interface(我指的是C#那樣的)的變數不給用null,而只有標記了問號(如Fuck?, int?)的類型才給用null會怎麼樣?

答案是,用起來更有安全感了。這樣你本來沒打算用null的變數,就絕對不會有null進去。而如果你想讓他可以是null,類型上又顯示的提醒你了。當你想把一個T?強制轉換成T的時候,只要是null當場就在那個點拋異常(而不是等到用它的時候)。一切都是那麼的醒目。

永遠也不會有類似var fucks = new MyFuckClass[10];,fucks[0].Shit(); 這樣的代碼出現了(因為變成語法錯誤了。

有些人可能會指出,那C++不就是這樣的嗎?當然不是,我這裡還是引用傳遞的。


其實有沒有 NULL 並不是事情的關鍵。比如 @vczh 提的那個語言試驗,完全是 overkilling。我一直說,能用 discpline 或者庫來實現的東西,就別放到語言里。否則得不償失。

我們看看 @vczh 的方案是設計一個不允許 NULL 的類型。但是他的方案里還是有一個可以允許 NULL 的對應類型。也就是說他的方案還是需要程序員自身的 dscpline。這種非強制方案放到語言里,不能說沒用,但是多此一舉。

其實 @vczh 的方案說白了很簡單,無非就是多用 assert。NASA 還有更絕的,他們的 assert 在 release 版里不會 turn off,只是行為從 abort 變成記 log。你看多簡單的方案,根本不用大費周章改一個 custom compiler。可是工業界就是不用。

邏輯上不能避免的事情,並不是放到語言里就一了百了了。如果人們沒有用其它方案解決的動力,加新的語言 feature 也是濫用。


如果沒有null,就會出現各種特殊值,所以還是要有null。

比如說Java int沒有null,他們就返回-1。

這樣的還好,要是對象默認值是不是還要重寫.equals(),不然就實現單例。

比如說類A,name="";如果說A必須要有名字,那你就需要if a.name.equals("")throw nullpointerexception


推薦閱讀:

位運算有什麼奇技淫巧?
非同步可重入函數與線程安全函數等價嗎?
大括弧不換行的壞處有什麼?為什麼有人不換行?
C語言內存中是否存在一個區域,存儲著變數的符號,變數的類型和變數的首地址?
c語言為什麼可以通過變數名來訪問變數的值?變數的值是存儲在計算機中,那麼變數名也同時存儲在計算機中嗎?

TAG:編程 | Java | C編程語言 | C | C# |