再次理解Move

前面對Move、Copy和所有權相關的內容做了詳細的解釋,相信變量賦值、函數傳參時的所有權問題應該不再難理解。

但是,所有權的轉移並不僅僅只發生在這兩種相對比較明顯的情況下。例如,解引用操作也需要轉移所有權。


#![allow(unused)]
fn main() {
let v = &vec![11, 22];
let vv = *v;
}

上面會報錯:

error[E0507]: cannot move out of `*v` which is behind a shared reference

從位置表達式和值的角度來思考也不難理解:當產生了一個位置,且需要向位置中放入值,就會發生移動(Moved and copied types)。只不過,這個值可能來自某個變量,可能來自計算結果(即來自於中間產生的臨時變量),這個值的類型可能實現了Copy Trait。

對於上面的示例來說,&vec![11, 22]中間產生了好幾個臨時變量,但最終有一個臨時變量是vec的所有者,然後對這個變量進行引用,將引用賦值給變量v。使用*v解引用時,也產生了一個臨時變量保存解引用得到的值,而這裡就出現了問題。因為變量v只是vec的一個引用,而不是它的所有者,它無權轉移值的所有權。

下面幾個示例,將不難理解:


#![allow(unused)]
fn main() {
let a = &"junmajinlong.com".to_string();
// let b = *a;         // (1).取消註釋將報錯
let c = (*a).clone();  // (2).正確
let d = &*a;           // (3).正確

let x = &3;
let y = *x;      // (4).正確
}

注意,不要使用println!("{}", *a);或類似的宏來測試,這些宏不是函數,它們真實的代碼中使用的是&(*a),因此不會發生所有權的轉移。

雖說【當產生了一個位置,且需要向位置中放入值,就會發生移動】這句話很容易理解,但有時候很難發現深層次的移動行為。

被丟棄的move

下面是一個容易令人疑惑的示例:

fn main(){
  let x = "hello".to_string();
  x;   // 發生Move
  println!("{}", x);  // 報錯:value borrowed here after move
}

從這個示例來看,【當值需要放進位置的時候,就會發生移動】,這句話似乎不總是正確,第三行的x;取得了x的值,但是它直接被丟棄了,所以x也被消耗掉了,使得println中使用x報錯。實際上,這裡也產生了位置,它等價於let _tmp = x;,即將值移動給了一個臨時變量。

如果上面的示例不好理解,那下面有時候會排上用場的示例,會有助於理解:

fn main() {
    let x = "hello".to_string();
    let y = {
        x // 發生Move,注意沒有結尾分號
    };
    println!("{}", x); // 報錯:value borrowed here after move
}

從結果上來看,語句塊將x通過返回值的方式移出來賦值給了y,所以認為x的所有權被轉移給了y。實際上,語句塊中那唯一的一行代碼本身就發生了一次移動,將x的所有權移動給了臨時變量,然後返回時又發生了一次移動。

什麼時候Move:使用值的時候

上面的結論說明了一個問題:雖然多數時候產生位置的行為是比較明確的,但少數時候卻非常難發現,也難以理解。

可以換個角度來看待:當使用值的時候,就會產生位置,就會發生移動

如果翻閱Rust Reference文檔,就會經常性地看到類似這樣的說法(例如Negation operators):

xxx are evaluated in value expression context so are moved or copied.

這裡需要明確:value expression表示的是會產生值的表達式,value expression context表示的是使用值的上下文。

有哪些地方會使用值呢?除了比較明顯的會移動的情況,還有一些隱式的移動(或Copy):

  • 方法調用的真實接收者,如a.meth(),a會被移動(注意,a可能會被自動加減引用,此時a不是方法的真實接收者)
  • 解引用時會Move(注意,解引用會得到那個值,但不一定會消耗這個值,有可能只是藉助這個值去訪問它的某個字段、或創建這個值的引用,這些操作可以看作是借值而不是使用值)
  • 字段訪問時會Move那個字段
  • 索引訪問時,會Move那個元素
  • 大小比較時,會Move(注意,a > b比較時會先自動取a和b的引用,然後再增減a和b的引用直到兩邊類型相同,因此實際上Move(Copy)的是它們的某個引用,而不會Move變量本身)

更完整更細緻的描述,參考Expression - Rust Reference

下面是幾個比較常見的容易疑惑的移動示例:


#![allow(unused)]
fn main() {
struct User {name: String}
let user = User {name: "junmajinlong".to_string()};
let nane = (&user).name;    // 報錯,想要移動name字段,但user正被引用著,此刻不允許移走它的一部分

let user1 = *(&user);  // 報錯,解引用臨時變量時觸發移動,此時user正被引用著
let user2 = &(*user);  // 不報錯,解引用得到值後,對這個值創建引用,不會消耗值

impl User {
  fn func(&self) {
    let xx = *self; // 報錯,解引用報錯,self自身不是所有者,例如user.func()時,user才是所有者
    
    if (*self).name < "hello".to_string(){} // 不報錯,比較時會轉換為&((*self).name) < &("hello".to_string())
  }
}
}