Box,Deref和Drop trait,Rc<T>和Arc<T>,Cell<T>和RefCell<T>
指针 (pointer)是一个包含内存地址的变量。这个地址指向一些其它的数据。Rust 中最常见的指针是引用(reference)。引用以&符号为标志并借用了他们所指向的值。
另一方面,智能指针(smart pointers)是一类数据结构,它们表现类似于指针,但是也拥有额外的元数据,最明显的,它们拥有一个引用计数。引用计数记录智能指针总共有多少个所有者,并且当没有任何所有者时清除数据。
智能指针通常使用结构体实现。智能指针区别于常规结构体的显著特征在于其实现了 Deref(解引用)和 Drop(清理) trait。
考虑到智能指针是一个在 Rust经常被使用的通用设计模式,本章并不会覆盖所有现存的智能指针。很多库都有自己的智能指针而你也可以编写属于你自己的智能指针。这里将会讲到的是来自标准库中最常用的一些:
Box<T>,用于在堆上分配值Rc<T>,一个不可变的引用计数类型,用于同一线程内部其数据可以有多个所有者。Arc<T>,一个不可变的引用计数类型,用于多线程内部其数据可以有多个所有者。Cell<T>,只能用于 T 实现了 Copy 的情况。RefCell<T>,是一个在运行时而不是在编译时执行借用规则的类型。最简单直接的智能指针是box,其类型是 Box<T>。box允许你将一个值放在堆上而不是栈上。留在栈上的则是指向堆数据的指针。除了数据被储存在堆上而不是栈上之外,box 没有性能损失。
box适合用于如下场景:
trait 而不是其具体类型的时候。在讨论 Box<T> 的堆存储用例之前,让我们熟悉一下语法以及如何与储存在 Box<T> 中的值进行交互。使用box 在堆上储存一个i32:
fn main() {
let b = Box::new(5);
println!("b = {}", b);
}
这里定义了变量b存储在栈上,其值是一个指向被分配在堆上的值 5的Box,b指向5所在的内存。
递归类型(recursive type)的值可以拥有另一个同类型的值作为其的一部分。这会产生一个问题因为 Rust 需要在编译时知道类型占用多少空间。递归类型的值嵌套理论上可以无限的进行下去,所以Rust不知道递归类型需要多少空间。因为 box 有一个已知的大小,所以通过在循环类型定义中插入 box,就可以创建递归类型了。
例如这里有一个包含列表 1,2,3 的 cons list 的伪代码表示,其每一个列表在一个括号中:
(1, (2, (3, Nil)))
cons list利用两个参数来构造一个新的列表。通过对一个包含值的列表和另一个值调用 cons,可以构建由递归列表组成的 cons list。它并不是一个 Rust中常见的类型。大部分在 Rust中需要列表的时候,Vec<T>是一个更好的选择。定义包含一个 cons list的枚举:
use crate::List::{Cons, Nil};
enum List {
Cons(i32, List),
Nil,
}
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}
第一个 Cons 储存了1 和另一个List值。这个 List 是另一个包含 2 的 Cons 值和下一个List 值。接着又有另一个存放了 3 的Cons 值和最后一个值为 Nil的List,非递归成员代表了列表的结尾。
如果尝试编译得到错误:
$ cargo run
Compiling pointer v0.1.0 (/rsut/pointer)
error[E0072]: recursive type `List` has infinite size
--> src/main.rs:3:1
|
3 | enum List {
| ^^^^^^^^^
4 | Cons(i32, List),
| ---- recursive without indirection
|
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
4 | Cons(i32, Box<List>),
| ++++ +
For more information about this error, try `rustc --explain E0072`.
error: could not compile `pointer` due to previous error
这个错误表明这个类型 “有无限的大小”。其原因是 List 的一个成员被定义为是递归的:它直接存放了另一个相同类型的值。这意味着 Rust 无法计算为了存放 List 值到底需要多少空间。
因为 Box<T>是一个指针,我们总是知道它需要多少空间:指针的大小并不会根据其指向的数据量而改变。这意味着可以将 Box 放入 Cons成员中而不是直接存放另一个 List 值。Box 会指向另一个位于堆上的 List 值,而不是存放在Cons成员中:
use crate::List::{Cons, Nil};
#[derive(Debug)]
enum List {
Cons(i32, Box<List>),
Nil,
}
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
println!("{:#?}", list);
}
Cons 成员将会需要一个i32 的大小加上储存 box 指针数据的空间。Nil成员不储存值,所以它比 Cons 成员需要更少的空间。现在我们知道了任何 List 值最多需要一个 i32 加上 box 指针数据的大小。通过使用 box,打破了这无限递归的连锁,这样编译器就能够计算出储存 List 值需要的大小了。现在 Cons 成员看起来像什么:
box只提供了间接存储和堆分配;他们并没有任何其他特殊的功能。
Box<T> 类型是一个智能指针,因为它实现了 Deref trait,它允许 Box<T> 值被当作引用对待。当 Box<T>值离开作用域时,由于 Box<T> 类型 Drop trait 的实现,box所指向的堆数据也会被清除。
实现 Dereftrait 允许我们重载 解引用运算符(dereference operator)*。通过这种方式实现Deref trait 的智能指针可以被当作常规引用来对待。
常规引用是一个指针类型,一种理解指针的方式是将其看成指向储存在其他某处值的箭头。创建了一个i32值的引用,接着使用解引用运算符来跟踪所引用的值:
fn main() {
let a = 5;
let b = &a;
assert_eq!(5, a);
assert_eq!(5, *b);
}
变量 a 存放了一个 i32 值 5。b 等于 a 的一个引用。可以断言 a 等于 5。然而,如果希望对 b的值做出断言,必须使用 *b来追踪引用所指向的值(也就是 解引用),这样编译器就可以比较实际的值了。一旦解引用了 b,就可以访问 b 所指向的整型值并可以与 5 做比较。
相反如果尝试编写assert_eq!(5, b);,则会得到如下编译错误:
$ cargo run
Compiling pointer v0.1.0 (/rsut/pointer)
error[E0277]: can't compare `{integer}` with `&{integer}`
--> src/main.rs:6:5
|
6 | assert_eq!(5, b);
| ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
|
= help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}`
= help: the following other types implement trait `PartialEq<Rhs>`:
f32
f64
i128
i16
i32
i64
i8
isize
and 6 others
= note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0277`.
error: could not compile `pointer` due to previous error
不允许比较数字的引用与数字,因为它们是不同的类型。必须使用解引用运算符追踪引用所指向的值。
可以使用Box<T> 代替引用来重写代码:
fn main() {
let a = 5;
let b = Box::new(a);
assert_eq!(5, a);
assert_eq!(5, *b);
}
两个示例主要不同的地方就是将 b 设置为一个指向 a 值拷贝的 Box<T>实例,而不是指向a 值的引用。在最后的断言中,可以使用解引用运算符以 b 为引用时相同的方式追踪Box<T> 的指针。
为了体会默认情况下智能指针与引用的不同,让我们创建一个类似于标准库提供的Box<T>类型的智能指针:
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(t: T) -> Self {
MyBox(t)
}
}
这里定义了一个结构体 MyBox 并声明了一个泛型参数T,因为我们希望其可以存放任何类型的值。MyBox 是一个包含T类型元素的元组结构体。MyBox::new函数获取一个 T 类型的参数并返回一个存放传入值的 MyBox 实例。
fn main() {
let a = 5;
let b = MyBox::new(a);
assert_eq!(5, a);
assert_eq!(5, *b);
}
尝试以使用引用和 Box<T> 相同的方式使用 MyBox<T>,得到的编译错误是:
$ cargo run
Compiling pointer v0.1.0 (/rsut/pointer)
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
--> src/main.rs:14:19
|
14 | assert_eq!(5, *b);
| ^^
For more information about this error, try `rustc --explain E0614`.
error: could not compile `pointer` due to previous error
MyBox<T> 类型不能解引用,因为我们尚未在该类型实现这个功能。为了启用*运算符的解引用功能,需要实现 Deref trait。
为了实现 trait,需要提供 trait 所需的方法实现。Dereftrait,由标准库提供,要求实现名为deref 的方法,其借用 self 并返回一个内部数据的引用:
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
type Target = T; 语法定义了用于此trait 的关联类型。关联类型是一个稍有不同的定义泛型参数的方式,现在不用管它;deref方法体中写入了 &self.0,这样 deref 返回了我希望通过 *运算符访问的值的引用。.0用来访问元组结构体的第一个元素。
当我们在输入 *b时,Rust 事实上在底层运行了如下代码:
*(b.deref())
Rust 将 *运算符替换为先调用deref方法再进行普通解引用的操作,如此我们便不用担心是否还需手动调用deref方法了。
Deref 强制转换可以将 &String 转换为 &str,因为 String 实现了Deref trait 因此可以返回 &str,Deref强制转换的加入使得Rust 程序员编写函数和方法调用时无需增加过多显式使用&和 * 的引用和解引用:
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
fn hello(name: &str) {
println!("Hello, {name}!");
}
Rust 可以通过deref 调用将 &MyBox<String> 变为 &String。标准库中提供了String 上的Deref实现,其会返回字符串 slice。Rust 再次调用 deref 将 &String 变为 &str,这就符合 hello函数的定义了。
如果 Rust没有实现 Deref 强制转换,为了使用 &MyBox<String>类型的值调用 hello,则不得不编写自行解引用:
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
解引用多态有如下三种情况:
对于智能指针模式来说第二个重要的 trait 是 Drop,其允许我们在值要离开作用域时执行一些代码。指定在值离开作用域时应该执行的代码的方式是实现 Drop trait。Drop trait 要求实现一个叫做drop的方法,它获取一个 self的可变引用。
struct Dog {
name: String,
}
impl Drop for Dog {
fn drop(&mut self) {
println!("{} 资源被释放", self.name);
}
}
fn main() {
let a = Dog {
name: "小白".to_string(),
};
{
let b = Dog {
name: "小黑".to_string(),
};
}
let c = Dog {
name: "小黄".to_string(),
};
}
我们在 Dog 上实现了 Drop trait,并提供了一个调用 println!的 drop方法实现。drop 函数体是放置任何当类型实例离开作用域时期望运行的逻辑的地方。
当运行这个程序,会出现如下输出:
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/pointer`
小黑 资源被释放
小黄 资源被释放
小白 资源被释放
当实例离开作用域 Rust 会自动调用 drop,并调用我们指定的代码。
b由于生命周期小于a和c所以优先被释放,同生命周期变量以被创建时相反的顺序被释放,所以 c 在 a 之前被释放。
整个 Drop trait 存在的意义在于其是自动处理的。然而,有时你可能需要提早清理某个值。一个例子是当使用智能指针管理锁时;你可能希望强制运行 drop 方法来释放锁以便作用域中的其他代码可以获取锁:
fn main() {
let a = Dog {
name: "小白".to_string(),
};
drop(a);
{
let b = Dog {
name: "小黑".to_string(),
};
}
let c = Dog {
name: "小黄".to_string(),
};
}
运行这段代码会打印出如下:
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.18s
Running `target/debug/pointer`
小白 资源被清理
小黑 资源被清理
小黄 资源被清理
Rust建立在所有权之上的这一套机制,它要求一个资源同一时刻有且只能有一个拥有所有权的绑定或 &mut引用,这在大部分的情况下保证了内存的安全。但是这样的设计是相当严格的,在另外一些情况下,它限制了程序的书写,无法实现某些功能。因此,Rust在 std 库中提供了额外的措施来补充所有权机制,以应对更广泛的场景。
默认 Rust 中,对一个资源,同一时刻,有且只有一个所有权拥有者。Rc 和Arc使用引用计数的方法,让程序在同一时刻,实现同一资源的多个所有权拥有者,多个拥有者共享资源。
Rc 用于同一线程内部,通过 use std::rc::Rc 来引入。它有以下几个特点:
Rc 包装起来的类型对象,是 immutable的,即 不可变的。即你无法修改 Rc<T>中的 T 对象,只能读;Rc 只能用于同一线程内部,不能用于线程之间的对象共享(不能跨线程传递);Rc 实际上是一个指针,它不影响包裹对象的方法调用形式(即不存在先解开包裹再调用值这一说)。例子:
use std::rc::Rc;
fn main() {
let f = Rc::new(5);
let _f2 = f.clone();
let _f3 = f.clone();
println!("f 计数 = {}", Rc::strong_count(&f));
}
Weak 通过 use std::rc::Weak 来引入。
Rc 是一个引用计数指针,而 Weak 是一个指针,但不增加引用计数,是Rc 的 Weak 版。它有以下几个特点:
Rc<T> 调用 downgrade 方法而转换成 Weak<T>;Weak<T>可以使用 upgrade 方法转换成Option<Rc<T>>,如果资源已经被释放,则 Option 值为 None;例子:
use std::rc::Rc;
fn main() {
let f = Rc::new(5);
let w_f = Rc::downgrade(&f);
drop(f);
let s_f = w_f.upgrade();
println!("{:?}", s_f);
}
如果在转换成 Option<Rc<T>>之前是否那Option 值为None;
Arc 是原子引用计数,是Rc 的多线程版本。Arc 通过 std::sync::Arc引入。
它的特点:
Arc可跨线程传递,用于跨线程共享一个对象;Arc 包裹起来的类型对象,对可变性没有要求;Arc实际上是一个指针,它不影响包裹对象的方法调用形式(即不存在先解开包裹再调用值这一说);Arc 对于多线程的共享状态几乎是必须的(减少复制,提高性能)。与 Rc 类似,Arc 也有一个对应的 Weak类型,从 std::sync::Weak引入。
意义与用法与 Rc Weak基本一致,不同的点是这是多线程的版本。故不再赘述。
Arc将在多线程详细讨论,这里不过多讨论。
前面我们提到,Rust 通过其所有权机制,严格控制拥有和借用关系,来保证程序的安全,并且这种安全是在编译期可计算、可预测的。但是这种严格的控制,有时也会带来灵活性的丧失,有的场景下甚至还满足不了需求。
因此,Rust标准库中,设计了这样一个系统的组件:Cell, RefCell,它们弥补了 Rust所有权机制在灵活性上和某些场景下的不足。同时,又没有打破 Rust 的核心设计。它们的出现,使得 Rust 革命性的语言理论设计更加完整,更加实用。
具体是因为,它们提供了 内部可变性(相对于标准的继承可变性来讲的)。
通常,我们要修改一个对象,必须
mut;&mut 的形式,借用;而通过 Cell, RefCell,我们可以在需要的时候,就可以修改里面的对象。而不受编译期静态借用规则束缚。
Cell 有如下特点:
Cell<T> 只能用于 T 实现了 Copy 的情况;.get()方法,返回内部值的一个拷贝。比如:
use std::cell::Cell;
fn main() {
let c = Cell::new(5);
let f = c.get();
println!("f = {}", f);
}
.set() 方法,更新值:
use std::cell::Cell;
fn main() {
let c = Cell::new(5);
c.set(10);
println!("c = {}", c.get());
}
相对于 Cell 只能包裹实现了 Copy 的类型,RefCell用于更普遍的情况(其它情况都用RefCell)。
相对于标准情况的 静态借用,RefCell 实现了 运行时借用,这个借用是临时的。这意味着,编译器对 RefCell 中的内容,不会做静态借用检查,也意味着,出了什么问题,用户自己负责。
RefCell 的特点:
Copy 时,直接选RefCell;RefCell 只能用于线程内部,不能跨线程;RefCell 常常与Rc配合使用(都是单线程内部使用);实例:
use std::{cell::RefCell, collections::HashMap, rc::Rc};
fn main() {
let s_map: Rc<RefCell<_>> = Rc::new(RefCell::new(HashMap::new()));
s_map.borrow_mut().insert("aaa", 111);
s_map.borrow_mut().insert("bbb", 222);
s_map.borrow_mut().insert("ccc", 333);
}
从上例可看出,用了 RefCell 后,外面是 不可变引用 的情况,一样地可以修改被包裹的对象。
不可变借用被包裹值。同时可存在多个不可变借用。
use std::cell::RefCell;
fn main() {
let c = RefCell::new(5);
let b_f1 = c.borrow();
let b_f2 = c.borrow();
}
可变借用被包裹值。同时只能有一个可变借用。
use std::cell::RefCell;
fn main() {
let c = RefCell::new(5);
let mut b_m_f = c.borrow_mut();
}
取出包裹值
use std::cell::RefCell;
fn main() {
let c = RefCell::new(5);
println!("{}", c.into_inner());
}
下面这个示例,表述的是如何实现两个对象的循环引用。综合演示了 Rc, Weak, RefCell 的用法
use std::{
cell::RefCell,
rc::{Rc, Weak},
};
struct Owner {
name: String,
gadgets: RefCell<Vec<Weak<Gadget>>>,
}
struct Gadget {
id: i32,
owner: Rc<Owner>,
}
fn main() {
// 创建一个可计数的Owner。
// 注意我们将gadgets赋给了Owner。
// 也就是在这个结构体里, gadget_owner包含gadets
let gadget_owner = Rc::new(Owner {
name: "Gadget 1".to_string(),
gadgets: RefCell::new(Vec::new()),
});
// 首先,我们创建两个gadget,他们分别持有 gadget_owner 的一个引用。
let g1 = Rc::new(Gadget {
id: 1,
owner: gadget_owner.clone(),
});
let g2 = Rc::new(Gadget {
id: 2,
owner: gadget_owner.clone(),
});
// 我们将从gadget_owner的gadgets字段中持有其可变引用
// 然后将两个gadget的Weak引用传给owner。
gadget_owner.gadgets.borrow_mut().push(Rc::downgrade(&g1));
gadget_owner.gadgets.borrow_mut().push(Rc::downgrade(&g2));
// 遍历 gadget_owner的gadgets字段
for gadget_opt in gadget_owner.gadgets.borrow().iter() {
// gadget_opt 是一个 Weak<Gadget> 。 因为 weak 指针不能保证他所引用的对象
// 仍然存在。所以我们需要显式的调用 upgrade() 来通过其返回值(Option<_>)来判
// 断其所指向的对象是否存在。
// 当然,这个Option为None的时候这个引用原对象就不存在了。
if let Some(opt) = gadget_opt.upgrade() {
println!("Gadget {} owned by {}", opt.id, opt.owner.name);
}
}
// 在main函数的最后, gadget_owner, gadget1和daget2都被销毁。
// 具体是,因为这几个结构体之间没有了强引用(`Rc<T>`),所以,当他们销毁的时候。
// 首先 gadget1和gadget2被销毁。
// 然后因为gadget_owner的引用数量为0,所以这个对象可以被销毁了。
// 循环引用问题也就避免了
} 如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!