☰
Rust 生成器语法:深入解读 RFC 3513 的 `gen` 块与 `yield` 迭代器
2026/10/16 2:23:31 网站建设 项目流程
  • 文档

【免费下载链接】rfcs

RFCs for changes to Rust

项目地址:https://gitcode.com/gh_mirrors/rf/rfcs
点击查看免费下载

导读

本文以 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 / Alpharditer关键字开启迭代器上下文,yield产出值可追溯至 1975 年前后的 Alphard 语言,后由 CLU 精炼为迭代器概念
Icon任何函数都可返回值序列,显式使用suspend生成器深植于语言(约 1977 年)
Python含yield的函数返回生成器最广为人知的生成器形态
JavaScriptfunction*生成器函数中的yield是通用协程:yield形成表达式,返回next传入的值
RubyEnumerator类实现迭代器;Fiber提供通用协程生成器构建在更通用的 Fiber 之上
Kotlinsequence表达式 +yield构造惰性Sequence
SwiftAsyncStream+yield异步生成器;同步生成器尚未提供
C#迭代器中的yield return/yield break与 Rust 不同:C# 的async Task在 await 前即开始执行
DGenerator+yield构建在通用Fiber之上(同 Ruby)
Dartsync*同步生成器返回Iterable,async*异步生成器返回Stream同步/异步两种形态
F#序列表达式(sequence expressions)+yield函数式风格
Racketgenerator+yield由于call/cc的能力,生成器可写成普通库
Haskell / Idris / Clean惰性列表,无需yield默认惰性,函数递归构造懒序列即可
Koka类型化 effect handleryield不是关键字而是 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

项目地址:https://gitcode.com/gh_mirrors/rf/rfcs
点击查看免费下载
上一篇:5个步骤轻松上手GRETNA:MATLAB脑网络分析的完整指南
下一篇:Container Lines 技能实战:用垂直容器边界线与角落小方块构建结构化网页布局

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询