Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

RefCell<T> và Mẫu Khả Biến Nội Bộ

Khả biến nội bộ (Interior mutability) là một mẫu thiết kế trong Rust cho phép bạn thay đổi dữ liệu ngay cả khi có các tham chiếu bất biến đến dữ liệu đó; thông thường, hành động này bị cấm bởi các quy tắc mượn. Để thay đổi dữ liệu, mẫu này sử dụng mã unsafe bên trong cấu trúc dữ liệu để uốn cong các quy tắc thông thường của Rust về việc thay đổi và mượn. Mã không an toàn chỉ ra cho trình biên dịch rằng chúng ta đang kiểm tra các quy tắc thủ công thay vì dựa vào trình biên dịch để kiểm tra chúng cho chúng ta; chúng ta sẽ thảo luận thêm về mã không an toàn trong Chương 20.

Chúng ta chỉ có thể sử dụng các kiểu sử dụng mẫu khả biến nội bộ khi chúng ta có thể đảm bảo rằng các quy tắc mượn sẽ được tuân thủ trong thời gian chạy, mặc dù trình biên dịch không thể đảm bảo điều đó. Mã unsafe liên quan sau đó được bọc trong một API an toàn, và kiểu bên ngoài vẫn bất biến.

Hãy khám phá khái niệm này bằng cách xem xét kiểu RefCell<T> theo mẫu khả biến nội bộ.

Thực Thi Quy Tắc Mượn trong Thời Gian Chạy với RefCell<T>

Không giống như Rc<T>, kiểu RefCell<T> đại diện cho quyền sở hữu duy nhất đối với dữ liệu mà nó chứa. Vậy điều gì làm cho RefCell<T> khác với một kiểu như Box<T>? Hãy nhớ lại các quy tắc mượn mà bạn đã học trong Chương 4:

  • Tại bất kỳ thời điểm nào, bạn có thể có hoặc là một tham chiếu có thể thay đổi hoặc bất kỳ số lượng tham chiếu bất biến nào (nhưng không phải cả hai).
  • Tham chiếu phải luôn hợp lệ.

Với tham chiếu và Box<T>, các bất biến của quy tắc mượn được thực thi tại thời điểm biên dịch. Với RefCell<T>, các bất biến này được thực thi trong thời gian chạy. Với tham chiếu, nếu bạn vi phạm các quy tắc này, bạn sẽ gặp lỗi trình biên dịch. Với RefCell<T>, nếu bạn vi phạm các quy tắc này, chương trình của bạn sẽ hoảng loạn và thoát.

Ưu điểm của việc kiểm tra các quy tắc mượn tại thời điểm biên dịch là các lỗi sẽ được phát hiện sớm hơn trong quá trình phát triển, và không có tác động đến hiệu suất thời gian chạy vì tất cả phân tích đều được hoàn thành trước đó. Vì những lý do đó, kiểm tra các quy tắc mượn tại thời điểm biên dịch là lựa chọn tốt nhất trong đa số trường hợp, đó là lý do tại sao đây là mặc định của Rust.

Ưu điểm của việc kiểm tra các quy tắc mượn trong thời gian chạy thay vì thời gian biên dịch là một số kịch bản an toàn bộ nhớ nhất định được cho phép, trong khi chúng sẽ bị từ chối bởi các kiểm tra thời gian biên dịch. Phân tích tĩnh, như trình biên dịch Rust, về bản chất là bảo thủ. Một số thuộc tính của mã không thể phát hiện được bằng cách phân tích mã: ví dụ nổi tiếng nhất là Bài toán Dừng (Halting Problem), nằm ngoài phạm vi của cuốn sách này nhưng là một chủ đề thú vị để nghiên cứu.

Bởi vì một số phân tích là không thể, nếu trình biên dịch Rust không thể chắc chắn rằng mã tuân thủ các quy tắc sở hữu, nó có thể từ chối một chương trình đúng; theo cách này, nó là bảo thủ. Nếu Rust chấp nhận một chương trình không chính xác, người dùng sẽ không thể tin tưởng vào các đảm bảo mà Rust đưa ra. Tuy nhiên, nếu Rust từ chối một chương trình đúng, lập trình viên sẽ bị bất tiện, nhưng không có gì thảm khốc có thể xảy ra. Kiểu RefCell<T> hữu ích khi bạn chắc chắn rằng mã của bạn tuân theo các quy tắc mượn nhưng trình biên dịch không thể hiểu và đảm bảo điều đó.

Tương tự như Rc<T>, RefCell<T> chỉ để sử dụng trong các tình huống đơn luồng và sẽ đưa ra lỗi thời gian biên dịch nếu bạn cố gắng sử dụng nó trong bối cảnh đa luồng. Chúng ta sẽ nói về cách lấy chức năng của RefCell<T> trong chương trình đa luồng trong Chương 16.

Đây là một tóm tắt về lý do để chọn Box<T>, Rc<T>, hoặc RefCell<T>:

  • Rc<T> cho phép nhiều chủ sở hữu của cùng một dữ liệu; Box<T>RefCell<T> có chủ sở hữu duy nhất.
  • Box<T> cho phép mượn bất biến hoặc có thể thay đổi được kiểm tra tại thời điểm biên dịch; Rc<T> chỉ cho phép mượn bất biến được kiểm tra tại thời điểm biên dịch; RefCell<T> cho phép mượn bất biến hoặc có thể thay đổi được kiểm tra trong thời gian chạy.
  • Bởi vì RefCell<T> cho phép mượn có thể thay đổi được kiểm tra trong thời gian chạy, bạn có thể thay đổi giá trị bên trong RefCell<T> ngay cả khi RefCell<T> là bất biến.

Thay đổi giá trị bên trong một giá trị bất biến là mẫu khả biến nội bộ. Hãy xem xét một tình huống mà khả biến nội bộ hữu ích và xem xét cách nó có thể thực hiện được.

Khả Biến Nội Bộ: Mượn Có Thể Thay Đổi cho một Giá Trị Bất Biến

Một hệ quả của các quy tắc mượn là khi bạn có một giá trị bất biến, bạn không thể mượn nó một cách có thể thay đổi. Ví dụ, đoạn mã này sẽ không biên dịch:

fn main() {
    let x = 5;
    let y = &mut x;
}

Nếu bạn cố gắng biên dịch mã này, bạn sẽ gặp lỗi sau:

$ cargo run
   Compiling borrowing v0.1.0 (file:///projects/borrowing)
error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
 --> src/main.rs:3:13
  |
3 |     let y = &mut x;
  |             ^^^^^^ cannot borrow as mutable
  |
help: consider changing this to be mutable
  |
2 |     let mut x = 5;
  |         +++

For more information about this error, try `rustc --explain E0596`.
error: could not compile `borrowing` (bin "borrowing") due to 1 previous error

Tuy nhiên, có những tình huống mà sẽ rất hữu ích nếu một giá trị có thể tự thay đổi bản thân trong các phương thức của nó nhưng xuất hiện bất biến đối với mã khác. Mã bên ngoài các phương thức của giá trị sẽ không thể thay đổi giá trị. Sử dụng RefCell<T> là một cách để có khả năng khả biến nội bộ, nhưng RefCell<T> không hoàn toàn vượt qua các quy tắc mượn: trình kiểm tra mượn trong trình biên dịch cho phép khả biến nội bộ này, và các quy tắc mượn được kiểm tra trong thời gian chạy thay vì. Nếu bạn vi phạm các quy tắc, bạn sẽ nhận được một panic! thay vì lỗi trình biên dịch.

Hãy làm việc thông qua một ví dụ thực tế mà chúng ta có thể sử dụng RefCell<T> để thay đổi một giá trị bất biến và xem tại sao điều đó lại hữu ích.

Một Trường Hợp Sử Dụng cho Khả Biến Nội Bộ: Đối Tượng Giả Lập

Đôi khi trong quá trình kiểm thử, một lập trình viên sẽ sử dụng một kiểu thay thế cho một kiểu khác, để quan sát hành vi cụ thể và khẳng định rằng nó được triển khai chính xác. Kiểu thay thế này được gọi là test double. Hãy nghĩ về nó theo nghĩa của một diễn viên đóng thế trong làm phim, nơi một người bước vào và thay thế cho một diễn viên để thực hiện một cảnh đặc biệt khó khăn. Test double đứng thay cho các kiểu khác khi chúng ta đang chạy kiểm thử. Đối tượng giả lập (Mock objects) là các loại test double cụ thể ghi lại những gì xảy ra trong quá trình kiểm thử để bạn có thể khẳng định rằng các hành động đúng đã diễn ra.

Rust không có đối tượng theo cùng nghĩa như các ngôn ngữ khác có đối tượng, và Rust không có chức năng đối tượng giả lập được tích hợp vào thư viện chuẩn như một số ngôn ngữ khác. Tuy nhiên, bạn chắc chắn có thể tạo một struct sẽ phục vụ cùng mục đích như một đối tượng giả lập.

Đây là kịch bản chúng ta sẽ kiểm thử: chúng ta sẽ tạo một thư viện theo dõi một giá trị so với một giá trị tối đa và gửi tin nhắn dựa trên mức độ gần với giá trị tối đa của giá trị hiện tại. Thư viện này có thể được sử dụng để theo dõi hạn ngạch của người dùng đối với số lượng cuộc gọi API mà họ được phép thực hiện, ví dụ.

Thư viện của chúng ta sẽ chỉ cung cấp chức năng theo dõi mức độ gần với tối đa của một giá trị và các tin nhắn nên là gì ở thời điểm nào. Các ứng dụng sử dụng thư viện của chúng ta sẽ được kỳ vọng cung cấp cơ chế để gửi tin nhắn: ứng dụng có thể đặt một tin nhắn trong ứng dụng, gửi email, gửi tin nhắn văn bản, hoặc làm việc khác. Thư viện không cần biết chi tiết đó. Tất cả những gì nó cần là một thứ triển khai một trait mà chúng ta sẽ cung cấp gọi là Messenger. Listing 15-20 hiển thị mã thư viện.

pub trait Messenger {
    fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
    T: Messenger,
{
    pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize) {
        self.value = value;

        let percentage_of_max = self.value as f64 / self.max as f64;

        if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quota!");
        } else if percentage_of_max >= 0.9 {
            self.messenger
                .send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 0.75 {
            self.messenger
                .send("Warning: You've used up over 75% of your quota!");
        }
    }
}

Một phần quan trọng của mã này là trait Messenger có một phương thức gọi là send nhận một tham chiếu bất biến đến self và văn bản của tin nhắn. Trait này là giao diện mà đối tượng giả lập của chúng ta cần triển khai để có thể được sử dụng theo cùng cách như một đối tượng thực. Phần quan trọng khác là chúng ta muốn kiểm thử hành vi của phương thức set_value trên LimitTracker. Chúng ta có thể thay đổi những gì chúng ta truyền vào cho tham số value, nhưng set_value không trả về bất cứ thứ gì để chúng ta đưa ra khẳng định. Chúng ta muốn có thể nói rằng nếu chúng ta tạo một LimitTracker với thứ gì đó triển khai trait Messenger và một giá trị cụ thể cho max, khi chúng ta truyền các số khác nhau cho value, messenger được yêu cầu gửi các tin nhắn thích hợp.

Chúng ta cần một đối tượng giả lập mà, thay vì gửi email hoặc tin nhắn văn bản khi chúng ta gọi send, sẽ chỉ theo dõi các tin nhắn mà nó được yêu cầu gửi. Chúng ta có thể tạo một thể hiện mới của đối tượng giả lập, tạo một LimitTracker sử dụng đối tượng giả lập, gọi phương thức set_value trên LimitTracker, và sau đó kiểm tra xem đối tượng giả lập có các tin nhắn mà chúng ta mong đợi không. Listing 15-21 hiển thị một nỗ lực triển khai một đối tượng giả lập để làm điều đó, nhưng trình kiểm tra mượn sẽ không cho phép.

pub trait Messenger {
    fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
    T: Messenger,
{
    pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize) {
        self.value = value;

        let percentage_of_max = self.value as f64 / self.max as f64;

        if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quota!");
        } else if percentage_of_max >= 0.9 {
            self.messenger
                .send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 0.75 {
            self.messenger
                .send("Warning: You've used up over 75% of your quota!");
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    struct MockMessenger {
        sent_messages: Vec<String>,
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger {
                sent_messages: vec![],
            }
        }
    }

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            self.sent_messages.push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        let mock_messenger = MockMessenger::new();
        let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

        limit_tracker.set_value(80);

        assert_eq!(mock_messenger.sent_messages.len(), 1);
    }
}

Mã kiểm thử này định nghĩa một struct MockMessenger có trường sent_messages với một Vec của các giá trị String để theo dõi các tin nhắn mà nó được yêu cầu gửi. Chúng ta cũng định nghĩa một hàm liên kết new để thuận tiện cho việc tạo các giá trị MockMessenger mới bắt đầu với một danh sách tin nhắn trống. Sau đó, chúng ta triển khai trait Messenger cho MockMessenger để chúng ta có thể cung cấp một MockMessenger cho một LimitTracker. Trong định nghĩa của phương thức send, chúng ta lấy tin nhắn được truyền vào dưới dạng một tham số và lưu trữ nó trong danh sách sent_messages của MockMessenger.

Trong kiểm thử, chúng ta đang kiểm tra những gì xảy ra khi LimitTracker được yêu cầu đặt value thành một cái gì đó lớn hơn 75 phần trăm của giá trị max. Đầu tiên, chúng ta tạo một MockMessenger mới, sẽ bắt đầu với một danh sách tin nhắn trống. Sau đó, chúng ta tạo một LimitTracker mới và cung cấp cho nó một tham chiếu đến MockMessenger mới và một giá trị max100. Chúng ta gọi phương thức set_value trên LimitTracker với một giá trị 80, lớn hơn 75 phần trăm của 100. Sau đó, chúng ta khẳng định rằng danh sách tin nhắn mà MockMessenger đang theo dõi bây giờ nên có một tin nhắn trong đó.

Tuy nhiên, có một vấn đề với kiểm thử này, như được hiển thị ở đây:

$ cargo test
   Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
  --> src/lib.rs:58:13
   |
58 |             self.sent_messages.push(String::from(message));
   |             ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable
   |
help: consider changing this to be a mutable reference in the `impl` method and the `trait` definition
   |
2  ~     fn send(&mut self, msg: &str);
3  | }
...
56 |     impl Messenger for MockMessenger {
57 ~         fn send(&mut self, message: &str) {
   |

For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker` (lib test) due to 1 previous error

Chúng ta không thể sửa đổi MockMessenger để theo dõi các tin nhắn, vì phương thức send nhận một tham chiếu bất biến đến self. Chúng ta cũng không thể làm theo gợi ý từ văn bản lỗi để sử dụng &mut self trong cả phương thức impl và định nghĩa trait. Chúng ta không muốn thay đổi trait Messenger chỉ vì mục đích kiểm thử. Thay vào đó, chúng ta cần tìm một cách để làm cho mã kiểm thử của chúng ta hoạt động chính xác với thiết kế hiện tại của chúng ta.

Đây là một tình huống mà khả biến nội bộ có thể giúp đỡ! Chúng ta sẽ lưu trữ sent_messages trong một RefCell<T>, và sau đó phương thức send sẽ có thể sửa đổi sent_messages để lưu trữ các tin nhắn chúng ta đã thấy. Listing 15-22 hiển thị nó trông như thế nào.

pub trait Messenger {
    fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
    T: Messenger,
{
    pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize) {
        self.value = value;

        let percentage_of_max = self.value as f64 / self.max as f64;

        if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quota!");
        } else if percentage_of_max >= 0.9 {
            self.messenger
                .send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 0.75 {
            self.messenger
                .send("Warning: You've used up over 75% of your quota!");
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::RefCell;

    struct MockMessenger {
        sent_messages: RefCell<Vec<String>>,
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger {
                sent_messages: RefCell::new(vec![]),
            }
        }
    }

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            self.sent_messages.borrow_mut().push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        // --snip--
        let mock_messenger = MockMessenger::new();
        let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

        limit_tracker.set_value(80);

        assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
    }
}

Trường sent_messages bây giờ có kiểu RefCell<Vec<String>> thay vì Vec<String>. Trong hàm new, chúng ta tạo một thể hiện RefCell<Vec<String>> mới xung quanh vectơ trống.

Đối với việc triển khai phương thức send, tham số đầu tiên vẫn là một mượn bất biến của self, phù hợp với định nghĩa trait. Chúng ta gọi borrow_mut trên RefCell<Vec<String>> trong self.sent_messages để có được một tham chiếu có thể thay đổi đến giá trị bên trong RefCell<Vec<String>>, đó là vectơ. Sau đó, chúng ta có thể gọi push trên tham chiếu có thể thay đổi đến vectơ để theo dõi các tin nhắn được gửi trong quá trình kiểm thử.

Thay đổi cuối cùng chúng ta phải thực hiện là trong khẳng định: để xem có bao nhiêu mục trong vectơ bên trong, chúng ta gọi borrow trên RefCell<Vec<String>> để có được một tham chiếu bất biến đến vectơ.

Bây giờ bạn đã thấy cách sử dụng RefCell<T>, hãy đi sâu vào cách nó hoạt động!

Theo dõi các Mượn tại Thời gian Chạy với RefCell<T>

Khi tạo tham chiếu bất biến và có thể thay đổi, chúng ta sử dụng cú pháp &&mut, tương ứng. Với RefCell<T>, chúng ta sử dụng các phương thức borrowborrow_mut, là một phần của API an toàn thuộc về RefCell<T>. Phương thức borrow trả về kiểu con trỏ thông minh Ref<T>, và borrow_mut trả về kiểu con trỏ thông minh RefMut<T>. Cả hai kiểu đều triển khai Deref, vì vậy chúng ta có thể coi chúng như các tham chiếu thông thường.

RefCell<T> theo dõi có bao nhiêu con trỏ thông minh Ref<T>RefMut<T> đang hoạt động. Mỗi lần chúng ta gọi borrow, RefCell<T> tăng số lượng mượn bất biến đang hoạt động. Khi giá trị Ref<T> ra khỏi phạm vi, số lượng mượn bất biến giảm xuống 1. Giống như các quy tắc mượn thời gian biên dịch, RefCell<T> cho phép chúng ta có nhiều mượn bất biến hoặc một mượn có thể thay đổi tại bất kỳ thời điểm nào.

Nếu chúng ta cố gắng vi phạm các quy tắc này, thay vì nhận được lỗi trình biên dịch như chúng ta sẽ làm với tham chiếu, việc triển khai RefCell<T> sẽ hoảng loạn trong thời gian chạy. Listing 15-23 hiển thị một sửa đổi của việc triển khai send trong Listing 15-22. Chúng ta cố tình cố gắng tạo hai mượn có thể thay đổi hoạt động cho cùng một phạm vi để minh họa rằng RefCell<T> ngăn chúng ta làm điều này trong thời gian chạy.

pub trait Messenger {
    fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where
    T: Messenger,
{
    pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize) {
        self.value = value;

        let percentage_of_max = self.value as f64 / self.max as f64;

        if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quota!");
        } else if percentage_of_max >= 0.9 {
            self.messenger
                .send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 0.75 {
            self.messenger
                .send("Warning: You've used up over 75% of your quota!");
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::RefCell;

    struct MockMessenger {
        sent_messages: RefCell<Vec<String>>,
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger {
                sent_messages: RefCell::new(vec![]),
            }
        }
    }

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            let mut one_borrow = self.sent_messages.borrow_mut();
            let mut two_borrow = self.sent_messages.borrow_mut();

            one_borrow.push(String::from(message));
            two_borrow.push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        let mock_messenger = MockMessenger::new();
        let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

        limit_tracker.set_value(80);

        assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
    }
}

Chúng ta tạo một biến one_borrow cho con trỏ thông minh RefMut<T> được trả về từ borrow_mut. Sau đó, chúng ta tạo một mượn có thể thay đổi khác theo cùng cách trong biến two_borrow. Điều này tạo ra hai tham chiếu có thể thay đổi trong cùng một phạm vi, không được phép. Khi chúng ta chạy các kiểm thử cho thư viện của mình, mã trong Listing 15-23 sẽ biên dịch mà không có bất kỳ lỗi nào, nhưng kiểm thử sẽ thất bại:

$ cargo test
   Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
     Running unittests src/lib.rs (target/debug/deps/limit_tracker-e599811fa246dbde)

running 1 test
test tests::it_sends_an_over_75_percent_warning_message ... FAILED

failures:

---- tests::it_sends_an_over_75_percent_warning_message stdout ----

thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at src/lib.rs:60:53:
already borrowed: BorrowMutError
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::it_sends_an_over_75_percent_warning_message

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

Lưu ý rằng mã hoảng loạn với thông báo already borrowed: BorrowMutError. Đây là cách RefCell<T> xử lý vi phạm các quy tắc mượn trong thời gian chạy.

Việc chọn để bắt các lỗi mượn trong thời gian chạy thay vì thời gian biên dịch, như chúng ta đã làm ở đây, có nghĩa là bạn có khả năng tìm thấy lỗi trong mã của mình sau này trong quá trình phát triển: có thể không phải cho đến khi mã của bạn được triển khai lên môi trường sản xuất. Ngoài ra, mã của bạn sẽ phải chịu một hình phạt hiệu suất thời gian chạy nhỏ do phải theo dõi các mượn trong thời gian chạy thay vì thời gian biên dịch. Tuy nhiên, việc sử dụng RefCell<T> làm cho việc viết một đối tượng giả lập có thể tự sửa đổi để theo dõi các tin nhắn mà nó đã thấy khi bạn đang sử dụng nó trong một bối cảnh chỉ cho phép các giá trị bất biến trở nên khả thi. Bạn có thể sử dụng RefCell<T> bất chấp sự đánh đổi của nó để có được nhiều chức năng hơn so với các tham chiếu thông thường cung cấp.

Cho Phép Nhiều Chủ Sở Hữu của Dữ Liệu Có Thể Thay Đổi với Rc<T>RefCell<T>

Một cách phổ biến để sử dụng RefCell<T> là kết hợp với Rc<T>. Hãy nhớ rằng Rc<T> cho phép bạn có nhiều chủ sở hữu của một số dữ liệu, nhưng nó chỉ cung cấp quyền truy cập bất biến vào dữ liệu đó. Nếu bạn có một Rc<T> chứa một RefCell<T>, bạn có thể có được một giá trị có thể có nhiều chủ sở hữu mà bạn có thể thay đổi!

Ví dụ, hãy nhớ lại ví dụ danh sách cons trong Listing 15-18 nơi chúng ta sử dụng Rc<T> để cho phép nhiều danh sách chia sẻ quyền sở hữu của một danh sách khác. Bởi vì Rc<T> chỉ chứa các giá trị bất biến, chúng ta không thể thay đổi bất kỳ giá trị nào trong danh sách một khi chúng ta đã tạo chúng. Hãy thêm vào RefCell<T> để có khả năng thay đổi các giá trị trong danh sách. Listing 15-24 hiển thị rằng bằng cách sử dụng RefCell<T> trong định nghĩa Cons, chúng ta có thể sửa đổi giá trị được lưu trữ trong tất cả các danh sách.

#[derive(Debug)]
enum List {
    Cons(Rc<RefCell<i32>>, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let value = Rc::new(RefCell::new(5));

    let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));

    let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));
    let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));

    *value.borrow_mut() += 10;

    println!("a after = {a:?}");
    println!("b after = {b:?}");
    println!("c after = {c:?}");
}

Chúng ta tạo một giá trị là một thể hiện của Rc<RefCell<i32>> và lưu trữ nó trong một biến có tên value để chúng ta có thể truy cập nó trực tiếp sau này. Sau đó, chúng ta tạo một List trong a với một biến thể Cons chứa value. Chúng ta cần sao chép value để cả avalue đều có quyền sở hữu của giá trị bên trong 5 thay vì chuyển quyền sở hữu từ value sang a hoặc có a mượn từ value.

Chúng ta bọc danh sách a trong một Rc<T> để khi chúng ta tạo danh sách bc, cả hai đều có thể tham chiếu đến a, đó là những gì chúng ta đã làm trong Listing 15-18.

Sau khi chúng ta đã tạo danh sách trong a, b, và c, chúng ta muốn thêm 10 vào giá trị trong value. Chúng ta làm điều này bằng cách gọi borrow_mut trên value, sử dụng tính năng tự động tham chiếu mà chúng ta đã thảo luận trong "Where's the -> Operator?") trong Chương 5 để tham chiếu đến Rc<T> đến giá trị RefCell<T> bên trong. Phương thức borrow_mut trả về một con trỏ thông minh RefMut<T>, và chúng ta sử dụng toán tử tham chiếu trên nó và thay đổi giá trị bên trong.

Khi chúng ta in a, b, và c, chúng ta có thể thấy rằng tất cả chúng đều có giá trị sửa đổi là 15 thay vì 5:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.63s
     Running `target/debug/cons-list`
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))

Kỹ thuật này khá gọn gàng! Bằng cách sử dụng RefCell<T>, chúng ta có một giá trị List bất biến bên ngoài. Nhưng chúng ta có thể sử dụng các phương thức trên RefCell<T> cung cấp quyền truy cập vào khả biến nội bộ của nó để chúng ta có thể sửa đổi dữ liệu của mình khi chúng ta cần. Các kiểm tra thời gian chạy của các quy tắc mượn bảo vệ chúng ta khỏi các cuộc đua dữ liệu, và đôi khi đáng để đánh đổi một chút tốc độ để có được sự linh hoạt này trong cấu trúc dữ liệu của chúng ta. Lưu ý rằng RefCell<T> không hoạt động cho mã đa luồng! Mutex<T> là phiên bản an toàn cho luồng của RefCell<T>, và chúng ta sẽ thảo luận về Mutex<T> trong Chương 16.