再次理解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()) } } }