我们再看看最开始那段示例:

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 的局部变量。

编译器把这个生成器处理之后,逻辑如下:

    1. 编译器实际上不是在源码级别做的转换,而是在MIR 做的转换,以下代码只是为了说明原理,与真实的编译器转换后的代码并不一致
    1. 编译器是如何做这个转换的,请参考源码 librustc_mir/transform/generator.rs
    1. 目前编译器实际上是转换为 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 指针之类的事情封装管理起来。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐