Rust:认识各种盒子吧!(Box, Rc, Arc, Cell, RefCell)
分类
Rust 有各种智能指针,用于管理内存。通常可以按照这些方法进行分类:
-
所有权。是独占所有权,还是共享所有权。
-
可变性。是可变的,还是不可变的。
-
原子性。是原子的,还是非原子的。
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 的修改,会编译失败:
但加到后面就不会:
这是因为,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>
几乎是一样的,区别在于:
-
RefCell<T>
提供引用,而Cell<T>
提供值(会产生 Copy)。 + 因此,Cell 通过 get/set 来访问值,而 RefCell 通过 borrow/borrow_mut 并结合解引用等方法来访问值。 -
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;
Rc
和 Arc
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 则是线程安全的版本。我们写一个程序来对比一下 Rc
和 Arc
的区别。
下面的程序中,我们创建了一个 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 访问或者函数传参时,会自动解引用。