Rust学习实践(四) -- Rust特性:智能指针
Rust学习实践,进一步学习梳理Rust特性:智能指针。
1. 背景
继续进一步学习下Rust特性,本篇学习梳理:智能指针。
说明:本博客作为个人学习实践笔记,可供参考但非系统教程,可能存在错误或遗漏,欢迎指正。若需系统学习,建议参考原链接。
2. 概要说明
智能指针往往是基于结构体实现,它与自定义的结构体最大的区别在于它实现了 Deref 和 Drop 特征:
Deref可以让智能指针像引用那样工作,这样就可以写出同时支持智能指针和引用的代码,例如*TDrop允许指定智能指针超出作用域后自动执行的代码,例如做一些数据清除等收尾工作
Rust中的智能指针有好几种,此处介绍以下最常用的几种:
Box<T>:将值分配到堆上Rc<T>:引用计数类型,允许多所有权存在Ref<T>和RefMut<T>:允许将借用规则检查从编译期移动到运行期进行(通过RefCell<T>实现)。
下述涉及代码,也可见:test_smart_ptr
3. Box<T>智能指针
Box<T>是Rust中最常见的智能指针,除了将值存储在堆上外,并没有其它性能上的损耗。
使用场景:
- 特意的将数据分配在堆上
- 数据较大时,又不想在转移所有权时进行数据拷贝
- 类型的大小在编译期无法确定,但是我们又需要固定大小的类型时
- 特征对象,用于说明对象实现了一个特征,而不是某个特定的类型
基本用法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
fn test_simple() {
// 创建一个智能指针指向了存储在堆上的 3,并且 a 持有了该指针
{
let a = Box::new(3);
// 智能指针实现了Deref 和 Drop特征,此处会隐式调用Deref特征,对指针进行解引用
println!("a = {}", a); // a = 3
// 下面一行代码将报错,表达式无法隐式调用Deref特征解引用
// let b = a + 1; // cannot add `{integer}` to `Box<{integer}>`
// 因此需要显式使用`*`,调用Deref特征解引用
let b = *a + 1;
}
// 作用域结束,上面的智能指针就被释放了,因为Box实现了Drop特征
// 下面使用会报错:cannot find value `a` in this scope
// let c = *a + 2;
}
fn test_array() {
// 在栈上创建一个长度为1000的数组
let arr = [0;1000];
// 将arr所有权转移arr1,由于 `arr` 分配在栈上,因此这里实际上是直接重新深拷贝了一份数据
let arr1 = arr;
// arr 和 arr1 都拥有各自的栈上数组,因此不会报错
println!("{:?}", arr.len());
println!("{:?}", arr1.len());
// 在堆上创建一个长度为1000的数组,然后使用一个智能指针指向它
let arr = Box::new([0;1000]);
// 将堆上数组的所有权转移给 arr1,由于数据在堆上,因此仅仅拷贝了智能指针的结构体,底层数据并没有被拷贝
// 所有权顺利转移给 arr1,arr 不再拥有所有权
let arr1 = arr;
println!("{:?}", arr1.len());
// 由于 arr 不再拥有底层数组的所有权,因此下面代码将报错
// println!("{:?}", arr.len());
}
fn test_box_arr() {
let arr = vec![Box::new(1), Box::new(2)];
// 使用 & 借用数组中的元素,否则会报所有权错误
let (first, second) = (&arr[0], &arr[1]);
// 表达式不能隐式的解引用,因此必须使用 ** 做两次解引用,
// 第一次将 &Box<i32> 类型转成 Box<i32>,第二次将 Box<i32> 转成 i32
let sum = **first + **second;
}
Box::leak:
Box中还提供了一个非常有用的关联函数:Box::leak,它可以消费掉 Box 并且强制目标值从内存中泄漏。
1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let s = gen_static_str();
println!("{}", s);
}
fn gen_static_str() -> &'static str{
let mut s = String::new();
s.push_str("hello, world");
// 原来的 String 被消费掉,但是它的内容被转移到了堆上,并且被标记为 'static,返回了不可变的引用
Box::leak(s.into_boxed_str())
}
Box 背后是调用 jemalloc 来做内存管理(glibc默认使用ptmalloc),所以堆上的空间无需我们手动管理。
4. Deref 和 Drop 特征
4.1. Deref解引用
当我们对智能指针 Box 进行解引用时,实际上 Rust 为我们调用了以下方法:*(y.deref())
- 即:首先调用
deref方法返回值的常规引用(由于所有权系统存在,不直接返回值,因而不涉及所有权转移),然后通过*对常规引用进行解引用,最终获取到目标值。 - 需要注意的是,
*不会无限递归替换,从*y到*(y.deref())只会发生一次,而不会继续进行替换然后产生形如*((y.deref()).deref())这样的表达式。
以下面示例进一步理解解引用动作:自定义简单的智能指针,实现Deref特征
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
use std::ops::Deref;
// newtype MyBox<T>,定义一个结构体,其中包含一个泛型 T
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
// 为智能指针实现 Deref 特征
impl<T> Deref for MyBox<T> {
// 类型别名(关联类型 Target,主要用于提升代码可读性)
type Target = T;
// 当解引用 MyBox 智能指针时,返回元组结构体中的元素 &self.0
fn deref(&self) -> &Self::Target {
// 返回的是一个常规引用,可以被 * 进行解引用
&self.0
}
}
fn test_deref() {
let x = 5;
let y = MyBox::new(x);
println!("x = {}, y = {}", x, *y);
}
4.2. 隐式Deref转换
对于函数和方法的传参,Rust 提供了一个极其有用的隐式转换:Deref转换。
若一个类型实现了 Deref 特征,那它的引用在传给函数或方法时,会根据参数签名来决定是否进行隐式的 Deref 转换。
规则总结:一个类型为 T 的对象 foo,如果 T: Deref<Target=U>,那么,相关 foo 的引用 &foo 在应用的时候会自动转换为 &U。
1
2
3
4
5
6
7
8
9
10
11
// 隐式 Deref
fn test_auto_deref() {
// String 实现了 Deref 特征,可以在需要时自动被转换为 &str 类型
let s = String::from("hello world");
// &s 是一个 &String 类型,当它被传给 display 函数时,自动通过 Deref 转换成了 &str
display(&s)
}
fn display(s: &str) {
println!("{}", s);
}
可通过 标准库手册 查询String类型(Struct std::string::String),及其实现的Deref特征:impl-Deref-for-String。
4.3. 三种Deref转换
除了上面的 Deref 不可变引用转换,Rust还提供了另外两种 Deref 转换,3种转换规则如下:
- 当
T: Deref<Target=U>,可以将&T转换为&U - 当
T: DerefMut<Target=U>,可以将&mut T转换为&mut U- 注意:要实现
DerefMut必须要先实现Deref特征
- 注意:要实现
- 当
T: Deref<Target=U>,可以将&mut T转换为&U- Rust 可以把可变引用隐式的转换成不可变引用,但反之则不行
4.4. Drop释放资源
在Rust中,可以指定在一个变量超出作用域时,执行一段特定的代码,最终编译器将帮你自动插入这段收尾代码。该段代码就是 Drop 特征的 drop 方法。(和C++中的析构函数类似)
简单实现示例:
1
2
3
4
5
6
7
8
9
10
11
struct Foo;
impl Drop for Foo {
fn drop(&mut self) {
println!("Dropping Foo!")
}
}
fn test_drop() {
let _foo = Foo;
println!("Running!");
}
运行:函数最后会调用到drop函数
1
2
Running!
Dropping Foo!
Drop 的顺序:
- 变量级别,按照逆序的方式,比如:若
_x在_foo之前创建,则_x在_foo之后被drop - 结构体内部,按照顺序的方式,比如:结构体
_x中的字段按照定义中的顺序依次drop
Rust 自动为几乎所有类型都实现了 Drop 特征,因此就算不手动为结构体实现 Drop,它依然会调用默认实现的 drop 函数,同时再调用每个字段的 drop 方法。
手动释放:
针对编译器实现的 drop 函数,会拿走变量的所有权,因此,如果想要手动释放资源,可以使用 std::mem::drop 函数。
std::mem::drop 函数的签名为:pub fn drop<T>(_x: T),可见 标准库手册:mem drop
示例:
1
2
3
4
5
6
7
8
9
10
11
fn test_mem_drop() {
let mut foo = Foo;
// 报错:explicit destructor calls not allowed
// foo.drop();
// 调用编译器自动生成的drop函数,释放内存
drop(foo);
// 以下代码会报错:借用了所有权被转移的值
// println!("Running!:{:?}", foo);
}
使用场景:
- 回收内存资源,比如文件描述符、网络socket 等
- 执行一些收尾工作
无法为一个类型同时实现Copy和Drop特征:因为实现了Copy的类型会被编译器隐式的复制,因此非常难以预测析构函数执行的时间和频率。因此这些实现了Copy的类型无法拥有析构函数。
1
2
3
4
5
6
7
8
9
// 编译报错:error[E0184]: the trait `Copy` cannot be implemented for this type; the type has a destructor
#[derive(Copy)]
struct Foo;
impl Drop for Foo {
fn drop(&mut self) {
println!("Dropping Foo!")
}
}
5. 引用计数智能指针Rc<T>和Arc<T>
Rust的所有权机制,只允许一个数据在同一时刻只有一个所有者,但部分场景需要多个所有者,比如:
- 在图数据结构中,多个边可能会拥有同一个节点
- 在多线程中,多个线程可能会持有同一个数据,但受限于Rust的安全机制,无法同时获取该数据的可变引用
为了解决此类问题,Rust通过引用计数的方式,允许一个数据资源在同一时刻拥有多个所有者。有两种实现机制:
Rc<T>,Rc的全称是reference counting,引用计数- 一旦最后一个拥有者消失,则资源会自动被回收,这个生命周期是在编译期就确定下来的
Rc只能用于同一线程内部,想要用于线程之间的对象共享,需要使用Arc
Arc<T>,Arc的全称是atomic reference counting,原子引用计数Arc<T>能保证线程安全,它使用原子操作来保证线程安全,因此它比Rc<T>慢Arc和Rc并没有定义在同一个模块,前者通过use std::sync::Arc来引入,后者通过use std::rc::Rc
Rc<T>和Arc<T>指向的数据都是不可变引用。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use std::rc::Rc;
fn rc_ptr() {
let s = String::from("hello, world");
// 使用Rc类型,Rc::new 创建一个引用计数类型的智能指针
// 智能指针 Rc<T> 在创建时,引用计数会加1,可通过关联函数 Rc::strong_count(&a) 获取引用计数
let a = Rc::new(s);
println!("a referce count1: {}", Rc::strong_count(&a));
// 用 Rc::clone 克隆了一份智能指针,引用计数也会加1
// 此处clone不是深拷贝,仅仅复制了智能指针并增加了引用计数,并没有克隆底层数据
let b = Rc::clone(&a);
println!("a referce count2: {}", Rc::strong_count(&a));
println!("b referce count: {}", Rc::strong_count(&b));
// 使用 Arc::new 创建智能指针
use std::sync::Arc;
let s1 = Arc::new(String::from("test arc"));
// 可通过 *s1 解引用获取值
println!("s1 referce count: {}, *s1:{}", Arc::strong_count(&s1), *s1);
}
6. 可变智能指针Cell<T>和RefCell<T>
上节中的Rc<T>和Arc<T>是不可变的,Rust提供了 Cell 和 RefCell 用于内部可变性,即在拥有不可变引用的同时修改目标数据。
内部可变性:内部可变性是Rust中的一种设计模式,它允许在数据存在不可变引用时对数据进行修改(通常情况下,借用规则不允许此操作),底层基于unsafe机制修改。
Cell 和 RefCell 在功能上没有区别,区别在于 Cell<T> 适用于 T 实现 Copy 的情况。比如,下述示例中可以Cell::new("asdf"),但不能Cell::new(String::from("asdf"))。
Cell示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::cell::Cell;
fn cell_ptr() {
// 此处的"asdf"是&str类型,实现了Copy
let c = Cell::new("asdf");
// String 没有实现 Copy 特征,所以Cell中不能存放String类型
// let c2 = Cell::new(String::from("asdf"));
// 编译报错:error[E0599]: the method `get` exists for struct `Cell<String>`, but its trait bounds were not satisfied
// doesn't satisfy `String: Copy`
// println!("c2:{}", c2.get());
// 获取当前值,不是实时指针,后面修改不影响此处
let one = c.get();
// 获取值到one里后,通过c还能做修改,修改c不影响one的值
c.set("qwer");
let two = c.get();
// 结果:one:asdf, two:qwer, c.get():qwer
println!("one:{}, two:{}, c.get():{}", one, two, c.get());
// 不可通过 *c 解引用获取值
// println!("*c:{}", *c);
}
RefCell示例:
1
2
3
4
5
6
7
8
9
use std::cell::RefCell;
fn refcell_ptr() {
let s = RefCell::new(String::from("hello, world"));
let s1 = s.borrow();
// 编译器不会报错,但违背了借用规则,会导致运行期 panic
let s2 = s.borrow_mut();
println!("{},{}", s1, s2);
}
Cell和RefCell说明:
- 与
Cell用于可Copy的值不同,RefCell用于引用 RefCell只是将借用规则从编译期推迟到程序运行期,并不能帮你绕过这个规则RefCell适用于编译期误报或者一个引用被在多处代码使用、修改以至于难于管理借用关系时- 使用
RefCell时,违背借用规则会导致运行期的 panic Cell没有额外的性能损耗,RefCell有少量运行时开销,因为RefCell需要维护借用状态
注意:
Cell和RefCell类型都可以看作是智能指针,但它们并不像Box<T>或Rc<T>那样直接实现Deref和DerefMuttraits 来支持解引用操作。这是因为它们的设计目的是为了提供特定的内部可变性机制,而不仅仅是封装所有权和生命周期。- 即不能直接通过
*解引用获取值,需要使用对应的方法进行访问,比如 Cell:get/set,RefCell:borrow/borrow_mut,具体见标准库。
Rc 和 RefCell 组合使用:
在 Rust 中,一个常见的组合就是 Rc 和 RefCell 在一起使用,前者可以实现一个数据拥有多个所有者,后者可以实现数据的可变性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use std::cell::RefCell;
use std::rc::Rc;
fn rc_refcell() {
// 用 RefCell<String> 包裹一个字符串,并通过 Rc 创建了它的三个所有者
let s = Rc::new(RefCell::new("hello world".to_string()));
let s1 = s.clone();
let s2 = s.clone();
// 通过其中一个所有者 s2 对字符串内容进行修改
s2.borrow_mut().push_str(", xxxxx!");
// let mut s2 = s.borrow_mut();
// s2.push_str("xxxxx");
// 打印结果如下:三者共享同一个底层数据,都发生了变化
// RefCell { value: "hello world, xxxxx!" }
// RefCell { value: "hello world, xxxxx!" }
// RefCell { value: "hello world, xxxxx!" }
println!("{:?}\n{:?}\n{:?}", s, s1, s2);
}
7. 循环引用与自引用
循环引用可能引起内存泄漏。一个典型的例子就是同时使用 Rc<T> 和 RefCell<T> 创建循环引用,最终这些引用的计数都无法被归零,因此 Rc<T> 拥有的值也不会被释放清理。
循环引用示例:a-> b -> a -> b
可通过Weak来解决循环引用问题,通过use std::rc::Weak引入。
Weak非常类似于Rc,但是与Rc持有所有权不同,Weak不持有所有权,它仅仅保存一份指向数据的弱引用- 弱引用不保证引用关系依然存在,如果不存在,就返回一个
None
- 弱引用不保证引用关系依然存在,如果不存在,就返回一个
- 如果想要访问数据,需要通过
Weak指针的upgrade方法实现,该方法返回一个类型为Option<Rc<T>>的值。
此处暂时留个印象,后续再进一步学习,需要再结合更多的实际项目和实践加强体感。
8. 小结
学习Rust的几种智能指针。
9. 参考
1、Rust语言圣经(Rust Course) – 智能指针
2、The Rust Programming Language – Smart Pointers
3、标准库手册