智能指针
前言
本章节主要介绍 Rust 中“指针”的相关概念,包含智能指针以及其与引用的区别。
介绍标准库中常见的智能指针:
Box<T>
:在 heap 内存上分配值。Rc<T>
:启用多重所有权的引用计数类型。Ref<T>
和RefMut<T>
,通过RefCell<T>
访问:在运行时而不是编译时强制借用规则的类型。
此外还包括:
- 内部可变模式(interior mutability pattern):不可变类型暴露出可修改其内部值的 API。
- 引用循环(reference cycles):它们如何泄露内存,以及如何防止其发生。
智能指针基础
指针:一个变量在内存中包含的是一个地址(指向其它数据)。
Rust 中最常见的就是“引用”。
- 使用 &。
- 借用它指向的值。
- 没有其余开销。
- 最常见的指针类型。
智能指针:一种行为与指针相似的数据结构,有额外的元数据和功能。
引用计数智能指针类型:类似 C++ 中的 share ptr,通过记录所有使用者的数量,使一份数据被多个所有者同时持有。
- 并在没有任何使用者时自动清理数据。
引用与智能指针的不同:
- 引用:只借用数据。
- 智能指针:很多时候都是拥有它所指向的数据。
当前使用过的智能指针例子:String 和
Vec<T>
- 都拥有一片内存区域,且允许用户对其操作。
- 还拥有元数据(例如容量等)。
- 提供额外的功能或保证。
智能指针的实现
- 智能指针通常使用 struct 实现,并且实现了:
- Deref 和 Drop 这两个 trait。
- Deref trait:允许智能指针 struct 的实例像引用一样使用。
- Drop trait:允许你自定义当智能指针实例走出作用域时的代码。
使用 Box<T>
来指向 Heap 上的数据
Box<T>
是最简单的智能指针:- 允许你在 heap 上存储数据。
- stack 上是指向 heap 数据的指针。
- 没有性能开销。
- 没有其它额外功能。
- 作为智能指针其实现来 Deref 和 Drop trait。
Box 常见的使用场景
- 在编译时无法确定数据大小,但使用该类型,上下文却需要知道它确切但大小。
- 当有大量的数据想要移交所有权,但需要确保在操作时数据不会被复制。
- 使用某个值时,你只关心它是否实现来特定但 trait,而不管线它但具体类型。
fn main() {
let b = Box::new(5);
println!("b = {}", b);
}
使用 Box 赋能递归类型
- 在编译时,Rust 需要知道一个类型所占但空间大小。
- 而递归类型但大小无法再编译时确定(递归层级无法确保)。
- Box 的类型就相当于一个指针,它的大小是确定的使用这个特性可以实现递归。
Cons List
- Cons List 是来自 Lisp 语言的一种数据结构。
- Cons List 里每个成员由两个元素组成:
- 当前元素值。
- 下一个元素。
- Cons List 里最后一个成员只包含一个 nil 值,没有下一个元素。
Rust 中 Cons List 并不是常用的集合,通常使用
Vec<T>
。接下来尝试使用Vec<T>
模拟 Cons List。通常 Rust 计算非递归的数据结构大小通常为选取其最大成员大小。
/* 会提示递归存在无限的大小,导致编译失败。 Rust 无法计算递归类型所占用的空间。 error[E0072]: recursive type `List` has infinite size --> src/main.rs:7:1 | 7 | enum List { | ^^^^^^^^^ recursive type has infinite size 8 | Cons(i32, List), | ---- recursive without indirection | help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable | 8 | Cons(i32, Box<List>), | ++++ + */ use crate::List::{Cons, Nil}; fn main() { let list = Cons(1, Cons(2, Cons(3, Nil))); } enum List { Cons(i32, List), Nil, }
使用 Box 来获得确定大小的递归类型:
Box<T>
是一个指针,其大小是固定的,这样 Rust 在编译期就可以知道这个结构所占用的栈空间大小。
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}
enum List {
Cons(i32, Box<List>),
Nil,
}
Deref Trait
- 实现 Deref Trait 使我们可以自定义解引用运算符 * 的行为,使得智能指针可以像常规引用一样来处理。
解引用运算符
常规引用也是一种指针。
fn main() { let x = 5; let y = &x; assert_eq!(5, x); assert_eq!(5, *y); }
将
Box<T>
当作引用:fn main() { let x = 5; let y = Box::new(x); assert_eq!(5, x); assert_eq!(5, *y); }
定义自己的智能指针
Box<T>
被定义成拥有一个元素的 tuple struct。如下,当没有实现解引用方法时:
/* error[E0614]: type `MyBox<{integer}>` cannot be dereferenced --> src/main.rs:16:19 | 16 | assert_eq!(5, *y); | ^^ */ struct MyBox<T>(T); impl<T> MyBox<T> { /// Creates a new [`MyBox<T>`]. fn new(x: T) -> MyBox<T> { MyBox(x) } } fn main() { let x = 5; // let y = Box::new(x); let y = MyBox::new(x); assert_eq!(5, x); assert_eq!(5, *y); }
标准库中的 Deref trait 要求我们实现一个 deref 方法:
该方法借用 self。
返回一个指向内部数据的引用。
use std::ops::Deref; struct MyBox<T>(T); impl<T> MyBox<T> { /// Creates a new [`MyBox<T>`]. fn new(x: T) -> MyBox<T> { MyBox(x) } } // 实现 Deref impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &Self::Target { &self.0 } } fn main() { let x = 5; // let y = Box::new(x); let y = MyBox::new(x); assert_eq!(5, x); assert_eq!(5, *y); // == *(y.deref()) }
函数和方法的隐式解引用转化(Deref Coercion)
隐式解引用是为函数和方法提供的一种便捷特性。
假设 T 实现来 Deref trait:
- Deref Coercion 可以把 T 的引用转化为 T 经过 Deref 操作后生成的引用。
当把某类型的引用传递给函数或方法时,但它但类型与定义但参数类型不匹配:
Deref Coercion 就会自动发生。
编译器会对 deref 进行一系列调用,来把它转为所需的参数类型。
- 在编译时完成,没有额外的性能开销。
use std::ops::Deref; fn hello(name: &str) { println!("Hello, {}", name); } fn main() { let m = MyBox::new(String::from("Rust")); // &MyBox<String> deref &String // &String deref &str hello(&m); // 如果没有 deref hello(&(*m)[..]); hello("Rust"); } struct MyBox<T>(T); impl<T> MyBox<T> { /// Creates a new [`MyBox<T>`]. fn new(x: T) -> MyBox<T> { MyBox(x) } } impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &Self::Target { &self.0 } }
解引用与可变性
- 可使用 DerefMut Trait 重载可变引用的 * 运算符。
- 在类型和 trait 在下列三种情况发生时,Rust 会执行 deref coercion:
- 当
T:Deref<Target=U>
,允许 &T 转换为 &U。 - 当
T:DerefMut<Target=U>
,允许 &mut T 转化为 &mut U。 - 当
T:Deref<Target=U>
,允许 &mut T 转化为 &U。反过来将不可变转化为 mut 可变是不允许的。
- 当
Drop Trait
实现 Drop Trait,可以让我们自定义当值将要离开作用域时发生的动作。
- 例如:文件、网络资源释放等。
- 任何类型都可以实现 Drop trait。
Drop trait 只要求你实现 drop 方法。
- 参数:对 self 的可变引用。
Drop trait 在预导入模块里(prelude)。
// output:
// CustomSmartPointer created
// Dropping CustomSmartPointer with data `other stuff`!
// Dropping CustomSmartPointer with data `my stuff`!
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!("CustomSmartPointer created");
}
使用 std::mem::drop 来提前 drop 值
- 很难直接禁用自动的 drop 功能,也没有这个必要,Drop trait 的目的就是进行自动的释放处理逻辑。
- Rust 不允许手动调用 Drop trait 的 drop 方法。但是可以调用标准库的
std::mem::drop
函数,来提前 Drop 值。
/*
可以看出 my stuff 在最前边输出了
output:
Dropping CustomSmartPointer with data `my stuff`!
CustomSmartPointer created
Dropping CustomSmartPointer with data `other stuff`!
*/
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"),
};
// c.drop(); error
drop(c);
let d = CustomSmartPointer {
data: String::from("other stuff"),
};
println!("CustomSmartPointer created");
}
Rc<T>
:引用计数智能指针
- 有时,一个值会有多个所有者。
- 为了支持多重所有权:
Rc<T>
- reference couting(引用计数)。
- 追踪所有到值的引用。
- 0 个引用:该值可以被清理掉。
Rc<T>
使用场景
需要在 heap 上分配数据,这些数据被程序的多个部分读取(只读),但在编译时无法确定哪个部分最后使用完这些数据的情况。
Rc<T>
只能用于单线程场景。Rc<T>
不在预导入模块中(prelude)。Rc::clone(&a)
函数:增加引用计数。Rc::strong_count(&a)
:获得引用计数。Rc::weak_count
弱引用计数。
例子:
两个 List 共享,另一个 List 所有权。
以下这种会发生错误:
enum List { Cons(i32, Box<List>), Nil, } use crate::List::{Cons, Nil}; fn main() { let a = Cons(5, Box::new(Cons(10, Box::new(Nil)))); let b = Cons(3, Box::new(a)); let c = Cons(3, Box::new(a)); // a 的所有权已经移交到了 b }
使用 Rc 来实现:
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)); // 这样 b 不会获得所有权,而是增加引用计数 let c = Cons(3, Rc::clone(&a)); }
Rc::clone()
与类型的clone()
方法的区别:- Rc::clone():增加引用,不会执行数据的深度拷贝操作。
- 类型的 clone():很多回执行数据的深度拷贝。
Rc<T>
通过不可变引用,使得程序中不同部分之间共享只读数据。
RefCell<T>
和内部可变性
内部可变性
内部可变性是 Rust 的设计模式之一,它允许你在只持有不可变引用的前提下对数据进行修改。
- 数据结构中使用了 unsafe 代码来绕过 Rust 正常的可变性与借用规则。
可变的借用一个不可变的值,如下例中 y 就是借用不可变的 x,这将导致编译错误:
fn test() { let x = 5; let y = &mut x; }
- 但是有时需要对外部提供是不可变的,但是对于实现内部需要是可变的情况:
RefCell<T>
与
Rc<T>
不同,RefCell<T>
类型代表了其持有数据的唯一所有权。其与
Box<T>
的区别:借用规则在不同阶段进行检查比较:
RefCell<T>
与Rc<T>
类似,只能用于单线程的场景。
选择 Box、Rc、RefCell 的依据
Box |
Rc |
RefCell |
|
---|---|---|---|
同一数据的所有者 | 一个 | 多个 | 一个 |
可变性、借用检查 | 可变、不可变借用(编译时检查) | 不可变借用(编译时检查) | 可变、不可变借用(运行时检查) |
使用 RefCell<T>
在运行时记录借用信息
RefCell<T>
会记录当前存在多少个活跃的Ref<T>
和RefMut<T>
智能指针:- 每次调用 borrow:不可变引用计数加 1。
- 任何一个
Ref<T>
的值离开作用域被释放时:不可变借用计数减 1。 - 每次调用 borrow_mut:可变引用计数加 1。
- 任何一个
RefMut<T>
的值离开作用域被释放时:可变引用计数减 1。
以此技术来维护借用检查规则:
- 任何一个给定时间里,只允许拥有多个不可变引用或一个可变引用。
将 Rc<T>
和 RefCell<T>
结合使用
实现一个拥有多重所有权的可变数据:
#[derive(Debug)] enum List { Cons(Rc<RefCell<i32>>, Rc<List>), Nil, } use crate::List::{Cons, Nil}; use std::cell::RefCell; use std::rc::Rc; fn main() { // Rc 里包含了一个 RefCell 来进行修改 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); }
其他可实现内部可变性的类型
Cell<T>
:通过复制来访问数据。Mutex<T>
:用于实现跨线程情况下的内部可变性模式。
Rust 可能发生内存泄露
Rust 的内存安全机制可以保证很难发生内存泄露,但是也不是不可能的。
例如使用
Rc<T>
和RefCell<T>
就可能创造出循环引用,从而发生内存泄露:- 每个项的引用计数不会为 0,值也不会被处理掉。
如下例创建了一个循环:
// output:
// a initial rc count = 1
// a next item = Some(RefCell { value: Nil })
// a rc count after b creation = 2
// b initial rc count = 1
// b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
// b rc count after changing a = 2
// a rc count after changing a = 2
use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;
#[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());
}
防止内存泄露的解决方法
- 依靠开发之自觉,不依靠 Rust。
- 重新组织数据结构:一些引用来表达所有权,一些引用不表达所有权。
- 循环引用中的一部分具有所有权关系,另一部分不涉及所有权关系。
- 而只有所有权关系才影响值的清理。
防止循环引用把 Rc<T>
换成 Weak<T>
Rc::clone
为Rc<T>
实例的strong_count
加 1,Rc<T>
的实例只有在strong_count
为 0 的时候才会被清理。Rc<T>
实例通过调用 Rc::downgrade 方法可以已创建值的 Weak Reference (弱引用):- 返回类型是
Weak<T>
。 - 调用
Rc::downgrade
会为weak_count
加 1。
- 返回类型是
Rc<T>
使用weak_count
来追踪存在多少Weak<T>
。weak_count
不为 0 不影响Rc<T>
的清理。Weak<T>
使用前需要判断所指向的对象是否还存在:
use std::borrow::Borrow;
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
// 叶子节点需要保存分支信息,才可以进行向上查找,但是由于叶子是所属分支的
// 需要使用 weakptr 进行保存,避免循环
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
// upgrade 将 weak 提升
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}