簡體   English   中英

為什么在線程方面需要“move”關鍵字? 為什么我永遠不想要這種行為?

[英]Why is the “move” keyword necessary when it comes to threads; why would I ever not want that behavior?

例如(取自Rust 文檔):

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

這不是關於move什么的問題,而是關於為什么需要指定.

如果您希望閉包擁有外部值的所有權,是否有理由不使用move關鍵字? 如果在這些情況下總是需要move ,是否有任何理由不能僅僅暗示/省略move的存在? 例如:

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神奇地消失時,我不禁對自己想知道:為什么我想要這種行為?


我並不是說所需的語法有任何問題。 我只是想從比我更了解move的人那里獲得更深入的了解。 :)

這完全是關於生命周期注釋,以及很久以前 Rust 做出的設計決定。

看,您的thread::spawn示例無法編譯的原因是因為它需要一個'static閉包”。 由於新線程的運行時間比生成它的代碼長,因此我們必須確保任何捕獲的數據在調用者返回后仍然有效。 正如您所指出的,解決方案是使用move傳遞數據的所有權。

但是'static約束”是生命周期注釋,Rust 的基本原則是生命周期注釋永遠不會影響運行時行為 換句話說,生命周期注解只是為了讓編譯器相信代碼是正確的; 他們不能改變代碼的作用

如果 Rust 根據被調用者是否期望'static推斷出move關鍵字,則在刪除捕獲的數據時更改thread::spawn中的生命周期可能會發生變化。 這意味着生命周期注釋正在影響運行時行為,這違反了這一基本原則。 我們不能打破這個規則,所以move關鍵字仍然存在。


附錄:為什么要刪除生命周期注釋?

  • 讓我們可以自由地改變生命周期推斷的工作方式,從而允許改進非詞匯生命周期 (NLL)

  • 因此,像mrustc這樣的替代 Rust 實現可以通過忽略生命周期來節省工作量。

  • 大部分編譯器都假設生命周期以這種方式工作,因此要不然就需要付出巨大的努力並獲得可疑的收益。 (請參閱Aaron Turon 的這篇文章;它是關於專業化的,而不是閉包,但它的觀點同樣適用。)

這里實際上有一些事情在起作用。 為了幫助回答您的問題,我們必須首先了解move存在的原因。

Rust 有 3 種類型的閉合:

  1. FnOnce ,一個消耗其捕獲變量的閉包(因此只能調用一次),
  2. FnMut ,一個可變地借用其捕獲的變量的閉包,以及
  3. Fn ,一個不可變地借用其捕獲變量的閉包。

創建閉包時,Rust 根據閉包使用環境值的方式推斷要使用的特征。 閉包捕獲其環境的方式取決於其類型。 FnOnce按值捕獲(如果類型是可Copy的,則可能是移動或副本), FnMut可變借用,而Fn不可變借用。 但是,如果在聲明閉包時使用move關鍵字,它將始終“按值捕獲”,或者在捕獲之前取得環境的所有權。 因此, move關鍵字與FnOnce無關,但它改變了FnFnMut捕獲數據的方式。

以您的示例為例, Rust 將閉包的類型推斷為Fn ,因為println! 只需要引用它正在打印的值(您鏈接的 Rust 書頁在解釋錯誤時沒有move時談到了這一點)。 因此,閉包嘗試借用v ,並且適用標准生命周期規則。 由於thread::spawn要求傳遞給它的閉包具有'static生命周期,因此捕獲的環境還必須具有'static生命周期, v不會超過該生命周期,從而導致錯誤。 因此,您必須明確指定您希望閉包擁有v的所有權。

這可以通過將閉包更改為編譯器推斷為FnOnce的東西來進一步舉例說明 -- || v || v ,作為一個簡單的例子。 由於編譯器推斷閉包是一個FnOnce ,它默認捕獲v的值,並且行let handle = thread::spawn(|| v); 無需move即可編譯。

現有的答案有很好的信息,這讓我有了一個更容易思考的理解,希望其他 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 的核心特征是所有權 你不能只告訴編譯器,“嘿,這是我寫的一堆代碼,現在請完美識別我打算引用、借用、復制、移動等的任何地方。Kthnxsbye!” &move等符號和關鍵字是該語言必不可少的組成部分。

事后看來,這似乎很明顯,讓我的問題看起來有點傻!

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

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