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> และรูปแบบการแก้ไขภายใน (Interior Mutability Pattern)

การแก้ไขภายใน (Interior mutability) คือรูปแบบการออกแบบ (design pattern) ใน Rust ที่ช่วยให้คุณสามารถแก้ไขข้อมูลได้ แม้ว่าจะมี การอ้างอิงแบบแก้ไขไม่ได้ไปยังข้อมูลนั้นก็ตาม โดยปกติแล้ว แอ็กชันนี้จะไม่ได้รับอนุญาตตามกฎการยืม ในการแก้ไขข้อมูล รูปแบบนี้จะใช้โค้ด unsafe ภายในโครงสร้างข้อมูลเพื่อผ่อนผันกฎปกติของ Rust ที่ควบคุมการแก้ไขและการยืม โค้ด Unsafe บ่งบอกต่อคอมไพเลอร์ว่าเรากำลังตรวจสอบกฎด้วยตนเองแทนที่จะพึ่งพาคอมไพเลอร์ในการตรวจสอบให้เรา เราจะพูดถึงโค้ด unsafe เพิ่มเติมในบทที่ 20

เราสามารถใช้ชนิดข้อมูลที่ใช้รูปแบบการแก้ไขภายในได้เฉพาะเมื่อเราสามารถมั่นใจได้ว่ากฎการยืมจะได้รับการปฏิบัติตามขณะรันไทม์ (runtime) แม้ว่าคอมไพเลอร์จะไม่สามารถรับประกันสิ่งนั้นในขณะคอมไพล์ได้ โค้ด unsafe ที่เกี่ยวข้องจะถูกห่อหุ้มไว้ใน API ที่ปลอดภัย (safe API) และชนิดข้อมูลภายนอกยังคงแก้ไขไม่ได้

มาสำรวจแนวคิดนี้โดยดูที่ชนิดข้อมูล RefCell<T> ซึ่งเป็นไปตามรูปแบบการแก้ไขภายใน

การบังคับใช้กฎการยืมขณะรันไทม์

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

  • ณ เวลาใดเวลาหนึ่ง คุณสามารถมีการอ้างอิงแบบแก้ไขได้หนึ่งอัน หรือมีการอ้างอิงแบบแก้ไขไม่ได้กี่อันก็ได้ (แต่ไม่ใช่มีทั้งสองแบบพร้อมกัน)
  • การอ้างอิงจะต้องถูกต้องอยู่เสมอ

สำหรับการอ้างอิงและ Box<T> เงื่อนไขคงที่ของกฎการยืมจะถูกบังคับใช้ในขณะคอมไพล์ (compile time) สำหรับ RefCell<T> เงื่อนไขคงที่เหล่านี้จะถูกบังคับใช้ขณะรันไทม์ (runtime) สำหรับการอ้างอิง หากคุณละเมิดกฎเหล่านี้ คุณจะได้ข้อผิดพลาดจากคอมไพเลอร์ สำหรับ RefCell<T> หากคุณละเมิดกฎเหล่านี้ โปรแกรมของคุณจะเกิด panic และตื่นตระหนกจนจบการทำงาน

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

ข้อดีของการตรวจสอบกฎการยืมขณะรันไทม์แทนคือ สถานการณ์ด้านความปลอดภัยของหน่วยความจำบางอย่างจะได้รับอนุญาต ในจุดที่พวกมันเคยถูกปฏิเสธโดยการตรวจสอบในขณะคอมไพล์ การวิเคราะห์แบบสแตติก (static analysis) เช่น คอมไพเลอร์ของ Rust โดยธรรมชาติแล้วจะเน้นความปลอดภัยไว้ก่อน (conservative) คุณสมบัติบางประการของโค้ดไม่สามารถตรวจจับได้โดยการวิเคราะห์โค้ด: ตัวอย่างที่มีชื่อเสียงที่สุดคือปัญหาการหยุดทำงาน (Halting Problem) ซึ่งอยู่นอกเหนือขอบเขตของหนังสือเล่มนี้ แต่เป็นหัวข้อที่น่าสนใจในการค้นคว้า

เนื่องจากการวิเคราะห์บางอย่างเป็นไปไม่ได้ หากคอมไพเลอร์ของ Rust ไม่สามารถแน่ใจได้ว่าโค้ดปฏิบัติตามกฎความเป็นเจ้าของ คอมไพเลอร์อาจจะปฏิเสธโปรแกรมที่ถูกต้อง ด้วยวิธีนี้ มันจึงเน้นความปลอดภัยไว้ก่อน หาก Rust ยอมรับโปรแกรมที่ไม่ถูกต้อง ผู้ใช้จะไม่สามารถไว้วางใจในการรับประกันที่ Rust มอบให้ได้ อย่างไรก็ตาม หาก Rust ปฏิเสธโปรแกรมที่ถูกต้อง โปรแกรมเมอร์อาจจะได้รับความไม่สะดวก แต่จะไม่มีเหตุการณ์หายนะเกิดขึ้น ชนิดข้อมูล RefCell<T> มีประโยชน์เมื่อคุณแน่ใจว่าโค้ดของคุณปฏิบัติตามกฎการยืม แต่คอมไพเลอร์ไม่สามารถเข้าใจและรับประกันสิ่งนั้นได้

คล้ายกับ Rc<T> ชนิดข้อมูล RefCell<T> มีไว้สำหรับใช้ในสถานการณ์เธรดเดียวเท่านั้น และจะให้ข้อผิดพลาดในขณะคอมไพล์หากคุณพยายามใช้มันในบริบทหลายเธรด เราจะพูดถึงวิธีรับฟังก์ชันการทำงานของ RefCell<T> ในโปรแกรมแบบหลายเธรดในบทที่ 16

นี่คือสรุปเหตุผลในการเลือกใช้ Box<T>, Rc<T>, หรือ RefCell<T>:

  • Rc<T> อนุญาตให้มีเจ้าของหลายคนสำหรับข้อมูลเดียวกัน; Box<T> และ RefCell<T> มีเจ้าของเพียงคนเดียว
  • Box<T> ยินยอมให้ยืมแบบแก้ไขไม่ได้หรือแก้ไขได้ซึ่งตรวจสอบในขณะคอมไพล์; Rc<T> ยินยอมเฉพาะการยืมแบบแก้ไขไม่ได้ซึ่งตรวจสอบในขณะคอมไพล์; RefCell<T> ยินยอมให้ยืมแบบแก้ไขไม่ได้หรือแก้ไขได้ซึ่งตรวจสอบขณะรันไทม์
  • เนื่องจาก RefCell<T> ยินยอมให้ยืมแบบแก้ไขได้ซึ่งตรวจสอบขณะรันไทม์ คุณจึงสามารถแก้ไขค่าภายใน RefCell<T> ได้ แม้ว่า RefCell<T> นั้นจะแก้ไขไม่ได้ก็ตาม

การแก้ไขค่าภายในค่าที่ไม่สามารถแก้ไขได้คือรูปแบบการแก้ไขภายใน มาดูสถานการณ์ที่รูปแบบการแก้ไขภายในมีประโยชน์ และพิจารณาว่ามันเป็นไปได้อย่างไร

การใช้รูปแบบการแก้ไขภายใน

ผลลัพธ์ประการหนึ่งของกฎการยืมคือเมื่อคุณมีค่าที่ไม่สามารถแก้ไขได้ คุณจะไม่สามารถยืมมันแบบแก้ไขได้ ตัวอย่างเช่น โค้ดนี้จะไม่สามารถคอมไพล์ได้:

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

หากคุณลองคอมไพล์โค้ดนี้ คุณจะได้ข้อผิดพลาดต่อไปนี้:

$ 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

อย่างไรก็ตาม มีสถานการณ์ที่มีประโยชน์สำหรับค่าในการแก้ไขตัวเองในเมธอดของมัน แต่ปรากฏว่าแก้ไขไม่ได้ต่อโค้ดอื่น โค้ดภายนอกเมธอดของค่านั้นจะไม่สามารถแก้ไขค่านั้นได้ การใช้ RefCell<T> เป็นวิธีหนึ่งในการรับความสามารถในการแก้ไขภายใน แต่ RefCell<T> ไม่ได้ก้าวข้ามกฎการยืมไปโดยสิ้นเชิง: ตัวตรวจการยืม (borrow checker) ในคอมไพเลอร์ยินยอมการแก้ไขภายในนี้ และกฎการยืมจะถูกตรวจสอบขณะรันไทม์แทน หากคุณละเมิดกฎ คุณจะได้ panic! แทนที่จะเป็นข้อผิดพลาดจากคอมไพเลอร์

มาทำงานผ่านตัวอย่างเชิงปฏิบัติที่เราสามารถใช้ RefCell<T> เพื่อแก้ไขค่าที่แก้ไขไม่ได้ และดูว่าเหตุใดสิ่งนั้นจึงมีประโยชน์

การทดสอบด้วย Mock Objects

บางครั้งในการทดสอบ โปรแกรมเมอร์จะใช้ชนิดข้อมูลหนึ่งแทนอีกชนิดข้อมูลหนึ่ง เพื่อสังเกตพฤติกรรมเฉพาะและยืนยันว่ามันถูกประยุกต์ใช้อย่างถูกต้อง ชนิดข้อมูลตัวแทนนี้เรียกว่า ตัวแสดงแทนในการทดสอบ (test double) ให้ลองคิดในแง่ของตัวแสดงแทนสตันท์ (stunt double) ในการทำภาพยนตร์ ที่บุคคลหนึ่งก้าวเข้ามาแสดงแทนนักแสดงเพื่อทำฉากที่ยากเป็นพิเศษ ตัวแสดงแทนในการทดสอบจะทำหน้าที่แทนชนิดข้อมูลอื่นเมื่อเรารันการทดสอบ วัตถุจำลอง (Mock objects) เป็นชนิดข้อมูลเฉพาะของตัวแสดงแทนในการทดสอบที่จะบันทึกสิ่งที่เกิดขึ้นระหว่างการทดสอบ เพื่อให้คุณสามารถยืนยันได้ว่าแอ็กชันที่ถูกต้องได้เกิดขึ้นแล้ว

Rust ไม่มีวัตถุ (objects) ในความหมายเดียวกับที่ภาษาอื่นมี และ Rust ไม่มีฟังก์ชันการทำงานของวัตถุจำลองที่บิลต์อินมาในไลบรารีมาตรฐานเหมือนที่บางภาษาอื่นมี อย่างไรก็ตาม คุณสามารถสร้าง struct ที่จะทำหน้าที่เพื่อวัตถุประสงค์เดียวกับวัตถุจำลองได้อย่างแน่นอน

นี่คือสถานการณ์ที่เราจะทดสอบ: เราจะสร้างไลบรารีที่ติดตามค่าเทียบกับค่าสูงสุด และส่งข้อความโดยพิจารณาจากว่าปัจจุบันค่าใกล้อยู่ที่ค่าสูงสุดเท่าใด ไลบรารีนี้สามารถนำไปใช้ติดตามโควตาของผู้ใช้สำหรับจำนวนการเรียกใช้ API ที่พวกเขาสามารถทำได้ เป็นต้น

ไลบรารีของเราจะให้เฉพาะฟังก์ชันการทำงานในการติดตามว่าค่าใกล้อยู่ที่ค่าสูงสุดเท่าใด และข้อความควรจะเป็นอย่างไร ณ เวลาใด แอปพลิเคชันที่ใช้ไลบรารีของเราจะได้รับการคาดหวังให้จัดเตรียมกลไกสำหรับการส่งข้อความเอง: แอปพลิเคชันอาจแสดงข้อความให้ผู้ใช้เห็นโดยตรง ส่งอีเมล ส่งข้อความ SMS หรือทำสิ่งอื่น ไลบรารีไม่จำเป็นต้องรู้รายละเอียดนั้น สิ่งที่มันต้องการคือบางสิ่งที่ประยุกต์ใช้เทรตที่เราจะจัดเตรียมให้ ซึ่งเรียกว่า Messenger โค้ดตัวอย่างที่ 15-20 แสดงโค้ดของไลบรารี

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!");
        }
    }
}

ส่วนสำคัญประการหนึ่งของโค้ดนี้คือเทรต Messenger มีเมธอดหนึ่งชื่อ send ที่รับการอ้างอิงแบบแก้ไขไม่ได้ไปยัง self และข้อความ เทรตนี้เป็นอินเทอร์เฟซที่วัตถุจำลองของเราจำเป็นต้องประยุกต์ใช้เพื่อให้วัตถุจำลองสามารถใช้งานได้ในลักษณะเดียวกับวัตถุจริง ส่วนสำคัญอีกประการหนึ่งคือเราต้องการทดสอบพฤติกรรมของเมธอด set_value บน LimitTracker เราสามารถเปลี่ยนสิ่งที่เราส่งเข้าไปสำหรับพารามิเตอร์ value ได้ แต่ set_value ไม่ได้คืนค่าใด ๆ ให้เราทำข้อกำหนดการยืนยัน เราต้องการสามารถบอกได้ว่าหากเราสร้าง LimitTracker ด้วยบางสิ่งที่ประยุกต์ใช้เทรต Messenger และค่าเฉพาะสำหรับ max ตัวส่งข้อความจะได้รับการบอกให้ส่งข้อความที่เหมาะสมเมื่อเราส่งตัวเลขต่าง ๆ สำหรับ value

เราต้องการวัตถุจำลองที่ แทนที่จะส่งอีเมลหรือข้อความ SMS เมื่อเราเรียก send จะเพียงแค่คอยติดตามข้อความที่มันได้รับการบอกให้ส่ง เราสามารถสร้างอินสแตนซ์ใหม่ของวัตถุจำลอง สร้าง LimitTracker ที่ใช้วัตถุจำลองนั้น เรียกเมธอด set_value บน LimitTracker จากนั้นตรวจสอบว่าวัตถุจำลองมีข้อความที่เราคาดหวังไว้หรือไม่ โค้ดตัวอย่างที่ 15-21 แสดงความพยายามประยุกต์ใช้วัตถุจำลองเพื่อทำเช่นนั้น แต่ตัวตรวจการยืมจะไม่ยินยอม

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);
    }
}

โค้ดการทดสอบนี้นิยาม struct MockMessenger ที่มีฟิลด์ sent_messages เป็น Vec ของค่า String เพื่อคอยติดตามข้อความที่มันได้รับการบอกให้ส่ง เรายังนิยามฟังก์ชันที่เชื่อมโยง new เพื่อให้สะดวกในการสร้างค่า MockMessenger ใหม่ที่เริ่มต้นด้วยรายการข้อความที่ว่างเปล่า จากนั้นเราประยุกต์ใช้เทรต Messenger สำหรับ MockMessenger เพื่อให้เราสามารถส่ง MockMessenger ให้กับ LimitTracker ได้ ในการนิยามเมธอด send เรารับข้อความที่ส่งเข้ามาเป็นพารามิเตอร์และเก็บไว้ในรายการ sent_messages ของ MockMessenger

ในการทดสอบ เรากำลังทดสอบว่าจะเกิดอะไรขึ้นเมื่อ LimitTracker ถูกบอกให้ตั้งค่า value เป็นสิ่งที่มากกว่า 75 เปอร์เซ็นต์ของค่า max อันดับแรก เราสร้าง MockMessenger ใหม่ ซึ่งจะเริ่มด้วยรายการข้อความที่ว่างเปล่า จากนั้นเราสร้าง LimitTracker ใหม่ และให้การอ้างอิงไปยัง MockMessenger ใหม่ และค่า max เท่ากับ 100 เราเรียกเมธอด set_value บน LimitTracker ด้วยค่า 80 ซึ่งมากกว่า 75 เปอร์เซ็นต์ของ 100 จากนั้นเรายืนยันว่ารายการข้อความที่ 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 it 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 รับการอ้างอิงแบบแก้ไขไม่ได้ไปยัง self เรายังไม่สามารถรับคำแนะนำจากข้อความผิดพลาดให้ใช้ &mut self ทั้งในเมธอด impl และนิยามเทรตได้ เราไม่ต้องการเปลี่ยนเทรต Messenger เพียงเพื่อการทดสอบเท่านั้น แทนที่จะเป็นเช่นนั้น เราจำเป็นต้องหาวิธีทำให้โค้ดการทดสอบของเราทำงานได้อย่างถูกต้องกับดีไซน์เดิมที่เรามี

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

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);
    }
}

ตอนนี้ฟิลด์ sent_messages เป็นชนิด RefCell<Vec<String>> แทนที่จะเป็น Vec<String> ในฟังก์ชัน new เราสร้างอินสแตนซ์ RefCell<Vec<String>> ใหม่ล้อมรอบเวกเตอร์ที่ว่างเปล่า

สำหรับการประยุกต์ใช้เมธอด send พารามิเตอร์แรกยังคงเป็นการยืมแบบแก้ไขไม่ได้ของ self ซึ่งตรงกับนิยามเทรต เราเรียก borrow_mut บน RefCell<Vec<String>> ใน self.sent_messages เพื่อรับการอ้างอิงแบบแก้ไขได้ไปยังค่าภายใน RefCell<Vec<String>> ซึ่งก็คือเวกเตอร์ จากนั้นเราสามารถเรียก push บนการอ้างอิงแบบแก้ไขได้ของเวกเตอร์เพื่อติดตามข้อความที่ถูกส่งระหว่างการทดสอบได้

การเปลี่ยนแปลงสุดท้ายที่เราต้องทำคือในการยืนยัน: ในการดูว่ามีสมาชิกกี่ตัวในเวกเตอร์ภายใน เราเรียก borrow บน RefCell<Vec<String>> เพื่อรับการอ้างอิงแบบแก้ไขไม่ได้ไปยังเวกเตอร์

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

การติดตามการยืมขณะรันไทม์

เมื่อสร้างการอ้างอิงแบบแก้ไขไม่ได้และแบบแก้ไขได้ เราใช้ไวยากรณ์ & และ &mut ตามลำดับ สำหรับ RefCell<T> เราใช้เมธอด borrow และ borrow_mut ซึ่งเป็นส่วนหนึ่งของ API ที่ปลอดภัยของ RefCell<T> เมธอด borrow จะคืนค่าชนิดพอยน์เตอร์อัจฉริยะ Ref<T> และ borrow_mut จะคืนค่าชนิดพอยน์เตอร์อัจฉริยะ RefMut<T> ทั้งสองชนิดประยุกต์ใช้ Deref ดังนั้นเราจึงสามารถปฏิบัติต่อพวกมันเหมือนการอ้างอิงทั่วไปได้

RefCell<T> จะคอยติดตามจำนวนพอยน์เตอร์อัจฉริยะ Ref<T> และ RefMut<T> ที่ใช้งานอยู่ในปัจจุบัน ทุกครั้งที่เราเรียก borrow RefCell<T> จะเพิ่มตัวนับการยืมแบบแก้ไขไม่ได้ที่ใช้งานอยู่ขึ้น 1 เมื่อค่า Ref<T> หลุดออกจากขอบเขต ตัวนับการยืมแบบแก้ไขไม่ได้จะลดลงทีละ 1 เช่นเดียวกับกฎการยืมในขณะคอมไพล์ RefCell<T> ยินยอมให้เรามีการยืมแบบแก้ไขไม่ได้หลายอัน หรือการยืมแบบแก้ไขได้อันเดียว ณ จุดใด ๆ ในเวลา

หากเราพยายามละเมิดกฎเหล่านี้ แทนที่จะได้ข้อผิดพลาดจากคอมไพเลอร์เหมือนที่เราจะได้กับการอ้างอิง การประยุกต์ใช้งานของ RefCell<T> จะเกิด panic ขณะรันไทม์แทน โค้ดตัวอย่างที่ 15-23 แสดงการปรับเปลี่ยนการประยุกต์ใช้ send ในโค้ดตัวอย่างที่ 15-22 เราพยายามเจตนาสร้างการยืมแบบแก้ไขได้สองอันที่ใช้งานในขอบเขตเดียวกันเพื่อแสดงให้เห็นว่า RefCell<T> ป้องกันไม่ให้เราทำเช่นนี้ขณะรันไทม์

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);
    }
}

เราสร้างตัวแปร one_borrow สำหรับพอยน์เตอร์อัจฉริยะ RefMut<T> ที่คืนมาจาก borrow_mut จากนั้นเราสร้างการยืมแบบแก้ไขได้อีกอันในลักษณะเดียวกันในตัวแปร two_borrow สิ่งนี้ทำให้เกิดการอ้างอิงแบบแก้ไขได้สองอันในขอบเขตเดียวกัน ซึ่งไม่ได้รับอนุญาต เมื่อเรารันการทดสอบสำหรับไลบรารีของเรา โค้ดในโค้ดตัวอย่างที่ 15-23 จะคอมไพล์ผ่านโดยไม่มีข้อผิดพลาดใด ๆ แต่การทดสอบจะล้มเหลว:

$ 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' (6028024) 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> จัดการกับการละเมิดกฎการยืมขณะรันไทม์

การเลือกตรวจจับข้อผิดพลาดการยืมขณะรันไทม์แทนที่จะเป็นขณะคอมไพล์ตามที่เราทำตรงนี้ หมายความว่าคุณอาจจะพบข้อผิดพลาดในโค้ดของคุณในภายหลังของกระบวนการพัฒนา: อาจจะไม่พบจนกว่าโค้ดของคุณถูกปรับใช้ไปยังสภาพแวดล้อมจริง (production) นอกจากนี้ โค้ดของคุณยังต้องแลกกับบทลงโทษด้านประสิทธิภาพเล็กน้อยขณะรันไทม์ซึ่งเป็นผลจากการคอยติดตามการยืมขณะรันไทม์แทนที่จะเป็นขณะคอมไพล์ อย่างไรก็ตาม การใช้ RefCell<T> ช่วยให้สามารถเขียนวัตถุจำลองที่สามารถแก้ไขตัวเองเพื่อติดตามข้อความที่พบได้ ขณะที่คุณกำลังใช้งานมันในบริบทที่อนุญาตเฉพาะค่าที่ไม่สามารถแก้ไขได้เท่านั้น คุณสามารถใช้ RefCell<T> แม้จะมีข้อแลกเปลี่ยน เพื่อให้ได้ฟังก์ชันการทำงานที่มากกว่าการอ้างอิงทั่วไปมอบคุณให้

การยินยอมให้มีเจ้าของหลายคนสำหรับข้อมูลที่แก้ไขได้

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

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

#[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:?}");
}

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

เราห่อหุ้มลิสต์ a ไว้ใน Rc<T> เพื่อให้เมื่อเราสร้างลิสต์ b และ c ทั้งคู่สามารถอ้างอิงถึง aได้ ซึ่งเป็นสิ่งที่เราทำในโค้ดตัวอย่างที่ 15-18

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

เมื่อเราพิมพ์ 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 ที่ภายนอกดูเหมือนแก้ไขไม่ได้ แต่เราสามารถใช้เมธอดบน RefCell<T> ที่ให้การเข้าถึงการแก้ไขภายในของมัน เพื่อให้เราสามารถแก้ไขข้อมูลของเราเมื่อเราจำเป็นต้องทำได้ การตรวจสอบกฎการยืมขณะรันไทม์ช่วยปกป้องเราจากสภาวะแข่งขันของข้อมูล (data races) และบางครั้งก็คุ้มค่าที่จะแลกความเร็วเล็กน้อยสำหรับความยืดหยุ่นในโครงสร้างข้อมูลของเรา โปรดทราบว่า RefCell<T> จะไม่ทำงานสำหรับโค้ดหลายเธรด! Mutex<T> คือเวอร์ชันที่ปลอดภัยสำหรับเธรดของ RefCell<T> และเราจะพูดถึง Mutex<T> ในบทที่ 16