相信很多人跟我一樣,看到「move semantics(移動語義)」這個詞的時候不知道它是什麼東西。所以我請教了 ChatGPT:
在 Rust 中,move semantics 是指當變數的所有權(ownership)從一個範疇(scope)移動到另一個範疇時,發生的行為。這是 Rust 獨特的所有權系統的一部分,旨在確保內存安全和數據競爭(data race)的避免。
以下是一些有關 move semantics 的關鍵概念:
- 所有權:
在 Rust 中,每個值都有一個所有者(owner)。這個所有者在一個特定的範疇中有效。- 變量的移動(Move):
當一個變數賦值給另一個變數時,所有權會移動到新變數。原變數不再有效,且無法使用。let s1 = String::from("hello"); let s2 = s1; // s1的所有權移動到s2 // println!("{}", s1); // 這行會導致編譯錯誤,因為s1已經無效
- 變量的拷貝(Copy):
對於一些簡單的數據類型,如整數和浮點數,Rust 使用拷貝語義(copy semantics)。這些類型實現了Copy
trait,因此當它們被賦值或傳遞時,會發生拷貝而不是移動。let x = 5; let y = x; // x 被拷貝到 y println!("x = {}, y = {}", x, y); // x 和 y 都是有效的
- 函數參數與返回值:
當一個變數被作為參數傳遞給函數或從函數返回時,所有權也會移動。fn takes_ownership(some_string: String) { println!("{}", some_string); } fn main() { let s = String::from("hello"); takes_ownership(s); // s 的所有權移動到函數參數 // println!("{}", s); // 這行會導致編譯錯誤,因為 s 已經無效 }
- 克隆(Clone):
如果需要在移動後保留原變數,可以使用clone
方法來顯式地進行深拷貝(deep copy)。let s1 = String::from("hello"); let s2 = s1.clone(); // 深拷貝 s1 到 s2 println!("s1 = {}, s2 = {}", s1, s2); // s1 和 s2 都是有效的總結來說,Rust 的 move semantics 通過移動所有權來確保內存安全並避免數據競爭。理解這個概念對於寫出安全且高效的 Rust 代碼至關重要。
看來是跟「所有權」有關的東西,這是一個 Rust 獨有的特色,我們來認識它吧!
上一篇:05_vecs
本系列主頁面:Rust繁中簡學!
繁體中文版 Rustlings:https://github.com/TimLai666/rustlings-zh-TW
安裝方法:00_intro
06_move_semantics(移動語義)
移動語義(Move Semantics)
這些練習改編自 pnkfelix 的 Rust Tutorial ——感謝 Felix!!!
進一步了解
在這一部分中的連結特別重要。
練習一(move_semantics1.rs)
// move_semantics1.rs // // 執行 `rustlings hint move_semantics1` 或使用 `hint` watch 子命令來獲取提示。 // I AM NOT DONE #[test] fn main() { let vec0 = vec![22, 44, 66]; let vec1 = fill_vec(vec0); assert_eq!(vec1, vec![22, 44, 66, 88]); } fn fill_vec(vec: Vec<i32>) -> Vec<i32> { let vec = vec; vec.push(88); vec }
! 編譯 exercises/06_move_semantics/move_semantics1.rs 失敗!請再試一次。以下是輸出: error[E0596]: cannot borrow `vec` as mutable, as it is not declared as mutable --> exercises/06_move_semantics/move_semantics1.rs:19:5 | 19 | vec.push(88); | ^^^ cannot borrow as mutable | help: consider changing this to be mutable | 17 | let mut vec = vec; | +++ error: aborting due to 1 previous error For more information about this error, try `rustc --explain E0596`.
let vec = vec;
將 vec
向量宣告為不可變變數,所以當我們改變它的值時出現錯誤。
我們應該幫它加上一個 mut
。
// move_semantics1.rs // // 執行 `rustlings hint move_semantics1` 或使用 `hint` watch 子命令來獲取提示。 // I AM NOT DONE #[test] fn main() { let vec0 = vec![22, 44, 66]; let vec1 = fill_vec(vec0); assert_eq!(vec1, vec![22, 44, 66, 88]); } fn fill_vec(vec: Vec<i32>) -> Vec<i32> { let mut vec = vec; vec.push(88); vec }
✓ 成功測試 exercises/06_move_semantics/move_semantics1.rs! 🎉 🎉 代碼正在編譯,並且測試通過! 🎉 🎉 您可以繼續進行此練習, 或通過刪除 `I AM NOT DONE` 註釋來進入下一個練習: 3 | // 執行 `rustlings hint move_semantics1` 或使用 `hint` watch 子命令來獲取提示。 4 | 5 | // I AM NOT DONE 6 | 7 | #[test]
從這題我們學到,可以用 let mut
重新宣告一個變數,讓它從不可變變成可變。
練習二(move_semantics2.rs)
// move_semantics2.rs // // 使測試通過,找到一種方法讓兩個 Vec 保持分離! // // 執行 `rustlings hint move_semantics2` 或使用 `hint` watch 子命令來獲取提示。 // I AM NOT DONE #[test] fn main() { let vec0 = vec![22, 44, 66]; let vec1 = fill_vec(vec0); assert_eq!(vec0, vec![22, 44, 66]); assert_eq!(vec1, vec![22, 44, 66, 88]); } fn fill_vec(vec: Vec<i32>) -> Vec<i32> { let mut vec = vec; vec.push(88); vec }
! 編譯 exercises/06_move_semantics/move_semantics2.rs 失敗!請再試一次。以下是輸出: error[E0382]: borrow of moved value: `vec0` --> exercises/06_move_semantics/move_semantics2.rs:15:5 | 11 | let vec0 = vec![22, 44, 66]; | ---- move occurs because `vec0` has type `Vec<i32>`, which does not implement the `Copy` trait 12 | 13 | let vec1 = fill_vec(vec0); | ---- value moved here 14 | 15 | assert_eq!(vec0, vec![22, 44, 66]); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ value borrowed here after move | note: consider changing this parameter type in function `fill_vec` to borrow instead if owning the value isn't necessary --> exercises/06_move_semantics/move_semantics2.rs:19:18 | 19 | fn fill_vec(vec: Vec<i32>) -> Vec<i32> { | -------- ^^^^^^^^ this parameter takes ownership of the value | | | in this function = note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info) help: consider cloning the value if the performance cost is acceptable | 13 | let vec1 = fill_vec(vec0.clone()); | ++++++++ error: aborting due to 1 previous error For more information about this error, try `rustc --explain E0382`.
這題要把兩個向量分開,我們可以在 let vec1 = fill_vec(vec0);
使用克隆方法,保留原本的變數。
// move_semantics2.rs // // 使測試通過,找到一種方法讓兩個 Vec 保持分離! // // 執行 `rustlings hint move_semantics2` 或使用 `hint` watch 子命令來獲取提示。 // I AM NOT DONE #[test] fn main() { let vec0 = vec![22, 44, 66]; let vec1 = fill_vec(vec0.clone()); assert_eq!(vec0, vec![22, 44, 66]); assert_eq!(vec1, vec![22, 44, 66, 88]); } fn fill_vec(vec: Vec<i32>) -> Vec<i32> { let mut vec = vec; vec.push(88); vec }
✓ 成功測試 exercises/06_move_semantics/move_semantics2.rs! 🎉 🎉 代碼正在編譯,並且測試通過! 🎉 🎉 您可以繼續進行此練習, 或通過刪除 `I AM NOT DONE` 註釋來進入下一個練習: 5 | // 執行 `rustlings hint move_semantics2` 或使用 `hint` watch 子命令來獲取提示。 6 | 7 | // I AM NOT DONE 8 | 9 | #[test]
練習三(move_semantics3.rs)
// move_semantics3.rs // // 使我編譯通過且不新增新行 -- 只更改現有行!(不需要多個分號的行!) // // 執行 `rustlings hint move_semantics3` 或使用 `hint` watch 子命令來獲取提示。 // I AM NOT DONE #[test] fn main() { let vec0 = vec![22, 44, 66]; let vec1 = fill_vec(vec0); assert_eq!(vec1, vec![22, 44, 66, 88]); } fn fill_vec(vec: Vec<i32>) -> Vec<i32> { vec.push(88); vec }
! 編譯 exercises/06_move_semantics/move_semantics3.rs 失敗!請再試一次。以下是輸出: error[E0596]: cannot borrow `vec` as mutable, as it is not declared as mutable --> exercises/06_move_semantics/move_semantics3.rs:19:5 | 19 | vec.push(88); | ^^^ cannot borrow as mutable | help: consider changing this to be mutable | 18 | fn fill_vec(mut vec: Vec<i32>) -> Vec<i32> { | +++ error: aborting due to 1 previous error For more information about this error, try `rustc --explain E0596`.
這題跟第一題很像,但題目要求我們不要新增行。
我們可以在 fn fill_vec(vec: Vec<i32>)
的括號裡面直接加上 mut
。
// move_semantics3.rs // // 使我編譯通過且不新增新行 -- 只更改現有行!(不需要多個分號的行!) // // 執行 `rustlings hint move_semantics3` 或使用 `hint` watch 子命令來獲取提示。 // I AM NOT DONE #[test] fn main() { let vec0 = vec![22, 44, 66]; let vec1 = fill_vec(vec0); assert_eq!(vec1, vec![22, 44, 66, 88]); } fn fill_vec(mut vec: Vec<i32>) -> Vec<i32> { vec.push(88); vec }
✓ 成功測試 exercises/06_move_semantics/move_semantics3.rs! 🎉 🎉 代碼正在編譯,並且測試通過! 🎉 🎉 您可以繼續進行此練習, 或通過刪除 `I AM NOT DONE` 註釋來進入下一個練習: 5 | // 執行 `rustlings hint move_semantics3` 或使用 `hint` watch 子命令來獲取提示。 6 | 7 | // I AM NOT DONE 8 | 9 | #[test]
練習四(move_semantics4.rs)
// move_semantics4.rs // // 重構這段代碼,使得 `vec0` 不再被傳遞給 `fill_vec` 函數, // 而是在函數內部創建 Vector 並傳回給主函數。 // // 執行 `rustlings hint move_semantics4` 或使用 `hint` watch 子命令來獲取提示。 // I AM NOT DONE #[test] fn main() { let vec0 = vec![22, 44, 66]; let vec1 = fill_vec(vec0); assert_eq!(vec1, vec![22, 44, 66, 88]); } // `fill_vec()` 不再接受 `vec: Vec<i32>` 作為參數 - 不要更改這點! fn fill_vec() -> Vec<i32> { // 相反,讓我們在這裡創建並填充 Vec - 您該怎麼做呢? let mut vec = vec; vec.push(88); vec }
! 編譯 exercises/06_move_semantics/move_semantics4.rs 失敗!請再試一次。以下是輸出: error[E0423]: expected value, found macro `vec` --> exercises/06_move_semantics/move_semantics4.rs:22:19 | 22 | let mut vec = vec; | ^^^ not a value error[E0061]: this function takes 0 arguments but 1 argument was supplied --> exercises/06_move_semantics/move_semantics4.rs:14:16 | 14 | let vec1 = fill_vec(vec0); | ^^^^^^^^ ---- | | | unexpected argument of type `Vec<{integer}>` | help: remove the extra argument | note: function defined here --> exercises/06_move_semantics/move_semantics4.rs:20:4 | 20 | fn fill_vec() -> Vec<i32> { | ^^^^^^^^ error: aborting due to 2 previous errors Some errors have detailed explanations: E0061, E0423. For more information about an error, try `rustc --explain E0061`.
這題跟前面類似,只不過改成在函數裡面宣告向量。
我們把跟 vec0
有關的程式碼刪掉,然後把 vec![22, 44, 66];
移到函數裡的 let mut vec =
後面。
// move_semantics4.rs // // 重構這段代碼,使得 `vec0` 不再被傳遞給 `fill_vec` 函數, // 而是在函數內部創建 Vector 並傳回給主函數。 // // 執行 `rustlings hint move_semantics4` 或使用 `hint` watch 子命令來獲取提示。 // I AM NOT DONE #[test] fn main() { let vec1 = fill_vec(); assert_eq!(vec1, vec![22, 44, 66, 88]); } // `fill_vec()` 不再接受 `vec: Vec<i32>` 作為參數 - 不要更改這點! fn fill_vec() -> Vec<i32> { // 相反,讓我們在這裡創建並填充 Vec - 您該怎麼做呢? let mut vec = vec![22, 44, 66]; vec.push(88); vec }
✓ 成功測試 exercises/06_move_semantics/move_semantics4.rs! 🎉 🎉 代碼正在編譯,並且測試通過! 🎉 🎉 您可以繼續進行此練習, 或通過刪除 `I AM NOT DONE` 註釋來進入下一個練習: 6 | // 執行 `rustlings hint move_semantics4` 或使用 `hint` watch 子命令來獲取提示。 7 | 8 | // I AM NOT DONE 9 | 10 | #[test]
練習五(move_semantics5.rs)
// move_semantics5.rs // // 使我編譯通過,只需重新排列 `main()` 中的行,但不添加、更改或刪除其中任何一行。 // // 執行 `rustlings hint move_semantics5` 或使用 `hint` watch 子命令來獲取提示。 // I AM NOT DONE #[test] fn main() { let mut x = 100; let y = &mut x; let z = &mut x; *y += 100; *z += 1000; assert_eq!(x, 1200); }
! 編譯 exercises/06_move_semantics/move_semantics5.rs 失敗!請再試一次。以下是輸出: error[E0499]: cannot borrow `x` as mutable more than once at a time --> exercises/06_move_semantics/move_semantics5.rs:13:13 | 12 | let y = &mut x; | ------ first mutable borrow occurs here 13 | let z = &mut x; | ^^^^^^ second mutable borrow occurs here 14 | *y += 100; | --------- first borrow later used here error: aborting due to 1 previous error For more information about this error, try `rustc --explain E0499`.
這題會錯誤是因為 let z = &mut x;
把 x
借走之後,*y += 100;
就沒辦法使用 x
了。
我們只要把 let z = &mut x;
移到 *y += 100;
之後,就能解決。
// move_semantics5.rs // // 使我編譯通過,只需重新排列 `main()` 中的行,但不添加、更改或刪除其中任何一行。 // // 執行 `rustlings hint move_semantics5` 或使用 `hint` watch 子命令來獲取提示。 // I AM NOT DONE #[test] fn main() { let mut x = 100; let y = &mut x; *y += 100; let z = &mut x; *z += 1000; assert_eq!(x, 1200); }
✓ 成功測試 exercises/06_move_semantics/move_semantics5.rs! 🎉 🎉 代碼正在編譯,並且測試通過! 🎉 🎉 您可以繼續進行此練習, 或通過刪除 `I AM NOT DONE` 註釋來進入下一個練習: 5 | // 執行 `rustlings hint move_semantics5` 或使用 `hint` watch 子命令來獲取提示。 6 | 7 | // I AM NOT DONE 8 | 9 | #[test]
練習六(move_semantics6.rs)
// move_semantics6.rs // // 您只能添加或刪除引用(reference),不能更改其他任何內容。 // // 執行 `rustlings hint move_semantics6` 或使用 `hint` watch 子命令來獲取提示。 // I AM NOT DONE fn main() { let data = "Rust is great!".to_string(); get_char(data); string_uppercase(&data); } // 不應該取得所有權 fn get_char(data: String) -> char { data.chars().last().unwrap() } // 應該取得所有權 fn string_uppercase(mut data: &String) { data = &data.to_uppercase(); println!("{}", data); }
! 編譯 exercises/06_move_semantics/move_semantics6.rs 失敗!請再試一次。以下是輸出: error[E0382]: borrow of moved value: `data` --> exercises/06_move_semantics/move_semantics6.rs:14:22 | 10 | let data = "Rust is great!".to_string(); | ---- move occurs because `data` has type `String`, which does not implement the `Copy` trait 11 | 12 | get_char(data); | ---- value moved here 13 | 14 | string_uppercase(&data); | ^^^^^ value borrowed here after move | note: consider changing this parameter type in function `get_char` to borrow instead if owning the value isn't necessary --> exercises/06_move_semantics/move_semantics6.rs:18:19 | 18 | fn get_char(data: String) -> char { | -------- ^^^^^^ this parameter takes ownership of the value | | | in this function help: consider cloning the value if the performance cost is acceptable | 12 | get_char(data.clone()); | ++++++++ error[E0716]: temporary value dropped while borrowed --> exercises/06_move_semantics/move_semantics6.rs:24:13 | 23 | fn string_uppercase(mut data: &String) { | - let's call the lifetime of this reference `'1` 24 | data = &data.to_uppercase(); | --------^^^^^^^^^^^^^^^^^^^- temporary value is freed at the end of this statement | | | | | creates a temporary value which is freed while still in use | assignment requires that borrow lasts for `'1` error: aborting due to 2 previous errors Some errors have detailed explanations: E0382, E0716. For more information about an error, try `rustc --explain E0382`.
這題我也不會,請 ChatGPT 來講解:
這題目要求我們只用引用(reference)來修改代碼,而不能改變其他任何內容。目標是讓代碼能夠正確執行而不會發生所有權轉移錯誤。
在 Rust 中,引用可以分為「不可變引用」和「可變引用」,我們可以用這兩者來避免所有權的轉移。這題目中的
get_char
和string_uppercase
函數涉及到這兩者的使用。首先,來看
main
函數:fn main() { let data = "Rust is great!".to_string(); get_char(data); string_uppercase(&data); }這段代碼試圖將
data
傳遞給get_char
和string_uppercase
函數。這裡的問題在於get_char
函數嘗試取得data
的所有權,這樣一來,data
在傳遞給get_char
後就無法再被使用。接著,看
get_char
函數:fn get_char(data: String) -> char { data.chars().last().unwrap() }這個函數目前是取得所有權,但我們只需要傳遞引用來讀取字符,因此可以改為:
fn get_char(data: &String) -> char { data.chars().last().unwrap() }在這裡,我們將
data
的類型從String
改為&String
,使其成為不可變引用。再來看
string_uppercase
函數:fn string_uppercase(mut data: &String) { data = &data.to_uppercase(); println!("{}", data); }這裡應該取得
data
的所有權並進行大寫轉換。由於to_uppercase
方法會返回一個新的String
,我們應該使string_uppercase
函數接受String
而不是引用。修正後的
string_uppercase
函數如下:fn string_uppercase(mut data: String) { data = data.to_uppercase(); println!("{}", data); }這樣
string_uppercase
函數能夠正確地取得String
的所有權並修改它。最後修改
main
函數來正確傳遞參數:fn main() { let data = "Rust is great!".to_string(); get_char(&data); string_uppercase(data); }這樣,我們傳遞
data
的引用給get_char
,而傳遞data
本身給string_uppercase
。完整的解法如下:
fn main() { let data = "Rust is great!".to_string(); get_char(&data); string_uppercase(data); } // 不應該取得所有權 fn get_char(data: &String) -> char { data.chars().last().unwrap() } // 應該取得所有權 fn string_uppercase(mut data: String) { data = data.to_uppercase(); println!("{}", data); }這樣一來,代碼能夠正確運行而不會有所有權轉移的問題。
看來 Rust 跟其他程式語言比較不一樣,如果將一個變數傳入函數,變數的所有權就會轉移給函數,主程式就無法再使用該變數。解決這個問題的方法是利用「引用」(或稱「參考」),只要在變數前面加上 &
就能改成使用參考。
// move_semantics6.rs // // 您只能添加或刪除引用(reference),不能更改其他任何內容。 // // 執行 `rustlings hint move_semantics6` 或使用 `hint` watch 子命令來獲取提示。 // I AM NOT DONE fn main() { let data = "Rust is great!".to_string(); get_char(&data); string_uppercase(data); } // 不應該取得所有權 fn get_char(data: &String) -> char { data.chars().last().unwrap() } // 應該取得所有權 fn string_uppercase(mut data: String) { data = data.to_uppercase(); println!("{}", data); }
✓ 成功運行 exercises/06_move_semantics/move_semantics6.rs! 🎉 🎉 代碼正在編譯! 🎉 🎉 輸出: ==================== RUST IS GREAT! ==================== 您可以繼續進行此練習, 或通過刪除 `I AM NOT DONE` 註釋來進入下一個練習: 5 | // 執行 `rustlings hint move_semantics6` 或使用 `hint` watch 子命令來獲取提示。 6 | 7 | // I AM NOT DONE 8 | 9 | fn main() {
總結
Rust 的所有權和參考、借用還滿難懂的,感覺以後應該會常常卡在這裡。