1. 引用
很多場景中,我們可能只是想讀取某個變數指向的值,並不想獲得其所有權,這個時候就可以使用引用。其實在很多其他的程式語言中,也有引用的概念。
- 簡單來講,引用是建立一個變數,指向另個指標的地址,而不是直接指向 該指標指向的堆記憶體地址
- 通過 & 取地址符獲取對一個指標變數的引用
例如在 Rust 中,我們這樣建立引用:
let s1 = String::from("hello");
// 獲取 s1 的引用
let s = &s1;
下圖就很好的表示了 引用 的關係
- 變數s1是棧記憶體中的一個指標地址,通過 ptr 記錄了儲存於堆記憶體中的 String("hello") 的地址
- 變數s也存在棧記憶體中,通過 ptr 記錄了 s1 的指標地址,來實現對 String("hello") 的引用
2. 借用
使用引用作為函式引數的行為被稱作借用,如何使用借用來規避某個變數的所有權發生移動,我們可以看以下例子:
fn main() {
let s = String::from("hello!");
// 使用 s 的引用作為入參,s 的所有權就不會發生移動
let len = get_length(&s);
println!("the length of {} is {}", s, len); // the length of hello! is 6
}
fn get_length(string: &String) -> usize {
// 引用和變數一樣,預設也是不可變的
// string.push_str("world"); // `string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
string.len()
}
3.可變引用
從上面的例子我們可以知道,引用和變數一樣,預設也是不可變的,我們不能修改引用所指向的值。但如果想要這麼做,那麼可以使用 mut 關鍵字,將引用轉為可變的:
fn main() {
let mut s = String::from("hello!");
/*
* 使用 mut 關鍵字,將 s 的可變引用作為入參,
* 這樣 s 的所有權既不會發生移動,函式中也能通過 可變引用 來修改 s 的值
*/
let len = get_length(&mut s);
// 在 get_length 函式中我們實現了對 s 的修改
println!("the length of {} is {}", s, len); // the length of hello!world is 11
}
fn get_length(string: &mut String) -> usize {
// 通過可變引用對值進行修改
string.push_str("world");
string.len()
}
3.1 可變引用的重要限制
- 對應一個指標變數,在一個特定的作用域內,只能有一個可變引用。原因也很好理解,如果在一塊作用域內,當一個變數存在兩個可變引用,那就意味著同一時間可能有兩個變數控制著同一塊記憶體空間,就會發生資料競爭,很容易在執行時產生bug,因此 Rust 通過編譯時檢查,來規避這樣的問題出現
3.1.1 同一作用域,只能存在一個可變引用
通過例子可以看到,當我們對 變數origin 進行了兩次可變引用,編譯時就直接報錯
fn main() {
let mut origin = String::from("hello");
let ref1 = &mut origin;
let ref2 = &mut origin; // error: cannot borrow `origin` as mutable more than once at a time
println!("{}, {}", ref1, ref2);
}
3.1.2 不可變引用可以有多個
如果是同時使用多個不可變引用,則不會有這個限制,因為不可變引用其實就是隻讀的,不存在可能的記憶體安全風險。通過這個例子我們可以看到,Rust是允許這樣使用的:
fn main() {
let origin = String::from("hello");
let ref1 = &origin;
let ref2 = &origin;
println!("{}, {}", ref1, ref2);
}
3.1.3 某些場景下,可以存在多個可變引用
從上面的兩個例子我們已經知道,如果一個作用域內同時存在兩個以上的可變引用,那麼就可能發生資料競爭,那麼是否存在某些場景,會出現多個可變引用呢?我們看下面這個例子:
其實在程式執行過程中,只要走出作用域,那麼作用域中的變數就會被釋放,這個例子中當宣告 ref2 時,ref1 已經被銷燬了,所以還是可以保證可變引用的第一條原則
fn main() { let mut origin = String::from("hello"); { let ref1 = &mut origin; println!("{}", ref1); } // 當 ref2 宣告時,ref1 已經被銷燬了 let ref2 = &mut origin; println!("{}", ref2); }
3.1.4 不能同時擁有一個可變引用和一個不可變引用
對於一個指標變數,它的可變引用和不可變引用其實是互斥的,不能同時存在。原因很簡單,可變引用可以修改指向記憶體空間的值,當值被修改,不可變引用的意義也就不存在了。因此 Rust 在編譯時會進行檢查,發現這種情況則會直接報錯:
fn main() {
let mut origin = String::from("hello");
let ref1 = &origin;
let ref2 = &mut origin;// cannot borrow `origin` as mutable because it is also borrowed as immutable
println!("{} {}", ref1, ref2);
}
4. 懸垂引用
這一種出現在 C/C++ 等語言中的 bug 場景,描述的是這樣一種場景,一個變數所指向的記憶體空間已經被釋放或分配給其他程式使用,但這個變數任然有效。在 Rust 中,編譯器會檢查這種場景,來避免出現懸垂引用。假設編譯通過,下面就是一個會產生懸垂引用的場景:
fn main() {
let s = String::from("haha");
// s 的所有權被移動到 helper 的作用域中
let a = helper(s);
/*
* 假設編譯通過:
* 當 helper 呼叫完畢,s 指向的記憶體空間就被釋放了,但在 helper 中返回了 s 的引用,
* 其實這個引用已經失效,這時 變數a 就變為了一個 懸垂引用
*/
}
fn helper(s: String) -> &String {
println!("{}", s);
&s // error: missing lifetime specifier
}
但其實在編譯時,Rust就不會允許 helper 返回 s 的引用