The Rust Programming Language - Chapter 15 smart pointers - 15.5 RefCell < T > and internal variability mode

15 smart pointer

The pointer points to the memory address of the variable. It has no other functions except reference data, so there is no running overhead

Smart pointer is a kind of data structure. Although it behaves like a pointer, it has additional metadata and functions. Rust's smart pointer provides functions other than including references, but the concept of pointer is not unique to rust

In Rust, ordinary pointers only borrow data, while smart pointers also have the data they point to, such as String and Vec. They are wise pointers. They have data and can be modified. They also have metadata (such as capacity) and additional functions and guarantees (String data is always valid UTF-8 encoding)

Smart pointers are usually implemented using structures. The difference from conventional structures is that they implement deref and drop trait. deref trait allows smart pointer structure instances to behave like references, so that you can write code that can be used for both references and smart pointers. Drop trait allows us to customize the code that runs when the smart pointer leaves the scope

Smart pointer is a general design pattern in Rust. Many libraries have smart pointers, and you can also write your own smart pointers. In this chapter, we will learn the following:

Box is used to allocate values on the heap. It implements the Deref trait value and allows the box to be treated as a reference. When the box leaves the scope, the data pointed to by the box will also be cleared due to the implementation of the box type Drop trait

Rc, a reference count type whose data can have multiple owners

Ref and RefMut, accessed through RefCell (RefCell is a type that executes borrowing rules at run time rather than at compile time)

We will also cover the internal variability pattern, which is an immutable type that exposes API s that change its internal values. We will also discuss how reference loops can leak memory and how to avoid it

15.5 RefCell and internal variability mode

Internal variability is a design pattern in Rust, which allows us to change data even when there are immutable references, but this is usually not allowed by borrowing rules. In order to change the data, the pattern uses unsafe code in the data structure to blur the usual variability and borrowing rules of Rust. The unsafe code involved will be encapsulated in a secure API, and the external types will remain immutable

Check borrowing rules at run time through RefCell

Unlike Rc,RefCell represents the sole ownership of data, so why are they different? Let's go back to the borrowing rules

1. Have only one variable reference or one of any number of immutable references at any given time

2. References must always be valid

For references and boxes, the immutability of borrowing rules works at compile time

For recell, these immutabilities act on the runtime. For reference, if these rules are violated, an error will be obtained. For recell, if these rules are violated, the program will panic and exit

Comparison of checking borrowing at compile time and checking borrowing at run time

Check borrowing at compile time. The advantage is that errors will be caught early in development and will not affect performance, because all analysis has been completed in advance. Checking at compile time is the best choice and the default behavior of Rust. However, we should also know that static analysis is conservative and some code properties will not be found

The benefit of checking at run time is to allow specific memory security scenarios, which are not allowed at compile time

RefCell is used when you are sure that the code complies with the borrowing rules, but the compiler can't understand and determine it

Similar to Rc,RefCell can only be used in single thread scenarios. If you try to use RefCell in the context, you will get compilation errors. We will talk about the use of RefCell in multithreading later

Under what circumstances should we choose Rc\Box\RefCell? Reference is as follows

1.Rc allows multiple owners of the same data: Box and RefCell have a single owner

2. The box runtime performs immutable or variable borrowing check during compilation; Rc only allows immutable borrowing checking at compile time; RefCell allows variable or immutable borrowing checks to be performed at u runtime

3. Because RefCell allows variable borrowing checking at runtime, we can modify the internal value of RefCell even if it is immutable

Modifying values inside immutable values is the internal variability pattern

Internal variability: variable borrowing of immutable values

A corollary of the borrowing rule is that when there is an immutable value, it cannot be borrowed variably, as follows:

fn main() {

    let x = 5;
    let y = &mut x;
}
error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
 --> src/main.rs:6:13
  |
5 |     let x = 5;
  |         - help: consider changing this to be mutable: `mut x`
6 |     let y = &mut x;
  |             ^^^^^^ cannot borrow as mutable

If you force compilation, the above error will occur

However, under certain circumstances, the rule that a value can modify itself within its method and remains immutable in other code is very useful. RefCell is a method to obtain internal variability. RefCell does not bypass borrowing rules. The borrowing checker in the compiler allows internal variability and checks borrowing rules at run time accordingly. If these rules are violated, panic will occur instead of compilation errors

Let's look at a practical example

Use case of internal variability: mock object

Test avatar is a general programming concept, which represents a type that replaces a type in a test. mock objects are specific types of test avatars that record what happens during the test so that you can assert that the operation is correct

Although the objects in the t rust language are different from those in other languages, and there is no built-in mock object function in the standard library, we can create a structure with the same function as the mock object, as shown below

We want to test a scenario: we are writing a library of the gap between a certain value and the maximum value, and sending messages according to the gap between the current value and the maximum value

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:&T,max:usize)->LimitTracker<T> {
        LimitTracker {
            messenger,
            value:0,
            max,
       }
    }
    pub fn set_value(&mut self,value:usize){
        self.value = value;

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

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

Let's look at the above code

The behavior provided by the send method has a Messenger trait. Then we define a structure and two methods in the impl block. That is, if a value implements Messenger trait and we use a max to create a LimitTracker, when different values are passed, the message sender should be told to send appropriate messages

The mock object we need is that calling send does not actually send email or messages, but only records that the information is notified to be sent. Let's build a test:

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

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

Run this test

error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
  --> src/lib.rs:51:16
   |
2  |      fn send(&self,msg:&str);
   |              ----- help: consider changing that to be a mutable reference: `&mut self`
...
51 |                self.sent_messages.push(String::from(message));
   |                ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable

You cannot modify MockMessenger to log messages because the send method gets an immutable reference to self

At this time, internal variability can come in handy. We use RefCell to store sent_messages, and then send will be able to modify send_ Messages and store messages

#[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_send_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);
     }
}

Now send_ The type of the messages field is refcell < Vec > instead of Vec. A new refcell < Vec > instance is created in the new function to replace the empty vector

For the implementation of the send method, the first parameter is still the immutable borrowing of self, which conforms to the method definition. We call self. Send_ Border of RefCell in messages_ Mut method to get the variable reference of the value in RefCell, which is a vector. You can then call push on the variable reference in the vector to record the messages you see during the test

Finally, in the assertion, in order to see how many items are in its internal vector, you need to call Refcell's border to get the immutable reference of the vector

Next, let's study how RefCell works

RefCell records borrowing at run time

We use the & and & mut syntax for variable and immutable references, respectively

For RefCell, it is row and row_ Mut method, which is part of the RefCell security API. The row method returns a smart pointer of type Ref, row_ Mut returns a smart pointer of type RefMut. Both types implement deref, so they can be treated as regular references

RefCell records how many Ref and RefMut smart pointers are currently active. Each time you call borrow, RefCell will add + 1 to the active immutable borrowing. When the Ref value leaves the scope, the immutable borrowing count will be reduced by 1. Just like the borrowing rule at compile time, RefCell only allows multiple immutable borrowings or one variable borrowing at any time

If the rule is violated, the RefCell implementation will have panic at run time compared to the reference compilation error

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

running 1 test
thread 'tests::it_send_an_over_75_percent_warning_message' panicked at 'already borrowed: BorrowMutError', src/lib.rs:52:56
test tests::it_send_an_over_75_percent_warning_message ... FAILED

failures:

failures:
    tests::it_send_an_over_75_percent_warning_message

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

We can see that it is not allowed to create two variable references in the same scope. RefCell handles the violation of borrowing rules at run time, which means that errors may not be found until later in the development process. However, using RefCell makes it possible to write a mock object that modifies itself to record messages in the context that only immutable values are allowed. Although there are trade-offs, using RefCell gives us more functionality than regular references

Combining Rc and RefCell to have multiple variable data owners

A common method of RefCell is to combine with Rc

Rc allows data to have multiple owners, but read-only. If we use it together, we can let multiple owners modify the data

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

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

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(6)),Rc::clone(&a));
    let c = Cons(Rc::new(RefCell::new(10)),Rc::clone(&a));

    *value.borrow_mut() += 10;

    println!("a after = {:?}",a);
    println!("b after = {:?}",b);
    println!("c after = {:?}",c);
}
 Running `target/debug/smartPoint`
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 6 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 10 }, Cons(RefCell { value: 15 }, Nil))

Using these two smart pointers together, the result is as we wish

There are other types that provide variability in the standard library, such as Cell, which is similar to RefCell, except that it does not provide references to internal values, but copies values into and out of Cell. And Mutex, which provides safe internal variability between threads

Tags: Back-end Rust

Posted on Sat, 27 Nov 2021 20:17:55 -0500 by gojakie