- 文档
【免费下载链接】rfcs
RFCs for changes to Rust
导读
本文以 Rust RFC 仓库中 text/3513-gen-blocks.md 为绝对主体,系统讲解gen { .. }块(generator block)这一将"惰性迭代器"直接写进语言的设计:它借鉴async块与.await的对应关系,让gen块产生一个可以交给for循环迭代的值。读完本文,你将掌握gen块的语法与返回值约束、yield与?运算符在块内的语义、跨yield点持有引用的限制及其根源,以及该 RFC 在 2024 edition 关键字保留、实现原理与未来演进方向上的完整设计脉络。
背景与动机:为什么需要gen块
手写迭代器是 Rust 中众所周知的痛点。大量场景可以通过链式组合Iterator的组合子(combinator)完成,但仍有部分逻辑不得不落到手动impl Iterator的next方法中。这种落差会促使开发者干脆绕开迭代器,转而去写"把结果急切地塞进可变状态"的循环,牺牲惰性与组合性。
RFC 3513 给出的解法是:允许直接用一条简单的for循环写出惰性的迭代器。为了让对比足够直观,RFC 以行程长度编码(run-length encoding)为例,给出了三种写法的完整对照——这正是本文接下来要逐段分析的核心素材。
三种实现行程长度编码的方式
方案一:使用gen块(RFC 引入的新语法)
// This example uses `gen` blocks, introduced in this RFC. fn rl_encode<I: IntoIterator<Item = u8>>( xs: I, ) -> impl Iterator<Item = u8> { gen { let mut xs = xs.into_iter(); let (Some(mut cur), mut n) = (xs.next(), 0) else { return }; for x in xs { if x == cur && n < u8::MAX { n += 1; } else { yield n; yield cur; (cur, n) = (x, 0); } } yield n; yield cur; }.into_iter() }可以看到,逻辑完全保持"普通顺序代码"的形态:读首元素、逐项比较、必要时yield输出。编译器负责把这段代码改造成惰性迭代器的状态机。
方案二:手动实现Iterator
// This example uses a manual implementation of `Iterator`. fn rl_encode<I: IntoIterator<Item = u8>>( xs: I, ) -> impl Iterator<Item = u8> { struct RlEncode<I: IntoIterator<Item = u8>> { into_xs: Option<I>, xs: Option<<I as IntoIterator>::IntoIter>, cur: Option<<I as IntoIterator>::Item>, n: u8, yield_x: Option<<I as IntoIterator>::Item>, } impl<I: IntoIterator<Item = u8>> Iterator for RlEncode<I> { type Item = u8; fn next(&mut self) -> Option<Self::Item> { let xs = self.xs.get_or_insert_with(|| unsafe { self.into_xs.take().unwrap_unchecked().into_iter() }); if let Some(x) = self.yield_x.take() { return Some(x); } loop { match (xs.next(), self.cur) { (Some(x), Some(cx)) if x == cx && self.n < u8::MAX => self.n += 1, (Some(x), Some(cx)) => { let n_ = self.n; (self.cur, self.n) = (Some(x), 0); self.yield_x = Some(cx); return Some(n_); } (Some(x), None) => { (self.cur, self.n) = (Some(x), 0); } (None, Some(cx)) => { self.cur = None; self.yield_x = Some(cx); return Some(self.n); } (None, None) => return None, } } } } RlEncode { into_xs: Some(xs), xs: None, cur: None, n: 0, yield_x: None, } }这段代码暴露出手动实现的全部代价:需要手工设计一个承载全部中间状态的结构体(into_xs、xs、cur、n、yield_x五个字段),再为它编写分支繁多、易出错的next逻辑——match的四种组合、unsafe的unwrap_unchecked、状态字段的反复搬运,都是典型的"机械劳动"。
方案三:使用iter::from_fn
// This example uses `iter::from_fn`. fn rl_encode<I: IntoIterator<Item = u8>>( xs: I, ) -> impl Iterator<Item = u8> { let (mut cur, mut n, mut yield_x) = (None, 0, None); let (mut into_xs, mut xs) = (Some(xs), None); core::iter::from_fn(move || loop { let xs = xs.get_or_insert_with(|| unsafe { into_xs.take().unwrap_unchecked().into_iter() }); if let Some(x) = yield_x.take() { return Some(x); } match (xs.next(), cur) { (Some(x), Some(cx)) if x == cx && n < u8::MAX => n += 1, (Some(x), Some(cx)) => { let n_ = n; (cur, n) = (Some(x), 0); yield_x = Some(cx); return Some(n_); } (Some(x), None) => (cur, n) = (Some(x), 0), (None, Some(cx)) => { cur = None; yield_x = Some(cx); return Some(n); } (None, None) => return None, } }) }标准库 std::iter::from_fn 虽然比手写impl Iterator略省样板,但正如 RFC 所指出的,它对可读性的改善依然有限——状态仍然散落在闭包捕获的变量里,loop+match的复杂度原封不动。
三份代码放在一起,gen块的优势一目了然:把状态机的搭建交给编译器,把可读性还给程序员。
gen迭代器的终止与熔断行为
RFC 明确规定了由gen块产生的迭代器的终止语义:
- 当
gen块返回(隐式地走到块尾,或显式地执行return)之后,next返回None; - 这样的迭代器是熔断的(fused):一旦
next返回过一次None,之后对next的调用会永远返回None,不会再"复活"。
gen块的语法与返回值约束
新关键字:gen在 2024 edition 成为保留字
自 2024 edition 起,gen成为一个关键字,不能再用作任何 item 或绑定的名字。因此在向 2024 edition 迁移期间,所有名为gen的变量、函数、模块、类型等都必须改名,或者通过原始标识符r#gen来引用。
这一保留与 Rust 的 edition 机制一脉相承:gen是为未来的生成器/协程特性预留的语言空间。2024 edition 的目标之一正是"保留gen关键字以便未来引入(异步)生成器",这与 text/3672-Project-Goals-2024h2.md 中记载的 2024 edition 规划(支持 trait 中的-> impl Trait与async fn、保留gen关键字、调整!类型回退)相互印证。
返回值:必须发散或返回单元类型
gen块被严格约束为:必须发散(返回!)或返回单元类型()。具体来说:
- 块的尾部表达式必须是
()或!类型; - 块内任何
return语句要么不携带参数,要么携带()或!类型的参数。
这个约束是有意为之——gen块的全部产出都应通过yield表达,块自身的"完成值"不承载业务含义,这与async块形成呼应又形成对比(async块会返回()或表达式的值,而gen块只负责产生可迭代序列)。
发散块:无限序列与 panic 行为
一个永远不返回的gen块可以产生无限序列。例如产生0, 1, 0, 1, 0, 1, ..的块:
gen { loop { yield 0; yield 1; } }这种块永远不会让next返回None,其捕获的状态只在迭代器被 drop 时才被释放。
RFC 还明确了 panic 语义:如果gen块发生 panic,行为与return类似,但区别在于——调用next的现场会直接 unwind,而不是返回None。
错误处理:?在gen块内的特殊语义
?运算符在gen块内有一套独特的行为规则:
- 当其参数是"不应短路"的值(如
Option::Some(..)、Result::Ok(..)、ControlFlow::Continue(..))时,该值照常成为表达式的结果; - 当其参数是"应当短路"的值(如
Option::None、Result::Err(..)、ControlFlow::Break(..))时,该值会先被yield出去(照常经过FromResidual::from_residual转换),然后块立即return。
换言之,?在gen块中不是简单"向上传播错误",而是把错误作为序列的一个元素产出再终止迭代。即便使用了?,块的返回值仍必须是()或!——它不会像普通函数那样返回Some(..)、Ok(..)或Continue(..)。
由此引出一个重要的类型一致性推论:一旦在块内使用了?,所有yield语句的参数类型必须彼此兼容。例如,如果某个表达式使用了None?,那么所有yield的参数都必须是某种Option类型(或!类型)。
RFC 在 Reference-level 部分给出了foo?的等价脱糖形式,帮助理解其精确语义:
match foo.branch() { ControlFlow::Break(err) => { yield <_ as FromResidual>::from_residual(err); return }, ControlFlow::Continue(val) => val, }即:?相当于对表达式调用branch(),短路分支先把转换后的残差yield出去再return,继续分支则取出值继续执行。
熔断(Fusing):行为熔断但暂不实现 trait
gen块产生的迭代器在行为上是熔断的——next返回过一次None后就永远返回None。但 RFC 明确指出,它们当前不实现FusedIteratortrait,因为该 trait 并非语言内建(language item),未来可能会实现(详见下文"未来可能性")。
跨yield持有借用:当前的关键限制
这是gen块当前最显著的能力边界。由于Iterator::next接收的是&mut self而不是Pin<&mut Self>,无法在yield点之间持有指向局部变量的借用——即不能构造自引用(self-referential)的gen块。RFC 给出的反例:
gen { let xs = vec![1, 2, 3, 4]; for x in xs.iter() { yield x * 2; } //~^ ERROR borrow may still be in use when `gen` block yields }xs.iter()产生的迭代器借用着块内的局部xs,一旦yield x * 2挂起,借用将跨越挂起点存活,编译器会以 "borrow may still be in use whengenblock yields" 拒绝该代码。
RFC 作者坦承,这可能是一个严重且令人意外的限制,是否应在稳定化之前采取措施解决它,被列为开放问题(详见后文)。
实现原理:建立在既有协程(coroutine)之上
RFC 的 Reference-level 部分说明了实现路径:该特性大部分基于编译器既有的协程机制实现,但有若干特殊处理。可以概括为:gen块与不稳定(nightly)的协程基本相同,差异仅在:
- 不带参数;
- 增加了一项检查,禁止在
yield点之间持有借用; - 自动实现一个 trait,使该类型可用于
for循环(细节见开放问题); - 返回之后再次被调用时不会 panic(即熔断行为)。
这一设计思路与 Rust 的协程谱系高度一致。仓库中的 text/2033-experimental-coroutines.md 是协程的实验性 RFC,其中描述了"无栈协程(stackless coroutines)"的核心设计目标:兼容 libcore(不依赖运行时分配与内建指令)、大体上被编译为随恢复不断推进的状态机(每当yield时把自己留在可从该语句恢复的状态)、像闭包一样捕获变量且不引入动态分发开销。gen块正是这套状态机变换在"惰性迭代器"场景的落点。
与async块的对称关系
RFC 的 Summary 直接点明了设计对称性:就像async块产生可以用.await等待的值一样,gen块产生可以用for迭代的值。仓库中的 text/2394-async_await.md 记载了async块(text/2394-async_await.md)的引入,二者共用同一套状态机变换底座:async块把"顺序代码"编译成可暂停的 Future,gen块把"顺序代码"编译成可暂停的迭代器。
权衡与备选方案
为什么选gen而不是iter做关键字
RFC 认真考虑了用iter作为关键字的方案,但最终否决,理由如下:
- 不稳定的协程在
rustc与 nightly Rust 中最初就名为 "generators"(生成器),部分作者认为"生成器"这一名称天然指向这种更强大的控制流构造——它能做到gen块与async块能做的一切,甚至更多; - 从语法上把
Iteratortrait 与iter块联系起来虽有吸引力,但iter在生态中(乃至libstd、libcore内部)被广泛用作模块名与方法名,若将其定为关键字将不得不为它开凿大量例外。
反过来,现在不用gen保留该名字,就为未来留下了可能性:将来可以用gen命名一种产生更强类型的块——例如实现更强大的Generatortrait(也许以 pin 引用接收self)或实现Coroutine的块。
备选:不做这个特性
RFC 逐一论证了三条"不做"的替代路线,并说明它们各自的不足:
1. 增加更多组合子。与其加语言特性,不如给Iterator加辅助方法。但新手熟悉现有海量方法已属不易,且部分想加的方法需要高度泛型化(类似array::try_map),收益有限。
2. 使用第三方 crate。例如genawaiter(stable Rust 上可用,提供行为类似gen块的gen!宏块,但没有编译器级别的诊断支持,也不支持?运算符的语言级语义)、propane与iterator_item(都基于 nightly 的Coroutinetrait,因此需要不稳定 Rust)。这些方案要么受限于宏的"二等公民"地位,要么受制于 nightly。
3. 使用iter::from_fn。正如动机部分的对照代码所示,from_fn相比手写impl Iterator的改善相当有限,状态管理仍然繁琐。
备选:让尾部表达式 yield 最后一个元素
RFC 还讨论了一个诱人的变体:让尾部表达式的值被 yield 出去后再终止迭代。例如:
fn foo() -> impl Iterator<Item = i32> { gen { 42 } }可以等价于std::iter::once(42)。但该设计会引入一连串歧义:
- 必须新增某种"不产生值直接终止"的机制,例如魔法般地让返回
()终止迭代,那么gen { 42; }(带分号)就得理解为不返回值——语义微妙且反直觉; gen {}这种空块既可能合理地对应std::iter::once(()),也可能对应std::iter::empty::<()>(),语义无法自洽。
因此 RFC 放弃了该方案,并指出现行设计下依然可以简洁地用return yield EXPR表达"yield 最后一个元素",因为块内return接受()参数而yield表达式的值是()。
先例:其他语言中的生成器
RFC 用大量篇幅梳理了其他语言中"生成器/迭代器"的设计,构成了完整的先例对照。这些先例既印证了"yield 产生惰性序列"这一思想的普遍性,也帮助定位 Rustgen块的独特取舍——特别是"块直到被迭代才开始执行"(与 C# 的async Task不同)这一设计选择。
| 语言 | 机制 | 关键点 |
|---|---|---|
| CLU / Alphard | iter关键字开启迭代器上下文,yield产出值 | 可追溯至 1975 年前后的 Alphard 语言,后由 CLU 精炼为迭代器概念 |
| Icon | 任何函数都可返回值序列,显式使用suspend | 生成器深植于语言(约 1977 年) |
| Python | 含yield的函数返回生成器 | 最广为人知的生成器形态 |
| JavaScript | function*生成器函数中的yield | 是通用协程:yield形成表达式,返回next传入的值 |
| Ruby | Enumerator类实现迭代器;Fiber提供通用协程 | 生成器构建在更通用的 Fiber 之上 |
| Kotlin | sequence表达式 +yield | 构造惰性Sequence |
| Swift | AsyncStream+yield | 异步生成器;同步生成器尚未提供 |
| C# | 迭代器中的yield return/yield break | 与 Rust 不同:C# 的async Task在 await 前即开始执行 |
| D | Generator+yield | 构建在通用Fiber之上(同 Ruby) |
| Dart | sync*同步生成器返回Iterable,async*异步生成器返回Stream | 同步/异步两种形态 |
| F# | 序列表达式(sequence expressions)+yield | 函数式风格 |
| Racket | generator+yield | 由于call/cc的能力,生成器可写成普通库 |
| Haskell / Idris / Clean | 惰性列表,无需yield | 默认惰性,函数递归构造懒序列即可 |
| Koka | 类型化 effect handler | yield不是关键字而是 effect,生成器、async、协程自然涌现 |
| Rust(现状) | 基于async块 + 无操作Waker手工构造 | 需借助 crate(如genawaiter) |
其中 Rust 自身的"手工构造"示例值得细看——它展示了在没有gen语法时,如何用协程变换暴露生成器能力:
let odd_dup = |xs| { Gen::new(async move |mut y| { for x in xs { if x % 2 == 1 { y.r#yield(x * 2).await; } } }) }; let odd_dup = pin!(odd_dup(1u8..20)); let odd_dup = odd_dup.init(); for (i, x) in odd_dup.enumerate() { assert_eq!((i as u8 * 2 + 1) * 2, x); }这段代码以async块承载生成逻辑,用一个"空的Waker"让 Future 的挂起变成纯同步的暂停,r#yield(..).await模拟了yield。genawaiter这类 crate 正是基于该技巧,其复杂性也反过来证明了把gen块直接做进语言的必要性。
开放问题:稳定化之前需要回答的设计悬案
RFC 列出了一系列开放问题,它们是理解gen块边界与未来形态的关键:
1. 是否直接实现Iterator。让gen块的返回类型不直接实现Iterator,而是实现IntoIterator或新的IntoGeneratortrait,可能为支持自引用gen块留下更优选项。悬而未决。
2. 自引用gen块。是否允许gen块在yield点之间持有借用,并应否纳入初次稳定化?RFC 给出了三种可能的实现路径及其代价:
- 为 pin 迭代新增独立 trait,同时兼容
gen与for——代价是会出现两个用途几乎相同的 trait; - 向后兼容地改变
Iterator::next的参数类型——可行性存疑; - 改为对
Pin<&mut G>实现Iterator——next会接收&mut Pin<&mut G>这种双重间接引用,可能不利于优化。
这与 text/2996-async-iterator.md 中关于生成器函数(gen fn/async gen fn)的讨论一脉相承:Iterator的核心next(&mut self)不要求 pin,而AsyncIterator的核心接口是 pin 的,导致同步/异步生成器在 pin 处理时机上天然不同。
3. 关键字选择。是坚持gen(将来可扩展其能力以涵盖更强大的生成器或协程),还是改用iter?悬而未决。
4. 是否做成上下文关键字。流行 crate(如rand)的 trait 上有名为gen的方法。若把gen定为完整关键字,Rust 2024 及后续 edition 的用户在相关 crate 更新适配前,都需要把这些方法调用写成r#gen。备选方案是将其做成上下文关键字,只在以下位置禁止使用:绑定(binding)、字段名(涉及解构绑定)、枚举变体、类型名。
5.Iterator::size_hint。是否应尝试计算保守的size_hint?这会把生成器体的信息暴露出来;但至少在简单情况下,用户会期望size_hint不只是默认值。事后以向后兼容方式补充对size_hint的机会式实现是可行的。
6. 其他Iteratortrait。未来是否希望/能够实现DoubleEndedIterator、ExactSizeIterator等?
7. 2015 与 2018 edition 怎么办。由于 text/3101-reserved_prefixes.md 保留了前缀#标识符形式的语法(如<identifier>#<identifier>整体被保留给语言未来使用),可以在 Rust 2021 中用k#gen使用该特性。但 2015/2018 edition 是否以及如何支持该特性,留给未来决定。
未来可能性
RFC 为gen块描绘了清晰的演进蓝图,全部与当前设计向前兼容:
1.yield from(转发运算符)。类似 Python 的yield from,本质是"遍历迭代器并逐个 yield"的语法糖。备选实现有:什么都不做(用户直接写for x in iter { yield x });语言级支持(如后置yield写成iter.yield,或引入全新关键字);标准库宏(如yield_all!(iter),展开为for循环 +yield,加入 prelude)。
2. 完整的Coroutine支持。nightly 上已有比Iterator更强大的Coroutinetrait(曾名Generator):resume接收Pin<&mut Self>从而允许跨 yield 点自引用;yield返回resume传入的参数。未来可能把协程论证为gen闭包,而gen块保持简单形态。
3.async交互。未来可支持gen async块内的await,机制类似?在gen块内的用法。在没有自引用生成器解决方案之前,此类块不能跨await点持有引用。
4.try交互。可允许gen try fn foo() -> i32表示类似gen fn foo() -> Result<i32, E>的语义,且应与未来的try fn语义保持一致。
5.gen fn。本 RFC不引入gen fn。其语法设计空间很大,且围绕"返回类型还是 yield 类型"存在开放问题。RFC 罗列了候选语法:
fn foo(..) yield .. { .. } fn foo(..) yields .. { .. } fn foo(..) => .. { .. } // Each of the below may instead be combined // with `yield`, `yields`, or `=>`. fn* foo(..) -> .. { .. } gen fn foo(..) -> .. { .. } gen foo(..) -> .. { .. } generator fn foo(..) -> .. { .. }6. 实现FusedIterator。gen块产生的迭代器行为上已熔断,但因FusedIterator不是语言内建而暂未实现;未来可能让其实现该 trait。
与生态及后续 RFC 的衔接
gen块并非孤立特性,它在仓库的 RFC 体系中与多条线索相互咬合:
- edition 规划:text/3501-edition-2024.md 确立 2024 edition 的发布框架;
gen关键字保留正是该 edition 的语言变更之一。 - 协程谱系:text/2033-experimental-coroutines.md 奠定了状态机式无栈协程的实现基础,
gen块、async块都建立其上。 - 异步迭代器:text/2996-async-iterator.md 展望了
gen fn(同步)与async gen fn(异步)两种生成器函数形态,并讨论了 pin 处理差异——与gen块的开放问题直接相关。 - 闭包方向:text/3668-async-closures.md 指出
gen(RFC 3513)与async gen等协程类修饰符的存在,提示未来还需考虑在闭包与Fn()trait 约束中支持它们。
结语
RFC 3513 为 Rust 带来了"用普通for循环写出惰性迭代器"的能力,与async块构成完整的对称设计:.await等待一个值,for迭代一串值。它对返回值施加()/!约束、为?定义了"yield 错误再终止"的独特语义、并坦诚地把"跨 yield 点持有借用"这一限制列为稳定化前的核心开放问题。理解这份 RFC,既有助于掌握 2024 edition 中gen关键字的来历,也能看清 Rust 在迭代器语法糖与完整协程之间正在铺设的路径。
延伸阅读(仓库内相关文档)
- text/3513-gen-blocks.md:本 RFC 原文
- text/2033-experimental-coroutines.md:无栈协程的实验性 RFC,
gen块与async块共同的实现底座 - text/2394-async_await.md:
async/await语法与async块的设计 - text/2996-async-iterator.md:异步迭代器与
gen fn/async gen fn的前瞻设计 - text/3101-reserved_prefixes.md:
前缀#标识符保留语法,k#gen的合法性来源 - text/3501-edition-2024.md:2024 edition 发布框架
- text/3668-async-closures.md:协程类修饰符(含
gen)在闭包方向上的展望 - text/3672-Project-Goals-2024h2.md:2024 edition 对"保留
gen关键字"的规划记录
- 文档
【免费下载链接】rfcs
RFCs for changes to Rust
相关推荐
CoffeeScript 生成器函数完全指南:yield、yield from 与 for…from 迭代
CoffeeScript 生成器函数完全指南:yield、yield from 与 for…from 迭代 CoffeeScript 全面支持 ES2015 的
编程语言编译器REFramework 启动崩溃修复完整指南:RE2重制版黑屏闪退回桌面,三条路径恢复
REFramework 启动崩溃修复完整指南:RE2重制版黑屏闪退回桌面,三条路径恢复 装好 REFramework 后,双击启动《生化危机2重制版》却黑屏一闪
游戏开发逆向工程插件系统深入理解realtime-chat-supabase-react状态管理:React Context API实战应用
深入理解realtime chat supabase react状态管理:React Context API实战应用 想要构建一个功能完整、响应迅速的全栈实时聊
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考