Rust:认识各种盒子吧!(Box, Rc, Arc, Cell, RefCell)

分类

Rust 有各种智能指针,用于管理内存。通常可以按照这些方法进行分类:

  1. 所有权。是独占所有权,还是共享所有权。

  2. 可变性。是可变的,还是不可变的。

  3. 原子性。是原子的,还是非原子的。

Sync Trait

这里原子与否常常取决于泛型参数是否原子。我们将其抽象为概念 Sync,即是否可以在多个线程间安全地共享。

  • Sync:如果类型 &T 是线程安全的,则 T 是 Sync 的。 例如,我们说,Box<T> 是 Sync 的。意思是,Box<T> 是否线程安全,取决于 T 是否线程安全。

Box

  • Box<T>:
    • 用途:在栈帧里得到一个内存单元(Box),然后用这个内存单元存放一个地址,这个地址指向堆上的内存单元(T)。

    • 生命期:这个内存单元(T)的生命周期是由 Box 管理的。Box 本身的生命周期是由栈帧管理的。

      这么来看,其实 Box<T> 类似 C++ 的 std::unique_ptr<T>。不同的是,Box<T> 是不可变的,而 std::unique_ptr<T> 是可变的。

    • 可变性:Box 本身是不可变的,但是它指向的内存单元(T)是可变的。

    • 原子性:Box<T> 是 Sync 的。但多线程用了意义不大,因为所有权是独占的。

      下面是一个测试,用于证实 Box 的指向的内存是可变的。

      1    // 在堆上分配一个 i32 类型的值,然后用栈帧里的 b 变量来存放它的地址
      2    let b = Box::<i32>::new(1);
      3    // 通过解引用操作符来获取 Box 中的值
      4    let k = b.deref();
      5    println!("k = {}", k);
      6    assert_eq!(*k, 1);
      

      运行结果:

      1k = 1
      
      1    let b = Box::<i32>::new(1);
      2    let mut k = b.deref();
      3    // 这里虽然改变了 k,但 k 是一个引用,堆上的值并没有改变
      4    k = &2;
      5    println!("{}", k);
      6    println!("{}", b);
      

      运行结果:

      12
      21
      
      1    let b = Box::<i32>::new(1);
      2    let mut k = b.deref();
      3    // 试图改变堆上的值
      4    *k = 2;
      5    println!("{}", k);
      6    println!("{}", b);
      

      运行结果:报错。这是因为 k 是一个 & 即不可变引用。改成这样呢?

      1    let b = Box::<i32>::new(1);
      2    let k = b.deref_mut();
      3    *k = 2;
      4    println!("{}", k);
      5    println!("{}", b);
      

      报错:cannot borrow b as mutable. 这是因为 b 是一个不可变的 Box。改成这样呢?

      1    let mut b = Box::<i32>::new(1);
      2    let k = b.deref_mut();
      3    *k = 2;
      4    println!("{}", k);
      5    println!("{}", b);
      

      运行结果:

      12
      22
      

内部可变性

1struct S<'x_lt> {
2    x: &'x_lt mut i32,
3}
4let mut x = 5;
5let s = S { x: &mut x };
6// incr x by 1
7*s.x += 1;

上面的代码中,虽然 s 是不可变的,但是它的字段 x 是可变的。这种可变性是内部的。

同时,如果在修改 s 内部的 x 前,加入对 x 的修改,会编译失败:

upgit_20230127_1674814052.png

但加到后面就不会:

upgit_20230127_1674814067.png

这是因为,Rust 有一条原则:可以同时拥有一个值的不可变引用,但是只能有一个可变引用

  • x += 1; 在中间时,持有可变引用的是 s。因此不能再进行修改,否则造成两个可变引用同时存在。

  • x += 1; 在后面时,s 看似持有可变引用,但没有修改 s.x 的意图,因此不会造成两个可变引用同时存在。

在旧版编译器中,会更加严格,连 x += 1; 在后面都不行。

那么,什么时候可以同时持有两个可变引用呢?这就需要用到 RefCell

Cell

Cell<T>

  • 用途:在栈帧里得到一个内存单元(Cell),然后用这个内存单元直接存放一个值(T)。所以某种意义上,Cell 不具有 Box 那样的物理含义,只是编译器层面的一个约束。

  • 所有权:独占。

  • 生命期:这个内存单元(T)的生命周期是由 Cell 管理的。Cell 本身的生命周期是由栈帧管理的。

    在 C++ 中,不需要 Cell 或者 RefCell,因为 C++ 的内存单元可以自由操作。

  • 可变性:Cell 本身是不可变的,它存放的值(T)是可变的。不过使用 get 只能得到值的副本(因此 T 必须实现 Copy),而使用 get_mut 则可以得到值的可变引用。

  • 原子性:Cell<T> 不是 Sync 的。多线程别用。

    下面是一个测试,用于证实 Cell 的值是可变的。

    1    let c = Cell::<i32>::new(1);
    2    let k = c.get();
    3    println!("k = {}", k);
    4    assert_eq!(k, 1);
    

    运行结果:

    1k = 1
    
    1    let c = Cell::<i32>::new(1);
    2    let mut k = c.get();
    3    // 这里 get 之后得到的是副本。
    4    k = 2;
    5    println!("{}", k);
    6    println!("{}", c.get());
    

    运行结果:

    12
    21
    
    1    let mut c = Cell::new(1);
    2    c.set(2);
    3    assert_eq!(c.get(), 2);
    4    *c.get_mut() += 1;
    5    assert_eq!(c.get(), 3);
    

    运行结果:Pass

RefCell<T>

Cell<T> 几乎是一样的,区别在于:

  1. RefCell<T> 提供引用,而 Cell<T> 提供值(会产生 Copy)。 + 因此,Cell 通过 get/set 来访问值,而 RefCell 通过 borrow/borrow_mut 并结合解引用等方法来访问值。

  2. RefCell<T> 可能引起 panic,而 Cell<T> 不会。

前面提到,内部可变性,使得我们可以把一个可变的值放到不可变的容器里。在工程实践中,RefCell 的主要作用是,允许我们在同一作用域中多次对同一值进行更改。例子:

1let c = RefCell::<i32>::new(1);
2*c.borrow_mut() += 1;
3*c.borrow_mut() += 1;
4assert_eq!(*c.borrow(), 3);

Ref 和 &

我们注意到 RefCell<T> 提供的引用是 Ref<T>,而 Cell<T> 提供的是 &T

Ref<T>&T 有什么区别呢?

类似 C++ 的 std::ref(T)T&?

  • Ref<T> 是一个智能指针,它的生命周期是由 RefCell<T> 管理的,从而可以具有在运行时进行借用检查。如果在同一作用域中获取了可变借用,就会导致运行时错误而不是编译时错误。。&T 是一个普通的引用,它的生命周期是由栈帧管理的。

    例子:

    1    let c = RefCell::<i32>::new(1);
    2    let k = c.borrow_mut();
    3    // 这里 k 的生命周期是由 c 管理的。
    4    // 这里 k 的生命周期是由栈帧管理的。
    5    let k2 = &c;
    
  • Ref<T>&T 都是不可变的,但是 Ref<T> 可以转换为 RefMut<T>,而 &T 不能转换为 &mut T

    例子:

    1    let c = RefCell::<i32>::new(1);
    2    let k = c.borrow();
    3    let k2 = k.borrow_mut();
    

    不过这么做没有什么意义。

除此之外,Rust 还提供关键字 ref,和 & 的功能一样,只是语法略有不同。

1// let ref a=2; 和 let a = &2; 是等价的。
2let ref a=2; let a = &2;

RcArc

Rc(Reference Count) 和 Arc(Atomic Reference Count) 是 Rust 提供的引用计数智能指针。

相关的 API 如下:

  • fn new(value: T) -> Rc<T>: 用于创建一个新的 Rc<T>,其中 T 是一个普通的类型。

    • 效果:创建一个内容不可变只读的引用计数容器。
  • fn clone(&self) -> Rc<T>: 用于克隆一个 Rc<T>,其中 T 是一个普通的类型。

    • 效果:引用加一,同时计数加一。
  • fn strong_count(&self) -> usize: 用于获取引用计数的值。

    • 效果:返回引用计数的值。
  • fn weak_count(&self) -> usize: 用于获取弱引用计数的值。

    • 效果:返回引用计数的值。

例1,让三个作用域共享同一个 Rc<T>

 1let a = Rc::new(1);
 2println!("rc = {}", Rc::strong_count(&a));
 3{
 4    let b = a.clone();
 5    println!("rc = {}", Rc::strong_count(&b));
 6    {
 7        let b = a.clone();
 8        println!("rc = {}", Rc::strong_count(&b));
 9        {
10            let b = a.clone();
11            println!("rc = {}", Rc::strong_count(&b));
12        }
13    }
14}

结果:

1rc = 1
2rc = 2
3rc = 3
4rc = 4

例2,让三个作用域共享同一个 Rc<T>,并且各自修改其中的值。打印引用计数和值。

 1let a = Rc::new(RefCell::new(0));
 2*a.borrow_mut() += 1;
 3println!("rc = {}, a = {}", Rc::strong_count(&a), *(*a).borrow());
 4{
 5    let b = a.clone();
 6    *b.borrow_mut() += 1;
 7    println!("rc = {}, b = {}, a = {}", Rc::strong_count(&a), *(*b).borrow(), *(*a).borrow());
 8    {
 9        let c = a.clone();
10        *c.borrow_mut() += 1;
11        println!("rc = {}, c = {}, b = {}, a = {}", 
12            Rc::strong_count(&a), *(*c).borrow(), *(*b).borrow(), *(*a).borrow()
13        );
14    }
15}

结果:

1rc = 1, a = 1
2rc = 2, b = 2, a = 2
3rc = 3, c = 3, b = 3, a = 3

Arc 则是线程安全的版本。我们写一个程序来对比一下 RcArc 的区别。

下面的程序中,我们创建了一个 Arc,用于包裹一个使用 Mutex 加锁的 i32 值,并且在不同的线程中对其进行修改。

 1let data = Arc::new(Mutex::new(0));
 2let mut handles = vec![];
 3const N: usize = 10;
 4
 5for _ in 0..N {
 6    let data = data.clone();
 7    handles.push(thread::spawn(move || {
 8        let mut data = data.lock().unwrap();
 9        *data += 1;
10    }));
11}
12
13for handle in handles {
14    handle.join().unwrap();
15}
16
17let data = data.lock().unwrap();
18println!("Result: {}", *data);
19assert_eq!(N, *data);

在这里,Arc::new(Mutex::new(0)); 中,Arc 用于确保引用计数的原子性,Mutex 用于确保数据的原子性。

Traits

Deref trait

Deref trait 用于实现解引用运算符 *

 1use std::ops::Deref;
 2struct MyBox<T>(T);
 3impl<T> Deref for MyBox<T> {
 4    type Target = T;
 5    fn deref(&self) -> &T {
 6        &self.0
 7    }
 8}
 9let x = 5;
10let y = MyBox(x);
11assert_eq!(5, x);
12assert_eq!(5, *y);
13assert_eq!(*(y.deref()), *y);

同时,使用 Dot 访问或者函数传参时,会自动解引用。

参考资料

  1. rust - When I can use either Cell or RefCell, which should I choose? - Stack Overflow

  2. 19-探讨Rust智能指针 II_哔哩哔哩_bilibili

  3. Cell 与 RefCell 内部可变性 - Rust语言圣经(Rust Course)