Rust编程进阶 - 什么是自引用类型?为什么说目前的生成器只是一个在nightly版本中存在的功能,如何来解决使得借用跨yield 存在
我们再看看最开始那段示例:
let mut g = |{
let mut curr: u64 = 1;
let mut next: u64 = 1;
loop {
let new_next = curr.checked_add(next); // 下轮循环的时候要继续使用 curr next 的值
if let Some(new_next) = new_next {
curr = next;
next = new_next;
yield curr; //<-- 此处退出
} else {
return;
}
}
};
可以看到,再进入生成器的时候,局部变量curr next的值是马上就需要使用的, 因此变量g 里面无论如何都要给这两个变量留下位置,保存它们的值。否则,下次再调 用 resume 方法的时候,它们就无法恢复到上次退出时的状态了。而另外一个局部变量 new_next 则无须保存,因为它没有跨yield 存在,所以这个局部变量可以作为成员方法 resume 内部的局部变量,无须提升为g 的局部变量。
编译器把这个生成器处理之后,逻辑如下:
-
- 编译器实际上不是在源码级别做的转换,而是在MIR 做的转换,以下代码只是为了说明原理,与真实的编译器转换后的代码并不一致
-
- 编译器是如何做这个转换的,请参考源码 librustc_mir/transform/generator.rs
-
- 目前编译器实际上是转换为 struct,此处使用 enum 是为了更方便地演示大概的逻辑
# ! [feature(generators, generator_trait)] use std: :ops: :{
Generator,
GeneratorState
};
fn main() {
let mut g = {
enum_AnonymousGenerator {
Start {
curr: u64,
next: u64
},
Yield1 {
curr: u64,
next: u64
},
Done,
}
impl Generator
for AnonymousGenerator {
type Yield = u64;
type Return = ();
unsafe fn resume( & mut self) - >GeneratorState < Self: :Yield,
Self: :Return > {
use std: :mem;
match mem: :replace(self, AnonymousGenerator: :Done) {
_AnonymousGenerator: :Start {
curr,
next
}
_AnonymousGenerator: :Yieldl {
curr,
next
} = >{
let new_next = curr.checked_add(next);
if let Some(new_next) = new_next { * self = _AnonymousGenerator: :Yield1 {
curr: next,
next: new_next
};
return GeneratorState: :Yielded(curr);
} else { * self = _AnonymousGenerator: :Done;
return GeneratorState: :Complete(());
}
}
_AnonymousGenerator: :Done = >{
panic ! ("generator resumed after completion")
}
}
}
}
AnonymousGenerator: :Start {
curr: 1,
next: 1
}
};
loop {
unsafe {
match g.resume() {
GeneratorState: :Yielded(v) = >println ! ("{}", V),
GeneratorState: :Complete(_) = >
return,
}
}
}
}
可以看到,转换后的代码实际上和迭代器非常相似。所以,生成器实际上是让编译器帮 我们自动管理状态:哪些状态应该放到成员变量里面,哪些不需要;退出前如何保存状态, 重新进入的时候如何读取上次的状态等,都是编译器帮我们自动做好了的。
如果生成器内部存在多个yield 语句呢?比如下面这样:
let mut g = ||{
yield 1_i32;
yield 2_i32;
yield 3_i32;
return 4_i32;
};
那我们就再引入一个状态,来表达上次已经执行到哪条语句了,下次调用应该从哪条语 句开始执行。在进入resume 方法的时候,先判断这个状态,然后再跳转即可。
let mut g = {
struct_AnonymousGenerator {
state: u32
}
impl Generator for__AnonymousGenerator {
type Yield = i32;
type Return = i32;
unsafe fn resume( & mut self) - >GeneratorState < Self: :Yield,
Self: :Return > {
match self.state {
0 = >{ //从初始状态开始执行
self.state = 1;
return GeneratorState: :Yielded(1);
}
1 = >{ //上 一 次返回的是yield 1
self.state = 2;
return GeneratorState: :Yielded(2);
}
2 = >{ / 1上一次返回的是yield 2 self.state = 3;
return GeneratorState: :Yielded(3);
}
3 = >{ // 上一次返回的是yield 3
self.state = 4;
return GeneratorState: :Complete(4);
}
_ = >{ //上 一 次返回的是return 4
panic ! ("generator resumed after completion")
}
}
}
_AnonymousGenerator {
state: 0
}
};
总之,任何 一 个生成器,总能找到办法将它自动转换为类似迭代器的样子。之所以说是类似,是因为生成器的功能更强大,它的resume() 方法实际上可以设计为携带更多的参 数,只是目前的Rust还没有实现,这个需求并不是很紧急而已。
自 引 用 类 型
目前的生成器只是一个在nightly版本中存在的、实验性质的功能,它还有一些问题没有 解决。最主要的一个问题是如何使得借用跨yield 存在。示例如下:
# ! [feature(generators, generator_trait)]
fn {
}
main() {
let _g let let yield yield
};
{
local = 1;
ptr = &local;
local; * ptr;
}
编译,出现编译错误:
error[E0626]:borrow may still be in use when generator yields
这个错误究竟是什么意思呢?我们可以通过分析生成器的原理来理解这个错误的含义。 可以尝试看看这个生成器剥掉语法糖之后的样子。注意到,第一个yield之后变量ptr依 然被使用,且local这个变量也还存在,那么意味着我们要在生成的匿名类型的成员中, 保存ptr 和 local 这两个变量。再加上一个成员变量记录yield 的位置信息,我们可以设 计下面这样的匿名结构体:
struct_Generator {
local: i32,
ptr: &i32,
state: u32,
}
针对这个类型实现Generator这个trait, 基本上就等同于上面那段程序剥掉语法糖之 后的效果。
现在就可以更清楚地看到具体问题在哪里了。这里的关键点是: 一个结构体类型内部 出现了一个成员引用另外一个成员的现象。这种类型被称为“自引用类型”(Self-Referential Type) 。目前的Rust, 对自引用类型有很多限制。因为这个类型会破坏Rust 的一个基本假设: 任何类型都是可移动的。这个假设让Rust 的移动语义变得非常清晰简单(主要跟C++ 对比)。
但是自引用类型在移动的时候会出问题。原本成员ptr 是指向成员变量local 的,如果这个结构体整体发生了移动,ptr 指针的值保持不变,local的位置却发生了变化,那么就会 制造出悬空指针。所以,目前的Rust 是不允许这种情况出现的,这种代码会被生命周期检查 禁止掉。这就是上面那段示例代码无法编译通过的深层原因。
但是自引用现象未必就一定不安全。假如构成自引用之后这个对象就永远不再移动,那 么它其实是没问题的,也不会有悬空指针之类的情况出现。在写生成器的时候会很容易出现 自引用对象,如果完全禁止这种行为,会非常影响用户体验。如何让用户有权创建自引用的 生成器,同时又能避免安全性问题呢? Rust 设计组通过巧妙的设计做到了这一点。主要想 法是:
- 应该允许用户创建自引用生成器,因为在调用resume 方法之前的移动都是没问题的, 毕竟这个时候它内部的许多成员都是未初始化状态;
- 一 旦resume 被调用过了,以后就不能再移动这个对象了,因为这时候指针和被指向 的对象很可能已经初始化好了,再发生移动就会造成内存不安全。具体来说,设计组会做以下改变。
- 标准库引入一个新的智能指针类型 PinMut<'a,T>, 它可以指向一个T 类型的对象。它的作用是,当这个指针存在的时候,它所指向的对象是不可移动的。
- 允许更多的智能指针类型作为self 变量的类型,这样我们可以指定resume 方法的 第一个参数是self:PinMut 类型,而不是&mut self了 。
这样,就可以从逻辑上保证用户调用resume方法之前, 一 定先构造出 一 个PinMut的指针变量。这样,在这个变量存在的期间,生成器就无法移动,调用resume必须通过这个指针来完成。有了这个保证, resume方法前面的unsafe修饰 也就可以去掉了。预计这个设计到2018年下半年就可以稳定下来。
另外,生成器本身并不是直接面向广大用户的接口。用户真正需要的是完成异步任务。 实际上,“协程”才是最终用户用得最多的东西。生成器只是实现协程的一个底层工具。最终,协程库会把所有这些PinMut 指针之类的事情封装管理起来。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)