Rust 智能指针
本文在原文基础上有删减,原文参考Rust 智能指针目录
[*]使用Box指向堆上的数据
[*]使用 Box 在堆上储存数据
[*]Box 允许创建递归类型
[*]cons list 的更多内容
[*]计算非递归类型的大小
[*]使用 Box 给递归类型一个已知的大小
[*]通过 Deref trait 将智能指针当作常规引用处理
[*]追踪指针的值
[*]像引用一样使用 Box
[*]自定义智能指针
[*]通过实现 Deref trait 将某类型像引用一样处理
[*]函数和方法的隐式 Deref 强制转换
[*]Deref 强制转换如何与可变性交互
[*]使用 Drop Trait 运行清理代码
[*]通过 std::mem::drop 提早丢弃值
[*]Rc 引用计数智能指针
[*]使用 Rc 共享数据
[*]克隆 Rc 会增加引用计数
[*]RefCell 和内部可变性模式
[*]通过 RefCell 在运行时检查借用规则
[*]内部可变性:不可变值的可变借用
[*]内部可变性的用例:mock 对象
[*]RefCell 在运行时记录借用
[*]结合 Rc 和 RefCell 来拥有多个可变数据所有者
[*]引用循环与内存泄漏
[*]制造引用循环
[*]避免引用循环:将 Rc 变为 Weak
[*]创建树形数据结构:带有子节点的 Node
[*]增加从子到父的引用
[*]可视化 strong_count 和 weak_count 的改变
指针 (pointer)是一个包含内存地址的变量的通用概念,这个地址引用或 “指向”(points at)一些其他数据。Rust 中最常见的指针是引用(reference),引用以 & 符号为标志并借用了它们所指向的值。
普通引用和智能指针的一个额外的区别是引用是一类只借用数据的指针,在大部分情况下 智能指针拥有它们指向的数据。
智能指针通常使用结构体实现,智能指针不同于结构体的地方在于其实现了 Deref 和 Drop trait:
[*]Deref trait 允许智能指针结构体实例表现的像引用一样,可以编写既用于引用、又用于智能指针的代码。
[*]Drop trait 允许自定义当智能指针离开作用域时运行的代码。
使用Box指向堆上的数据
最简单直接的智能指针是 box,其类型是 Box:允许将一个值放在堆上而不是栈上,留在栈上的则是指向堆数据的指针。
box 没有性能损失,多用于如下场景:
[*]当有一个在编译时未知大小的类型,而又想要在需要确切大小的上下文中使用这个类型值的时候
[*]当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候
[*]当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候
使用 Box 在堆上储存数据
熟悉一下语法以及如何与储存在 Box 中的值进行交互:
fn main() {
//使用 box 在堆上储存一个 i32 值
let b = Box::new(5);
println!("b = {}", b);
}Box 允许创建递归类型
递归类型(recursive type)的值可以拥有另一个同类型的值作为其自身的一部分,编译时 Rust 不知道递归类型需要多少空间。因为 box 有一个已知的大小,所以通过在循环类型定义中插入 box 就可以创建递归类型了。
cons list 的更多内容
cons list 是一个来源于 Lisp 编程语言及其方言的数据结构,它由嵌套的列表组成。一个包含列表 1,2,3 的 cons list 的伪代码表示,其每一个列表在一个括号中:
(1, (2, (3, Nil)))cons list 的每一项都包含两个元素:当前项的值和下一项,其最后一项值包含一个叫做 Nil 的值且没有下一项。cons list 通过递归调用 cons 函数产生。代表递归的终止条件(base case)的规范名称是 Nil,它宣布列表的终止。
cons list 并不是一个 Rust 中常见的类型。大部分在 Rust 中需要列表的时候,Vec 是一个更好的选择。
计算非递归类型的大小
回忆之前定义的 Message 枚举:
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}因为 enum 实际上只会使用其中的一个成员,所以 Message 值所需的空间等于储存其最大成员的空间大小。
使用 Box 给递归类型一个已知的大小
Box 是一个指针:指针的大小并不会根据其指向的数据量而改变,可以将 Box 放入 Cons 成员中而不是直接存放另一个 List 值。Box 会指向另一个位于堆上的 List 值,而不是存放在 Cons 成员中。
为了拥有已知大小而使用 Box 的 List 定义:
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))))));
}任何 List 值最多需要一个 i32 加上 box 指针数据的大小,通过使用 box,打破了这无限递归的连锁:
https://img2024.cnblogs.com/blog/1495663/202402/1495663-20240223161317869-177043597.png
通过 Deref trait 将智能指针当作常规引用处理
实现 Deref trait 允许我们重载 解引用运算符(dereference operator)*(不要与乘法运算符或通配符相混淆)。通过这种方式实现 Deref trait 的智能指针可以被当作常规引用来对待,可以编写操作引用的代码并用于智能指针。
追踪指针的值
常规引用是一个指针类型,一种理解指针的方式是将其看成指向储存在其他某处值的箭头。使用解引用运算符来跟踪 i32 值的引用:
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
//对 y 的值做出断言,必须使用 *y 来追踪引用所指向的值(也就是 解引用)
assert_eq!(5, *y);
//编译报错:不允许比较数字的引用与数字
assert_eq!(5, y);
}像引用一样使用 Box
可以使用 Box 代替引用来重写上面的示例,在 Box 上使用解引用运算符:
fn main() {
let x = 5;
//将 y 设置为一个指向 x 值拷贝的 Box<T> 实例,而不是指向 x 值的引用
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}自定义智能指针
从根本上说,Box 被定义为包含一个元素的元组结构体,以相同的方式定义 MyBox 类型:
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
[*]MyBox 是一个包含 T 类型元素的元组结构体。
[*]MyBox::new 函数获取一个 T 类型的参数并返回一个存放传入值的 MyBox 实例。
尝试以使用引用和 Box 相同的方式使用 MyBox:
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}编译报错,MyBox 类型不能解引用,为了启用 *** 运算符**的解引用功能,需要实现 Deref trait。
通过实现 Deref trait 将某类型像引用一样处理
Deref trait 由标准库提供,要求实现名为 deref 的方法,其借用 self 并返回一个内部数据的引用。MyBox 上的 Deref 实现:
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
//定义了用于此 trait 的关联类型
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}deref 方法体中写入了 &self.0,这样 deref 返回了我希望通过 * 运算符访问的值的引用, .0 用来访问元组结构体的第一个元素。
输入 *y 时,Rust 事实上在底层运行了如下代码:
*(y.deref())
[*]Rust 将 * 运算符替换为先调用 deref 方法再进行普通解引用的操作,不需要手动调用 deref 方法。
[*]deref 方法返回值的引用,以及 *(y.deref()) 括号外边的普通解引用仍为必须的原因在于所有权。
注:每次在代码中使用 * 时, * 运算符都被替换成了先调用 deref 方法再接着使用 * 解引用的操作,且只会发生一次,不会对 * 操作符无限递归替换。
函数和方法的隐式 Deref 强制转换
Deref 强制转换(deref coercions)将实现了 Deref trait 的类型的引用转换为另一种类型的引用,如将 &String 转换为 &str。
Deref 强制转换是 Rust 在函数或方法传参上的一种便利操作,并且只能作用于实现了 Deref trait 的类型,当这种特定类型的引用作为实参传递给和形参类型不同的函数或方法时将自动进行。
一个有着字符串 slice 参数的函数定义:
fn hello(name: &str) {
println!("Hello, {name}!");
}因为 Deref 强制转换,使用 MyBox 的引用调用 hello 是可行的:
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
[*]使用 &m 调用 hello 函数,其为 MyBox 值的引用。
[*]Rust 可以通过 deref 调用将 &MyBox 变为 &String。
[*]标准库中提供了 String 上的 Deref 实现,其会返回字符串 slice,Rust 再次调用 deref 将 &String 变为 &str.
如果 Rust 没有 Deref 强制转换则必须编写的代码:
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
[*](*m) 将 MyBox 解引用为 String。
[*]接着 & 和 [..] 获取了整个 String 的字符串 slice 来匹配 hello 的签名。
这些解析都发生在编译时,所以利用 Deref 强制转换并没有运行时损耗。
Deref 强制转换如何与可变性交互
类似于如何使用 Deref trait 重载不可变引用的 * 运算符,Rust 提供了 DerefMut trait 用于重载可变引用的 * 运算符。
Rust 在发现类型和 trait 实现满足三种情况时会进行 Deref 强制转换:
[*]当 T: Deref 时从 &T 到 &U。
[*]当 T: DerefMut 时从 &mut T 到 &mut U。
[*]当 T: Deref 时从 &mut T 到 &U。
使用 Drop Trait 运行清理代码
第二个重要的 trait 是 Drop,其允许在值要离开作用域时执行一些代码,所指定的代码被用于释放类似于文件或网络连接的资源。
Drop trait 要求实现一个叫做 drop 的方法,它获取一个 self 的可变引用。结构体 CustomSmartPointer 实现了放置清理代码的 Drop trait:
//Drop trait 包含在 prelude 中无需导入
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
//打印一些文本以可视化地展示 Rust 何时调用 drop
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,并调用指定的代码
//变量以被创建时相反的顺序被丢弃,所以 d 在 c 之前被丢弃通过 std::mem::drop 提早丢弃值
Rust 并不允许主动调用 Drop trait 的 drop 方法,如果想在作用域结束之前就强制释放变量的话,应该使用的是由标准库提供的 std::mem::drop。
尝试手动调用 Drop trait 的 drop 方法提早清理,编译会报错:
fn main() {
let c = CustomSmartPointer {
data: String::from("some data"),
};
println!("CustomSmartPointer created.");
c.drop();
println!("CustomSmartPointer dropped before the end of main.");
}在值离开作用域之前调用 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.");
}Rc 引用计数智能指针
为了启用多所有权需要显式地使用 Rust 类型 Rc,其为引用计数(reference counting)的缩写。
注: Rc 只能用于单线程场景。
使用 Rc 共享数据
两个列表,b 和 c, 共享第三个列表 a 的所有权:
https://img2024.cnblogs.com/blog/1495663/202402/1495663-20240223161400615-795397068.png
使用 Rc 定义的 List:
enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::rc::Rc;//将 Rc<T> 引入作用域
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::clone 只会增加引用计数。
克隆 Rc 会增加引用计数
修改了 main 以便将列表 c 置于内部作用域中,观察当 c 离开作用域时引用计数如何变化:
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));
}在程序中每个引用计数变化的点,会打印出引用计数,其值可以通过调用 Rc::strong_count 函数获得。
RefCell 和内部可变性模式
内部可变性(Interior mutability)是 Rust 中的一个设计模式,它允许你即使在有不可变引用时也可以改变数据,这通常是借用规则所不允许的。
通过 RefCell 在运行时检查借用规则
对于引用和 Box 借用规则的不可变性作用于编译时,对于 RefCell这些不可变性作用于 运行时:
[*]在编译时检查借用规则的优势是这些错误将在开发过程的早期被捕获,同时对运行时没有性能影响。
[*]在运行时检查借用规则的好处则是允许出现特定内存安全的场景,而它们在编译时检查中是不允许的。
RefCell 只能用于单线程场景,如下为选择 Box、Rc 或 RefCell 的理由:
[*]Rc 允许相同数据有多个所有者,Box 和 RefCell 有单一所有者。
[*]Box 允许在编译时执行不可变或可变借用检查,Rc仅允许在编译时执行不可变借用检查,RefCell 允许在运行时执行不可变或可变借用检查。
[*]RefCell 允许在运行时执行可变借用检查,可以在即便 RefCell 自身是不可变的情况下修改其内部的值。
内部可变性:不可变值的可变借用
借用规则的一个推论是当有一个不可变值时,不能可变地借用它。
令一个值在其方法内部能够修改自身而在其他代码中仍视为不可变,特定情况下是很有用的,RefCell 是一个获得内部可变性的方法。
内部可变性的用例:mock 对象
有时在测试中程序员会用某个类型替换另一个类型以便观察特定的行为并断言它是被正确实现的,这个占位符类型被称为 测试替身(test double),mock 对象** 是特定类型的测试替身,它们记录测试过程中发生了什么以便可以断言操作是正确的。
一个记录某个值与最大值差距的库,并根据此值的特定级别发出警告:
pub trait Messenger { fn send(&self, msg: &str);}pub struct LimitTrackerwhere T: Messenger,{ pub fn new(messenger: &'a T, max: usize) -> LimitTracker
页:
[1]