The Rust Programming Language - 第15章 智能指针 - 15.5 RefCell<T> 和内部可变性模式

15 智能指针

指针指向变量的内存地址,除了引用数据没有其它的功能,因此没有运行开销

智能指针是一类数据结构,虽然表现类似指针,但是拥有额外的元数据和功能。Rust的智能指针提供了包含引用之外的其他功能,但是指针这个概念并不是Rust独有的

在Rust中,普通指针只是借用数据,而智能指针还拥有它们指向的数据,比如String和Vec,它们都是智者指针,它们拥有数据并且可以被修改。它们也带有元数据(比如容量)和额外的功能和保证(String的数据总是有效的UTF-8编码)

智能指针通常使用结构体来实现,区别与常规结构体的是它们实现了deref 和 drop trait。deref trait允许智能指针结构体实例表现的像引用一样,这样可以编写既可以用于引用又可以用于智能指针的代码。Drop trait 允许我们自定义智能指针离开作用域时运行的代码

智能指针在Rust中是一个通用设计模式,很多库都有智能指针并且你也可以编写自己的智能指针,本章我们将会学习以下:

Box,用于在堆上分配值,实现了Deref trait值,允许Box被当作引用对待,当Box离开作用域时,由于Box类型Drop trait的实现,box所指向的数据也会被清除

Rc,一个引用计数类型,其数据可以有多个所有者

Ref和RefMut,通过RefCell访问(RefCell是一个在运行时而不是在编译时执行借用规则的类型)

我们还会涉及内部可变性模式,这是不可变类型暴露出改变其内部值的API,我们也会讨论引用循环会如何泄露内存,以及如何避免

15.5 RefCell 和内部可变性模式

内部可变性是Rust中的一个设计模式,它允许我们即使在有不可变引用时也可以改变数据,但是这通常是借用规则不允许的。为了改变数据,该模式在数据结构中使用unsafe代码来模糊Rust通常的可变性和借用规则。所涉及的unsafe代码将被封装进安全的API中,外部类型仍然是不可变的

通过RefCell在运行时检查借用规则

不同于Rc,RefCell代表数据的唯一所有权,那么二者为什么不同呢?让我们回一下借用规则

1.在任意给定时刻,只拥有一个可变引用或者任意数量的不可变引用之一

2.引用必须总是有效的

对于引用和Box,借用规则的不可变性作用于编译时

对于ReCell,这些不可变性作用于运行时,对于引用,如果违反这些规则,会得到一个错误,对于Recell,如果违反这些规则程序会panic并退出

关于在编译时检查借用和运行时检查借用的比较

在编译时就检查借用,好处在于错误会在开发早起被捕获,同时不影响性能,因为所有的分析都提前完成了,在编译期检查是最好的选择,也是Rust的默认行为,但是我们也要知道静态分析是保守的,一些代码属性不会被发现

在运行时检查好处则是允许出现特定内存安全的场景,而它们在编译时检查是不允许的

RefCell正是用于当你确信代码遵守借用规则,而编译器不能理解和确定的时候

类似于Rc,RefCell也只能用于单线程场景,尝试在上下文中使用RefCell,会得到编译错误,我们在后面会讲RefCell在多线程中的使用

那我们应该在什么情况下选择Rc\Box\RefCell呢?参考如下

1.Rc允许相同数据有多个所有者:Box和RefCell有单一所有者

2.Box运行在编译时执行不可变或者可变借用检查;Rc仅允许在编译时执行不可变借用检查;RefCell允许在u运行时执行可变或者不可变借用检查

3.因为RefCell允许在运行时执行可变借用检查,所以我们可以在即便RefCell自身是不可变的情况下修改其内部的值

在不可变值内部修改值就是内部可变性模式

内部可变性:不可变值的可变借用

借用规则的一个推论是当有一个不可变值时,不能可变的借用它,如下:

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

如果强行编译,会出现如上错误

但是呢,在特定情况下,让某个值在其方法内部能够修改自身,在其他代码中仍不可变,这样的规则时非常有用的。RefCell是一个获得内部可变性的方法,RefCell并没有绕开借用规则,编译器中的借用检查器允许内部可变性并相应的在运行时检查借用规则。如果违反了这些规则,会出现panic而不是编译错误

让我们看看实际的例子

内部可变性的用例:mock对象

测试替身是一个通用编程概念,它代表的是在一个测试中代替某个类型的类型。mock对象是特定类型的测试替身,它们记录测试过程中发生了什么以便可以断言操作是正确的

虽然rust语言中的对象与其他语言中的对象不是一回事,标准库中也没有内建mock对象功能,但我们可以创建一个与mock对象有着相同功能的结构体,如下

我们想要测试一个场景:我们在编写某个值与最大值的差距的库,并根据当前值与最大值的差距来发送消息

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

我们来看一下上述代码

send方法提供的行为有一个Messenger trait,接着我们定义了一个结构体,并在impl块中定义了两个方法。也就是说某个值如果实现了Messenger trait,并且我们使用某个max创建LimitTracker,当传递不同的value时,消息发送者应该被告知发送合适的消息

而我们所需的mock对象是,调用send并不实际发送email或者消息,而只是记录信息被通知要发送了。我们来构建一个测试:

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

运行一下这个测试

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

不能修改MockMessenger来记录消息,因为send方法获取了self的不可变引用

这时,内部可变性就可以派上用场了,我们用RefCell来存储sent_messages,然后send将能够修改sent_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);
     }
}

现在sent_messenges字段的类型是RefCell<Vec>而不是Vec.在new函数中新建了一个RefCell<Vec>实例代替空vector

对send方法的实现,第一个参数仍为self的不可变借用,这符合方法定义。我们调用self.sent_messages中RefCell的borrow_mut方法来获取RefCell中值的可变引用,这是一个vector。接着可以对vector中的可变引用调用push以便记录测试过程中看到的消息

最后在断言中,为了看到其内部vector中有多少个项,需要调用Refcell的borrow以获取vector的不可变引用

接下来我么研究一下RefCell是怎么工作的

RefCell在运行时记录借用

可变引用和不可变引用我们分别使用&和&mut语法

对于RefCell来说,则是borrow和borrow_mut方法,这属于RefCell安全API的一部分。borrow方法返回Ref类型智能指针,borrow_mut返回RefMut类型的智能指针。这两个类型都实现了deref,所以可以当常规引用对待

RefCell记录当前有多少个活动的Ref和RefMut智能指针。每次调用borrow,RefCell将活动的不可变借用+1.当Ref值离开作用域时,不可变借用计数减1,就像编译时借用规则一样,RefCell在任何时候只允许有多个不可变借用或一个可变借用

如果违反规则,相比引用是的编译错误,RefCell的实现会在运行时出现panic

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

我们看到在相同作用域中创建两个可变引用这时不允许的,RefCell在运行时处理违反借用规则的情况,这意味着在开发过程后期才可能发现错误,但是使用RefCell使得在只允许不可变值的上下文中编写修改自身以记录消息的mock对象成为可能。虽然有取舍,但是使用RefCell让我们比常规引用获得了更多的功能

结合Rc和RefCell来拥有多个可变数据所有者

RefCell一个常用的方法是和Rc结合

Rc允许数据有多个所有者,但是只读,我们结合使用的话就可以让多个所有者修改数据了

#[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))

结合使用这两个智能指针结果如我们所愿

标准库中也有其他提供可变型的的类型,比如Cell,它类似RefCell但有一点除外,它并非提供内部值的引用,而是把值拷贝进和拷贝出Cell.还有Mutex.其提供线程间安全的内部可变性

猜你喜欢

转载自blog.csdn.net/weixin_51487151/article/details/121587182