簡體   English   中英

clone() 和 move 的驚人行為

[英]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()); });

發生的事情是

  1. 創建閉包,將my_string移動到閉包內
  2. 產生線程
  3. 克隆my_string
  4. 用克隆調用 f

當你克隆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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM