智能指针-Rust

智能指针(Smart Pointers) 往往都实现了 DerefDrop 特征,智能指针(Smart Pointers) 是用于管理内存和资源的核心工具之一。它们不仅封装了内存地址(像普通指针一样),还通过 所有权(Ownership)借用规则(Borrowing Rules) 提供额外的安全性和功能。

Box堆对象分配

Rust中的堆栈

具有垃圾回收机制的编程语言不需要考虑堆栈问题,,例如Python,Java,但是C,C++,Rust需要我们深入了解堆栈

栈内存从高位地址向下存储数据,例如0xFF存储第一个字节数据的话,那么0xFE就会存储第二个字节,当然这只是一个例子,实际内存地址根据计算机操作系统长度生成,例如32位操作系统就有32位长度的内存地址,并且根据存储数据的大小地址也需要对应相应大小的内存空间进行加减操作。操作系统对栈内存的大小都有限制

堆内存则是从低位地址向上增长,堆内存通常只受物理内存限制,而且通常是不连续的,因此从性能的角度看,栈往往比堆更高。Rust 堆上对象有一个特殊之处,它们都拥有一个所有者,因此受所有权规则的限制

BOX使用场景

特意的将数据分配在堆上

1
2
3
4
5
6
7
fn main() {
let a = Box::new(3);//强制将3存在堆内存上
println!("a = {}", a); // a = 3

// 下面一行代码将报错
// let b = a + 1; // cannot add `{integer}` to `Box<{integer}>`
}

数据较大时,又不想在转移所有权时进行数据拷贝

1、栈上数据转移所有权时,实际上是把数据拷贝了一份,最终新旧变量各自拥有不同的数据,因此所有权并未转移。

2、堆上则不然,底层数据并不会被拷贝,转移所有权仅仅是复制一份栈中的指针,再将新的指针赋予新的变量,然后让拥有旧指针的变量失效,最终完成了所有权的转移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
// 在栈上创建一个长度为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());
}

类型的大小在编译期无法确定,但是我们又需要固定大小的类型时

Rust 需要在编译时知道类型占用多少空间,如果一种类型在编译时无法知道具体的大小,那么被称为动态大小类型 DST(动态大小类型),比如字符串切片、trait 对象

递归类型:在类型定义中又使用到了自身

1
2
3
4
enum List {
Cons(i32, List),//本身包含自己,理论上空间可以无限大,编译时无法确认
Nil,
}

可以修改为可编译的类型

1
2
3
4
enum List {
Cons(i32, Box<List>),
Nil,
}

特征对象,用于说明对象实现了一个特征,而不是某个特定的类型

实现不同类型组成的数组的两个办法:枚举和特征对象

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
trait Draw{
fn draw(&self);
}

struct Button{
id:u32,
}

impl Draw for Button{
fn draw(&self) {
println!("这个选择框贼难用{}", self.id);
}
}

struct Select {
id: u32,
}

impl Draw for Select {
fn draw(&self) {
println!("这个选择框贼难用{}", self.id)
}
}


fn main() {
let elems: Vec<Box<dyn Draw>> = vec![Box::new(Button { id: 1 }), Box::new(Select { id: 2 })];

for e in elems {
e.draw()
}
}

image-20250425162014980

Box内存布局

Vec<i32> 的内存布局

image-20250425162116194

Vec<Box<i32>> 的内存布局

image-20250425162222007

我们从数组中取出某个元素时,取到的是对应的智能指针 Box,需要对该智能指针进行解引用,才能取出最终的值

1
2
3
4
5
6
fn main() {
let arr = vec![Box::new(1), Box::new(2)];
let (first, second) = (&arr[0], &arr[1]);
let sum = **first + **second;
println!("{}",sum);
}

Deref 解引用

常规解引用

1
2
3
4
5
6
7
fn main() {
let x = 5;
let y = &x;

assert_eq!(5, x);
assert_eq!(5, *y);
}

智能指针解引用

智能指针是结构体类型,如果直接使用*进行解引用,编译器不知道该如何操作,我们为智能指针实现Deref trait,它定义了解引用操作,允许类型通过解引用运算符*操作其内部数据

定义自己的智能指针,目前只是一个简单的泛型参数类型的元组结构体

image-20250425164102351

实现Deref

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::ops::Deref;
struct MyBox<T>(T);

impl<T> MyBox<T>{
fn new(s:T)->MyBox<T>{
MyBox(s)
}
}

impl<T> Deref for MyBox<T>{
type Target = T;

fn deref(&self) -> &Self::Target {
&self.0
}
}

fn main() {
let y=MyBox::new(3);
println!("{}",*y);

}

当我们对智能指针 Box 进行解引用时,实际上 Rust 为我们调用了以下方法:*(y.deref())

隐式 Deref 转换

若一个类型实现了 Deref 特征,那它的引用在传给函数或方法时,会根据参数签名来决定是否进行隐式的 Deref 转换

1
2
3
4
5
6
7
8
9
10
11
12
13
// fn main() {
// let m = MyBox::new(String::from("Rust"));
// display(&(*m)[..]);//*m将mybox解引用为String,String[..]转换为str切片,&str取引用
// }

fn main() {
let s = MyBox::new(String::from("hello world"));
display(&s)
}

fn display(s: &str) {
println!("{}",s);
}

赋值中自动应用 Deref

1
2
3
4
5
fn main() {
let s = MyBox::new(String::from("hello, world"));
let s1: &str = &s;
let s2: String = s.to_string();
}

Deref 规则总结

Rust 编译器实际上只能对 &v 形式的引用进行解引用操作,那么问题来了,如果是一个智能指针或者 &&&&v 类型,如何对这两个进行解引用

  • 把智能指针(比如在库中定义的,Box、Rc、Arc、Cow 等)从结构体脱壳为内部的引用类型,也就是转成结构体内部的 &v
  • 把多重&,例如 &&&&&&&v,归一成 &v
1
2
3
4
5
6
7
impl<T: ?Sized> Deref for &T {
type Target = T;

fn deref(&self) -> &T {
*self
}
}

**impl<T: ?Sized>**:
泛型参数 T 的约束为 ?Sized,表示 T 可以是动态大小类型(DST),例如 str[i32]

**Deref for &T**:
为引用类型 &T 实现 Deref trait,使其支持解引用操作

三种 Deref 转换

  • T: Deref<Target=U>,可以将 &T 转换成 &U,也就是我们之前看到的例子
  • T: DerefMut<Target=U>,可以将 &mut T 转换成 &mut U
  • T: Deref<Target=U>,可以将 &mut T 转换成 &U

Rc 与 Arc

Rust 所有权机制要求一个值只能有一个所有者,在大多数情况下,都没有问题,但是考虑以下情况:

  • 在图数据结构中,多个边可能会拥有同一个节点,该节点直到没有边指向它时,才应该被释放清理
  • 在多线程中,多个线程可能会持有同一个数据,但是你受限于 Rust 的安全机制,无法同时获取该数据的可变引用

Rc

Rc<T> 是指向底层数据的不可变的引用

引用计数(reference counting),顾名思义,通过记录一个数据被引用的次数来确定该数据是否正在被使用。当引用次数归零时,就代表该数据不再被使用,因此可以被清理释放

当我们希望在堆上分配一个对象供程序的多个部分使用且无法确定哪个部分最后一个结束时,就可以使用 Rc 成为数据值的所有者

1
2
3
4
5
6
7
fn main() {
let s = String::from("hello, world");
// s在这里被转移给a
let a = Box::new(s);
// 报错!此处继续尝试将 s 转移给 b
let b = Box::new(s);
}

智能指针

1
2
3
4
5
6
7
8
use std::rc::Rc;

fn main() {

let a=Rc::new(String::from("xiu"));
let b=Rc::clone(&a);//复制了智能指针并增加了引用计数,并没有克隆底层数据
println!("{}",Rc::strong_count(&a));
}

配合其它数据类型来一起使用,例如内部可变性的 RefCell<T> 类型以及互斥锁 Mutex<T>

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
use std::rc::Rc;

struct Owner {
name: String,
}

struct Gadget {
id: i32,
owner: Rc<Owner>,
}

fn main() {
let gadget_owner = Rc::new(Owner { name: "xiu".to_string() });

// 创建 Gadget 前引用计数为 1
println!("Initial count: {}", Rc::strong_count(&gadget_owner)); // 1

let gadget1 = Gadget {
id: 1,
owner: Rc::clone(&gadget_owner),
};
// 创建后引用计数变为 2
println!("After gadget1: {}", Rc::strong_count(&gadget_owner)); // 2

let gadget2 = Gadget {
id: 2,
owner: Rc::clone(&gadget_owner),
};
// 创建后引用计数变为 3
println!("After gadget2: {}", Rc::strong_count(&gadget_owner)); // 3

// 主动释放 gadget_owner(减少引用计数到 2)
drop(gadget_owner);

// 通过 gadget1.owner 获取当前引用计数(此时为 2)
println!("After drop: {}", Rc::strong_count(&gadget1.owner)); // 2

// 数据未被释放,仍可访问
println!("Gadget {} owned by {}", gadget1.id, gadget1.owner.name); // 正常输出
println!("Gadget {} owned by {}", gadget2.id, gadget2.owner.name); // 正常输出
}

Arc

多线程实现数据共享,原子化或者其它锁会造成性能消耗

1
2
3
4
5
6
7
8
9
10
11
12
use std::sync::Arc;
use std::thread;

fn main() {
let s = Arc::new(String::from("多线程漫游者"));
for _ in 0..10 {
let s = Arc::clone(&s);
let handle = thread::spawn(move || {
println!("{}", s)
});
}
}

在 Rust 中,所有权机制保证了一个数据只会有一个所有者,但如果你想要在图数据结构、多线程等场景中共享数据,这种机制会成为极大的阻碍。好在 Rust 为我们提供了智能指针 RcArc,使用它们就能实现多个所有者共享一个数据的功能

Cell 和 RefCell

Rust 提供了 CellRefCell 用于内部可变性,简而言之,可以在拥有不可变引用的同时修改目标数据

CellRefCell 在功能上没有区别,区别在于 Cell<T> 适用于 T 实现 Copy 的情况

Cell

1
2
3
4
5
6
7
8
9
10
11
use std::cell::Cell;
use std::cell::RefCell;

fn main() {
let c=Cell::new("xiu");
let one=c.get();
c.set("yyds");
let two=c.get();
println!("{}-{}",one,two);

}
  • “asdf” 是 &str 类型,它实现了 Copy 特征
  • c.get 用来取值,c.set 用来设置新值

RefCell

由于 Cell 类型针对的是实现了 Copy 特征的值类型,因此在实际开发中,Cell 使用的并不多,因为我们要解决的往往是可变、不可变引用共存导致的问题,此时就需要借助于 RefCell 来达成目的

image-20250427102207960

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 导入RefCell,它允许在不可变引用内部进行可变借用(内部可变性模式)
// 但会在运行时检查借用规则,违反规则会触发panic
use std::cell::RefCell;

fn main() {
// 创建一个RefCell,包裹初始值为"hello, world"的String
// RefCell在堆上分配内存,并提供运行时借用检查
let s = RefCell::new(String::from("hello, world"));

// 第一次借用:通过borrow()获取不可变引用s1
// 此时RefCell内部标记有一个活跃的不可变借用
let s1 = s.borrow(); // ✅ 成功,不可变借用计数+1(当前:1个不可变借用)

// 尝试第二次借用:通过borrow_mut()获取可变引用s2
// ❌ 这里将触发运行时panic!
// 因为RefCell的规则是:
// 1. 当存在活跃的不可变借用时,禁止获取可变借用
// 2. 或者当存在可变借用时,禁止任何其他借用
let s2 = s.borrow_mut(); // ⚠️ 运行时检查失败:Already immutably borrowed

// 这行代码永远不会执行,因为程序已经在上一行panic终止
println!("{},{}", s1, s2);
}

RefCell的核心作用就是你确信自己的代码是正确的,而编译器却发生了误判,使用它可以让你的代码编译通过