为什么在线程方面需要 "move" 关键字;为什么我永远不想要这种行为?

Why is the "move" keyword necessary when it comes to threads; why would I ever not want that behavior?

例如(取自the Rust docs):

let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
    println!("Here's a vector: {:?}", v);
});

这不是关于 move 做什么的问题,而是关于为什么有必要指定的问题。

如果您希望闭包获得外部值的所有权,是否有理由不使用 move 关键字?如果在这些情况下 move 总是 ,那么 move 的存在不能只是 implied/omitted 的原因是什么?例如:

let v = vec![1, 2, 3];
let handle = thread::spawn(/* move is implied here */ || {
    // Compiler recognizes that `v` exists outside of this closure's
    // scope and does black magic to make sure the closure takes
    // ownership of `v`.
    println!("Here's a vector: {:?}", v);
});

上面的例子给出了以下编译错误:

closure may outlive the current function, but it borrows `v`, which is owned by the current function

当错误通过添加 move 神奇地消失时,我不禁想知道自己:为什么我 想要这种行为?


我并不是说所需的语法有任何问题。我只是想从比我更了解 Rust 的人那里更深入地了解 move。 :)

这里实际上有一些事情在起作用。为了帮助回答您的问题,我们必须首先了解 move 存在的原因。

Rust 有 3 种类型的闭包:

  1. FnOnce消耗其捕获变量(因此只能调用一次)的闭包,
  2. FnMut可变借用其捕获变量的闭包,以及
  3. Fn,一个不可变地借用它捕获的变量的闭包。

创建闭包时,Rust 会根据闭包如何使用环境中的值来推断要使用的特征。闭包捕获其环境的方式取决于它的类型。 FnOnce 按值捕获(如果类型 Copyable,则可以是移动或复制),FnMut 可变借用,Fn 不可变借用。 但是,如果你在声明闭包时使用move关键字,它会总是 "capture by value",或者取得所有权捕捉前的环境。因此,move 关键字与 FnOnces 无关,但它改变了 Fns 和 FnMuts 捕获数据的方式。

对于你的例子,Rust 推断闭包的类型是 Fn,因为 println! 只需要引用它正在打印的值(Rust 书页你在解释错误时没有 move) 链接谈论这个。因此闭包尝试借用 v,并且标准生命周期规则适用。由于 thread::spawn 要求传递给它的闭包具有 'static 生命周期,因此捕获的环境也必须具有 'static 生命周期,而 v 不会超过,从而导致错误。因此,您必须明确指定您希望闭包获得 v 的所有权。

这可以通过将闭包更改为编译器将推断为 FnOnce -- || v 的内容来进一步举例说明,作为一个简单的示例。由于编译器推断闭包是 FnOnce,它默认按值捕获 v,并且行 let handle = thread::spawn(|| v); 编译时不需要 move.

这都是关于生命周期注解,以及很久以前 Rust 做出的设计决定。

看,您的 thread::spawn 示例编译失败的原因是它需要一个 'static 闭包。由于新线程可以 运行 比生成它的代码长,我们必须确保任何捕获的数据在调用者 returns 之后保持活动状态。正如您所指出的,解决方案是通过 move.

传递数据的所有权

但是 'static 约束是一个 生命周期注释 ,Rust 的一个基本原则是 生命周期注释永远不会影响 运行 -时间行为。换句话说,生命周期注解只是为了让编译器相信代码是正确的。他们无法更改代码 .

的作用

如果 Rust 根据被调用者是否期望 'static 推断出 move 关键字,那么当捕获的数据被丢弃时,更改 thread::spawn 中的生命周期可能会发生变化。这意味着生命周期注释正在影响 运行time 行为,这违反了这一基本原则。我们不能打破这个规则,所以 move 关键字保留。


附录:为什么要删除生命周期注释?

  • 让我们可以自由地改变生命周期推理的工作方式,这允许像 non-lexical lifetimes (NLL).

  • 这样的改进
  • 因此 mrustc 等替代 Rust 实现可以通过忽略生命周期来节省精力。

  • 大部分编译器都假定生命周期以这种方式工作,因此要以其他方式进行,将需要付出巨大的努力,但收获不明。 (参见 this article by Aaron Turon;它是关于专业化,而不是闭包,但它的要点同样适用。)

现有的答案提供了很好的信息,这让我有了一个更容易思考的理解,希望其他 Rust 新手更容易理解。


考虑这个简单的 Rust 程序:

fn print_vec (v: &Vec<u32>) {
    println!("Here's a vector: {:?}", v);
}

fn main() {
    let mut v: Vec<u32> = vec![1, 2, 3];
    print_vec(&v); // `print_vec()` borrows `v`
    v.push(4);
}

现在,问为什么不能隐含 move 关键字就像问为什么不能隐含 print_vec(&v) 中的“&”。

Rust’s central feature is ownership。你不能只告诉编译器,"Hey, here's a bunch of code I wrote, now please discern perfectly everywhere I intend to reference, borrow, copy, move, etc. Kthnxsbye!" &move 等符号和关键字是语言的必要组成部分。

事后看来,这似乎很明显,让我的问题看起来有点傻!