文档【免费下载链接】rfcsRFCs for changes to Rust项目地址https://gitcode.com/gh_mirrors/rf/rfcs点击查看免费下载let-else 是 Rust 引入的一种let PATTERN: TYPE EXPRESSION else DIVERGING_BLOCK;语句它是if let表达式在成功路径继续、失败路径提前发散这一常见错误处理场景下的天然对应物。本文以 RFC 仓库中的 text/3137-let-else.md 为骨架结合 text/0160-if-let.md、text/1216-bang-type.md 等仓库文档完整讲解该语法的设计动机、参考级语义、desugar 过程、限制条件与备选方案帮助你彻底理解并正确使用 let-else。特性概览let-else 是什么RFC 3137Start Date: 2021-05-31Rust Issue: rust-lang/rust#87335提出引入一个新的let语句形态let PATTERN: TYPE EXPRESSION else DIVERGING_BLOCK;它被非正式地称为let-else statement是if let表达式的对应物counterpart。其核心语义是如果赋值表达式与模式匹配成功则模式中的绑定被引入周围作用域surrounding scope如果匹配不成功则必须执行一个发散块diverging block即以return、break、continue、loop { ... }、panic等返回 never 类型!的代码路径退出当前上下文。从类型系统的角度看let-else 语句是一种可反驳的refutablelet语句。该 RFC 是 2015 年一个几乎相同特性的旧提案pull request 1303的现代化版本。该 RFC 后来被 Rust 官方接受let-else 最终在 Rust 1.65 中作为稳定语言特性发布如今已经是 Rust 错误处理与解构场景中的常用语法。设计动机if-let 的局限与右向漂移if-let 为什么不够用text/0160-if-let.mdRFC 160if-let 的原始提案指出if let PAT EXPR { BODY }允许在条件位置进行可反驳模式匹配从而避免了完整match的语法与语义开销并减少了右向漂移rightward drift。if let对解包Option这类有明确成功变体、但没有专门失败变体的类型非常有用。然而if let有一个结构性缺陷它只能在自身代码块体内创建绑定。这会导致后续逻辑被迫向右缩进产生嵌套条件判断与错误处理路径被分隔开else 分支可能离条件很远成功路径的逻辑越长缩进和嵌套问题越严重。let-else 把失败情况放进else块中同时让成功情况在外层上下文继续执行而无需额外嵌套。RFC 中还强调对于Option/Result之外的枚举类型let-else 比用match或if let模拟等价逻辑更简洁、更自然——后者通常需要引入中间绑定且往往与最终变量同名。真实项目示例重构前后对比RFC 给出了一个取自真实项目作者参与编写的示例展示用 let-else 消除右向漂移的过程。先看没有 let-else 的原始写法注意else分支错误路径离它对应的条件很远impl ActionView { pub(crate) fn new(history: HistoryAction) - ResultSelf, eyre::Report { let mut iter history.iter(); let event iter .next() // RFC comment: ok_or_else works fine to early return when working with Option. .ok_or_else(|| eyre::eyre!(Entity has no history))?; if let Action::Register { actor: String, x: VecString y: u32, z: String, } event.action().clone() { let created *event.created(); let mut view ActionView { registered_by: (actor, created), a: (actor.clone(), x, created), b: (actor.clone(), y, created), c: (z, created), d: Vec::new(), e: None, f: None, g: None, }; for event in iter { view.update(event)?; } // more lines omitted Ok(view) } else { // RFC comment: Far away from the associated conditional. Err(eyre::eyre!(must begin with a Register action)); } } }再对比使用 let-else 的版本模式解构直接发生在语句层面else块紧邻对应的条件后续构造ActionView的代码不再需要任何额外缩进impl ActionView { pub(crate) fn new(history: HistoryAction) - ResultSelf, eyre::Report { let mut iter history.iter(); let event iter .next() // RFC comment: ok_or_else works fine to early return when working with Option. .ok_or_else(|| eyre::eyre!(Entity has no history))?; let Action::Register { actor: String, x: VecString y: u32, z: String, } event.action().clone() else { // RFC comment: Directly located next to the associated conditional. return Err(eyre::eyre!(must begin with a Register action)); }; let created *event.created(); let mut view ActionView { registered_by: (actor, created), a: (actor.clone(), x, created), b: (actor.clone(), y, created), c: (z, created), d: Vec::new(), e: None, f: None, g: None, }; for event in iter { view.update(event)?; } // more lines omitted Ok(view) } }对比可见错误分支被直接移到了对应条件旁边成功路径整体左移代码可读性显著提升。用 match 模拟的代价RFC 指出今天也可以用match表达式模拟同样的逻辑但代价是长度与可读性。作者在一个 HTTP 服务代码库中做的一次重构把大型 if-let 块迁移为提前返回产生了531 −529 行的差异代码库共 4111 行 Rust 代码其中最大的一个 90 行代码块被整体左移、错误分支上移到条件附近。用match模拟的写法包含大量语法样板例如必须写代码块且与 rustfmt 的格式规则存在冲突let features match geojson { GeoJson::FeatureCollection(features) features, _ { return Err(format_err_status!( 422, GeoJSON was not a Feature Collection, )); } };同样的逻辑用 let-else 可以更简洁清晰let GeoJson::FeatureCollection(features) geojson else { return Err(format_err_status!( 422, GeoJSON was not a Feature Collection, )); };快速上手语法与核心语义基本形式RFC 的 Guide-level explanation 给出了最直观的示例let Some(a) an_option else { // Called if an_option is not Option::Some(T). // This block must diverge (stop executing the existing context to the parent block or function). return; }; // a is now in scope and is the type which the Option contained.这里a在else块之后直接可用其类型就是Option内部包裹的类型。模式匹配的工作方式与if let完全一致唯一的区别是匹配值被绑定到周围作用域而非块作用域。else 块必须发散else块必须发散diverge即必须返回 never 类型!。可以是发散的关键字return、break、continue、loop { ... }发散的函数调用如std::process::abort、std::process::exitpanic包括panic!、unreachable!、todo!、unimplemented!等。这一要求背后的类型系统机制由 text/1216-bang-type.mdRFC 1216奠定!被提升为完整类型等价于一个没有变体的enum。空类型没有任何可能的值因此拥有一个Never类型值在作用域中是这段代码永远不会被执行的静态保证同理类型为!的表达式如return可以被强制转换为任何其他类型。正是基于这一机制rustc 的类型检查器可以表示匹配失败路径必然发散从而保证 let-else 在匹配失败时不会没有值可用地继续执行。匹配失败时表达式不会被消费一个容易被忽略的细节如果模式没有匹配表达式不会被消费consumed因此周围作用域中既有的变量可以像平常一样被访问。也就是说else块中可以正常读取外部变量来构造错误信息等。参考级规范语法细节与 desugar语法糖与 desugar 规则let-else 本质上是match的语法糖其中非匹配情况必须发散。RFC 给出的顶层描述是let pattern expr else { /* diverging expr */ };desugar 为let (each, binding) match expr { pattern (each, binding), _ { /* diverging expr */ } };具体的 desugar 示例RFC 还给出了一个更贴近实际类型的示例。源码let Some(x) y else { return; };desugar 为let x match y { Some(x) x, _ { let nope: ! { return; }; match nope {} } }可以看到_分支中先用!类型绑定一个必然发散的表达式return再对不可达的nope做一次空match完成类型层面的收尾。表达式位置的两条限制绝大多数表达式都可以出现在 let-else 的表达式位置但有两条例外表达式不得以}结尾在宏展开之前。这类表达式必须用括号包裹例如let PATTERN (if { a } else { b }) else { c };。表达式不得只是一个惰性布尔表达式或||即不得是LazyBooleanExpression。RFC 解释虽然让if {} else {}直接出现在表达式位置在技术上是可行的但为了程序员的清晰性而被禁止以避免出现... else {} else {}这种歧义局面详见 Drawbacks 部分。布尔匹配对 let-else 没有实际用途因此惰性布尔表达式被禁止理由见 Future possibilities 中的 if-let-chains 讨论。需要说明的是这两类表达式并非完全不能使用——只要通过括号等不那么歧义的方式组合就仍然允许。此外宏展开产生的不可见分组也是允许的不过 RFC 建议宏展开表示在给人看的输出中如果表达式以}结尾应当尽可能加上括号或显示不可见分组。模式要求与 irrefutable 警告任何能够放进if let模式位置的可反驳模式都可以放进 let-else 的模式位置。如果模式是不可反驳的irrefutablerustc 会发出irrefutable_let_patterns警告 lint——与在if let中使用不可反驳模式时行为一致。or-patterns多变体匹配对于通过|语法匹配多个变体的模式所有变体必须产生相同的绑定忽略参差不齐模式中的额外绑定并且这些绑定必须同名。RFC 给出的合法示例let MyEnum::VariantA(_, _, x) | MyEnum::VariantB { x, .. } a else { return; };let-else 不与 if-let 的 let 组合let-else不能与if let中的let组合——因为if let中的let并不是真正的let语句。如果你试图写if let p e else { } { }应当改用普通的 if-elseif let p e { } else { }。RFC 在 Future possibilities 中进一步强调Rust 应当永远避免允许if let Some(x) y else { return; } { ... }这类写法因为它在语法上令人困惑且功能上与if let p e { } else { }类似却带来更多缺点。设计权衡与限制必须发散是一种异常要求必须发散这一要求在当时的语言中并不存在于其他位置对刚接触该特性的程序员可能难以解释甚至导致令人困惑的错误信息。不过 RFC 指出rustc 已经能够通过!或任何其他无人居住uninhabited类型在类型检查器中表示发散性因此实现层面没有问题——这正对应 text/1216-bang-type.md 建立的!类型体系。let PATTERN if {} else {} else {};歧义该特性与普通 if-else 表达式组合时可能出现一种令人费解的写法let PATTERN if { a } else { b } else { c };RFC 分析认为这不构成语法问题let PATTERN if y { a } else { b };永远应该被解释为let Enum(x) (if y { a } else { b });此时因为没有发散块会产生error[E0005]: refutable pattern in local binding: ...编译错误因为编译器不会把它解读成let PATTERN (if y { a }) else { b };——()不是枚举。彻底消除歧义的办法是把裸的 if-else 出现在表达式位置变成编译错误强制用括号消歧如let PATTERN (if { a } else { b }) else { c };。实现上可以在解析之后、宏展开之前检查表达式是否以}结尾来实现这一限制。备选方案全景RFC 的 Rationale and alternatives 部分对多种备选设计做了系统评估。let-else 的总体设计原则是尽可能与现有相似语法保持一致它本质上是let语句因此需要赋值与末尾分号模式匹配与 if-let 完全一致不引入任何新的否定模式匹配规则的运算符优先级处理得与 if-let 类似把本已是错误的用例改成略有不同的错误为 if-let-chains 的未来扩展留出空间表达式不能以}结尾以避免else {} else {}混淆else必须紧跟一个代码块如同if {} else {}且必须发散——因为外层上下文在没有赋值的情况下无法被保证安全地继续执行且本 RFC 不提供替代赋值语法。与ok_or/?的组合对比Option和Result类型上的ok_or/ok_or_else配合 Try 运算符?可以部分覆盖这一需求。但 RFC 指出这些函数的局限它们不会自动存在于自定义枚举类型上需要非显而易见且不平凡的实现对于成功变体取决于上下文、且变体众多的枚举可能根本无法映射成Option/Result风格的函数对希望返回Option/Result之外类型的代码无效无法覆盖做其他事情的发散块例如continue e或break outer_loop e或者目标是一个外层try块的情况。各具体备选方案评估备选方案形式RFC 的结论改用otherwise关键词let ... otherwise { ... }会失去与 if-else、if-let-else 的对称性且需引入新关键字增加教学负担与特例处理逗号前置, else { ... }let PAT e, else { ... }可能让含块表达式组合更易读但与当时 Rust 语言整体风格不一致引入 introducer 关键字guard let ... {}会引入绑定却不以let开头/开启新块Rust 中目前只有let语句能在当前块引入绑定而不开新块static、const是 item可前向引用。这会让扫描绑定的难度上升。该语法在 Swift 中有先例guard-let-else模式否定if !let PAT EXPR2015 旧 RFC 的最初方案需引入新的特殊模式匹配规则旧 RFC 讨论共识认为否定语法远不如if PATTERN EXPR else { /* diverge */ };清晰且同样存在不以 let 开头引入绑定的缺点else后跟任意发散表达式let Some(foo) some_option else return None;与 if-else 中else必须跟块表达式的规则不一致故被否决此建议源自旧 RFC 的讨论else块回退赋值else { a 42; }没有与现有特性一致的清晰语法单回退场景罕见而发散块场景非常常见回退场景用match已足够。该提案与 RFC 不兼容无法作为扩展添加允许从match中向外部作用域赋值Happy(x) let x不是 if-let 的明显对应物会给let引入全新的位置含义模式匹配中使用||作为回退let Some(x) a \|\| { return; };更复杂、更灵活但更不明显会与 if-let-chains 直接交互对新手可能相当困惑宏方案类似guard!的宏crates.io 上的同类宏存在约 6 年截至 RFC 撰写时仅有约 4.7 万次下载生态采用率不高不做任何改动Null用match/if let或宏维持现状但如前文真实示例所示代价是缩进与可读性先例与参考RFC 在 Prior art 部分梳理了相关先例2015 年旧 RFCpull request 1303本 RFC 是它的现代化版本大量设计来自旧 RFC 及其后续讨论。Swift 的guard语句Swift 启发了 Rust 的if let其guard let ... else语句与本文提案在除关键词选择外基本等价。guard!宏自 2015 年旧 RFC 时期起就可在 crates.io 上获得实现了与本 RFC 非常相似的功能。match方案在多错误条件的项目中相当普遍。Try 运算符 ok_or_else仅对Option/Result有效被视为惯用 Rust但覆盖面有限见上文对比。未决问题RFC 明确提出了一些当时尚未解决的问题实际可读性let ... else { ... };在实际代码中对人是否足够清晰还是需要某种 introducer 语法与 if-let-chains 的冲突本特性与 if-let-chains 是否互相冲突RFC 强调两者都不应在不考虑对方的情况下单独稳定。特例数量与其他特性的特例交互是否过多语法清晰度本 RFC 有一些略微不寻常的语法要求语法是否需要进一步澄清未来可能性if-let-chainstext/2497-if-let-chains.mdRFC 2497提出了一个在当时未实现的特性——if let链式组合if let Some(foo) expr() foo.is_baz() let Ok(yay) qux(foo) { ... }本 RFC 虽未为 let-else 引入同样机制但试图为未来与 if-let-chains 的一致性留出空间。主要障碍是既有运算符优先级按普通的优先级规则上面的例子会被解析成let Some(foo) (expr() foo.is_baz() let Ok(yay) qux(foo) else { ... })不过由于本 RFC 之前所有出现这种行为的地方本来就是类型错误因此可以专门避开布尔情况改变解析方式为这一扩展打开大门。这个布尔情况总是等价于一个更不灵活的if语句因而没有实际用处。RFC 举例说明let true a b else { return; };这样的写法被提议改为编译错误以保护未来与 if-let-chains 的兼容性。同时指出这类写法今天本来也无法通过编译因为布尔值是可反驳模式error[E0005]: refutable pattern in local binding: false not covered -- src/main.rs:5:9 | 5 | let true a b; | ^^^^ pattern false not covered | note: let bindings require an irrefutable pattern, like a struct or an enum with only one variant回退赋值Fall-back assignmentRFC 明确表示不提议实现这些但列出若干未来方向let-else-else-chains模式依次匹配每个 else 后的表达式直到一个必需的发散块let Some(x) a else b else c else { return; };可以这样理解match接受一个表达式、对它应用多个模式直到一个匹配而 let-else-else-chains 接受一个模式、把它应用到多个表达式直到一个匹配。RFC 警告这存在 or-pattern 复杂度问题——表达式可能轻易变得指数级or-pattern 带 guard 时已经可能如此但这里更容易遇到let A(x) | B(x) foo() else bar() else { return; };let-else-matchmatch必须覆盖除 let 赋值模式之外的所有模式let Ok(a) x else match { Err(e) return Err(e.into()), }||模式匹配的变体若||允许可反驳可以形成类似 let-else-else-chains 的构造从而补充而非替换 let-elselet Some(x) a || b else { return; };let-else 出现在 if-let 中本 RFC 自然引出if-let 的 let 位置是否允许 let-else的问题。RFC 的结论是由于 if-let 中的let是 if-let 表达式的一部分、并非真正的let语句这必须被显式允许才行而 RFC 不提议允许它Rust 应当永远避免允许因为它语法上令人困惑功能上又等价于if let p e { } else { }却带有更多缺点。在 RFC 仓库中的定位与延伸阅读本文所述内容完整收录于本仓库的 text/3137-let-else.md。该仓库是 Rust 的 RFC 官方仓库RFCs for changes to RustREADME.md 说明了 RFC 流程实质性的语言变更如语义或语法变更、移除特性、对std的大规模添加需要走 RFC 流程新提案从 0000-template.md 模板起步经社区与子团队评审后决定合并或关闭。仓库通过 generate-book.py 依据text目录布局自动生成 mdBook 的SUMMARY.md并由 book.toml 配置构建为 The Rust RFC Book因此每个 RFC 既是源码文件、也是书籍的一章。延伸阅读建议text/0160-if-let.mdif-let 的原始提案理解 let-else 所对应的表达式形态与 desugar 先例text/1216-bang-type.mdnever 类型!的完整设计理解 let-else 发散语义的类型系统根基text/2497-if-let-chains.mdif-let-chains 提案理解 let-else 未来可能的扩展方向与限制的由来。小结let-else 用一行语句同时完成了解构成功值进入外层作用域与失败路径就地发散两件事是对if let在错误处理场景中最有力的补充。理解它的关键点包括绑定进入外层作用域、else块必须发散!、表达式不得以}结尾且不得为惰性布尔表达式、可反驳模式与irrefutable_let_patterns警告、以及它与 if-let 和 if-let-chains 之间的边界。掌握这些语义你就能在Option/Result之外的自定义枚举、网络与 I/O 等静态保证无法完全满足的代码中写出更平直、更易读的提前返回风格代码。赞分享文档【免费下载链接】rfcsRFCs for changes to Rust项目地址https://gitcode.com/gh_mirrors/rf/rfcs点击查看免费下载相关推荐Rust 模式可反驳性Refutability权威指南可反驳与不可反驳模式、let...else 与编译器错误修复Rust 模式可反驳性Refutability权威指南可反驳与不可反驳模式、let...else 与编译器错误修复 导读 可反驳性Refutabilit教程文档Rust 控制流精讲用 if let 处理可反驳模式匹配基于 rust-by-exampleRust 控制流精讲用 if let 处理可反驳模式匹配基于 rust by example if let 是 Rust 中针对“只关心一个匹配分支、忽略文档教程Rust 不可反驳 if-let 模式从 E0162 编译错误到可关闭的 error-by-default lintRFC 2086 深度解读Rust 不可反驳 if let 模式从 E0162 编译错误到可关闭的 error by default lintRFC 2086 深度解读 导读 本文文档上一篇Playwright for Java vs Selenium为什么它是更优的Web自动化选择下一篇终极性能优化工具sbt-jmh插件让你的Scala代码基准测试更简单创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
阅读完成 · 觉得有帮助?