Skip to main content
13 智能指针

13 智能指针

•2 min•348 words

指针

变量存储了“地址”数据就是指针

按理说,凡是变量内存中存储的有内存地址数据的变量,都是指针。

Rust 中最常见的指针是第四章介绍的引用(reference)。引用使用 & 符号来表示,而且会借用它们指向的值。它们除了地址数据之外还可能有云数据。

Todo
  • 对于 &dyn Trait,元数据是虚表指针(vtable pointer)。
  • 对于 &[T](切片),元数据是数据长度(len)。

^1f41bc

引用分为瘦指针与胖指针,取引用等于再套一层

引用会对变量再取指针变量,相当于再套了一层&符号代表引用,也代表取引用操作,这个操作符的运算等级低于.所以通常放在最外面。
*符号代表解引用。

引用只是借用没有所有权

指针完全可以拥有所指向内容的所有权。实际上就是多了所有权标记。
拥有所有权标记的变量可比没有所有权标记的变量牛逼多了。

“解引用操作”

对于一个引用,将其代指的对象拿出来,以取引用正好相反这个操作就是解引用。
“*”解引用符号。

Warning

如果代指对象没有使用copy trait,那么解引用出来赋值给其他变量会带所有权

“透引用操作”

对于指针与引用,有直接操作本变量的操作也有透过指针操作指针所代表数据的操作。
形象而言,通过“引用”操作引用背后的代表,叫作“透引用操作”。

Summary

“.”符号

对于“引用变量”自动帮你插入 *(即调用 Deref),所以.就相当于了*
对于“所有权变量”自动取引用“&”,所以所有权变量也能最小权限操作。
自动无限透引用,寻找匹配的方法或字段(比如 Rc<RefCell<Vec<T>>>,你写 .len(),编译器会自动解引用三层)

旨在让你无感知地操作深层数据。

智能指针

智能指针就是指针配套指针元数据

有一些空间专门存储指针的数据,比如引用数,所有权共享之类的。但是本质上核心功能仍然是指针。需要提供指针的基本功能“透指针”。

智能指针不同于普通结构体的地方在于它实现了 Deref 和Drop trait。

Rust当变量离开作用域会被调用Drop trait的方法

用于自定义销毁。

Deref trait用于指针类型数据结构返回其代表的方法

“*”操作符会调用这个trait的方法来获得指针所代表的对象

Box<T>

Summary
let b = Box::new(5);

Box 允许你将数据存储在堆上而不是栈上。留在栈上的则是指向堆数据的指针。

Drop释放指针的时候,会释放其栈上变量、堆上数据两处

释放时既会清理 box 本身(存储在栈上),也会清理它指向的数据(存储在堆上)。

Summary

而指针可以用固定的内存大小表示不固定的内存大小,或者说指针可以用特定的大小表示任意的大小,所以我们只需要给指针分配特定大小的空间即可表示任务大小的空间。

// 一个递归枚举类型
enum List {
	Cons(i32, Box<List>),
	Nil,
}

智能指针与常规引用

解引用符号”” 用于 “引用”与“指针”

[!summary] 解引用符号”*” 用于 “引用”与“指针”
实际上都是调用的deref trait的方法,返回这个指针、引用所代表的对象。
如果不使用解引用,那么表示读这个引用变量本身,而非其代表的背后的对象。

Summary

比如我们自己创建一个类型MyBox,只要实现了Deref那么也能像引用,指针一样的解引用,但是结构体默认分配在栈上。

本质上就是的反向操作,deref返回配合得到原对象

[!summary] *本质上就是&的反向操作,deref返回&配合*得到原对象
输入 *y 时事实上在底层运行*(y.deref())

每次当我们在代码中使用 * 时, * 运算符都被替换成了先调用 deref 方法再接着使用
*解引用的操作,且只会发生一次,不会对 * 操作符无限递归替换,解引用出上面 i32 类型的值就停止

Deref引用强制转换:外层引用完全可以转换为内层引用

因为实现了deref的指针类型完全是代表某对象的,所以可以直接转换为所代表的对象的引用。
而转换方法就是通过deref方法。

自动进行、运行时无开销

当这种特定类型的引用作为实参传递给和形参类型不同的函数或方法时将自动进行强制转换。
这些解析都发生在编译时,所以利用 Deref 强制转换并没有运行时开销!

例如 &String 完全可以转换成 &str,无论是通过调用deref()还是通过自动进行强制转换。

String类型实现了Deref但直接解引用有坑

语法上能解引用:编译器允许你写 *s,但是str 是动态大小类型,它在编译时大小未知,因此不能作为普通变量存储在栈上。
实际上不能“接住”,*s 唯一的存在意义,就是立刻借用它,或者通过它调用方法,但这完全是多此一举,直接使用&str或者&String即可。

Summary
  1. 当 T: Deref<Target=U> 时从 &T 到 &U 。
  2. 当 T: DerefMut<Target=U> 时从 &mut T 到 &mut U 。
  3. 当 T: Deref<Target=U> 时从 &mut T 到 &U 。

Rust 也会将可变引用强转为不可变引用。但反之是不可能 的:不可变引用永远也不能强转为可变引用。

也就是说,要解出不可变引用可以通过Deref和DerefMut两种trait方法提供,但是要解出可变引用只能通过DerefMut trait的方法提供。

Summary

有些时候,智能指针所指向的空间并非简单的内存,而是一些其他资源,例如网络套接字资源、文件句柄等,我们需要实现在所有权变量被drop时的清理操作。

当实例离开作用域时,Rust 会自动替我们调用 drop ,并运行我们指定的代码。变量会按照创建顺序的逆序被丢弃,所以 d 会先于 c 被丢弃。

变量的生命周期作用域,通常不是整个作用域的生命,而是在最后一次使用变量的时,下一行就会直接终结这个变量。

Rust不允许我们直接调用drop函数

因为rust默认会帮我们调用,如果我们先调用了而rust并不知道还会再次调用drop导致二次释放错误。
所以我们需要使用一种能通知到rust的方法,由标准库提供的 std::mem::drop 函数来间接调用drop函数,这样rust就不会再次调用被drop的对象的drop了。

Warning
  1. 提前结束借用期,让编译器闭嘴。(有些时候编译器固执认为没有结束生命)
  2. 显式语法意图,提高可读性

Rc<T>

Summary

每个引用都是相等的地位。

Summary

注:我们可以使用 a.clone(),但是为了区别普通的深拷贝,这里只是浅拷贝所以调用Rc::clone(a) 更好

Rc的计数代码是单线程代码,没有做并发控制

所以只能在单线程中使用Rc

强引用与弱引用

强引用就是明面引用,必须要源存在,否则强引用会崩溃。
弱引用就是暗地引用,不必关心他们的存在,可以直接清空源,弱引用崩溃就崩溃吧。

指针可变性与内部可变性

虽然我们的指针变量看似是具有所有权的,但是这只代表了指针变量,而不代表指针背后的对象的可变性。
当指针计数为1的时候,我们可以取出背后的对象,或者使用背后对象的可变引用。

Rc的方法与函数

Summary
Summary
尝试移出

如果你确定当前这个Rc 是最后一个强引用(引用计数为 1)
Rc::try_unwrap(rc)

尝试获取可变引用

必须满足一个极其严格的前提:强引用计数(strong_count)必须正好为 1,且弱引用计数(weak_count)也必须为 0。
并且指针必须是mut
Rc::get_mut(&mut rc) 返回的是 &mut T(独占可变引用)

RefCell<T>与内部可变性

Summary

之前的所有修改链都遵循一个模式,受限于路径上任何一个不可变环境。而内部可变性则让修改链在RefCell上从不可变变为可变!!!

相当于可以从RefCell变色。

也就是不管外部引用或者指针路径,有多少不可变,可变的拦截,只要最终能访问到RefCell,就能从这个ReCell拿到可变的内部,或者不可变的内部,这是运行时决定的。

而运行时违反,就比如我暂存了一个不可变的内部引用,存储在变量中,然后我再获得了一个可变的内部引用也存储在变量中,这个时候我再操作第一个不可变变量就不行了,这使得可变借用与不可变借用生命周期交叉。

将RefCell放到最后是最有效的!

并且将RefCell放在最后是最有效的,想想:不变不可变不可变最后可变,如果放在中间或者前面也许路径只要出现不可变那么最终就不可变。

RefCell的特殊性:借用规则的判断时机

对于引用和Box<T>,借用规则的不可变性(invariants)在编译时就会被强制执行。对于RefCell<T> ,这些不可变性作用于运行时。对于引用,如果违反这些规则,会得到一个编译错误。而对于 RefCell<T> ,如果违反这些规则程序会 panic 并退出。

只能单线程使用

其内部的借用检查机制没有使用任何同步原语(如锁),因此在多线程下不是线程安全的

RefCell的可变借用与不可变借用机制

实际上仍然利用了指针计数,当不可变借用的时候检查可变计数=0,借出并增加不可变计数。
可变借用的时候检查当前RefCell的不可变计数=0,且可变计数=0,才借出增加可变计数。

最常见的就是嵌套

[!tip] 最常见的就是Option<Rc<RefCell<xxx>>>嵌套
允许多个可变数据所有者

引用循环与内存泄露

Rust有可能造成内存泄露:当引用循环时

想要释放内存,我们需要让计数=0,当形成循环的时候,在任何时刻每个节点都有一个节点指向他们所以计数>=1,当我们想要释放任何一个节点,那么drop的顺序会先释放节点中的指针成员,而指针成员的drop又兜兜转转到自己身上。

index - 1

Warning

如果你有包含 Rc<T> 的 RefCell<T> 值或类似的嵌套结合了内部可变性和引用计数的类型,请务必小心确保你没有形成一个引用循环;

Summary

为了使用 Weak<T> 所指向的值,我们必须确保其值仍然有效。

为此可以调用Weak<T> 实例的 upgrade 方法,这会返回 Option<Rc<T>> 。

如果Rc<T> 值还未被丢弃,则结果是 Some ;如果 Rc<T> 已被丢弃,则结果是 None 。

弱引用所表达的逻辑关系:我默默注视着你但你不属于我

父节点应该拥有它的子节点。如果父节点被丢弃了,它的子节点也应该被丢弃。然而,子节点不应该拥有它的父节点。如果我们丢弃一个子节点,父节点仍然应该存在。这正是弱引用适用的场景!

RefCell的方法与函数

可变借用与不可变借用

返回所代表对象的借用,分为可变与不可变,所以有两个方法。
refcell.borrow() 返回 &
refcell.borrow_mut() 返回 &mut

Cow指针指针

Cow即Clone on Write,可延迟克隆

它表示是这样的一个数据:
只读时直接用引用读取,当要修改的时候,检查Cow自身是只读引用还是所有权,如果是只读引用再进行克隆,然后修改。
如果是所有权就直接进行修改。

延迟克隆的作用

实际Cow上是为了更好的传输数据,有的数据传输过去只是为了只读,这种用途用不着再克隆,可以让Cow携带只读引用。对于无法保留的数据,Cow也能直接携带数据本体。

Cow最常用的用法

最常用的两个方法是.into_owned()(无条件从引用转换为实体数据)
 .to_mut()(获取可变引用,自动触发写时克隆)。