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
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!");
}
}
}
ส่วนสำคัญหนึ่งของโค้ดนี้คือ 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 จะไม่อนุญาต
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);
}
}
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
แสดงว่าดูเป็นยังไง
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);
}
}
RefCell<T> เพื่อ mutate ค่าภายในในขณะที่ค่าภายนอกถือว่า immutablefield 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
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);
}
}
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 ทั้งหมดได้
#[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>> เพื่อสร้าง 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