Rust 導引筆記系列 #07 -- 借用與參照
物件借用 (Borrowing)
在上次的最後有提到,若是要暫時取得某個不具 Copy
特徵的使用權,就要把所有權傳給該函式,再傳回來。
這顯然不是一個好方法,所以這次就要來介紹「借用」這個機制。
與 C 語言中的取址運算子相當類似,在一個變數的前方加上 &
符號,就可以借用一個變數的數值。在 Rust 中,「借用」會讓我們拿到一個參照,指向被借用的數值。
利用這個機制,我們可以將上一篇最後的範例改為
fn print_string(s: &String) {
println!("The string is: {}", s);
}
fn main() {
let str = String::from("My name is Leo, nice to meet you.");
print_string(&str);
// Only borrow the string here,
// we still hold the ownership.
println!("{}", &str);
}
這樣就只需要傳一個指向 String
的參照進去,不會導致所有權的轉移。
參照 (Reference)
在「借用」一個數值之後,會拿到一個參照,參照究竟是什麼呢?
概念上,參照和指標是極為相似的東西,他們都指向某個在記憶體中存在的數值,可以避免大量數值的複製、修改存在於其他位置的數值等。
但這也是許多棘手問題的根源,因此,Rust 在參照上加了一些限制,若是違反了這些規則,就會導致編譯失敗 (Compile Error)。
- 參照必須永遠指向有效的數值
- 若要更動數值,必須使用可變參照(用
&mut str
取得可變參照) - 對於每個數值,同時間只能有多個不可變參照或是一個可變參照指向它
參照有效性
在使用或回傳時,參照不能指向已經被拋棄的數值
fn dangling() -> &mut String {
let s = String::new();
// Return a reference of the string
&mut s
} // But the string dropped here!!!
上面的範例中,回傳了一個參照指向只存活在這個函式中的 String
,因此在離開函式之後,它就變成一個無效的參照了!
除此之外,在 if
/ while
迴圈等區塊內部宣告的變數也要注意這個問題喔,Rust 是以大括號作為變數存活區域的判斷喔!!
可變參照
Rust 的參照也是有不可變 / 可變之分,預設我們只會拿到不可變參照 (immutable reference),若要修改數值,就要用 &mut
取得可變參照 (mutable reference),另外,原本的變數也必須宣告為可變變數才能取它的可變參照喔。
fn append_world(s: &mut String) {
s.push_str(", world");
}
fn main() {
let mut s = String::from("Hello");
append_world(&mut s);
// Print: Hello, world
println!("{}", &s);
}
在使用可變參照時,有時候會需要重新指派數值,在這種情況下,就會需要在參照前加上 *
運算子 (Dereference Operator),來強制表示我們是要對數值進行操作。
fn set_to_five(num: &mut i32) {
*num = 5;
}
fn main() {
let mut n = 150;
// Print: 150
println!("{}", n);
set_to_five(&mut n);
// Print: 5
println!("{}", n);
}
可變參照互斥性
為了避免資料競搶 (Data Race) [1],Rust 限制每個數值同時只能有多個不可變參照或是一個可變參照指向它,以上的兩種狀況是互斥的,當有可變參照存在時,就不可以有不可變參照;反之亦然。
以下是剛剛出現的範例,但這次同時存在兩種參照,所以會編譯失敗!
fn append_world(s: &mut String) {
s.push_str(", world");
}
fn main() {
let mut s = String::from("Hello");
// Get a reference here
let s_ref = &s;
// Get a mutable reference here
// Oops, mutable & immutable reference
// exists at the same time!!!
append_world(&mut s);
// Print: Hello, world
println!("{}", s_ref);
}
以上的三種限制是 Rust 對於避免參照被不當的使用,進而導致各種非預期行為所作的保護。對於經常編寫 C++ 的人來說,這些都是一些基本的安全原則,但是卻沒有被語言強制要求遵守。
對於新進入 Rust 的人可能會對於經常性的編譯失敗很感冒,但是我必須說,後續的除錯往往才是更消耗時間與精力的。試著改變寫程式的習慣與觀念吧!
Note
- 資料競搶 (Data Race) 與 競搶條件 (Race Condition) 是不同的東西,詳情請看 Race Condition vs. Data Race (English)
終於來到參照了呢,在接下來會轉而細談 Rust 的判斷、迴圈與資料結構。
也許會有人關心多執行緒程式的撰寫,基於以上的規則,可以避免往後發生資料競搶與競搶條件,不過也需要一些額外的同步方式才能寫入資料到共有的變數就是了…
上一篇:#06 -- 物件所有權
下一篇:#08 -- if 條件敘述