avatar
substackSubstackthreadsinstagram

Rust 所有權 (3) - Borrow、可變參考、切片 Slice

目錄

什麼是借用 (Borrowing)?

昨天的最後看到這段程式碼中,可以在 calculate_length 這個函式中將原本的傳入的 String 變數的所有權,再用 tuple 的方式傳回去,達到能在原本的 main 去取得傳入變數的所有權的做法:

fn main() {
    let s1 = String::from("hello");
 
    // 拿回原本該 String 變數的所有權
    let (s2, len) = calculate_length(s1);
 
    println!("'{}' 的長度為 {}。", s2, len);
}
 
fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();
 
    // 將原本傳入的 s 的所有權再傳回去
    (s, length)
}

但這樣寫稍微太囉唆了,因此在 Rust 中有更優雅的處理方式,可以用傳參考的運算符號 & 來實現,來看看下面怎麼改寫:

fn main() {
    let s1 = String::from("hello");
 
    // 只傳入 s1 的參考值
    let len = calculate_length(&s1);
 
    println!("'{}' 的長度為 {}。", s1, len);
}
 
fn calculate_length(s: &String) -> usize {
    let length = s.len();
    length
}

在上面的程式中將原本 calculate_length(s1) 這段改成傳入 &s1 的寫法。參考下圖,這個意思是指傳入這個函式的 s 會去建立一個指向 s1 變數的參考指標,藉此來在不同函式中去取得 s1 的數值,但不取得其所有權,這個行為就稱為 borrowing

borrowing

也就是對 main 這個函式而言,在呼叫 calculate_length 時會跟它說「我不給你 s1 這個變數的所有權,但我可以給你。而當 calculate_length 執行完後,這個 s 的指標就會完成任務被釋放,這樣的做法也達到了避免在只是要取一個變數的值時,需要去複製一份記憶體的浪費。

借用的值能不能被修改?

那延續上面的例子,如果今天想要在原本計算長度的函式中對這個借來的 String 去做修改是可行的嗎?

沒錯,這是做的到的,這裡來看看要怎麼修改。就是將原本的 &s1 改成 &mut s1 就能達到此效果,這個操作又稱為傳入可變的參考 (Mutable References):

fn process_string(s: &mut String) -> usize {
    // 因為 s 是可變的參考,所以可以對它修改
    s.push_str(", world");
    let length = s.len();
 
    length
}
 
fn main() {
    // 將 s1 改為 mutable
    let mut s1 = String::from("hello");
 
    // 傳入 mutable 版本的參考
    let len = process_string(&mut s1);
 
    // 印出 “修改後的 'hello, world' 長度為 12。”
    println!("修改後的 '{}' 長度為 {}。", s1, len);
}

可以一次把某個變數借給很多人嗎?

需要特別注意的是,借用這件事是可以一次借給很多人,像下面這樣是能被正確編譯過的:

fn main() {
    let s = String::from("hello");
 
    let r1 = &s;
    let r2 = &s;
    let r3 = &s;
 
    println!("{}, {}, {}", r1, r2, r3);
}

但如果是要用可變參考的方式來借用的話會有一些限制在,以下來看幾個例子。

  1. 不能同時有多個可變參考:
fn main() {
    let mut s = String::from("hello");
 
    // cannot borrow `s` as mutable more than once at a time
    let r1 = &mut s;
    let r2 = &mut s;
    let r3 = &mut s;
 
    println!("{}, {}, {}", r1, r2, r3);
}
  1. 不能在有不可變參考的同時又借用為可變參考:
fn main() {
    let mut s = String::from("hello");
 
    let r1 = &s;
    let r2 = &s;
 
    // cannot borrow `s` as mutable because it is also borrowed as immutable
    let r3 = &mut s;
 
    println!("{}, {}, {}", r1, r2, r3);
}
  1. 當借用的行為結束後,則不在此限制內:
fn main() {
    let mut s = String::from("hello");
 
    let r1 = &s;
    let r2 = &s;
 
    println!("{}, {}", r1, r2);
 
    // 這裡是可行的,因為前面借用已經完成
    let r3 = &mut s;
 
    println!("{}", r3);
}

這主要是 Rust 中為了避免資料在取用時會互相競爭 (被稱為 Data Races) 的防呆,以防借出去的變數在其他人正在使用時,因為在某處被以可變的狀態借出而改值因而造成不可預期的錯誤。

Dangling Reference

最後再來看另一個關於借用的經典問題 —— 懸空參考 (Dangling Reference):

fn main() {
    // 因為此時回傳值的實際記憶體已被釋放
    // 這裡會指向一個無效的 String
    let reference_to_nothing = dangle();
}
 
fn dangle() -> &String {
    let s = String::from("hello");
 
    // 回傳 String s 的參考
    &s
} // s 在離開這個作用域後就被釋放

當執行上面這段程式碼時,會得到一段訊息說明 this function's return type contains a borrowed value, but there is no value for it to be borrowed from,因為在 dangle 中最後去借用了 s 這個變數,但在這個作用域結束後 s 就被釋放掉,成為一個已經不存在的值,因此這個回傳的參考就會變成一個懸空參考,實際會指向一個無效的 String。而根本的解法也很容易,就是不要借用直接回傳 s 就正確了。

💡 而上面這段程式中其實在編譯時的完整錯誤訊息中,還會提到生命週期相關的資訊,但那會是另一個新戰場這裡就先不深入展開了。

Slice

最後再簡單提一下切片 (Slice) 的概念,從文件上看起來切片的用途是為了讓你不需要一次參考到整段資料,而只去取某一段需要的資料,而這樣的一小段就稱為切片:

fn main() {
    let s = String::from("hello world");
 
    let hello = &s[0..5];
    let world = &s[6..11];
}

我有個好奇是看起來切片也是可以設定為 &mut 來做可變參考的,但當今天我是借用不同段的資料像下面這樣的話編譯的過嗎:

fn main() {
    let mut s = String::from("hello world");
 
    let hello = &s[0..5]; // 不可變切片
 
    // cannot borrow `s` as mutable because it is also borrowed as immutable
    let mut world = &mut s[6..11]; // 可變切片
 
    world.make_ascii_uppercase();
    println!("{}, {}", hello, world);
}

從以上這個例子來看在編譯前 IDE 就已經提示了一樣不可同時借出為可變與不可變,看來就算是切片也是需要遵守「一次只能有一個可變或多個不可變參考」這件事。

所有權系統的小結

今天算是初步將 Rust 中的整個所有權系統入門的內容走過一遍了,以下來做個總整理:

  • 什麼是所有權系統
    • Rust 實現記憶體安全 (memory safety) 和無懼並行 (fearless concurrency) 特色的核心機制
  • 特色:
    • 不需要像 C/C++ 這種需要手動管理記憶體的語言一樣,要用 mallocfreenewdelete 等方法來操作
    • 在編譯中防止常見的記憶體錯誤像是 dangling references、double free 等
    • 透過所有權鐵則,在不需要仰賴 GC (garbage collector) 的機制自動釋放記憶體
    • 透過所有權機制,明確在程式上能理解誰擁有所有權、誰只是借用參考值