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> และ Pattern Interior Mutability

Interior mutability เป็น design pattern ใน Rust ที่อนุญาตให้คุณ mutate ข้อมูลแม้เมื่อมี immutable reference ของข้อมูลนั้น — โดยปกติ การกระทำนี้ไม่ได้รับอนุญาตโดยกฎ borrowing เพื่อ mutate ข้อมูล pattern ใช้โค้ด unsafe ภายในโครงสร้างข้อมูลเพื่อ bend กฎปกติของ Rust ที่ ควบคุม mutation และ borrowing โค้ด unsafe ระบุให้ compiler ว่าเรา กำลังตรวจสอบกฎด้วยมือแทนการพึ่ง compiler ในการตรวจให้เรา — เราจะพูด ถึงโค้ด unsafe เพิ่มในบทที่ 20

เราใช้ type ที่ใช้ pattern interior mutability ได้เฉพาะเมื่อเรา รับประกันได้ว่ากฎ borrowing จะถูกปฏิบัติตามที่ runtime แม้ว่า compiler รับประกันสิ่งนั้นไม่ได้ โค้ด unsafe ที่เกี่ยวข้องถูก wrap ใน API ที่ปลอดภัย และ type ภายนอกยังเป็น immutable

มาสำรวจแนวคิดนี้โดยดู type RefCell<T> ที่ตามหา pattern interior mutability

บังคับใช้กฎ Borrowing ที่ Runtime

ต่างจาก Rc<T> type RefCell<T> แทน single ownership เหนือข้อมูล ที่มันเก็บ ดังนั้น อะไรทำให้ RefCell<T> ต่างจาก type เช่น Box<T>? จำกฎ borrowing ที่คุณเรียนในบทที่ 4:

  • ในเวลาใดเวลาหนึ่ง คุณมีได้ ไม่ก็ หนึ่ง mutable reference หรือ จำนวนเท่าไรก็ได้ของ immutable reference (แต่ไม่ทั้งสอง)
  • Reference ต้อง valid เสมอ

ด้วย reference และ Box<T> invariant ของกฎ borrowing ถูกบังคับใช้ ที่ compile time ด้วย RefCell<T> invariant เหล่านี้ถูกบังคับใช้ ที่ runtime ด้วย reference ถ้าคุณทำผิดกฎเหล่านี้ คุณจะได้ error compiler ด้วย RefCell<T> ถ้าคุณทำผิดกฎเหล่านี้ โปรแกรมของคุณจะ panic และออก

ข้อดีของการตรวจสอบกฎ borrowing ที่ compile time คือ error จะถูกจับ เร็วขึ้นในกระบวนการพัฒนา และไม่มีผลต่อ performance ที่ runtime เพราะการวิเคราะห์ทั้งหมดเสร็จล่วงหน้า ด้วยเหตุผลเหล่านั้น การตรวจสอบ กฎ borrowing ที่ compile time เป็นทางเลือกที่ดีที่สุดในกรณีส่วนใหญ่ ซึ่งเป็นเหตุผลที่นี่เป็น default ของ Rust

ข้อดีของการตรวจสอบกฎ borrowing ที่ runtime แทนคือ scenario ที่ memory-safe บางอย่างถูกอนุญาตที่ที่จะไม่ถูกอนุญาตโดยการตรวจสอบ compile-time การวิเคราะห์ static เช่น Rust compiler โดยธรรมชาติเป็น conservative คุณสมบัติบางอย่างของโค้ดเป็นไปไม่ได้ที่จะตรวจจับโดย การวิเคราะห์โค้ด — ตัวอย่างที่มีชื่อเสียงที่สุดคือ Halting Problem ซึ่งอยู่นอก scope ของหนังสือเล่มนี้แต่เป็นหัวข้อที่น่าสนใจที่ค้นคว้า

เพราะการวิเคราะห์บางอย่างเป็นไปไม่ได้ ถ้า Rust compiler แน่ใจไม่ได้ ว่าโค้ดเป็นไปตามกฎ ownership มันอาจปฏิเสธโปรแกรมที่ถูกต้อง — ในวิธี นี้ มันเป็น conservative ถ้า Rust ยอมรับโปรแกรมที่ไม่ถูกต้อง user จะไม่สามารถเชื่อการรับประกันที่ Rust ทำ อย่างไรก็ตาม ถ้า Rust ปฏิเสธ โปรแกรมที่ถูกต้อง programmer จะไม่สะดวก แต่ไม่มีอะไร catastrophic เกิด type RefCell<T> มีประโยชน์เมื่อคุณแน่ใจว่าโค้ดของคุณตามกฎ borrowing แต่ compiler ไม่สามารถเข้าใจและรับประกันสิ่งนั้น

คล้ายกับ Rc<T> RefCell<T> ใช้เฉพาะใน scenario เธรดเดียวและจะให้ error ที่ compile time ถ้าคุณพยายามใช้มันใน context multithreaded เราจะพูดถึงวิธีรับ functionality ของ RefCell<T> ในโปรแกรม multithreaded ในบทที่ 16

นี่คือสรุปเหตุผลที่จะเลือก Box<T>, Rc<T> หรือ RefCell<T>:

  • Rc<T> เปิดใช้ owner หลายตัวของข้อมูลเดียวกัน — Box<T> และ RefCell<T> มี owner เดียว
  • Box<T> อนุญาต immutable หรือ mutable borrow ที่ตรวจสอบที่ compile time — Rc<T> อนุญาตเฉพาะ immutable borrow ที่ตรวจสอบที่ compile time — RefCell<T> อนุญาต immutable หรือ mutable borrow ที่ตรวจสอบที่ runtime
  • เพราะ RefCell<T> อนุญาต mutable borrow ที่ตรวจสอบที่ runtime คุณ mutate ค่าภายใน RefCell<T> แม้เมื่อ RefCell<T> เป็น immutable ได้

การ mutate ค่าภายในค่า immutable คือ pattern interior mutability มาดูสถานการณ์ที่ interior mutability มีประโยชน์และตรวจสอบว่ามันเป็น ไปได้ยังไง

ใช้ Interior Mutability

ผลของกฎ borrowing คือเมื่อคุณมีค่า immutable คุณ borrow มันแบบ mutable ไม่ได้ ตัวอย่างเช่น โค้ดนี้จะไม่คอมไพล์:

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

ถ้าคุณพยายามคอมไพล์โค้ดนี้ คุณจะได้ error ต่อไปนี้:

$ 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

อย่างไรก็ตาม มีสถานการณ์ที่จะมีประโยชน์ที่ค่า mutate ตัวเองในเมธอด ของมันแต่ปรากฏเป็น immutable ต่อโค้ดอื่น โค้ดภายนอกเมธอดของค่าจะ ไม่สามารถ mutate ค่าได้ การใช้ RefCell<T> เป็นวิธีหนึ่งที่จะรับ ความสามารถที่จะมี interior mutability แต่ RefCell<T> ไม่ได้หลบ กฎ borrowing สมบูรณ์ — borrow checker ใน compiler อนุญาต interior mutability นี้ และกฎ borrowing ถูกตรวจสอบที่ runtime แทน ถ้าคุณ ละเมิดกฎ คุณจะได้ panic! แทน error compiler

มาทำงานผ่านตัวอย่างที่ปฏิบัติได้ที่เราใช้ RefCell<T> เพื่อ mutate ค่า immutable และดูทำไมนั่นมีประโยชน์

ทดสอบด้วย Mock Object

บางครั้งระหว่างการทดสอบ programmer จะใช้ type ในที่ของอีก type เพื่อ สังเกตพฤติกรรมเฉพาะและ assert ว่ามันถูก implement ถูกต้อง type placeholder นี้เรียก test double คิดถึงมันในความหมายของ stunt double ในการทำหนัง ที่บุคคลก้าวเข้าและทดแทนนักแสดงเพื่อทำฉากที่ ยุ่งยากเฉพาะ Test double ยืนแทน type อื่นเมื่อเรารันเทส Mock object เป็นประเภทเฉพาะของ test double ที่บันทึกสิ่งที่เกิดระหว่าง เทสเพื่อให้คุณ assert ว่าการกระทำที่ถูกต้องเกิดได้

Rust ไม่มี object ในความหมายเดียวกับที่ภาษาอื่นมี object และ Rust ไม่มี functionality mock object built-in ใน standard library เหมือนที่บางภาษาอื่นมี อย่างไรก็ตาม คุณสร้าง struct ที่จะรับใช้ จุดประสงค์เดียวกับ mock object ได้แน่นอน

นี่คือ scenario ที่เราจะทดสอบ — เราจะสร้าง library ที่ตามค่าเทียบ กับค่าสูงสุดและส่งข้อความตามว่าค่าปัจจุบันใกล้ค่าสูงสุดแค่ไหน library นี้ใช้ตาม quota ของ user สำหรับจำนวนการเรียก API ที่พวกเขา ได้รับอนุญาตให้ทำได้ ตัวอย่างเช่น

library ของเราจะให้เฉพาะ functionality ของการตามว่าค่าใกล้สูงสุด แค่ไหนและข้อความควรเป็นอะไรที่เวลาไหน Application ที่ใช้ library ของเราจะถูกคาดหวังให้ให้กลไกในการส่งข้อความ — application แสดง ข้อความให้ user โดยตรง ส่ง email ส่งข้อความ text หรือทำอย่างอื่น ได้ library ไม่ต้องรู้รายละเอียดนั้น ทั้งหมดที่มันต้องการคืออะไร ที่ implement trait ที่เราจะให้ที่เรียก Messenger Listing 15-20 แสดงโค้ด library

Filename: src/lib.rs
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!");
        }
    }
}
Listing 15-20: Library เพื่อตามว่าค่าใกล้ค่าสูงสุดแค่ไหนและเตือนเมื่อค่าอยู่ที่ระดับเฉพาะ

ส่วนสำคัญหนึ่งของโค้ดนี้คือ trait Messenger มีหนึ่งเมธอดที่เรียก send ที่รับ immutable reference ของ self และ text ของข้อความ trait นี้คือ interface ที่ mock object ของเราต้อง implement เพื่อให้ mock ถูกใช้ในแบบเดียวกับที่ object จริงถูกใช้ ส่วนสำคัญอื่นคือเรา ต้องการทดสอบพฤติกรรมของเมธอด set_value บน LimitTracker เรา เปลี่ยนสิ่งที่เราส่งเข้าสำหรับ parameter value ได้ แต่ set_value ไม่ return อะไรให้เราทำ assertion บน เราต้องการที่จะพูดว่าถ้าเรา สร้าง LimitTracker ด้วยอะไรที่ implement trait Messenger และค่า เฉพาะสำหรับ max messenger ถูกบอกให้ส่งข้อความที่เหมาะสมเมื่อเรา ส่งตัวเลขต่างกันสำหรับ value

เราต้องการ mock object ที่แทนการส่ง email หรือข้อความ text เมื่อเรา เรียก send จะเพียงตามข้อความที่มันถูกบอกให้ส่ง เราสร้าง instance ใหม่ของ mock object สร้าง LimitTracker ที่ใช้ mock object เรียก เมธอด set_value บน LimitTracker แล้วตรวจสอบว่า mock object มี ข้อความที่เราคาดหวังได้ Listing 15-21 แสดงการพยายาม implement mock object เพื่อทำสิ่งนั้น แต่ borrow checker จะไม่อนุญาต

Filename: src/lib.rs
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);
    }
}
Listing 15-21: การพยายาม implement MockMessenger ที่ borrow checker ไม่อนุญาต

โค้ดเทสนี้นิยาม struct MockMessenger ที่มี field sent_messages กับ Vec ของค่า String เพื่อตามข้อความที่มันถูกบอกให้ส่ง เรายัง นิยาม associated function new เพื่อทำให้สะดวกในการสร้างค่า MockMessenger ใหม่ที่เริ่มด้วย list ข้อความว่าง เราจากนั้น implement trait Messenger สำหรับ MockMessenger เพื่อให้เราให้ MockMessenger ให้ LimitTracker ได้ ในนิยามของเมธอด send เรา รับข้อความที่ส่งเข้าเป็น parameter และเก็บมันใน list sent_messages ของ MockMessenger

ในเทส เรากำลังทดสอบสิ่งที่เกิดเมื่อ LimitTracker ถูกบอกให้ตั้ง value เป็นอะไรที่มากกว่า 75 เปอร์เซ็นต์ของค่า max ก่อนอื่น เรา สร้าง MockMessenger ใหม่ซึ่งจะเริ่มด้วย list ข้อความว่าง จากนั้น เราสร้าง LimitTracker ใหม่และให้มัน reference ของ MockMessenger ใหม่และค่า max ของ 100 เราเรียกเมธอด set_value บน LimitTracker ด้วยค่า 80 ซึ่งมากกว่า 75 เปอร์เซ็นต์ของ 100 จาก นั้น เรา assert ว่า list ของข้อความที่ MockMessenger กำลังตามควร มีหนึ่งข้อความในนั้นตอนนี้

อย่างไรก็ตาม มีปัญหาหนึ่งกับเทสนี้ ดังที่แสดงที่นี่:

$ 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

เราแก้ MockMessenger ให้ตามข้อความไม่ได้ เพราะเมธอด send รับ immutable reference ของ self เรายังรับข้อเสนอแนะจาก text error ที่จะใช้ &mut self ในทั้งเมธอด impl และนิยาม trait ไม่ได้ เรา ไม่ต้องการเปลี่ยน trait Messenger เพียงเพื่อการทดสอบ แทน เราต้อง หาวิธีทำให้โค้ดเทสของเราทำงานถูกต้องกับการออกแบบที่มีอยู่ของเรา

นี่คือสถานการณ์ที่ interior mutability ช่วยได้! เราจะเก็บ sent_messages ภายใน RefCell<T> และจากนั้นเมธอด send จะ สามารถแก้ sent_messages เพื่อเก็บข้อความที่เราเห็น Listing 15-22 แสดงว่าดูเป็นยังไง

Filename: src/lib.rs
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);
    }
}
Listing 15-22: ใช้ RefCell<T> เพื่อ mutate ค่าภายในในขณะที่ค่าภายนอกถือว่า immutable

field sent_messages ตอนนี้เป็น type RefCell<Vec<String>> แทน Vec<String> ในฟังก์ชัน new เราสร้าง instance RefCell<Vec<String>> ใหม่รอบ vector ว่าง

สำหรับ implementation ของเมธอด send parameter แรกยังเป็น immutable borrow ของ self ซึ่งตรงกับนิยาม trait เราเรียก borrow_mut บน RefCell<Vec<String>> ใน self.sent_messages เพื่อ รับ mutable reference ของค่าภายใน RefCell<Vec<String>> ซึ่งเป็น vector จากนั้น เราเรียก push บน mutable reference ของ vector เพื่อตามข้อความที่ส่งระหว่างเทสได้

การเปลี่ยนแปลงสุดท้ายที่เราต้องทำคือใน assertion — เพื่อเห็นว่ามี item เท่าไรใน vector ภายใน เราเรียก borrow บน RefCell<Vec<String>> เพื่อรับ immutable reference ของ vector

ตอนนี้คุณเห็นวิธีใช้ RefCell<T> แล้ว มาขุดลงไปว่ามันทำงานยังไง!

ตาม Borrow ที่ Runtime

เมื่อสร้าง immutable และ mutable reference เราใช้ syntax & และ &mut ตามลำดับ ด้วย RefCell<T> เราใช้เมธอด borrow และ borrow_mut ซึ่งเป็นส่วนของ API ที่ปลอดภัยที่เป็นของ RefCell<T> เมธอด borrow return type smart pointer Ref<T> และ borrow_mut return type smart pointer RefMut<T> ทั้งสอง type implement Deref ดังนั้นเราปฏิบัติกับพวกมันเหมือน reference ปกติได้

RefCell<T> ตามว่ามี smart pointer Ref<T> และ RefMut<T> เท่าไร ที่ active ปัจจุบัน ทุกครั้งที่เราเรียก borrow RefCell<T> เพิ่ม count ของว่ามี immutable borrow เท่าไรที่ active เมื่อค่า Ref<T> ออกจาก scope count ของ immutable borrow ลง 1 เช่นเดียวกับกฎ borrowing ที่ compile-time RefCell<T> ให้เรามี immutable borrow หลายตัวหรือหนึ่ง mutable borrow ที่จุดใดในเวลา

ถ้าเราพยายามละเมิดกฎเหล่านี้ แทนที่จะได้ error compiler ที่เราจะได้ กับ reference implementation ของ RefCell<T> จะ panic ที่ runtime Listing 15-23 แสดงการแก้ของ implementation send ใน Listing 15-22 เรากำลังจงใจพยายามสร้างสอง mutable borrow ที่ active สำหรับ scope เดียวกันเพื่อแสดงว่า RefCell<T> ป้องกันเราจากการทำสิ่งนี้ ที่ runtime

Filename: src/lib.rs
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);
    }
}
Listing 15-23: สร้างสอง mutable reference ใน scope เดียวกันเพื่อเห็นว่า RefCell<T> จะ panic

เราสร้างตัวแปร one_borrow สำหรับ smart pointer RefMut<T> ที่ return จาก borrow_mut จากนั้น เราสร้าง mutable borrow อื่นในแบบ เดียวกันในตัวแปร two_borrow นี่ทำสอง mutable reference ใน scope เดียวกัน ซึ่งไม่ได้รับอนุญาต เมื่อเรารันเทสสำหรับ library ของเรา โค้ดใน Listing 15-23 จะคอมไพล์โดยไม่มี error แต่เทสจะ fail:

$ 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:
RefCell already borrowed
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`

สังเกตว่าโค้ด panic ด้วยข้อความ already borrowed: BorrowMutError นี่คือวิธีที่ RefCell<T> จัดการการละเมิดกฎ borrowing ที่ runtime

การเลือกจับ error borrowing ที่ runtime ไม่ใช่ที่ compile time ดัง ที่เราทำที่นี่ หมายความว่าคุณอาจหาความผิดพลาดในโค้ดของคุณภายหลังใน กระบวนการพัฒนา — อาจไม่จนกว่าโค้ดของคุณถูก deploy ไปยัง production ด้วย โค้ดของคุณจะรับ performance runtime ปรับเล็กเป็นผลจากการตาม borrow ที่ runtime ไม่ใช่ที่ compile time อย่างไรก็ตาม การใช้ RefCell<T> ทำให้เป็นไปได้ที่จะเขียน mock object ที่แก้ตัวเองเพื่อ ตามข้อความที่มันเห็นในขณะที่คุณใช้มันใน context ที่อนุญาตเฉพาะค่า immutable คุณใช้ RefCell<T> แม้ trade-off ของมันเพื่อรับ functionality มากขึ้นกว่าที่ reference ปกติให้

อนุญาตให้มี Owner หลายตัวของ Mutable Data

วิธีทั่วไปในการใช้ RefCell<T> คือร่วมกับ Rc<T> จำได้ว่า Rc<T> ให้คุณมี owner หลายตัวของข้อมูลบางตัว แต่มันให้เฉพาะการเข้าถึง immutable ของข้อมูลนั้น ถ้าคุณมี Rc<T> ที่เก็บ RefCell<T> คุณ รับค่าที่มี owner หลายตัว_และ_ ที่คุณ mutate ได้!

ตัวอย่างเช่น จำได้ตัวอย่าง cons list ใน Listing 15-18 ที่เราใช้ Rc<T> เพื่ออนุญาตให้หลาย list แชร์ ownership ของอีก list เพราะ Rc<T> เก็บเฉพาะค่า immutable เราเปลี่ยนค่าใน list ไม่ได้เมื่อ เราสร้างพวกมันแล้ว มาเพิ่ม RefCell<T> สำหรับความสามารถที่จะเปลี่ยน ค่าใน list Listing 15-24 แสดงว่าโดยใช้ RefCell<T> ในนิยาม Cons เราแก้ค่าที่เก็บใน list ทั้งหมดได้

Filename: src/main.rs
#[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:?}");
}
Listing 15-24: ใช้ Rc<RefCell<i32>> เพื่อสร้าง List ที่เรา mutate ได้

เราสร้างค่าที่เป็น instance ของ Rc<RefCell<i32>> และเก็บมันใน ตัวแปรชื่อ value เพื่อให้เราเข้าถึงมันโดยตรงภายหลัง จากนั้น เรา สร้าง List ใน a กับ variant Cons ที่เก็บ value เราต้อง clone value เพื่อให้ทั้ง a และ value มี ownership ของค่า 5 ภายในไม่ใช่โอน ownership จาก value ให้ a หรือให้ a borrow จาก value

เรา wrap list a ใน Rc<T> เพื่อให้เมื่อเราสร้าง list b และ c พวกมันทั้งคู่อ้างถึง a ได้ ซึ่งคือสิ่งที่เราทำใน Listing 15-18

หลังจากที่เราสร้าง list ใน a, b และ c แล้ว เราต้องการเพิ่ม 10 ให้ค่าใน value เราทำสิ่งนี้โดยเรียก borrow_mut บน value ซึ่งใช้ฟีเจอร์ dereferencing อัตโนมัติที่เราพูดถึงใน “Operator -> อยู่ที่ไหน?” ในบทที่ 5 เพื่อ dereference Rc<T> ไปยังค่า RefCell<T> ภายใน เมธอด borrow_mut return smart pointer RefMut<T> และเราใช้ dereference operator บนมันและเปลี่ยนค่าภายใน

เมื่อเรา print a, b และ c เราเห็นว่าพวกมันทั้งหมดมีค่าที่ แก้ของ 15 ไม่ใช่ 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))

เทคนิคนี้เนียนสุด! โดยใช้ RefCell<T> เรามีค่า List ที่ปรากฏ ภายนอกเป็น immutable แต่เราใช้เมธอดบน RefCell<T> ที่ให้สิทธิ์ เข้าถึง interior mutability ของมันเพื่อให้เราแก้ข้อมูลของเราเมื่อ เราต้องการได้ การตรวจสอบ runtime ของกฎ borrowing ปกป้องเราจาก data race และบางครั้งคุ้มค่าที่จะ trade ความเร็วบ้างเพื่อความยืดหยุ่นนี้ ในโครงสร้างข้อมูลของเรา สังเกตว่า RefCell<T> ไม่ทำงานสำหรับโค้ด multithreaded! Mutex<T> เป็นเวอร์ชัน thread-safe ของ RefCell<T> และเราจะพูดถึง Mutex<T> ในบทที่ 16