智能指针-Rust
智能指针-Rust
xiu智能指针(Smart Pointers) 往往都实现了 Deref
和 Drop
特征,智能指针(Smart Pointers) 是用于管理内存和资源的核心工具之一。它们不仅封装了内存地址(像普通指针一样),还通过 所有权(Ownership) 和 借用规则(Borrowing Rules) 提供额外的安全性和功能。
Box堆对象分配
Rust中的堆栈
具有垃圾回收机制的编程语言不需要考虑堆栈问题,,例如Python,Java,但是C,C++,Rust需要我们深入了解堆栈
栈内存
从高位地址向下存储数据,例如0xFF存储第一个字节数据的话,那么0xFE就会存储第二个字节,当然这只是一个例子,实际内存地址根据计算机操作系统长度生成,例如32位操作系统就有32位长度的内存地址,并且根据存储数据的大小地址也需要对应相应大小的内存空间进行加减操作。操作系统对栈内存的大小都有限制
堆内存
则是从低位地址向上增长,堆内存通常只受物理内存限制,而且通常是不连续的,因此从性能的角度看,栈往往比堆更高。Rust 堆上对象有一个特殊之处,它们都拥有一个所有者,因此受所有权规则的限制
BOX使用场景
特意的将数据分配在堆上
1 | fn main() { |
数据较大时,又不想在转移所有权时进行数据拷贝
1、栈上数据转移所有权时,实际上是把数据拷贝了一份,最终新旧变量各自拥有不同的数据,因此所有权并未转移。
2、堆上则不然,底层数据并不会被拷贝,转移所有权仅仅是复制一份栈中的指针,再将新的指针赋予新的变量,然后让拥有旧指针的变量失效,最终完成了所有权的转移
1 | fn main() { |
类型的大小在编译期无法确定,但是我们又需要固定大小的类型时
Rust 需要在编译时知道类型占用多少空间,如果一种类型在编译时无法知道具体的大小,那么被称为动态大小类型 DST(动态大小类型),比如字符串切片、trait 对象
递归类型:在类型定义中又使用到了自身
1 | enum List { |
可以修改为可编译的类型
1 | enum List { |
特征对象,用于说明对象实现了一个特征,而不是某个特定的类型
实现不同类型组成的数组的两个办法:枚举和特征对象
1 | trait Draw{ |
Box内存布局
Vec<i32>
的内存布局
Vec<Box<i32>>
的内存布局
我们从数组中取出某个元素时,取到的是对应的智能指针 Box
,需要对该智能指针进行解引用,才能取出最终的值
1 | fn main() { |
Deref 解引用
常规解引用
1 | fn main() { |
智能指针解引用
智能指针是结构体类型,如果直接使用*
进行解引用,编译器不知道该如何操作,我们为智能指针实现Deref trait,它定义了解引用操作,允许类型通过解引用运算符*
操作其内部数据
定义自己的智能指针,目前只是一个简单的泛型参数类型的元组结构体
实现Deref
1 | use std::ops::Deref; |
当我们对智能指针 Box
进行解引用时,实际上 Rust 为我们调用了以下方法:*(y.deref())
隐式 Deref 转换
若一个类型实现了 Deref
特征,那它的引用在传给函数或方法时,会根据参数签名来决定是否进行隐式的 Deref
转换
1 | // fn main() { |
赋值中自动应用 Deref
1 | fn main() { |
Deref 规则总结
Rust 编译器实际上只能对 &v
形式的引用进行解引用操作,那么问题来了,如果是一个智能指针或者 &&&&v
类型,如何对这两个进行解引用
- 把智能指针(比如在库中定义的,Box、Rc、Arc、Cow 等)从结构体脱壳为内部的引用类型,也就是转成结构体内部的
&v
- 把多重
&
,例如&&&&&&&v
,归一成&v
1 | impl<T: ?Sized> Deref for &T { |
**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 | fn main() { |
智能指针
1 | use std::rc::Rc; |
配合其它数据类型来一起使用,例如内部可变性的 RefCell<T>
类型以及互斥锁 Mutex<T>
1 | use std::rc::Rc; |
Arc
多线程实现数据共享,原子化或者其它锁会造成性能消耗
1 | use std::sync::Arc; |
在 Rust 中,所有权机制保证了一个数据只会有一个所有者,但如果你想要在图数据结构、多线程等场景中共享数据,这种机制会成为极大的阻碍。好在 Rust 为我们提供了智能指针 Rc
和 Arc
,使用它们就能实现多个所有者共享一个数据的功能
Cell 和 RefCell
Rust 提供了 Cell
和 RefCell
用于内部可变性,简而言之,可以在拥有不可变引用的同时修改目标数据
Cell
和 RefCell
在功能上没有区别,区别在于 Cell<T>
适用于 T
实现 Copy
的情况
Cell
1 | use std::cell::Cell; |
- “asdf” 是
&str
类型,它实现了Copy
特征 c.get
用来取值,c.set
用来设置新值
RefCell
由于 Cell
类型针对的是实现了 Copy
特征的值类型,因此在实际开发中,Cell
使用的并不多,因为我们要解决的往往是可变、不可变引用共存导致的问题,此时就需要借助于 RefCell
来达成目的
1 | // 导入RefCell,它允许在不可变引用内部进行可变借用(内部可变性模式) |
RefCell的核心作用就是你确信自己的代码是正确的,而编译器却发生了误判,使用它可以让你的代码编译通过