1. 程式人生 > >MIT 6.031 Software Construction 學習筆記:(三) Mutability & Immutability

MIT 6.031 Software Construction 學習筆記:(三) Mutability & Immutability

這節主要是講 可變物件給程式設計帶來的危害,所謂不可變物件,就是整個生命週期中不可變的物件(廢話), e.g. : String

Risks of mutation

risk1:passing mutable values

看以下兩段程式碼:

/** @return the sum of the numbers in the list */
public static int sum(List<Integer> list) {
    int sum = 0;
    for (int x : list)
        sum += x;
    return sum;
}

/** @return the sum of the absolute values of the numbers in the list */
public static int sumAbsolute(List<Integer> list) { // let's reuse sum(), because DRY, so first we take absolute values for (int i = 0; i < list.size(); ++i) list.set(i, Math.abs(list.get(i))); return sum(list); } // meanwhile, somewhere else in the code... public static void
main(String[] args) { // ... List<Integer> myData = Arrays.asList(-5, -3, -2); System.out.println(sumAbsolute(myData)); System.out.println(sum(myData)); }
  • 可變性帶來了一個潛藏的bug(passing mutable objects around is a latent bug)
  • 可讀性非常不好,作為讀者,見到main方法的第一印象應該是sumAbs()是求絕對值,而sum是求和,而這導致了一個非常深的bug
  • 破壞了 fail-fast 原則, 往往很久才能定位真正的bug

Safe from bugs? In this example, it’s easy to blame the implementer of sum­Absolute() for going beyond what its spec allowed. But really, passing mutable objects around is a latent bug. It’s just waiting for some programmer to inadvertently mutate that list, often with very good intentions like reuse or performance, but resulting in a bug that may be very hard to track down.

Easy to understand? When reading main(), what would you assume about sum() and sum­Absolute()? Is it clearly visible to the reader that myData gets changed by one of them?

那麼怎麼解決呢?

至少可以做到兩點,

  • 儘量在不可變的物件前加 final修飾
  • 會改變引數的時候,在規範(specfication)中說明

risk2: returning mutable values

先看下面一段程式碼:

/** @return the first day of spring this year */
public static Date startOfSpring() {
    if (groundhogAnswer == null) groundhogAnswer = askGroundhog();
    return groundhogAnswer;
}
private static Date groundhogAnswer = null;

這段程式碼在求春天的第一天的時候加了一個 cache, 並且return 了一個可變物件。

如果有如下的client 做以下呼叫:

// somewhere else in the code...
public static void partyPlanning() {
    // let's have a party one month after spring starts!
    Date partyDate = startOfSpring();
    partyDate.setMonth(partyDate.getMonth() + 1);
    // ... uh-oh. what just happened?
}

那麼很顯然以後再次呼叫 startOfSpring原來的靜態私有變數 groundhogAnswer就被丟擲了。

至少有兩種解決方案可解決這個問題:

  1. 不用mutabledate而是用imutable 的替代物: package java.time: LocalDateTime, Instant,
  2. defensive copying pattern 也就是說返回的時候返回 groudAns的副本 return new Date(groundhogAnswer.getTime());

不過第二種方法雖然解決的問題,可是在大多數情況下我們都只需要一個共享的 groudAns因此會帶來一些效能上的問題。

Aliasing is what makes mutable types risky

重點說一些這個可變物件多出引用的問題,如果可變物件僅在一個區域性被一個變數引用,那麼用可變物件肯定是沒有什麼危害的,可是大多數情況下,可變物件被多出引用,這樣就導致了可變物件引入的bug

Useful immutable types

  • he primitive types and primitive wrappers are all immutable. If you need to compute with large numbers, BigInteger and BigDecimal are immutable.
  • Don’t use mutable Dates, use the appropriate immutable type from java.time based on the granularity of timekeeping you need.

Summary

The key design principle here is immutability: using immutable objects and unreassignable variables as much as possible. Let’s review how immutability helps with the main goals of this course:

  • Safe from bugs. Immutable objects aren’t susceptible to bugs caused by aliasing. Unreassignable variables always point to the same object.

  • Easy to understand. Because an immutable object or unreassignable variable always means the same thing, it’s simpler for a reader of the code to reason about — they don’t have to trace through all the code to find all the places where the object or variable might be changed, because it can’t be changed.

  • Ready for change. If an object or a variable can’t be changed at runtime, then code that depends on that object or variable won’t have to be revised when the program changes.

關於mutable物件帶來的更多危害,可見下面的原文:

reference