使用`move`关键字创建闭包时,如何创建一个FnMut闭包?

6

直到此刻,我认为move |...| {...}会将变量移动到闭包内部,并且闭包只会实现FnOnce,因为你只能移动变量一次。但令我惊讶的是,我发现以下代码也能工作:

extern crate futures;

use futures::stream;
use futures::stream::{Stream, StreamExt};
use std::rc::Rc;

#[derive(Debug)]
struct Foo(i32);

fn bar(r: Rc<Foo>) -> Box<Stream<Item = (), Error = ()> + 'static> {
    Box::new(stream::repeat::<_, ()>(()).map(move |_| {
        println!("{:?}", r);
    }))
}

fn main() {
    let r = Rc::new(Foo(0));
    let _ = bar(r);
}

尽管 "map" 函数具有以下签名:
fn map<U, F>(self, f: F) -> Map<Self, F>
where
    F: FnMut(Self::Item) -> U, 

对我来说,使用 move 关键字创建的 FnMut 闭包甚至具有 'static 生命周期,这让我感到惊讶。我在哪里可以找到关于 move 的详细信息?或者它实际上是如何工作的?


相关书籍章节:https://doc.rust-lang.org/stable/book/second-edition/ch13-01-closures.html#capturing-the-environment-with-closures 和 https://doc.rust-lang.org/stable/book/second-edition/ch16-01-threads.html#using-move-closures-with-threads - E net4
5
这里的重要区别在于move会导致变量在闭包创建时被“移动”到闭包内部,这不会阻止闭包被调用多次。如果闭包代码从捕获的变量中移出任何值,即使用这些值,闭包将变为FnOnce类型。只有当变量首先被移入闭包时,你才能将其值移出捕获的变量,但仅仅将值移入闭包本身并不会使闭包成为FnOnce类型。 - Sven Marnach
2个回答

16

是的,这一点相当令人困惑,我认为 Rust 书籍的措辞有所贡献。在我阅读完后,我和你一样认为:一个 move 闭包必须是 FnOnce,而一个非 move 闭包则是 FnMut(也可能是Fn)。但实际情况有点相反。

闭包可以捕获创建它的范围内的值。move 控制这些值进入闭包的方式:移动或通过引用。但是它们被捕获后如何使用才决定了闭包是不是 FnMut

如果闭包体消耗了任何捕获的值,则闭包只能是 FnOnce。在闭包第一次运行并消耗该值之后,它就不能再次运行了。

正如您提到的,您可以通过调用 drop 或其他方式在闭包内部消耗值,但最常见的情况是从闭包中返回它,这将使其从闭包中移出。以下是最简单的示例:

let s = String::from("hello world");
let my_fnonce = move || { s };

如果闭包的主体未使用其任何捕获项,则无论它是否是move,它都是FnMut。如果它还不会修改其任何捕获项,则它也是Fn; 任何一个Fn闭包也是FnMut。这里有一个简单的例子,虽然不是很好。

let s = "hello world";
let my_fn = move || { s.len() }

概述

move 修饰符控制在闭包被创建时捕获变量的移动方式进入闭包。而 FnMut 成员资格取决于当闭包执行时捕获变量的移动方式离开闭包(或以其他方式被消耗)。


这是一个更加完整的答案。我将不得不把它包括在“已采取文件”中(https://github.com/vitiral/taken/issues/1)。 - vitiral

4

在此之前,我一直认为move |...| {...}会移动闭包内的变量,而闭包只会实现FnOnce,因为你只能移动变量一次。

变量在闭包创建时就被移动了,而不是在调用时。由于你只创建了一个闭包,所以移动只会发生一次 - 不管map调用函数多少次。


我明白,但是这个主题仍然让我感到很困惑。例如,在TRPL中:FnOnce会消耗它从封闭作用域中捕获的变量,也就是闭包的环境。为了消耗这些捕获的变量,闭包必须拥有这些变量的所有权,并在定义时将它们移动到闭包中。名称中的Once部分表示闭包不能多次拥有相同变量的所有权,因此它只能被调用一次。 - AlexeyKarasev
另一个来源:http://huonw.github.io/blog/2015/05/finding-closure-in-rust/:同样地,如果闭包是 || drop(v); ——也就是说,移动 v 的所有权——那么实现 Fn 或 FnMut 都是非法的。但是这段代码完全可以运行: let r = Some(1); let c: Box<Fn()> = Box::new(move || { drop(r); }); c(); c(); - AlexeyKarasev
2
@AlexeyKarasev 关于你的第一个评论,这与答案完全一致 - 也请看一下我在问题上的评论。关于你的第二个评论,你给出的例子有点误导,因为 Option<i32> 实现了 Copy 特性,所以它实际上并没有被 drop() 消耗掉。如果你尝试使用不是 Copy 的东西,你将会看到那段代码出错。 - Sven Marnach
哦,我明白了,看起来我的措辞理解有问题。所以在TRPL中,通过consume他们的意思是如果let Some(x) = r在闭包内部。如果将某些东西移动到闭包内部(例如在这种情况下的r),则称之为捕获。因此,基本上FnOnce的意思是,你正在销毁环境,这就是为什么你只能调用它一次(下一次调用将没有相同的环境)。另一方面,Move处理对堆栈的引用,因此更像是生命周期问题。 - AlexeyKarasev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接