相信很多人跟我一樣,看到「move semantics(移動語義)」這個詞的時候不知道它是什麼東西。所以我請教了 ChatGPT:

在 Rust 中,move semantics 是指當變數的所有權(ownership)從一個範疇(scope)移動到另一個範疇時,發生的行為。這是 Rust 獨特的所有權系統的一部分,旨在確保內存安全和數據競爭(data race)的避免。

以下是一些有關 move semantics 的關鍵概念:

  1. 所有權
    在 Rust 中,每個值都有一個所有者(owner)。這個所有者在一個特定的範疇中有效。
  2. 變量的移動(Move)
    當一個變數賦值給另一個變數時,所有權會移動到新變數。原變數不再有效,且無法使用。
   let s1 = String::from("hello");
   let s2 = s1; // s1的所有權移動到s2
   // println!("{}", s1); // 這行會導致編譯錯誤,因為s1已經無效
  1. 變量的拷貝(Copy)
    對於一些簡單的數據類型,如整數和浮點數,Rust 使用拷貝語義(copy semantics)。這些類型實現了 Copy trait,因此當它們被賦值或傳遞時,會發生拷貝而不是移動。
   let x = 5;
   let y = x; // x 被拷貝到 y
   println!("x = {}, y = {}", x, y); // x 和 y 都是有效的
  1. 函數參數與返回值
    當一個變數被作為參數傳遞給函數或從函數返回時,所有權也會移動。
   fn takes_ownership(some_string: String) {
       println!("{}", some_string);
   }

   fn main() {
       let s = String::from("hello");
       takes_ownership(s); // s 的所有權移動到函數參數
       // println!("{}", s); // 這行會導致編譯錯誤,因為 s 已經無效
   }
  1. 克隆(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_charstring_uppercase 函數涉及到這兩者的使用。

首先,來看 main 函數:

fn main() {
    let data = "Rust is great!".to_string();

    get_char(data);

    string_uppercase(&data);
}

這段代碼試圖將 data 傳遞給 get_charstring_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 的所有權和參考、借用還滿難懂的,感覺以後應該會常常卡在這裡。

Similar Posts

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *