Rust学习_9.智能指针

智能指针

  • 指针:一个变量在内存中包含的是一个地址(指向其他数据);
  • Rust里最常见的指针就是引用
    • 使用&
    • 借用它指向的值
    • 没有其余开销
    • 最常见的指针类型
  • 智能指针是这样一些数据结构
    • 行为和指针类似;
    • 有额外的元数据和功能;

引用计数(reference counting)智能指针类型

  • 通过记录所有者的数量,使一份数据被多个所有者同时持有;
  • 并在没有任何所有者时自动清理数据。

引用和智能指针的不同:

  • 引用:只借用数据;
  • 智能指针:很多时候拥有它所指向的数据;

智能指针的例子:

  • String 和 Vec
    • 都拥有一片内存区域,且允许用户对其操作;
    • 还拥有元数据(例如容量等);
    • 提供额外的功能和保障(String 保障其数据是合法的UTF8编码)

智能指针的实现:

  • 智能指针通常使用 struct 实现,并且实现了:
    • Deref 和 Drop 这两个 trait;

Deref trait: 允许智能指针 struct 的实例像引用一样使用;(可以编写出同时用于引用和智能指针的代码)

Drop trait: 允许自定义当智能指针实例走出作用域时的代码。

1. Box

  • Box是最简单的智能指针:

    • 将数据存储在堆上,并在栈中保留一个指向堆数据的指针;
    • 没有性能开销;
    • 没有其他额外功能;
    • 实现了 Deref trait 和 Drop trait。
  • Box 的使用场景:

    • 在编译时,某类型的大小无法确定。但使用该类型时,上下文需要知道它的确切大小。例如:链表结构、树结构;
    • 当有大量数据,想移交所有权,但需要确保在操作时数据不会被复制;
    • 使用某值时,你只关心它是否实现了特定的trait,而不关心它的具体实现。

2. Deref Trait

  • 实现Deref Trait 使我们可以自定义解引用运算符 * 的行为;
  • 通过实现 Deref ,智能指针可像常规引用一样来处理。
1
2
3
4
5
6
7
8
fn main() {
let x = 5;
let y = &x;
let z = Box::new(x);

assert_eq!(5,*z);
assert_eq!(5,*y);
}
  • MyBox

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    struct MyBox<T> (T);

    impl<T> MyBox<T>{
    fn new(x:T)->MyBox<T>{
    MyBox(x)
    }
    }

    use std::ops::Deref;
    impl<T> Deref for MyBox<T> {
    type Target = T;
    fn deref(&self)->&T{
    &self.0
    }
    }

    fn main() {
    let x = 5;
    let y = MyBox::new(x);

    assert_eq!(5,*y);
    }
  • 解引用转换 deref coercion

    • 当某个类型T实现了Deref trait时,它能够将T的引用转换为T经过Deref操作后生成的引用。

    • 调用函数和方法时,无需多次显示地使用&和*运算符来进行引用和解引用操作。

      1
      2
      3
      4
      5
      6
      7
      8
      fn hello(name:&str){
      println!("Hello, {}",name);
      }

      fn main() {
      let m = MyBox::new(String::from("Rust"));
      hello(&m);
      }
  • 解引用转换与可变性

    • 使用 Deref trait 能够重载不可变引用的 * 运算符。
    • 执行解引用转换的3中情形:
      • T:Deref<Target=U>时,允许&T转换为&U
      • T:DerefMut<Target=U>时,允许 &mut T 转换为 &mut U
      • T:Deref<Target=U>时,允许&mut T转换为&U

3. Rc 引用计数智能指针

为了支持多重所有权!通过不可变引用,使你可以在程序不同部分之间共享只读数据。

3.1 Rc使用场景

  • 需要在heap上分配数据,这些数据被程序的多个部分读取(只读),但在编译时无法确定哪个部分最后使用完这些数据。
  • Rc只能用于单线程场景。

3.2 注意:

  1. Rc不在预导入模块;
  2. Rc::clone(&a) 函数:增加引用计数;
  3. Rc::strong_count(&a)函数:获得引用计数;
  4. Rc::weak_count(&a)函数;

3.3 例子

两个List共享另一个List的所有权

image-20220107102334482

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum List{
Cons(i32,Rc<List>),
Nil
}

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

fn main() {
let a = Rc::new(Cons(5,Rc::new(Cons(10,Rc::new(Nil)))));

let b = Cons(3,Rc::clone(&a));
let c = Cons(4,Rc::clone(&a));
}

3.4 Rc::clone() VS 类型的clone()方法

  • Rc::clone():增加引用,不会执行数据的深度拷贝操作;
  • 类型的clone(): 很多都会执行数据的深度拷贝操作;

4. RefCell和内部可变性

4.1 内部可变性(interior mutability)

  • 内部可变性是Rust的设计模式之一;
  • 允许在持有不可变引用的前提下对数据进行修改
    • 数据结构中使用了 unsafe 代码来绕过Rust 正常的可变性和借用规则。

4.2 RefCell

  • 与 Rc不同,RefCell类型代表了其持有数据的唯一所有权。
  • 只能用于单线程场景!
4.2.1 RefCell与Box的区别
Box RefCell
编译阶段强制代码遵守借用规则 运行阶段检查借用规则
否则出现错误 否则触发 panic

借用规则:在任何给定时间里,要么只能拥有一个可变引用,要么持有任意数量的不可变引用;

4.2.2 借用规则在不同阶段进行检查的比较
编译阶段 运行时
尽早暴露问题 问题暴露延后,甚至到生产环境
没有任何运行时开销 因借用计数产生些许性能损失
对大多数场景是最佳的 实现某些特定的内存安全场景(不可变环境中修改自身数据)
是Rust的默认行为
4.2.3 选择Box,Rc,RefCell的依据
Box Rc RefCell
同一数据的所有者 一个 多个 一个
可变性、借用检查 可变、不可变借用(编译时检查) 不可变借用(编译时检查) 可变、不可变借用(运行时检查)

4.3 内部可变性案例:

  • Trait Messengersend()方法接受一个不可变类型的引用,和一个字符串;
  • 但是,实现以上Trait的类MockMessenger,在实现send()方法时,需要修改自身属性的值,即需要使一个不可变引用可变。
  • 所以,可以将MockMessenger中需要修改的属性变为:RefCell<T>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
pub trait Messenger{
fn send(&self,msg:&str);
}

pub struct LimitTracker<'a,T:'a+Messenger>{
messenger:&'a T,
value:usize,
max:usize,
}

impl<'a,T> LimitTracker<'a,T>
where
T:Messenger,
{
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 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{
send_messages:RefCell<Vec<String>>,
}

impl MockMessenger{
fn new()->MockMessenger{
MockMessenger{
send_messages:RefCell::new(vec![]),
}
}
}

impl Messenger for MockMessenger{
fn send(&self,message:&str){
let len = self.send_messages.borrow().len();
self.send_messages.borrow_mut().push(String::from(message));
// borrow_mut(): 获得内部值得可变引用!
}
}

#[test]
fn it_sends_an_over_75_persent_warning_message(){
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger,100);
limit_tracker.set_value(80);
// borrow(): 获得内部值得不可变引用!
assert_eq!(mock_messenger.send_messages.borrow().len(),1);
}
}
  • borrow():返回内部值得不可变引用“智能指针Ref<T>”,实现了Deref;
  • borrow_mut():返回内部值得可变引用“智能指针RefMut<T>”,实现了Deref;

4.4 RefCell 在运行时记录借用信息

  • RefCell会记录当前存在多少个活跃的Ref和RefMut智能指针:

    • 每次调用borrow():不可变借用+1;
    • 任何一个Ref的值离开作用域被释放时:不可变借用数-1;
    • 每次调用borrow_mut():可变借用+1;
    • 任何一个RefMut的值离开作用域被释放时:可变借用数-1;
  • 以此技术来维护借用规则检查:

    • 任何一个给定时间里,只允许拥有多个不可变借用或一个可变借用。

4.5 拥有多重所有权的可变数据案例:Rc<T>+RefCell<T>

RefCell<T> 的一个常见用法是与 Rc<T> 结合。回忆一下 Rc<T> 允许对相同数据有多个所有者,不过只能提供数据的不可变访问。如果有一个储存了 RefCell<T>Rc<T> 的话,就可以得到有多个所有者 并且 可以修改的值了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::rc::Rc;
use std::cell::RefCell;

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

use crate::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::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!("b after = {:?}",c);
}