捕获子句作为效果

Lobsters Hottest 新闻

摘要

本文探讨了Rust中显式捕获子句作为move表达式的替代方案,提出了一种将引用转换为所有权值的一等语言特性,并分析了多种闭包捕获模式。

<p><a href="https://lobste.rs/s/nlhmco/capture_clauses_as_effects">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/07/21 16:41

# 将捕获子句视为效果 来源:https://blog.yoshuawuyts.com/capture-clauses-as-effects ## 引言 在我关于表达式提升(Hoisting Expressions)的文章(https://blog.yoshuawuyts.com/hoisting-expressions)中,我讨论了`move($expr)`特性,以及它如何像许多语言中`defer`特性的逆操作一样工作。与创建在作用域结束后*运行*的表达式不同,`move($expr)`在进入作用域*之前*运行代码¹(https://blog.yoshuawuyts.com/capture-clauses-as-effects#ordering)。其目标是让像`bevy`这样大量使用`clone`的生态系统在使用 Rust 时获得更好的体验。 尽管我认同这一目标,但我对该提议不太确定。我觉得显式捕获子句(https://smallcultfollowing.com/babysteps/blog/2025/10/22/explicit-capture-clauses/)提供了更简单的模型,因此更可取。但它们的缺点是写起来可能有点笨拙,并且可能中断写作流程。因此,在这篇文章中,我想探索一个潜在的解决方案,即将引用转换为拥有的值作为一等语言特性。 ## 设定场景 让我们从一个简化版的 RFC 3968(https://github.com/rust-lang/rfcs/pull/3968)中展示的激励例子开始。这是一个典型的`task::spawn`例子,在很多异步 Rust 程序中都能看到¹(https://blog.yoshuawuyts.com/capture-clauses-as-effects#unfortunately)。它从一个结构体中克隆出几个字段,将这些克隆值移入闭包,然后传递给另一个函数: ```rust let a = foo.a.clone(); let b = foo.b.clone(); let c = foo.c.clone(); task::spawn(async move { bar(a, b, c).await }); ``` 这段代码的问题在于仪式感过重。变量遮蔽有时会带来命名问题,每个克隆都独占一行,而且在编写闭包代码时,你经常需要来回跳转,以确保克隆的是正确的变量。如果我们想改善克隆的易用性,这就是我们需要改进的地方。 ## 各种移动方式 闭包捕获有不同的种类,对于任何提议的语言特性,覆盖一系列完整的例子都很重要。让我们逐一审视其中的一些,以便在我们开始设计时可以作为参考: ### 示例1:移动三个变量 变量`a`、`b`和`c`都被直接移入闭包。 ```rust bar(move || { baz(a, b, c); }); ``` ### 示例2:克隆三个变量 变量`a`、`b`和`c`在移入闭包前都被克隆。 ```rust let a = a.clone(); let b = b.clone(); let c = c.clone(); bar(move || { baz(a, b, c); }); ``` ### 示例3:移动一个变量,克隆两个 这里我们在移动之前克隆了`b`和`c`,但直接移动了`a`而没有克隆。 ```rust let b = b.clone(); let c = c.clone(); bar(move || { baz(a, b, c); }); ``` ### 示例4:引用一个变量,克隆两个 这里我们在移动之前克隆了`b`和`c`,但直接移动了`a`而没有克隆。 ```rust let b = b.clone(); let c = c.clone(); bar(move || { baz(&a, b, c); }); ``` ### 示例5:克隆三个字段 这里我们有一个结构体`foo`,它包含三个字段`a`、`b`和`c`,我们在移动前全部克隆。 ```rust let a = foo.a.clone(); let b = foo.b.clone(); let c = foo.c.clone(); bar(move || { baz(a, b, c); }); ``` ### 示例6:克隆两个字段,保留一个 这里我们有一个结构体`foo`,它包含三个字段`a`、`b`和`c`。我们只在移动前克隆了`b`和`c`。我们没有移动`a`。 ```rust let b = foo.b.clone(); let c = foo.c.clone(); bar(move || { baz(b, c); }); ``` ### 示例7:克隆两个字段,移动一个 这里我们有一个结构体`foo`,它包含三个字段`a`、`b`和`c`。我们只在移动前克隆了`b`和`c`。我们移动了`a`而没有克隆它。 ```rust let a = foo.a; let b = foo.b.clone(); let c = foo.c.clone(); bar(move || { baz(a, b, c); }); ``` ### 示例8:克隆两个字段,引用一个 这里我们有一个结构体`foo`,它包含三个字段`a`、`b`和`c`。我们只在移动前克隆了`b`和`c`。我们通过引用传递了`a`。 ```rust let b = foo.b.clone(); let c = foo.c.clone(); bar(move || { baz(&foo.a, b, c); }); ``` ### 示例9:移动三个字段 这里我们有一个结构体`foo`,它包含三个字段`a`、`b`和`c`。我们将所有三个字段移入闭包。 ```rust let b = foo.a; let b = foo.b; let c = foo.c; bar(move || { baz(a, b, c); }); ``` ### 示例10:移动一个字段,克隆一个,引用一个 这里我们有一个结构体`foo`,它包含三个字段`a`、`b`和`c`。我们移动了`a`,克隆了`b`,引用了`c`。 ```rust let b = foo.a; let b = foo.b.clone(); let c = &foo.c; bar(move || { baz(a, b, c); }); ``` ## 显式闭包捕获,第一轮 在显式捕获子句(Explicit Capture Clauses)(https://smallcultfollowing.com/babysteps/blog/2025/10/22/explicit-capture-clauses/)中,Niko Matsakis 展示了一种看起来相当不错的显式闭包捕获表示法。它基于字段访问,形式为`move(a.b.c)`来获取一个命名变量`c`。使用这种表示法,示例9(移动三个)看起来像这样: ```rust bar(move(foo.a, foo.b, foo.c) || { baz(a, b, c); }); ``` Niko 设想这是更通用的`place = expression`对的一种缩写。上面的例子只是下面形式的语法糖: ```rust bar(move( foo.a = foo.a, foo.b = foo.b, foo.c = foo.c, ) || { baz(a, b, c); }); ``` 这就是我看到的麻烦开始的地方。这种通用的`place = expression`表示法的原因是为了支持变量的显式克隆。毕竟,这是我们想要改进的主要方面。我们列表中的示例#7(克隆两个,移动一个)用这个特性可以写成如下形式: ```rust bar(move( foo.a, foo.b = foo.b.clone(), foo.c = foo.c.clone(), ) || { baz(foo.a, foo.b, foo.c); }); ``` 我们这里的例子用了7行代码。这比我们今天写出来要多一行¹(https://blog.yoshuawuyts.com/capture-clauses-as-effects#shadowing): ```rust let a = foo.a; let b = foo.b.clone(); let c = foo.c.clone(); task::spawn(async move { bar(a, b, c).await }); ``` 显式捕获子句的规则是,所有捕获现在都必须*显式*。这意味着闭包覆盖了未显式捕获的内容是错误。为了允许变量被引用,Niko 提议在`move`组内部使用一个额外的`ref`关键字。这样我们可以将示例#8(克隆两个,引用一个)重写如下: ```rust bar(move( foo.b = foo.b.clone(), foo.c = foo.c.clone(), ref, ) || { baz(&foo.a, foo.b, foo.c); }); ``` 同样,如果我们与最初的样子相比,这感觉并不像是在易用性方面取得巨大成功。感觉两种版本并排看起来非常相似: ```rust let b = foo.b.clone(); let c = foo.c.clone(); bar(move || { baz(&foo.a, b, c); }); ``` 我不奇怪讨论已经超越了显式捕获子句。简单的例子工作得很好,但一旦我们实际开始克隆变量和处理字段,事情看起来就不那么好了。 **我相信显式捕获子句的问题源于它必须能够支持任意表达式的假设。** 如果我们能放弃这个假设,我认为我们可以更紧密地限定该特性的范围,并最终得到一个更具表现力的表示法。 ## `own` 关键字 Rust 在模式中使用 `ref` 关键字将拥有的值转换为引用(`T` → `&T`)。尽管我们现在很少显式地写 `ref`,因为类型推断已经变得非常好。 对于我们的 `clone` 用例,我们基本上想做 `ref` 的相反操作:将引用转换为拥有的值(`&T` → `T`)。我们还没有用于这个的关键字,但我提议添加一个:`own`。 我们几乎肯定已经想为了语言中的其他原因保留 `own`,所以这没什么大不了的。`'own` 生命周期经常被讨论用于自引用类型。而 `&own` 引用经常被讨论用于不可移动类型。 `own` 关键字的工作方式是,如果你有一个 `&T`,你可以通过在 `move` 赋值中写 `own &T` 将其转换为 `T`。它将通过调用 `std::borrow::ToOwned`(https://doc.rust-lang.org/std/borrow/trait.ToOwned.html)来完成,这是 `Clone` 的泛化¹(https://blog.yoshuawuyts.com/capture-clauses-as-effects#generalization)。这样我们可以将示例#7(克隆两个,移动一个)重写如下: ```rust bar(move(foo.a, own &foo.b, own &foo.c) || { baz(foo.a, foo.b, foo.c); }); ``` 写 `own &foo.b` 看起来仍然有点杂乱,所以我提议将其简化为 `own foo.b`。额外的 `&` 并没有太大作用,可能可以被推断掉。这样我们的例子现在变成: ```rust bar(move(foo.a, own foo.b, own foo.c) || { baz(foo.a, foo.b, foo.c); }); ``` 不过,公平起见,让我们再次与最初用于此的表示法进行比较: ```rust let b = foo.b.clone(); let c = foo.c.clone(); bar(move || { baz(&foo.a, b, c); }); ``` 我们从5行减少到只有3行。而且每个捕获都整齐地放在一行上。就改进而言,我认为这是正确的方向。但我认为我们可以进一步推动它! ## 万物皆为效果 现在我们开始讨论效果。效果,简单来说,是某个作用域构造的一部分的修饰符。函数是最常见的,但在 Rust 中,我们也有块,当然还有闭包。 对于(显式)闭包捕获,我们试图做的是改变块和闭包的行为。`move` 关键字改变了闭包本身的种类(`Fn`/`FnMut` → `FnOnce`)。而通过显式闭包捕获,我们更进一步,对我们将捕获哪些值以及如何捕获做出额外的声明。 在“基于 With 子句和块的效果表示法”(https://blog.yoshuawuyts.com/a-with-based-effect-notation)一文中,我提议使用 `with` 关键字来表示效果。这是为了创建一种一致的表示法,可以在块、闭包和函数之间共享。多个效果将使用 `+` 组合,就像我们对 trait 所做的那样。以下是该帖子中快速总结的样子: ```rust // 使用今天的 Rust let x = async { .. }; // async 块 let foo = async || { .. }; // async 闭包 let foo = || async { .. }; // 返回 future 的闭包 (!) async fn foo() -> i32 { .. } // async 函数 // 使用 `with` 子句 let x = with async { .. }; // async 块 let foo = || with async { .. }; // async 闭包 fn foo() -> i32 with async { .. } // async 函数 // 多个效果 let x = with async + alloc { .. }; ``` 如果我们认为 `move`、`ref` 和 `own` 本身就是效果,我们可以直接将它们放入这个框架,并开始编写我们的例子。那么,让我们这样做,并按顺序逐一实现: ### 示例1:移动三个变量 变量 `a`、`b` 和 `c` 都被直接移入闭包。 ```rust bar(with move(a, b, c) || { baz(a, b, c); }); ``` ### 示例2:克隆三个变量 变量 `a`、`b` 和 `c` 在移入前都被克隆。 ```rust bar(with own(a, b, c) || { baz(a, b, c); }); ``` ### 示例3:移动一个变量,克隆两个 这里我们在移动之前克隆了 `b` 和 `c`,但直接移动了 `a` 而没有克隆。 ```rust bar(with move(a) + own(b, c) || { baz(a, b, c); }); ``` ### 示例4:引用一个变量,克隆两个 这里我们在移动之前克隆了 `b` 和 `c`,但直接移动了 `a` 而没有克隆。 ```rust bar(with ref + move(b, c) || { baz(a, b, c); }); ``` ### 示例5:克隆三个字段 这里我们有一个结构体 `foo`,它包含三个字段 `a`、`b` 和 `c`,我们在移动前全部克隆。 ```rust bar(with own(foo.a, foo.b, foo.c) || { baz(foo.a, foo.b, foo.c); }); ``` ### 示例6:克隆两个字段,保留一个 这里我们有一个结构体 `foo`,它包含三个字段 `a`、`b` 和 `c`。我们只在移动前克隆了 `b` 和 `c`。我们没有移动 `a`。 ```rust bar(with own(foo.b, foo.c) || { baz(foo.b, foo.c); }); ``` ### 示例7:克隆两个字段,移动一个 这里我们有一个结构体 `foo`,它包含三个字段 `a`、`b` 和 `c`。我们只在移动前克隆了 `b` 和 `c`。我们移动了 `a` 而没有克隆它。 ```rust bar(with move(foo.a) + own(foo.b, foo.c) || { baz(foo.a, foo.b, foo.c); }); ``` ### 示例8:克隆两个字段,引用一个 这里我们有一个结构体 `foo`,它包含三个字段 `a`、`b` 和 `c`。我们只在移动前克隆了 `b` 和 `c`。我们引用了 `a`。 ```rust bar(with ref + own(foo.b, foo.c) || { baz(&foo.a, foo.b, foo.c); }); ``` ### 示例9:移动三个字段 这里我们有一个结构体 `foo`,它包含三个字段 `a`、`b` 和 `c`。我们将所有三个字段移入闭包。 ```rust bar(with move(foo.a, foo.b, foo.c) || { baz(foo.a, foo.b, foo.c); }); ``` ### 示例10:移动一个字段,克隆一个,引用一个 这里我们有一个结构体 `foo`,它包含三个字段 `a`、`b` 和 `c`。我们移动了 `a`,克隆了 `b`,引用了 `c`。 ```rust bar(with own(foo.a) + move(foo.b) + ref || { baz(foo.a, foo.b, foo.c); }); ``` ## 为写入优化 我认为这里提出的 `own`、`move` 和 `ref` 与现状相比更养眼。但理想情况下,语言特性不仅易于阅读,也易于编写。而显式闭包捕获确实存在一个问题,即为了编写它们,你需要跳回代码的上方。对我来说,这似乎是 `move($expr)` 提议中最吸引人的部分。 甚至比 `move($expr)` 更好的是 `.use` 提议。但由于它有一些作用域上的奇怪之处¹(https://blog.yoshuawuyts.com/capture-clauses-as-effects#scope),`move($expr)` 似乎是更好的方向。但我看到 `.use` 的好处不在于*阅读*,而在于*编写*。我认为我们可以通过将其构建到 Rust-Analyzer 中,将这一点完全带入显式捕获子句,就像我们今天支持后缀 `.dbg` 一样: ```rust // 阶段1:我们刚打完 `foo.a` bar(|| { baz(foo.a }); // 阶段2:我们在其末尾添加 `.own` bar(|| { baz(foo.a.own }); // 阶段3:我们按下 `tab`,R-A 为我们重写代码 bar(with own(foo.a) || { baz(foo.a }); ``` 显式闭包捕获针对*阅读代码*进行了优化。`.use` 针对*编写代码*进行了优化。通过添加对后缀 `.move` 和 `.own` 命令的支持,我们可以两全其美。 ## 结论 我们已经看了很多例子,但让我们再来看一下帖子开头展示的 RFC 3968 的激励例子: ```rust let a = foo.a.clone(); let b = foo.b.clone(); let c = foo.c.clone(); tokio::task::spawn(async move { bar(a, b, c).await }); ``` 使用 RFC 3968 的 `move($expr)` 提议,我会将其重写如下: ```rust tokio::task::spawn(async move { let a = move(a.clone()); let b = move(b.clone()); let c = move(c.clone()); bar(a, b, c).await }); ``` 不过有些人(不是我)可能更喜欢以下面的风格编写: ```rust tokio::task::spawn(async move { bar( move(a.clone()), move(b.clone()), move(c.clone()), ).await }); ``` 使用我们在这篇文章中提出的表示法,同样的例子可以用 `with own` 重写: ```rust task::spawn( with async + own(foo.a, foo.b, foo.c) { bar(foo.a, foo.b, foo.c).await }, ); ``` 也许我们甚至可以从视图类型表示法(https://blog.yoshuawuyts.com/syntactic-musings-on-view-types)中借鉴一页,通过添加对批量字段访问的支持,使这更加简洁。这也使得效果行更易读: ```rust task::spawn(with async + own(foo.{ a, b, c }) { bar(foo.a, foo.b, foo.c).await }); ``` 而且如果我们直接引用字段,也许字段的名称应该直接成为变量的名称,简化一下函数体: ```rust task::spawn(with async + own(foo.{ a, b, c }) { ```

相似文章

Rust - 在 map 闭包中处理 Result

Lobsters Hottest

本文探讨了在 map 闭包中使用返回 Result 的函数时常见的 Rust 错误,并提供了多种解决方案:收集结果、使用循环、filter_map 和 try_fold。

擦除存在类型

Lobsters Hottest

深入探讨 Rust 类型系统中的存在量词,比较 `dyn Trait` 和 `impl Trait`,并探索超越 `Self` 的存在量化类型变量的高级模式。

使用延续抽象效果

Lobsters Hottest

本文演示了如何在Gleam编程语言中使用延续来抽象不同的计算效果(如错误处理和异步),从而实现可重用的业务逻辑。

3133X优化单个Rust Clippy lint

Lobsters Hottest

文章解释了Rust中的`clippy::nonstandard_macro_braces` lint如何被3133X优化,重点在于通过解决编译过程中宏展开的低效问题来提高性能。