iT邦幫忙

0

二、三天學一點點 Rust:來! References and Borrowing(23)

  • 分享至 

  • xImage
  •  

🦀不可變與可變參照(references)

從這小節開始,其實是持續延續對擁有權的討論,以下有三組程式,由第一組演變至第三組的思考和寫法。

✅ 第一組:回傳值的方式(使用所有權轉移)

main函式:

fn main() {
    let sushi = String::from("Salmon");
    let food = eat_meal(sushi);
    println!("{}", food);
}

fn eat_meal(mut meal: String) -> String {
    meal.push_str(" and Tuna.");
    meal
}

main函式:

  1. let sushi = String::from("Salmon");
  • 這一行在堆積記憶體中建立了一個新的 String 物件,其內容是 "Salmon"。
  • 變數 sushi 擁有這塊記憶體的所有權。
  1. let food = eat_meal(sushi);
  • 這裡呼叫了 eat_meal 函式,並將 sushi 作為參數傳遞進去。
  • 重點: 當你將一個擁有所有權的變數傳遞給另一個函式時,所有權會發生轉移。在這裡,sushi 的所有權被移動 (moved) 給了 eat_meal 函式的參數 meal。
  • 在這一行執行之後,sushi 這個變數在 main 函式中不再有效,你不能再使用它。
  • eat_meal 函式會回傳一個 String,這個回傳值被賦值給 main 函式中的 food 變數。
  1. println!("{}", food);
    這一行會印出 food 變數的值,這個值是 eat_meal 函式處理後回傳的 String。

eat_meal 函式:

  1. fn eat_meal(mut meal: String) -> String
  • 這一行定義了一個名為 eat_meal 的函式。
  • 它接收一個名為 meal 的參數,其型別是 String。由於使用了 mut 關鍵字,meal 在函式內部是可變的。函式會回傳一個 String。
  • 重點: 在 eat_meal 函式被呼叫時,meal 獲得了從 main 函式傳遞過來的 String 的所有權。
  1. meal.push_str(" and Tuna.");
  • 這一行使用 push_str 方法將字串 " and Tuna." 附加到 meal 的末尾。因為 meal 是可變的,所以這個操作是允許的。
  1. meal
  • 這一行是函式的回傳值。由於 meal 在函式內部被修改過,所以回傳的 String 的值是 "Salmon and Tuna."。
  • 當 eat_meal 函式執行完畢後,meal 這個變數的所有權會移動到呼叫它的地方,也就是 main 函式中的 food 變數。

小結:

使用回傳值是為了避免值被釋放,但確實會帶來不便,尤其是在需要頻繁操作字串時,需要不斷地返回並賦值。因此我們就繼續測試有沒有更好的方式。

⚠️ 第二組:不可變參照(&String)

fn main() {
    let sushi = String::from("Salmon");
    let food = eat_meal(&sushi);
    println!("{}", food);
}

fn eat_meal(meal: &String) -> String {
    meal.push_str(" and Tuna."); // ❌ 編譯錯誤:不可變參照不能修改內容
    meal
}

main函式:

  1. let food = eat_meal(&sushi);
  • 這裡呼叫了 eat_meal 函式,並將 &sushi 作為參數傳遞進去。
  • 重點:&sushi 創建了一個指向 sushi 的不可變參考 (immutable reference)。 這意味著 eat_meal 函式可以借用 (borrow) sushi 的值,但不會取得其所有權。sushi 在 main 函式執行完畢後仍然有效。
  1. println!("{}", food);
    這一行會印出 eat_meal 函式回傳的值,並將其格式化後輸出到控制台。

eat_meal 函式:

  1. fn eat_meal(meal: &String) -> String
  • 這一行定義了一個名為 eat_meal 的函式。
  • 它接收一個名為 meal 的參數,其型別是 &String。
  • 重點:meal: &String 表明 meal 是一個指向 String 的不可變參考。 這意味著 eat_meal 函式只能讀取 meal 所指向的 String 的內容,而不能修改它。
  • 函式預期回傳一個 String。
  1. meal.push_str(" and Tuna."); // 這行會編譯錯誤,因為課程需要而故意寫。
    由於 meal 是一個不可變參考 (&String),因此不允許透過這個參考來調用任何會修改其指向的 String 的方法。

小結:

  • 當使用 &String 時,meal 只是對 String 的一個不可變參考,它並不擁有 String 的所有權。所有權仍然在 main 函式中的 sushi 變數手中。
  • 如果 main 函式中只寫 eat_meal(sushi); 會出錯,因為 eat_meal 函式期望接收一個 &String,而你傳遞的是一個 String (所有權的移動)。因此需要使用 &sushi 來創建一個參考。

✅ 第三組:可變參照(&mut String)

fn main() {
    let mut sushi = String::from("Salmon");
    eat_meal(&mut sushi);
}

fn eat_meal(meal: &mut String) {
    meal.push_str(" and Tuna.");
    println!("Meal steps: {}", meal);
}
  • main 函式建立了一個可變的 String 變數 sushi。
  • &mut sushi 創建了一個指向 sushi 的可變參考 (mutable reference) 並傳遞給 eat_meal 函式的 meal 參數。這表示 eat_meal 函式可以借用 sushi 的值,並且可以修改其內容,但仍然不擁有其所有權。sushi 在 main 函式中仍然有效。
  • eat_meal 函式的 meal 參數是一個指向可變 String 的可變參考。因此,可以使用像 push_str 這樣會修改 String 內容的方法。
  • eat_meal 函式將 " and Tuna." 附加到 meal 所參考的 String 上,並印出修改後的 meal。
  • eat_meal 函式沒有回傳值 (-> String 被移除)。
  • main 函式在 eat_meal 函式呼叫後,sushi 的值已經被修改為 "Salmon and Tuna.",並且 main 函式會印出這個修改後的值。

📘 四種參數定義方式對照表:

寫法 擁有權 可否修改 備註
meal: String ✅ 是 ❌ 否 擁有資料,但不可變
mut meal: String ✅ 是 ✅ 是 擁有資料,可修改
meal: &String ❌ 否 ❌ 否 借用資料,唯讀參照
meal: &mut String ❌ 否 ✅ 是 借用資料,可修改參照

✅ 小結

  • 使用回傳值維持所有權可行但麻煩,適合不需重複使用的情境。
  • 若要修改又保留原變數,使用 &mut T 是較理想的方式。
  • 借用(borrow)設計讓 Rust 在不需垃圾回收的情況下仍能安全處理記憶體。

🔄 進階延伸:可變與不可變參照的借用規則

Rust 在借用變數時有一條重要規則:

你可以擁有任意數量的不可變參照(&T)
但在有可變參照(&mut T)時,不能同時存在任何其它參照(無論是否為可變)

讓我們透過三段程式碼進行比較:

✅ 範例一:兩個不可變參照(合法)

let coffee = String::from("Caramel Macchiato");
let drink = &coffee;
let soft_drink = &coffee;
println!("{} and {}", drink, soft_drink);
  • drinksoft_drink 都是 &coffee 的不可變參照
  • ✅ Rust 允許多個不可變引用同時存在
  • 這樣是安全的,因為沒有人能改變資料內容

⚠️ 範例二:同時擁有可變與不可變參照(實際可編譯)

let mut coffee = String::from("Caramel Macchiato");
let drink = &mut coffee;
let soft_drink = &coffee;
println!("{}", soft_drink); // ✅ 實際可編譯
  • drink 是可變參照,soft_drink 是不可變參照
  • ✅ 雖然理論上不能共存,但因為 drink 沒有被使用,Rust 編譯器判定它已經失效
  • ✅ 這是 Non-Lexical Lifetimes(NLL)的行為,允許非重疊借用

⚠️ 範例三:先使用完可變參照,再建立其他引用(合法)

let mut coffee = String::from("Caramel Macchiato");
let drink = &mut coffee;
println!("{}", drink); // ✅ 先用完 drink
let soft_drink = &coffee;
println!("{}", soft_drink);
  • 雖然建立了可變參照,但在可變參照被完全使用完畢並離開作用域後,才建立其他參照
  • ✅ Rust 編譯器可判定 drink 已經不再使用,因此允許 soft_drink 的建立
  • ✅ 這是 非重疊借用(non-overlapping borrow) 的例子,是合法的

📘 借用規則總結表

類型 可同時出現? 是否可修改原值 備註
多個不可變參照 &T ✅ 是 ❌ 否 可重疊借用
可變參照 &mut T + &T ❌ 否 ✅ 是(僅限 &mut) 資料競爭風險,不允許
先使用完可變再借不可變 ✅ 是 ✅ 是 編譯器能確認作用域不重疊,允許

這些借用規則確保 Rust 在不使用垃圾回收的情況下,依然能達成編譯時的記憶體安全檢查。理解可變與不可變參照的使用邊界,是掌握 Rust 擁有權系統的重要一環。


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言