- 原文地址:why null is bad跳轉(zhuǎn)中...
- 原文作者:Yegor Bugayenko
- 譯者:高老莊里的猿
先來(lái)看個(gè) JAVA 中使用 null 作為返回值的簡(jiǎn)單例子:

該方法最大的問(wèn)題是返回 null 代替了對(duì)象。在面向?qū)ο笠?guī)范中使用 null 是個(gè)非常糟糕的做法,應(yīng)該極力避免。有很多論據(jù)可以支持這一觀點(diǎn),包括 Tony Hoare 的演講《Null References, The Billion Dollar Mistake》和 David West 的《Object Thinking》 整本書(shū)。接下來(lái)我將所有的論據(jù)做一些整理并使用合適的面向?qū)ο蠼Y(jié)構(gòu)代替 null 作為返回值。目前看來(lái),至少有兩種方法可以代替使用 null 。
1、使用空對(duì)象設(shè)計(jì)模式(最好定義一個(gè)常量)

2、當(dāng)不能返回一個(gè)對(duì)象時(shí),可以拋出異常來(lái)讓調(diào)用方 fail-fast(快速失敗)

現(xiàn)在來(lái)看看反對(duì)使用 null 的依據(jù),除了上面提到的 Tony Hoares 的演講和 David West 的書(shū)籍,還有 Robert Martin 的 《Clean Code》、 Steve McConnell 的《Code Complete》、John Sonmez 的 《Say “No” to “Null”》以及 StackOverflow 上的討論《Is returning null bad design? 》,這些我在寫(xiě)這篇文章之前都看過(guò)。
特殊錯(cuò)誤處理
每次將對(duì)象引用作為輸入時(shí)都必須檢查它是 null 的還是有效的,如果忘了檢查,將會(huì)導(dǎo)致運(yùn)行時(shí) NPE(Null Pointer Exception)。因此,你的代碼邏輯會(huì)被多種檢查和 if/then/else 分支所污染。看看下面例子:

這是 c 語(yǔ)言和其他很多面向過(guò)程編程語(yǔ)言處理異常所使用的方法,面向?qū)ο缶幊桃氘惓L幚碇饕褪菫榱讼@些特殊的錯(cuò)誤處理邏輯。在面向?qū)ο缶幊讨校覀儗惓R悦芭莸姆绞讲粩嗟南蛏蠏伋鲋钡綉?yīng)用層,這樣我們的代碼將變得更加小而美。
dept.getByName("Jeffrey").transferTo(dept2);
null 是面向過(guò)程編程的"封建余孽",請(qǐng)使用 null 對(duì)象或者異常代替之。
語(yǔ)義的二義性
為了顯示的將"函數(shù)會(huì)返回真實(shí)的對(duì)象或者 null "這層含義表達(dá)出來(lái),getByName() 必須命名為getByNameOrNullIfNotFound()。每個(gè)類(lèi)似的函數(shù)都應(yīng)該這樣做,否則會(huì)給代碼閱讀者來(lái)帶來(lái)歧義。
為了語(yǔ)義的準(zhǔn)確性,你應(yīng)該為函數(shù)定義更長(zhǎng)的名稱(chēng)。
為了消除歧義,函數(shù)盡量返回一個(gè)真實(shí)的對(duì)象、一個(gè) null 對(duì)象或者拋出一個(gè)異常。
有些人會(huì)爭(zhēng)辯說(shuō)有時(shí)為了性能,不得不返回 null。比如 java Map 接口中的 get() 方法,當(dāng)在 map 中找不到相應(yīng)的條目時(shí)會(huì)返回 null,例如:

由于 Map 的 get() 方法返回 null ,上面代碼只會(huì)在 map 中搜索一次。如果我們想重寫(xiě) Map 的 get() 方法以讓其在查找不到條目時(shí)拋出異常,代碼應(yīng)該這樣寫(xiě):

很明顯,這個(gè)方法比第一個(gè)方法慢兩倍,怎么辦呢? 我覺(jué)得 Map 的接口設(shè)計(jì)存在缺陷(無(wú)意冒犯作者),它應(yīng)該返回一個(gè)迭代器 Iterator 以便讓我們代碼可以像如下這樣:

BTW,這正是 C++ 標(biāo)準(zhǔn)庫(kù)(STL)中 map::find() 方法的設(shè)計(jì)思路。
計(jì)算機(jī)思維 vs 對(duì)象思維
假如某人知道 Java 對(duì)象是一個(gè)指向某個(gè)數(shù)據(jù)結(jié)構(gòu)的指針,并且 知道 null 是一個(gè)空指針(在英特爾 x86 處理器中等于 0x00000000),那他應(yīng)該能接收 if(employee == null) 這個(gè)語(yǔ)句。但是,如果以對(duì)象思維來(lái)進(jìn)行思考,這個(gè)語(yǔ)句就沒(méi)意義了。 從一個(gè)對(duì)象的角度來(lái)看,我們的代碼是這樣的:
- Hello, 請(qǐng)問(wèn)是軟件部嗎?
- 是的。
- 麻煩讓我和你們的 employee(員工) Jeffrey 聊聊。
- 請(qǐng)稍等...
- Hello
- 你是 NULL ?
上面對(duì)話的最后一句看起來(lái)很奇怪,不是嗎? 相反,如果他們?cè)诮拥轿蚁肱c Jeffrey 進(jìn)行通話的需求后直接掛斷電話會(huì)快速給我們制造個(gè)故障(異常)。這時(shí)我們可以嘗試著再次撥過(guò)去或者直接告訴我們的主管無(wú)法聯(lián)系到 Jeffrey 來(lái)完成更大的交易。
或者,他們可以讓我們與另一個(gè)人交談,他不是 Jeffrey,但如果我們需要“特定的” Jeffrey(null 對(duì)象)的話,他可以幫助我們解決大多數(shù)問(wèn)題,也可以拒絕幫忙。
Slow Failing(慢失敗)
與 failing fast(快速失敗)相反,上述代碼嘗試緩慢死亡并殺死其他人。它向調(diào)用者隱藏了失敗而不是讓其知道出了問(wèn)題需要馬上進(jìn)行異常處理。這個(gè)結(jié)論與上面"特殊錯(cuò)誤處理"章節(jié)的討論很接近。最好讓代碼盡可能脆弱,必要時(shí)讓它崩潰。
要確保你的方法對(duì)調(diào)用方提供的操作數(shù)有著極高的要求,如果調(diào)用方提供的數(shù)據(jù)不夠或者根本不符合方法主要的使用場(chǎng)景,拋出異常吧。或者返回一個(gè) null 對(duì)象,該對(duì)象暴露一些常見(jiàn)行為,并對(duì)所有其他調(diào)用拋出異常,參考如下:

可變的和不完整的對(duì)象
一般來(lái)說(shuō),強(qiáng)烈建議在設(shè)計(jì)對(duì)象時(shí)考慮到不變性。這意味著對(duì)象在實(shí)例化過(guò)程中獲得所有必要的內(nèi)容,并且在整個(gè)生命周期中永遠(yuǎn)不會(huì)更改其狀態(tài)。 null 通常被用在延遲加載中以使對(duì)象不完整且可變。例如:

這種技術(shù)雖然應(yīng)用廣泛,但在面向?qū)ο缶幊讨惺且环N反設(shè)計(jì)模式的。主要是因?yàn)樗挂粋€(gè)對(duì)象負(fù)責(zé)計(jì)算平臺(tái)的性能問(wèn)題,而這對(duì) Employee 對(duì)象應(yīng)該是透明的。
與其管理狀態(tài)并公開(kāi)業(yè)務(wù)相關(guān)的行為,不如讓對(duì)象處理好其自身結(jié)果的緩存---這就是延遲加載的目的。緩存不是 employee 該在辦公室里做的事,不是嗎?
解決辦法是不要像上面的例子那樣,以這種原始的方式使用延遲加載。相反,將這個(gè)緩存問(wèn)題移到應(yīng)用程序的另一層。例如在 Java中可以使用面向切面編程技術(shù)。 例如,jcabi-aspects 使用 @Cacheable 注解來(lái)緩存方法返回的值:

希望通過(guò)這篇文章的分析,能讓你停止在代碼中繼續(xù)使用 null 作為返回值。
來(lái)源:掘金 鏈接:https://juejin.im/post/5d740e7f5188251325775966