为什么在线程方面需要 "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 种类型的闭包:
创建闭包时,Rust 会根据闭包如何使用环境中的值来推断要使用的特征。闭包捕获其环境的方式取决于它的类型。 FnOnce
按值捕获(如果类型 Copy
able,则可以是移动或复制),FnMut
可变借用,Fn
不可变借用。 但是,如果你在声明闭包时使用move
关键字,它会总是 "capture by value",或者取得所有权捕捉前的环境。因此,move
关键字与 FnOnce
s 无关,但它改变了 Fn
s 和 FnMut
s 捕获数据的方式。
对于你的例子,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
等符号和关键字是语言的必要组成部分。
事后看来,这似乎很明显,让我的问题看起来有点傻!
例如(取自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 种类型的闭包:
创建闭包时,Rust 会根据闭包如何使用环境中的值来推断要使用的特征。闭包捕获其环境的方式取决于它的类型。 FnOnce
按值捕获(如果类型 Copy
able,则可以是移动或复制),FnMut
可变借用,Fn
不可变借用。 但是,如果你在声明闭包时使用move
关键字,它会总是 "capture by value",或者取得所有权捕捉前的环境。因此,move
关键字与 FnOnce
s 无关,但它改变了 Fn
s 和 FnMut
s 捕获数据的方式。
对于你的例子,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
等符号和关键字是语言的必要组成部分。
事后看来,这似乎很明显,让我的问题看起来有点傻!