[譯]爲何 null 很差?

  • 原文地址:why null is bad
  • 原文做者:Yegor Bugayenko
  • 譯文出自:我的興趣(非掘金翻譯計劃)
  • 譯者:高老莊裏的猿
  • 校對者:無,首次翻譯,請讀者們在評論中指正

先來看個 Java 中使用 null 做爲返回值的簡單例子:html

public Employee getByName(String name) {
  int id = database.find(name);
  if (id == 0) {
    return null;
  }
  return new Employee(id);
}
複製代碼

該方法最大的問題是返回 null 代替了對象。在面向對象規範中使用 null 是個很是糟糕的作法,應該極力避免。有不少論據能夠支持這一觀點,包括 Tony Hoare 的演講《Null References, The Billion Dollar Mistake》和 David West 的《Object Thinking》這本書。接下來我將全部的論據作一些整理並使用合適的面向對象結構代替 null 做爲返回值。目前看來,至少有兩種方法能夠代替使用 null 。java

一、使用空對象設計模式(最好定義一個常量)編程

public Employee getByName(String name) {
  int id = database.find(name);
  if (id == 0) {
    return Employee.NOBODY;
  }
  return Employee(id);
}
複製代碼

二、當不能返回一個對象時,能夠拋出異常來讓調用方 fail-fast(快速失敗)設計模式

public Employee getByName(String name) {
  int id = database.find(name);
  if (id == 0) {
    throw new EmployeeNotFoundException(name);
  }
  return Employee(id);
}
複製代碼

如今來看看反對使用 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 分支所污染。看看下面的例子:數據結構

// 糟糕的設計,請勿複用
Employee employee = dept.getByName("Jeffrey");
if (employee == null) {
  System.out.println("can't find an employee");
  System.exit(-1);
} else {
  employee.transferTo(dept2);
}
複製代碼

這是 c 語言和其餘不少面向過程編程語言處理異常所使用的方法,面向對象編程引入異常處理機制主要就是爲了消除這些特殊的錯誤處理邏輯。在面向對象編程中,咱們將異常以冒泡的方式不斷的向上拋出直到應用層,這樣咱們的代碼將變得更加小而美。編程語言

dept.getByName("Jeffrey").transferTo(dept2);
複製代碼

null 是面向過程編程的"封建餘孽",請使用 null 對象或者異常代替之。ide

語義的二義性

爲了顯示的將"函數會返回真正的對象或者 null "這層含義表達出來,getByName()應該命名爲getByNameOrNullIfNotFound()。每一個相似的函數都應該這樣作,不然會給代碼閱讀者來帶來歧義。函數

爲了語義的準確性,值得爲函數定義更長的名稱。性能

爲了消除歧義,函數儘可能返回一個真正的對象、一個 null 對象或者拋出一個異常。

有些人會爭辯說有時爲了性能,不得不返回 null。好比 java Map 接口中的 get() 方法,當在 map 中找不到相應的條目時會返回 null,例如:

Employee employee = employees.get("Jeffrey");
if (employee == null) {
  throw new EmployeeNotFoundException();
}
return employee;
複製代碼

因爲 Map 的 get() 方法返回 null ,上面代碼只會在 map 中搜索一次。若是咱們想重寫 Map 的 get() 方法以讓其在查找不到條目時拋出異常,代碼應該這樣寫:

if (!employees.containsKey("Jeffrey")) { // first search
  throw new EmployeeNotFoundException();
}
return employees.get("Jeffrey"); // second search
複製代碼

很明顯,這個方法比第一個方法慢兩倍,怎麼辦呢? 我以爲 Map 的接口設計存在缺陷(無心冒犯做者),它應該返回一個迭代器 Iterator 以便讓咱們代碼能夠像以下這樣:

Iterator found = Map.search("Jeffrey");
if (!found.hasNext()) {
  throw new EmployeeNotFoundException();
}
return found.next();
複製代碼

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 對象,該對象暴露一些常見行爲,並對全部其餘調用拋出異常,參考以下:

public Employee getByName(String name) {
  int id = database.find(name);
  Employee employee;
  if (id == 0) {
    employee = new Employee() {
      @Override
      public String name() {
        return "anonymous";
      }
      @Override
      public void transferTo(Department dept) {
        throw new AnonymousEmployeeException(
          "I can't be transferred, I'm anonymous"
        );
      }
    };
  } else {
    employee = Employee(id);
  }
  return employee;
}
複製代碼

可變的和不完整的對象

通常來講,強烈建議在設計對象時考慮到不變性。這意味着對象在實例化過程當中就能得到全部必要的內容,而且在整個生命週期中永遠不會更改其狀態。 null 一般被用在延遲加載中以使對象不完整且可變。例如:

public class Department {
  private Employee found = null;
  public synchronized Employee manager() {
    if (this.found == null) {
      this.found = new Employee("Jeffrey");
    }
    return this.found;
  }
}
複製代碼

這種技術雖然應用普遍,但在面向對象編程中是一種反設計模式的。主要是由於它使一個 Employee 對象負責計算平臺的性能問題,而這對 Employee 對象應該是透明的。

與其管理狀態並公開業務相關的行爲,不如讓對象處理好其自身結果的緩存---這就是延遲加載的目的。緩存不是 employee 該在辦公室裏作的事,不是嗎?

解決辦法是不要像上面的例子那樣,以這種原始的方式使用延遲加載。相反,將這個緩存問題移到應用的其餘層。例如在 Java中可使用面向切面編程技術。 例如,jcabi-aspects 使用 @Cacheable 註解來緩存方法返回的值:

import com.jcabi.aspects.Cacheable;
public class Department {
  @Cacheable(forever = true)
  public Employee manager() {
    return new Employee("Jacky Brown");
  }
}
複製代碼

但願經過這篇文章的分析,能讓你中止在代碼中繼續使用 null 做爲返回值。

相關文章
相關標籤/搜索