将值移入闭包后,为什么仍然会出现“无法将不可变局部变量作为可变引用借用”的错误消息?

6
在下面的代码中,我明确强制将来自main函数的name移入闭包中,一切都可以正常运行:
fn main() {
    let name = String::from("Alice");

    let welcome = || {
        let mut name = name;
        name += " and Bob";
        println!("Welcome, {}", name);
    };

    welcome();
}

我原本认为在闭包的开头添加move就可以达到同样的效果,从而移动值并创建一个FnOnce

fn main() {
    let name = String::from("Alice");

    let welcome = move || {
        name += " and Bob";
        println!("Welcome, {}", name);
    };

    welcome();
}

相反,我却收到了错误信息:
error[E0596]: cannot borrow immutable local variable `welcome` as mutable
 --> main.rs:9:5
  |
4 |     let welcome = move || {
  |         ------- help: make this binding mutable: `mut welcome`
...
9 |     welcome();
  |     ^^^^^^^ cannot borrow mutably

error[E0596]: cannot borrow captured outer variable in an `FnMut` closure as mutable
 --> main.rs:5:9
  |
5 |         name += " and Bob";
  |         ^^^^

在这种情况下,如何正确思考闭包中的move


请注意编译器告诉你该怎么做help: make this binding mutable: \mut welcome`` - Shepmaster
1个回答

7
我原以为在闭包的开头添加`move`就可以达到同样的效果,但实际上也可以做到。你只是忘记将`name`和`welcome`声明为可变的了。以下代码可以正常工作:
fn main() {
    let mut name = String::from("Alice");

    let mut welcome = move || {
        name += " and Bob";
        println!("Welcome, {}", name);
    };

    welcome();
}

两个版本的闭包都会使得name变量进入闭包。第一个版本隐式地通过在闭包中使用name来实现。第二个版本并没有使用name,而是使用了move关键字来强制移动。
移动一个值到闭包中并不会使其成为FnOnce类型。如果一个闭包消耗掉了一个捕获的值,那么它就成为FnOnce类型,因为显然只能这样做一次。因此,第一个版本的闭包是FnOnce类型,因为它消耗了name。上面的闭包是FnMut类型,并且可以被调用多次。调用两次将导致输出。
Welcome, Alice and Bob
Welcome, Alice and Bob and Bob

我之前有些随意地使用函数特质名称。实际上,每个闭包都实现了FnOnce,因为每个闭包至少可以调用一次。一些闭包可以被多次调用,因此它们除了实现FnOnce外还实现了FnMut。而一些可以被多次调用但不改变其捕获状态的闭包,除了实现前两个特质外还实现了Fn


虽然您提供的重写代码(添加muts)可以编译,但在语义上与我提供的第一个版本的代码不同。特别是,在第一个版本中,由于使用了移动值,第二次调用welcome()是错误的。我意识到在这种情况下,我可以改为可变借用值,但我想知道为什么move没有强制将值移动到闭包中。 - Michael Snoyman
2
尝试在 welcome 之后打印 name 变量。这将由于 move 关键字而导致编译时错误。你可以多次调用 welcome,因为它是 FnMut,并且会捕获 name 变量。这个来自Rust by Example代码可以阐明此问题。 - Artemij Rodionov
2
@MichaelSnoyman move确实会强制将name移动到闭包中。第一个版本也将值移动到闭包中,但然后在闭包内部使用它。消耗捕获值的闭包只能被调用一次。 - Sven Marnach
@MichaelSnoyman 我在这个答案中添加了几句话 - 希望现在更清楚了。 - Sven Marnach
谢谢@SvenMarnach,这很有帮助。让我困惑的是移动值和消耗值之间的区别。Rust by Example的链接也很有启发性,谢谢Artemiy。 - Michael Snoyman

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