Mở đầu

Bạn đã từng đọc hay nghe đâu đó nói rằng: mutable là giá trị có thể thay đổi được còn immutable là giá trị không thể thay đổi được. Nhưng bạn đã thực sự hiểu đúng nó chưa?

>> test = 3
# => 3

>> test
# => 3

>> test = 2 * test
# => 6

>> test
# => 6

Bạn nghĩ đoạn code trên(trong ruby) chứng tỏ biến test có phải là mutable không? 

Trong ruby, kiểu number và boolean luôn là immutable 

Rõ ràng trong ruby, kiểu number luôn là immutable nhưng ta vẫn thấy test ở ví dụ trên có thể thay đổi được. Lý do là vì bạn đã hiểu sai từ giá trị trong định nghĩa.

Để hiểu đúng bạn cần biết được cách bộ nhớ lưu trữ một biến như thế nào.

Cách bộ nhớ lưu trữ và sử dụng biến

Nguyên lý 2 Von Neumann: Chương trình máy tính chỉ truy cập tới dữ liệu thông qua địa chỉ.

Bộ nhớ chương trình gồm có 2 thành phần là địa chỉ bộ nhớ và dữ liệu được lưu trữ trong bộ nhớ đó.

Khi có một phép toán như phép gán test = 3, rõ ràng bộ nhớ cần lưu trữ chữ test và giá trị 3 của nó vào bộ nhớ:

     Địa chỉ bộ nhớ

    Gía trị ô nhớ

           xxx1

            test

           xxx2

             3

 

Cần lưu trữ một thứ gì đó trong bộ nhớ để gán test = 3, giả sử ví dụ đơn giản bộ nhớ làm như sau để lưu trữ phép gán đó:

  Địa chỉ bộ nhớ

   Gía trị ô nhớ 1

   Gía trị ô nhớ 2

          xxx1

           test

 

          xxx2

             3

 

          xxx0

           xxx1

           xxx2

 

Thế khi nào thì phép toán test = test * 2 được xem là mutable, khi nào thì được xem là immutable?

  • Phép toán trên được xem là mutable khi bộ nhớ thành như sau:

Địa chỉ bộ nhớ

Gía trị ô nhớ 1

Gía trị ô nhớ 2

xxx1

test

 

xxx2

6

 

xxx0

xxx1

xxx2

 

  • Phép toán trên được xem là immutable khi bộ nhớ thành như sau:

Địa chỉ bộ nhớ

Gía trị ô nhớ 1

Gía trị ô nhớ 2

xxx1

test

 

xxx2

3

 

xxx3

6

 

xxx0

xxx1

xxx3

 

Như bạn thấy, khi giá trị của biến không bị ghi đè mà được copy sang một nơi khác thì đó mới là immutable , từ giá trị ở định nghĩa này chính là giá trị đi với ô nhớ.

Còn xxx2 không còn được dùng trong immutable sẽ đi về đâu? Trong ruby có chương trình dọn rác Garbage Collection sẽ tự động xóa biến này đi khi nó đi thu dọn bộ nhớ.

Giờ bạn có thể hiểu đoạn code ruby ở phần mở đầu rồi, nếu không tin bạn có thể in ra object_id của nó để xác thực:

>> test = 3
# => 3

>> test
# => 3
>> test.object_id
# => 7

>> test = 2 * test
# => 6

>> test
# => 6
>> test.object_id
# => 13

Thế vì sao lại phải cần tới immutable để làm gì khi nó giảm công suất của bộ nhớ đi như thế? Xem phần dưới.

Khi nào nên dùng mutable và immutable 

Khi nào nên dùng mutable

Khi cần thay đổi một biến trong vòng lặp thì bạn nên dùng mutable

Nếu lặp n phần tử mà ta dùng immutable như sau:

String s = "";
for (int i = 0; i < n; ++i) {
    s = s + n;
}

Trong java, kiểu String là immutable. Đoạn code trên cứ mỗi lần gán s = s + n là phải copy biến s cũ thành object khác rồi mới gán vào s mới được. Cứ mỗi lần copy s cũ là phải lặp số ký tự có trong s cũ để copy, dẫn tới thời gian tính toán của thuật toán là O(n²).

Ta có thể cải thiện thuật toán trên còn O(n) bằng cách sử dụng mutable:

StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; ++i) {
  sb.append(String.valueOf(n));
}

String s = sb.toString();

Khi nào nên dùng immutable

Trong đa số các trường hợp, bạn nên dùng immutable vì immutable dễ hiểu và không xảy ra bug liên quan tới reference còn mutable khiến cho code không tuân theo luồng suy nghĩ của lập trình viên, khó đọc, dễ gây bug khó chịu liên quan tới reference.

VD1 gây bug:

/** @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));
}
  • Hàm sum tính tổng các phần tử của list truyền vào
  • Hàm sumAbsolute tính tổng giá trị tuyệt đối của mỗi phần tử của list truyền vào. Vì không muốn tạo một mảng tạm trong hàm này nên lập trình viên đã dùng lại luôn list truyền vào để đỡ tốn bộ nhớ và hiệu năng.

Vấn để xảy ra khi ai đó gọi hàm sumAbsolute(myData) với mảng bất kỳ myData rồi gọi tiếp sum(myData), khi gọi sumAbsolute(myData) thì hàm này đã làm mảng myData bị thay đổi nên hàm sum(myData) không còn gọi đúng với myData ban đầu như dự kiến nữa.

VD2 gây bug:

private static Date startSpringDate = null;

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


// 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);
}
  • Kiểu Date trong java là mutable 
  • Hàm startOfSpring() để lấy ngày đầu tiên của mùa xuân, vì hàm getStartOfSpring() phía trong có xử lý rất phức tạp và tốn thời gian nên lập trình viên đã gán một biến global startSpringDate để lưu giá trị của startOfSpring() vào giúp khỏi phải gọi lại khi cần thiết.

Vấn đề xảy ra khi một lập trình viên nào đó muốn lấy ngày đầu tiên của mùa xuân và cộng thêm 1 tháng nữa như đoạn code trên. Vì startSpringDate và partyDate lúc này đang cùng trỏ đến một giá trị và kiểu Date trong java là mutable nên khi gọi hàm partyDate.setMonth(... thì vô tình chung họ đã làm biến startSpringDate bị thay đổi, thiết kế ban đầu nhằm mục đích biến này không bị thay đổi nên nó gây ra bug.

Link tham khảo:

https://launchschool.com/blog/references-and-mutability-in-ruby

https://en.wikipedia.org/wiki/Variable_(computer_science)

http://web.mit.edu/6.005/www/fa15/classes/09-immutability/