- 原文地址:why null is bad跳轉中...
- 原文作者:Yegor Bugayenko
- 譯者:高老莊里的猿
先來看個 JAVA 中使用 null 作為返回值的簡單例子:
該方法最大的問題是返回 null 代替了對象。在面向對象規范中使用 null 是個非常糟糕的做法,應該極力避免。有很多論據可以支持這一觀點,包括 Tony Hoare 的演講《Null References, The Billion Dollar Mistake》和 David West 的《Object Thinking》 整本書。接下來我將所有的論據做一些整理并使用合適的面向對象結構代替 null 作為返回值。目前看來,至少有兩種方法可以代替使用 null 。
1、使用空對象設計模式(最好定義一個常量)
2、當不能返回一個對象時,可以拋出異常來讓調用方 fail-fast(快速失敗)
現在來看看反對使用 null 的依據,除了上面提到的 Tony Hoares 的演講和 David West 的書籍,還有 Robert Martin 的 《Clean Code》、 Steve McConnell 的《Code Complete》、John Sonmez 的 《Say “No” to “Null”》以及 StackOverflow 上的討論《Is returning null bad design? 》,這些我在寫這篇文章之前都看過。
特殊錯誤處理
每次將對象引用作為輸入時都必須檢查它是 null 的還是有效的,如果忘了檢查,將會導致運行時 NPE(Null Pointer Exception)。因此,你的代碼邏輯會被多種檢查和 if/then/else 分支所污染。看看下面例子:
這是 c 語言和其他很多面向過程編程語言處理異常所使用的方法,面向對象編程引入異常處理主要就是為了消除這些特殊的錯誤處理邏輯。在面向對象編程中,我們將異常以冒泡的方式不斷的向上拋出直到應用層,這樣我們的代碼將變得更加小而美。
dept.getByName("Jeffrey").transferTo(dept2);
null 是面向過程編程的"封建余孽",請使用 null 對象或者異常代替之。
語義的二義性
為了顯示的將"函數會返回真實的對象或者 null "這層含義表達出來,getByName() 必須命名為getByNameOrNullIfNotFound()。每個類似的函數都應該這樣做,否則會給代碼閱讀者來帶來歧義。
為了語義的準確性,你應該為函數定義更長的名稱。
為了消除歧義,函數盡量返回一個真實的對象、一個 null 對象或者拋出一個異常。
有些人會爭辯說有時為了性能,不得不返回 null。比如 java Map 接口中的 get() 方法,當在 map 中找不到相應的條目時會返回 null,例如:
由于 Map 的 get() 方法返回 null ,上面代碼只會在 map 中搜索一次。如果我們想重寫 Map 的 get() 方法以讓其在查找不到條目時拋出異常,代碼應該這樣寫:
很明顯,這個方法比第一個方法慢兩倍,怎么辦呢? 我覺得 Map 的接口設計存在缺陷(無意冒犯作者),它應該返回一個迭代器 Iterator 以便讓我們代碼可以像如下這樣:
BTW,這正是 C++ 標準庫(STL)中 map::find() 方法的設計思路。
計算機思維 vs 對象思維
假如某人知道 Java 對象是一個指向某個數據結構的指針,并且 知道 null 是一個空指針(在英特爾 x86 處理器中等于 0x00000000),那他應該能接收 if(employee == null) 這個語句。但是,如果以對象思維來進行思考,這個語句就沒意義了。 從一個對象的角度來看,我們的代碼是這樣的:
- Hello, 請問是軟件部嗎?
- 是的。
- 麻煩讓我和你們的 employee(員工) Jeffrey 聊聊。
- 請稍等...
- Hello
- 你是 NULL ?
上面對話的最后一句看起來很奇怪,不是嗎? 相反,如果他們在接到我想與 Jeffrey 進行通話的需求后直接掛斷電話會快速給我們制造個故障(異常)。這時我們可以嘗試著再次撥過去或者直接告訴我們的主管無法聯系到 Jeffrey 來完成更大的交易。
或者,他們可以讓我們與另一個人交談,他不是 Jeffrey,但如果我們需要“特定的” Jeffrey(null 對象)的話,他可以幫助我們解決大多數問題,也可以拒絕幫忙。
Slow Failing(慢失敗)
與 failing fast(快速失敗)相反,上述代碼嘗試緩慢死亡并殺死其他人。它向調用者隱藏了失敗而不是讓其知道出了問題需要馬上進行異常處理。這個結論與上面"特殊錯誤處理"章節的討論很接近。最好讓代碼盡可能脆弱,必要時讓它崩潰。
要確保你的方法對調用方提供的操作數有著極高的要求,如果調用方提供的數據不夠或者根本不符合方法主要的使用場景,拋出異常吧。或者返回一個 null 對象,該對象暴露一些常見行為,并對所有其他調用拋出異常,參考如下:
可變的和不完整的對象
一般來說,強烈建議在設計對象時考慮到不變性。這意味著對象在實例化過程中獲得所有必要的內容,并且在整個生命周期中永遠不會更改其狀態。 null 通常被用在延遲加載中以使對象不完整且可變。例如:
這種技術雖然應用廣泛,但在面向對象編程中是一種反設計模式的。主要是因為它使一個對象負責計算平臺的性能問題,而這對 Employee 對象應該是透明的。
與其管理狀態并公開業務相關的行為,不如讓對象處理好其自身結果的緩存---這就是延遲加載的目的。緩存不是 employee 該在辦公室里做的事,不是嗎?
解決辦法是不要像上面的例子那樣,以這種原始的方式使用延遲加載。相反,將這個緩存問題移到應用程序的另一層。例如在 Java中可以使用面向切面編程技術。 例如,jcabi-aspects 使用 @Cacheable 注解來緩存方法返回的值:
希望通過這篇文章的分析,能讓你停止在代碼中繼續使用 null 作為返回值。
來源:掘金 鏈接:https://juejin.im/post/5d740e7f5188251325775966