變數的作用域最小化原則

黃志斌發表於2016-06-10

起源

前幾天,趙明威在圖靈社群發表了“演算法導論學習之補漏:斐波那契數列”,該文中最後的 Java 程式中有一個 fibonacci 函式,如左欄所示 。我在評論中指出,這個函式應該如右欄這樣寫:

static BigInteger fibonacci(int num) {
  BigInteger x = BigInteger.ZERO;
  BigInteger y = BigInteger.ONE;
  BigInteger z;
  for(int i = 0; i &lt num; i++) {
    z = y;
    y = x.add(y);
    x = z;
  }
  return x;
}
static BigInteger fibonacci(int num) {
  BigInteger x = BigInteger.ZERO;
  BigInteger y = BigInteger.ONE;

  for(int i = 0; i < num; i++) {
    BigInteger z = y;
    y = x.add(y);
    x = z;
  }
  return x;
}

在左欄的程式中,變數 z 的作用域有 8 行,而右欄只有 4 行。

比較這兩個程式

可能有人會認為,修改後的程式每次迴圈都要重新定義變數 z,意味著每次迴圈都要重新在棧中分配一個區域性變數,導致效能沒有修改前的程式好。

我們使用 javac 分別編譯這兩個程式,然後使用 javap -c 分別把編譯後的 .class 檔案反彙編為 Java bytecode,如下所示:

bytecode

從上圖中可以看出,除了因為宣告變數的順序不同,導致變數 z 在這兩個程式中分別是第 3 號和第 4 號變數,而變數 i 在這兩個程式中分別是第 4 號和第 3 號變數之外,這個兩個程式是相同的。也就是,它們的執行速度完全一樣。

fibonacci 函式中,各個量如下所示:

  • 量 0: num
  • 量 1: x
  • 量 2: y
  • 量 3: z 或 i
  • 量 4: i 或 z
  • 量 5: BigInteger.ZERO
  • 量 6: BigInteger.ONE
  • 量 7: BigInteger.add

實際上,區域性變數表在使用 javac 編譯這兩個程式時就決定了,在 fibonacci 函式被呼叫之前就分配好所有的區域性變數(包括函式的引數),在函式結束時隨退棧操作一起釋放。

變數的作用域最小化原則

根據《程式碼大全》第 10 章“使用變數的一般事項”第 4 節“作用域”:

  • 作用域或者可見性指的是變數在程式內的可見和可引用的範圍。
  • 減少變數作用域的方法之一就是儘量使變數區域性化。
  • 當對變數的作用域猶豫不決的時候,你應該傾向於選擇該變數所能具有的最小的作用域:首先將變數侷限於某個特定的迴圈,然後是侷限於某個子程式,其次成為類的 private 變數、protected 變數,再其次對包可見(如果你的程式語言支援包),最後在不得已的情況下再把它作為全域性變數。
  • 你應該把每個變數定義成只對需要看到它的、最小範圍的程式碼段可見。如果你能把變數的作用域限定到一個單獨的的迴圈或者子程式,那是再好不過的了。
  • 一般而言,應使變數的作用域最小化,把變數引用點儘可能集中在一起,從而能夠對變數施加控制。將區域性變數的作用域最小化,可以增強程式碼的可讀性和可維護性,並降低出錯的可能性。

Code

參考資料

  1. 豆瓣:程式碼大全
  2. Wikipedia: Java bytecode

相關文章