智能指针-1
指针 (pointer)是一个包含内存地址的变量的通用概念。这个地址引用,或 “指向”(points at)一些其他数据。Rust 中最常见的指针是第四章介绍的 引用(reference)。引用以
&
符号为标志并借用了它们所指向的值。除了引用数据没有任何其他特殊功能,也没有额外开销。另一方面,智能指针(smart pointers)是一类数据结构,它们的表现类似指针,但是也拥有额外的元数据和功能。智能指针的概念并不为 Rust 所独有;其起源于 C++ 并存在于其他语言中。Rust 标准库中定义了多种不同的智能指针,它们提供了多于引用的额外功能。为了探索其基本概念,我们来看看一些智能指针的例子,这包括 引用计数 (reference counting)智能指针类型。这种指针允许数据有多个所有者,它会记录所有者的数量,当没有所有者时清理数据。在 Rust 中因为引用和借用,普通引用和智能指针的一个额外的区别是引用是一类只借用数据的指针;相反,在大部分情况下,智能指针 拥有 它们指向的数据。
智能指针通常使用结构体实现。智能指针不同于结构体的地方在于其实现了
Deref
和Drop
trait。Deref
trait 允许智能指针结构体实例表现的像引用一样,这样就可以编写既用于引用、又用于智能指针的代码。Drop
trait 允许我们自定义当智能指针离开作用域时运行的代码。
使用Box
指向堆上的数据
最简单直接的智能指针是 box,其类型是
Box<T>
。box 允许你将一个值放在堆上而不是栈上。留在栈上的则是指向堆数据的指针。除了数据被储存在堆上而不是栈上之外,box 没有性能损失。不过也没有很多额外的功能。它们多用于如下场景:
- 当有一个在编译时未知大小的类型,而又想要在需要确切大小的上下文中使用这个类型值的时候
- 当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候
- 当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候
使用 Box
在堆上储存数据
/*
这里定义了变量 b,其值是一个指向被分配在堆上的值 5 的 Box。这个程序会打印出 b = 5;在这个例子中,我们可以像数据是储存在栈上的那样访问 box 中的数据。正如任何拥有数据所有权的值那样,当像 b 这样的 box 在 main 的末尾离开作用域时,它将被释放。这个释放过程作用于 box 本身(位于栈上)和它所指向的数据(位于堆上)。
*/
fn main() {
let b = Box::new(5);
println!("b = {b}");
}
Box 允许创建递归类型
递归类型(recursive type)的值可以拥有另一个同类型的值作为其自身的一部分。但是这会产生一个问题,因为 Rust 需要在编译时知道类型占用多少空间。递归类型的值嵌套理论上可以无限地进行下去,所以 Rust 不知道递归类型需要多少空间。因为 box 有一个已知的大小,所以通过在循环类型定义中插入 box,就可以创建递归类型了。
enum List {
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
//Cons 成员将会需要一个 i32 的大小加上储存 box 指针数据的空间。Nil 成员不储存值,所以它比 Cons 成员需要更少的空间。现在我们知道了任何 List 值最多需要一个 i32 加上 box 指针数据的大小。通过使用 box,打破了这无限递归的连锁,这样编译器就能够计算出储存 List 值需要的大小了
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}
通过 Deref
trait 将智能指针当作常规引用处理
实现
Deref
trait 允许我们重载 解引用运算符(dereference operator)*
(不要与乘法运算符或通配符相混淆)。通过这种方式实现Deref
trait 的智能指针可以被当作常规引用来对待,可以编写操作引用的代码并用于智能指针。
追踪指针的值
//常规引用是一个指针类型,一种理解指针的方式是将其看成指向储存在其他某处值的箭头。
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}
像引用一样使用 Box
//将 y 设置为一个指向 x 值拷贝的 Box<T> 实例,而不是指向 x 值的引用。在最后的断言中,可以使用解引用运算符以 y 为引用时相同的方式追踪 Box<T> 的指针。
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
自定义智能指针
//通过实现 Deref trait 将某类型像引用一样处理
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;//type Target = T; 语法定义了用于此 trait 的关联类型。
fn deref(&self) -> &Self::Target {
&self.0//deref 方法体中写入了 &self.0,这样 deref 返回了我希望通过 * 运算符访问的值的引用。
}
}
函数和方法的隐式 Deref 强制转换
Deref 强制转换(deref coercions)将实现了
Deref
trait 的类型的引用转换为另一种类型的引用。例如,Deref 强制转换可以将&String
转换为&str
,因为String
实现了Deref
trait 因此可以返回&str
。Deref 强制转换是 Rust 在函数或方法传参上的一种便利操作,并且只能作用于实现了Deref
trait 的类型。当这种特定类型的引用作为实参传递给和形参类型不同的函数或方法时将自动进行。这时会有一系列的deref
方法被调用,把我们提供的类型转换成了参数所需的类型。Deref 强制转换的加入使得 Rust 程序员编写函数和方法调用时无需增加过多显式使用
&
和*
的引用和解引用。这个功能也使得我们可以编写更多同时作用于引用或智能指针的代码。
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> 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
}
}
fn hello(name: &str) {
println!("Hello, {name}!");
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
Deref 强制转换如何与可变性交互
类似于如何使用
Deref
trait 重载不可变引用的*
运算符,Rust 提供了DerefMut
trait 用于重载可变引用的*
运算符。Rust 在发现类型和 trait 实现满足三种情况时会进行 Deref 强制转换:
- 当
T: Deref<Target=U>
时从&T
到&U
。- 当
T: DerefMut<Target=U>
时从&mut T
到&mut U
。- 当
T: Deref<Target=U>
时从&mut T
到&U
。头两个情况除了第二种实现了可变性之外是相同的:第一种情况表明如果有一个
&T
,而T
实现了返回U
类型的Deref
,则可以直接得到&U
。第二种情况表明对于可变引用也有着相同的行为。第三个情况有些微妙:Rust 也会将可变引用强转为不可变引用。但是反之是 不可能 的:不可变引用永远也不能强转为可变引用。因为根据借用规则,如果有一个可变引用,其必须是这些数据的唯一引用(否则程序将无法编译)。将一个可变引用转换为不可变引用永远也不会打破借用规则。将不可变引用转换为可变引用则需要初始的不可变引用是数据唯一的不可变引用,而借用规则无法保证这一点。因此,Rust 无法假设将不可变引用转换为可变引用是可能的。