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](/_next/image?url=%2Fblog%2Frust%2Fownership%2Fborrow.png&w=1200&q=75)
也就是對 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);
}
但如果是要用可變參考的方式來借用的話會有一些限制在,以下來看幾個例子。
- 不能同時有多個可變參考:
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);
}
- 不能在有不可變參考的同時又借用為可變參考:
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);
}
- 當借用的行為結束後,則不在此限制內:
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++ 這種需要手動管理記憶體的語言一樣,要用
malloc
、free
、new
、delete
等方法來操作 - 在編譯中防止常見的記憶體錯誤像是 dangling references、double free 等
- 透過所有權鐵則,在不需要仰賴 GC (garbage collector) 的機制自動釋放記憶體
- 透過所有權機制,明確在程式上能理解誰擁有所有權、誰只是借用參考值
- 不需要像 C/C++ 這種需要手動管理記憶體的語言一樣,要用