1. Why is internal variability required?
Typically, the Rust compiler ensures that:
- There can only be one variable reference, or any multiple immutable references at the same time;
- Quotations are always valid.
This strict borrowing rule allows many memory errors to be caught during the compilation stage, but it is therefore too conservative in some scenarios.
For example, when we need to modify the state inside an immutable object (such as logging, counting, etc.), we need to rely on internal mutability. Through internal variability, we can achieve internal data changes through encapsulation while maintaining immutability externally, and the security of these changes is guaranteed by runtime inspection.
2. RefCell<T>: The guardian of runtime borrowing rules
andBox<T>
andRc<T>
different,RefCell<T>
Use runtime instead of compile time to check borrow rules. It provides two core methods:
-
borrow()
Return oneRef<T>
Smart pointer, equivalent to immutable reference. -
borrow_mut()
Return oneRefMut<T>
Smart pointer, equivalent to variable reference.
Whenever calledborrow
orborrow_mut
hour,RefCell<T>
The current borrowing status will be recorded internally. If you try to get multiple variable references at the same time, or if there is already a variable reference,RefCell<T>
Panic will be triggered at runtime, preventing data competition.
For example, the following code tries to create two variable borrows within the same scope and panic is triggered:
let cell = RefCell::new(5); let _borrow1 = cell.borrow_mut(); let _borrow2 = cell.borrow_mut(); // Here panic: already borrowed: BorrowMutError
The advantage of this design is that it allows us to ensure data security in certain scenarios that cannot be covered by static checks; the disadvantage is that these checks will bring certain runtime overhead and may expose errors to production environments.
3. Actual case: Use RefCell<T> to write a Mock object
In test code, we often need to simulate the behavior of some real objects (that is, the so-called "test stand-in" or mock objects) to verify that the code logic is correct.
Suppose we have oneMessenger
interface, itssend
Methods accept only immutable references. This brings problems when writing mock objects: we want to callsend
The sent information is recorded, but only the method signature is accepted&self
, directly modifying the internal state will violate Rust's borrowing rules.
The solution is to useRefCell<T>
To package the variable state inside.
For example, we can define aMockMessenger
:
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) { // Although `self` is an immutable reference, we can get mutable references at runtime via `RefCell<T>` self.sent_messages.borrow_mut().push(String::from(message)); } }
In this way, in the test, we can callborrow()
To check internally saved messages without modificationMessenger
The definition of trait.
RefCell<T>
The internal borrow count ensures that we do not violate the borrowing rules when using it.
4. Combined with Rc<T> to achieve variable data with multiple ownership
Sometimes we want multiple owners to share the same copy of data and be able to modify the values in it. You can use it in combination at this timeRc<T>
andRefCell<T>
。Rc<T>
Allows multiple owners to share data, andRefCell<T>
This allows us to modify the data in the context of immutable references.
For example, the following example shows how to create a shared mutable value and modify it with multiple owners:
use std::rc::Rc; use std::cell::RefCell; enum List { Cons(Rc<RefCell<i32>>, Rc<List>), Nil, } use List::{Cons, Nil}; fn main() { let value = Rc::new(RefCell::new(5)); let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil))); let b = Cons(Rc::clone(&value), Rc::clone(&a)); let c = Cons(Rc::clone(&value), Rc::clone(&a)); // Modify internal values *value.borrow_mut() += 10; // The values stored in a, b, c will all reflect the changes in the internal value println!("a after modification: {:?}", a); }
In this way, we can enjoy the convenience of multiple ownership and maintain the variability of internal data. This is very useful in scenarios where shared state is required, but it should be noted that this mode is only suitable for single-threaded scenarios; if in a multi-threaded environment, it should be usedMutex<T>
etc. thread-safe data structure.
5. Summary
Internal variability: Allows modifying internal data in immutable references. By packagingunsafe
Code, hand over the responsibility for checking borrowing rules at runtime toRefCell<T>
。
Features of RefCell: Record immutable and mutable borrowed states at runtime, and if the borrowing rules are violated, panic will be caused. This provides a solution for certain scenarios where static checks cannot be covered.
Application scenarios:
- Mock object: Record call information in the test to meet the interface requirements without modifying the method signature.
-
Combining multiple ownership and variability: Combined
Rc<T>
andRefCell<T>
, can be shared and modified by multiple owners, but only for single-threaded environments.
Internal variability provides Rust programmers with a flexible solution to keep memory safe beyond strict compile-time borrow checks. Just use it with caution and understand the limitations of its runtime checking to better solve problems in certain complex scenarios.
Hope this blog helps you understand betterRefCell<T>
Its practical application in Rust.
The above is personal experience. I hope you can give you a reference and I hope you can support me more.