
13 智能指针
指针
按理说,凡是变量内存中存储的有内存地址数据的变量,都是指针。
Rust 中最常见的指针是第四章介绍的引用(reference)。引用使用 & 符号来表示,而且会借用它们指向的值。它们除了地址数据之外还可能有云数据。
- 对于
&dyn Trait,元数据是虚表指针(vtable pointer)。 - 对于
&[T](切片),元数据是数据长度(len)。
^1f41bc
引用会对变量再取指针变量,相当于再套了一层&符号代表引用,也代表取引用操作,这个操作符的运算等级低于.所以通常放在最外面。
*符号代表解引用。
指针完全可以拥有所指向内容的所有权。实际上就是多了所有权标记。
拥有所有权标记的变量可比没有所有权标记的变量牛逼多了。
对于一个引用,将其代指的对象拿出来,以取引用正好相反这个操作就是解引用。
“*”解引用符号。
如果代指对象没有使用copy trait,那么解引用出来赋值给其他变量会带所有权
对于指针与引用,有直接操作本变量的操作也有透过指针操作指针所代表数据的操作。
形象而言,通过“引用”操作引用背后的代表,叫作“透引用操作”。
“.”符号
对于“引用变量”自动帮你插入 *(即调用 Deref),所以.就相当于了*
对于“所有权变量”自动取引用“&”,所以所有权变量也能最小权限操作。
自动无限透引用,寻找匹配的方法或字段(比如 Rc<RefCell<Vec<T>>>,你写 .len(),编译器会自动解引用三层)
旨在让你无感知地操作深层数据。
智能指针
有一些空间专门存储指针的数据,比如引用数,所有权共享之类的。但是本质上核心功能仍然是指针。需要提供指针的基本功能“透指针”。
智能指针不同于普通结构体的地方在于它实现了 Deref 和Drop trait。
用于自定义销毁。
“*”操作符会调用这个trait的方法来获得指针所代表的对象
Box<T>
let b = Box::new(5);
Box 允许你将数据存储在堆上而不是栈上。留在栈上的则是指向堆数据的指针。
释放时既会清理 box 本身(存储在栈上),也会清理它指向的数据(存储在堆上)。
而指针可以用固定的内存大小表示不固定的内存大小,或者说指针可以用特定的大小表示任意的大小,所以我们只需要给指针分配特定大小的空间即可表示任务大小的空间。
// 一个递归枚举类型
enum List {
Cons(i32, Box<List>),
Nil,
}
智能指针与常规引用
[!summary] 解引用符号”*” 用于 “引用”与“指针”
实际上都是调用的deref trait的方法,返回这个指针、引用所代表的对象。
如果不使用解引用,那么表示读这个引用变量本身,而非其代表的背后的对象。
比如我们自己创建一个类型MyBox,只要实现了Deref那么也能像引用,指针一样的解引用,但是结构体默认分配在栈上。
[!summary] *本质上就是&的反向操作,deref返回&配合*得到原对象
输入 *y 时事实上在底层运行*(y.deref())
每次当我们在代码中使用 * 时, * 运算符都被替换成了先调用 deref 方法再接着使用
*解引用的操作,且只会发生一次,不会对 * 操作符无限递归替换,解引用出上面 i32 类型的值就停止
因为实现了deref的指针类型完全是代表某对象的,所以可以直接转换为所代表的对象的引用。
而转换方法就是通过deref方法。
当这种特定类型的引用作为实参传递给和形参类型不同的函数或方法时将自动进行强制转换。
这些解析都发生在编译时,所以利用 Deref 强制转换并没有运行时开销!
例如 &String 完全可以转换成 &str,无论是通过调用deref()还是通过自动进行强制转换。
语法上能解引用:编译器允许你写 *s,但是str 是动态大小类型,它在编译时大小未知,因此不能作为普通变量存储在栈上。
实际上不能“接住”,*s 唯一的存在意义,就是立刻借用它,或者通过它调用方法,但这完全是多此一举,直接使用&str或者&String即可。
- 当 T: Deref<Target=U> 时从 &T 到 &U 。
- 当 T: DerefMut<Target=U> 时从 &mut T 到 &mut U 。
- 当 T: Deref<Target=U> 时从 &mut T 到 &U 。
Rust 也会将可变引用强转为不可变引用。但反之是不可能 的:不可变引用永远也不能强转为可变引用。
也就是说,要解出不可变引用可以通过Deref和DerefMut两种trait方法提供,但是要解出可变引用只能通过DerefMut trait的方法提供。
有些时候,智能指针所指向的空间并非简单的内存,而是一些其他资源,例如网络套接字资源、文件句柄等,我们需要实现在所有权变量被drop时的清理操作。
当实例离开作用域时,Rust 会自动替我们调用 drop ,并运行我们指定的代码。变量会按照创建顺序的逆序被丢弃,所以 d 会先于 c 被丢弃。
变量的生命周期作用域,通常不是整个作用域的生命,而是在最后一次使用变量的时,下一行就会直接终结这个变量。
因为rust默认会帮我们调用,如果我们先调用了而rust并不知道还会再次调用drop导致二次释放错误。
所以我们需要使用一种能通知到rust的方法,由标准库提供的 std::mem::drop 函数来间接调用drop函数,这样rust就不会再次调用被drop的对象的drop了。
- 提前结束借用期,让编译器闭嘴。(有些时候编译器固执认为没有结束生命)
- 显式语法意图,提高可读性
Rc<T>
每个引用都是相等的地位。
注:我们可以使用 a.clone(),但是为了区别普通的深拷贝,这里只是浅拷贝所以调用Rc::clone(a) 更好
所以只能在单线程中使用Rc
强引用就是明面引用,必须要源存在,否则强引用会崩溃。
弱引用就是暗地引用,不必关心他们的存在,可以直接清空源,弱引用崩溃就崩溃吧。
虽然我们的指针变量看似是具有所有权的,但是这只代表了指针变量,而不代表指针背后的对象的可变性。
当指针计数为1的时候,我们可以取出背后的对象,或者使用背后对象的可变引用。
Rc的方法与函数
如果你确定当前这个Rc 是最后一个强引用(引用计数为 1)
Rc::try_unwrap(rc)
必须满足一个极其严格的前提:强引用计数(strong_count)必须正好为 1,且弱引用计数(weak_count)也必须为 0。
并且指针必须是mut
Rc::get_mut(&mut rc) 返回的是 &mut T(独占可变引用)
RefCell<T>与内部可变性
之前的所有修改链都遵循一个模式,受限于路径上任何一个不可变环境。而内部可变性则让修改链在RefCell上从不可变变为可变!!!
相当于可以从RefCell变色。
也就是不管外部引用或者指针路径,有多少不可变,可变的拦截,只要最终能访问到RefCell,就能从这个ReCell拿到可变的内部,或者不可变的内部,这是运行时决定的。
而运行时违反,就比如我暂存了一个不可变的内部引用,存储在变量中,然后我再获得了一个可变的内部引用也存储在变量中,这个时候我再操作第一个不可变变量就不行了,这使得可变借用与不可变借用生命周期交叉。
并且将RefCell放在最后是最有效的,想想:不变不可变不可变最后可变,如果放在中间或者前面也许路径只要出现不可变那么最终就不可变。
对于引用和Box<T>,借用规则的不可变性(invariants)在编译时就会被强制执行。对于RefCell<T> ,这些不可变性作用于运行时。对于引用,如果违反这些规则,会得到一个编译错误。而对于 RefCell<T> ,如果违反这些规则程序会 panic 并退出。
其内部的借用检查机制没有使用任何同步原语(如锁),因此在多线程下不是线程安全的
实际上仍然利用了指针计数,当不可变借用的时候检查可变计数=0,借出并增加不可变计数。
可变借用的时候检查当前RefCell的不可变计数=0,且可变计数=0,才借出增加可变计数。
[!tip] 最常见的就是Option<Rc<RefCell<xxx>>>嵌套
允许多个可变数据所有者
引用循环与内存泄露
想要释放内存,我们需要让计数=0,当形成循环的时候,在任何时刻每个节点都有一个节点指向他们所以计数>=1,当我们想要释放任何一个节点,那么drop的顺序会先释放节点中的指针成员,而指针成员的drop又兜兜转转到自己身上。

如果你有包含 Rc<T> 的 RefCell<T> 值或类似的嵌套结合了内部可变性和引用计数的类型,请务必小心确保你没有形成一个引用循环;
为了使用 Weak<T> 所指向的值,我们必须确保其值仍然有效。
为此可以调用Weak<T> 实例的 upgrade 方法,这会返回 Option<Rc<T>> 。
如果Rc<T> 值还未被丢弃,则结果是 Some ;如果 Rc<T> 已被丢弃,则结果是 None 。
父节点应该拥有它的子节点。如果父节点被丢弃了,它的子节点也应该被丢弃。然而,子节点不应该拥有它的父节点。如果我们丢弃一个子节点,父节点仍然应该存在。这正是弱引用适用的场景!
RefCell的方法与函数
返回所代表对象的借用,分为可变与不可变,所以有两个方法。
refcell.borrow() 返回 &
refcell.borrow_mut() 返回 &mut
Cow指针指针
它表示是这样的一个数据:
只读时直接用引用读取,当要修改的时候,检查Cow自身是只读引用还是所有权,如果是只读引用再进行克隆,然后修改。
如果是所有权就直接进行修改。
实际Cow上是为了更好的传输数据,有的数据传输过去只是为了只读,这种用途用不着再克隆,可以让Cow携带只读引用。对于无法保留的数据,Cow也能直接携带数据本体。
最常用的两个方法是.into_owned()(无条件从引用转换为实体数据)
.to_mut()(获取可变引用,自动触发写时克隆)。
