【RUST_BASIC】Rust 智能指针
1 Box<T>
最简单直接的智能指针是 box,其类型是 Box<T>
,其允许将一个值放在堆上,留在栈上的则是指向堆数据的指针,多用于如下场景:
- 当有一个在编译时未知大小的类型,而又想要在需要确切大小的上下文中使用这个类型值的时候
- 当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候
- 当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候
使用 box 在堆上储存一个 i32
:
fn main() {
let b = Box::new(5);
println!("b = {}", b);
}
使用 box 在堆上储存递归数据结构:
enum List {
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1,
Box::new(Cons(2,
Box::new(Cons(3,
Box::new(Nil))))));
}
Box<T> 类型实现了 Deref
trait,允许 Box<T> 值被当作引用对待;其也实现了 Drop
trait,离开作用域时指针所指向的堆数据也会被清除。
2 Deref trait
实现 Deref
trait 允许重载解引用运算符(dereference operator)*
。
使用解引用运算符来跟踪 i32
值的引用:
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}
如果编写为 assert_eq!(5, y);
,则会得到编译错误。
实现 *y
过程为 *(y.deref())
,其中 deref()
为 Deref
trait中的方法。
Deref
强制转换(deref coercions)是 Rust 在函数或方法传参上的一种便利,其将实现了 Deref
的类型的引用转换为原始类型通过 Deref
所能够转换的类型的引用:
fn hello(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
MyBox<T>
实现了 Deref
trait,Rust 可以通过 deref
调用将 &MyBox<String>
变为 &String
,标准库中提供了 String
上的 Deref
trait 实现,其会返回字符串 slice,Rust 再次调用 deref
将 &String
变为 &str
,这就符合 hello
函数的定义了。
如果 Rust 没有 Deref
强制转换则必须编写的代码如下:
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
Rust 在发现类型和 trait 实现满足三种情况时会进行 Deref
强制转换:
- 当
T: Deref<Target=U>
时从&T
到&U
。 - 当
T: DerefMut<Target=U>
时从&mut T
到&mut U
。 - 当
T: Deref<Target=U>
时从&mut T
到&U
。
3 Drop trait
指定在值离开作用域时应该执行的代码的方式是实现 Drop
trait,Drop
trait 要求实现一个叫做 drop
的方法,它获取一个 self
的可变引用:
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer { data: String::from("my stuff") };
let d = CustomSmartPointer { data: String::from("other stuff") };
println!("CustomSmartPointers created.");
}
Rust 并不允许我们主动调用 Drop
trait 的 drop
方法,因为整个 Drop
trait 存在的意义在于其是自动处理的,但是有时可能需要提早清理某个值,应该使用的是由标准库提供的 std::mem::drop
:
fn main() {
let c = CustomSmartPointer { data: String::from("some data") };
println!("CustomSmartPointer created.");
drop(c);
println!("CustomSmartPointer dropped before the end of main.");
}
4 Rc<T>
Rust 有一个叫做 Rc<T>
的类型,其名称为引用计数(reference counting)的缩写。引用计数意味着记录一个值引用的数量来知晓这个值是否仍在被使用,如果某个值有零个引用,就代表没有任何有效引用并可以被清理。
Rc<T>
用于当我们希望在堆上分配一些内存供程序的多个部分读取,而且无法在编译时确定程序的哪一部分会最后结束使用它的时候。如果确实知道哪部分是最后一个结束使用的话,就可以令其成为数据的所有者,正常的所有权规则就可以在编译时生效。
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));
}
Rc::clone
的实现并不像大部分类型的 clone
实现那样对所有数据进行深拷贝,其只会增加引用计数,其值可以通过调用 Rc::strong_count
函数获得:
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
println!("count after creating a = {}", Rc::strong_count(&a));
let b = Cons(3, Rc::clone(&a));
println!("count after creating b = {}", Rc::strong_count(&a));
{
let c = Cons(4, Rc::clone(&a));
println!("count after creating c = {}", Rc::strong_count(&a));
}
println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}
这段代码会打印出:
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2
通过不可变引用, Rc<T>
允许在程序的多个部分之间只读地共享数据。
另外注意 Rc<T>
只能用于单线程场景。
5 RefCell<T>
内部可变性(Interior mutability)是 Rust 中的一个设计模式,允许即使在有不可变引用时也可以改变数据,这通常是借用规则所不允许的。为了改变数据,该模式在数据结构中使用 unsafe
代码来模糊 Rust 通常的可变性和借用规则。
RefCell<T>
代表其数据的唯一的所有权。对于引用和 Box<T>
,借用规则的不可变性作用于编译时。对于 RefCell<T>
,这些不可变性作用于运行时;对于引用,如果违反这些规则,会得到一个编译错误。而对于 RefCell<T>,如果违反这些规则程序会 panic
并退出。
类似于 Rc<T>
,RefCell<T>
只能用于单线程场景。
如下为选择 Box<T>
,Rc<T>
或 RefCell<T>
的理由:
-
Rc<T>
允许相同数据有多个所有者;Box<T>
和RefCell<T>
有单一所有者。 -
Box<T>
允许在编译时执行不可变或可变借用检查;Rc<T>
仅允许在编译时执行不可变借用检查;RefCell<T>
允许在运行时执行不可变或可变借用检查。 - 因为
RefCell<T>
允许在运行时执行可变借用检查,所以我们可以在即便RefCell<T>
自身是不可变的情况下修改其内部的值。
当创建不可变和可变引用时,我们分别使用 &
和 &mut
语法。对于 RefCell<T>
来说,则是 borrow
和 borrow_mut
方法,borrow 方法返回 Ref<T>
类型的智能指针,borrow_mut
方法返回 RefMut<T>
类型的智能指针。
RefCell<T>
记录当前有多少个活动的 Ref<T>
和 RefMut<T>
智能指针。每次调用 borrow
,RefCell<T>
将活动的不可变借用计数加一,当 Ref<T>
值离开作用域时,不可变借用计数减一。就像编译时借用规则一样,RefCell<T>
在任何时候只允许有多个不可变借用或一个可变借用。
RefCell<T>
的一个常见用法是与 Rc<T>
结合,如果有一个储存了 RefCell<T>
的 Rc<T>
的话,就可以得到有多个所有者并且可以修改的值了。
#[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);
}
这里创建了一个 Rc<RefCell<i32>>
实例并储存在变量 value
中以便之后直接访问,接着在 a
中用包含 value
的 Cons
成员创建了一个 List
。需要克隆 value
以便 a
和 value
都能拥有其内部值 5
的所有权,而不是将所有权从 value
移动到 a
或者让 a
借用 value
。
将列表 a
封装进了 Rc<T>
这样当创建列表 b
和 c
时,他们都可以引用 a
。一旦创建了列表 a
、b
和 c
,我们将 value
的值加 10。为此对 value
调用了 borrow_mut
方法返回 RefMut<T>
智能指针,可以对其使用解引用运算符并修改其内部值。
6 循环引用
use std::rc::Rc;
use std::cell::RefCell;
use crate::List::{Cons, Nil};
#[derive(Debug)]
enum List {
Cons(i32, RefCell<Rc<List>>),
Nil,
}
impl List {
fn tail(&self) -> Option<&RefCell<Rc<List>>> {
match self {
Cons(_, item) => Some(item),
Nil => None,
}
}
}
fn main() {
let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
println!("a initial rc count = {}", Rc::strong_count(&a));
println!("a next item = {:?}", a.tail());
let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));
println!("a rc count after b creation = {}", Rc::strong_count(&a));
println!("b initial rc count = {}", Rc::strong_count(&b));
println!("b next item = {:?}", b.tail());
if let Some(link) = a.tail() {
*link.borrow_mut() = Rc::clone(&b);
}
println!("b rc count after changing a = {}", Rc::strong_count(&b));
println!("a rc count after changing a = {}", Rc::strong_count(&a));
// Uncomment the next line to see that we have a cycle;
// it will overflow the stack
// println!("a next item = {:?}", a.tail());
}
可以看到将列表 a
修改为指向 b
之后, a
和 b
中的 Rc<List>
实例的引用计数都是 2。在 main 的结尾,Rust 首先丢弃变量 b
,b
中 Rc<List>
实例的引用计数减 1。然而,因为 a
仍然引用 b
中的 Rc<List>
,Rc<List>
的引用计数是 1 ,所以 b
中的 Rc<List>
在堆上的内存不会被丢弃。接下来 Rust 会丢弃 a
,这同理会将 a
中 Rc<List>
实例的引用计数从 2 减为 1。这个实例的内存也不能被丢弃,因为其他的 Rc<List>
实例仍在引用它。这些列表的内存将永远保持未被回收的状态。
可以通过调用 Rc::downgrade
并传递 Rc<T>
实例的引用来创建其值的弱引用(weak reference),得到 Weak<T>
类型的智能指针,将 weak_count
加1。Rc<T>
类型使用 weak_count
来记录其存在多少个 Weak<T>
引用,weak_count
无需计数为 0 就能使 Rc<T>
实例被清理。
参考
https://kaisery.github.io/trpl-zh-cn/ch15-00-smart-pointers.html