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
所指向的堆数据也会被清除。
实现 Deref
trait 允许我们重载 解引用运算符(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 所需的方法实现。Deref
trait,由标准库提供,要求实现名为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,所以这个对象可以被销毁了。
// 循环引用问题也就避免了
}
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!