[英]Surprising behavior of clone() and move
我正在嘗試通過智能克隆和借用來優化應用程序,並且我正在觀察以下行為。 下面的程序不起作用:
fn f( string: String) {
println!("{}", string );
}
fn main() {
let my_string: String = "ABCDE".to_string();
f( my_string );
f( my_string );
}
它會產生眾所周知的“移動后使用”錯誤。
7 | f( my_string );
| --------- value moved here
8 | f( my_string );
| ^^^^^^^^^ value used here after move
這可以通過克隆my_string
來解決。 下面的程序工作正常:
fn f( string: String) {
println!("{}", string );
}
fn main() {
let my_string: String = "ABCDE".to_string();
f( my_string.clone() );
f( my_string.clone() );
}
但是,如果您在多線程環境中使用相同的方法,克隆就不再有用了。 當函數調用嵌入到線程中時:
use std::thread;
fn f( string: String) {
println!("{}", string );
}
fn main() {
let my_string: String = "ABCDE".to_string();
thread::spawn( move || { f( my_string.clone() ); } );
thread::spawn( move || { f( my_string.clone() ); } );
}
程序再次生成“移動后使用”錯誤:
10 | thread::spawn( move || { f( my_string.clone() ); } );
| ^^^^^^^ --------- use occurs due to use in closure
| |
| value used here after move
但是,您可以通過將線程移動到函數中來解決這個問題,具有相同的凈效果:
use std::thread;
fn f( string: String) {
thread::spawn( move || { println!("{}", string ); } );
}
fn main() {
let my_string: String = "ABCDE".to_string();
f( my_string.clone() );
f( my_string.clone() );
}
上面的程序工作正常。 或者,如果您願意,可以提前克隆my_string
並在第二個函數調用中使用克隆:
use std::thread;
fn f( string: String) {
println!("{}", string );
}
fn main() {
let my_string: String = "ABCDE".to_string();
let my_second_string: String = my_string.clone();
thread::spawn( move || { f( my_string.clone() ); } );
thread::spawn( move || { f( my_second_string ); } );
}
它看起來有點像試錯法,但一些理論或許可以解釋它。
編輯:還有一個關於“移動后使用”錯誤的問題。 另一個問題討論了to_string()
的影響,而這個問題討論了線程環境中的clone()
。
EDIT2 :閱讀答案后,結論如下:在函數調用f(my_string.clone())
中,首先進行克隆,然后將克隆的變量移動到函數中,因此永遠不會移動my_string
。 在閉包中move || { f(my_string.clone())
move || { f(my_string.clone())
首先將my_string
移動到閉包中,因此克隆my_string
無法解決它。 這是函數和閉包之間的區別。 Thread
與此無關,只是它在閉包中強行move
。
move
關鍵字意味着 Rust 現在必須將所有使用的變量從外部移動到閉包中。 所以通過在move || {}
中使用my_string
move || {}
您將其移入。如果您只想在閉包內使用引用,您可以在閉包外借用:
fn f(string: String) {
println!("{}", string);
}
fn main() {
let my_string: String = "ABCDE".to_string();
{
// Take the reference outside of the `move`
// closures so only the reference but not the
// actual string gets moved inside.
//
// Since shared references are `Copy` they get
// copied instead and you can reuse them.
let my_string: &String = &my_string;
let a = move || {
f(my_string.clone());
};
let b = move || {
f(my_string.clone());
};
}
}
但是因為你想在不同的線程中使用它,它可能比當前函數的壽命更長,所以引用必須是'static
。
因此,為了避免將my_string
移動到傳遞給另一個線程的閉包中,您必須在閉包之外進行clone
。
fn f(string: String) {
println!("{}", string);
}
fn main() {
let my_string: String = "ABCDE".to_string();
{
let my_string = my_string.clone();
thread::spawn(move || {
f(my_string);
});
}
{
let my_string = my_string.clone();
thread::spawn(move || {
f(my_string);
});
}
}
但是,如果您在多線程環境中使用相同的方法,克隆就不再有用了。 [...] 它看起來有點像反復試驗,但一些理論也許可以解釋它。
如果你做對了,它確實有幫助。 這里的問題是move
閉包意味着值在閉包運行之前被移動到閉包中1 。
所以當你寫
thread::spawn(move || { f( my_string.clone()); });
發生的事情是
my_string
移動到閉包內my_string
當你克隆my_string
時已經太晚了,因為它已經從外部函數移到了閉包和線程的內部。 就好像您試圖通過更改f
的內容來修復原始片段:
fn f(string: String) {
println!("{}", string.clone());
}
fn main() {
let my_string = "ABCDE".to_string();
f(my_string);
f(my_string);
}
這顯然不能解決任何問題。
通常的解決方案是所謂的“精確捕獲模式”。 “Capture 子句”來自 C++,它是本機功能,但在 Rust 中不是。 你所做的是,不是直接創建一個閉包,而是從一個塊中創建並返回閉包,在創建和返回閉包之前,你可以創建一堆綁定,然后將它們移動到閉包中。 它本質上提供了一個隔離的“閉包設置”:
thread::spawn({
// shadow the outer `my_string` with a clone of itself
let my_string = my_string.clone();
// then move the clone into the closure
move || { f(my_string); }
});
順便說一句,如果您不需要修改String
,另一個選擇是將它放在Arc
中。 盡管您仍然需要在閉包外克隆並將弧線移動到閉包內。 優點是克隆一個弧只會增加它的引用計數,它是原子的,所以它不是免費的,但它比克隆一個復雜/昂貴的對象更便宜。
[1]:從技術上講,非移動閉包也會發生這種情況,更准確地說, move
閉包將move
(/ copy
)它所指的所有內容,而非移動閉包可能會移動或只是借用,具體取決於項目的使用方式。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.