我們的上一遍內容的代碼有這樣一個問題:我們必須將String返回給調用函數,以便在調用calculateLen后仍能使用String,因為String被移動到了calculateLen內。
下面是如何定義並使用一個(新的)calculateLen函數,它以一個對象的引用作為參數而不是獲取值的所有權:
fun main(){ let s1 = String::from("test"); let len = calculateLen(&s1); println!("s:{},len:{}", s1, len) } fn calculateLen(s: &String) -> usize { s.len() }
首先,注意變量聲明和函數返回值中的所有元組代碼都沒有了。其次,注意我們傳遞&s1給calculateLen,同時在函數定義中,我們獲取&String而不是String。
這些&符號就是引用,它們允許你使用值但不獲取其所有權。以下展示了一張示意圖,&String s 指向 String s1:
注意:與使用 & 引用相反的操作是解引用(dereferencing),它使用解引用運算符,* 。
fn calculateLen(s: &String) -> usize {//s是對String的引用 s.len() }//這里,s離開作用域。但因為它並不擁有引用值的所有權,所以什么也不會發生。
變量s有效的作用域與函數參數的作用域一樣,不過當引用離開作用域后並不丟棄它指向的數據,因為我們沒有所有權。當函數使用引用而不是實際值作為參數,無需返回值來交還所有權,因為就不曾擁有所有權。
我們將獲取引用作為函數參數稱為借用(borrowing)。正如現實生活中,如果一個人擁有某樣東西,你可以從他那里借來。當你使用完比,必須還回去。
如果我們要修改借用的變量呢?是不行的。
正如變量默認是不可變的,引用也一樣。(默認)不允許修改引用的值。
fn main2(){ let s = String::from("hello"); change(&s) } fn change(someString: &String){ //這里是錯誤的:Cannot borrow immutable local variable `someString` as mutable someString.push_str(", world") }
借用規則:當擁有某值的不可變引用時,就不能再獲取一個可變引用。
可變引用
我們通過一個小調整就能修復上一個代碼中的錯誤:
fn main2(){ let mut s = String::from("hello"); change(&mut s) } fn change(someString: &mut String){ someString.push_str(", world") }
首先,必須將 s 改為 mut。然后必須創建一個可變引用 &mut s 和接受一個可變引用 someString: &mut String。
不過可變引用有一個很大的限制:在特定作用域中的特定數據只能有一個可變引用。以下代碼會失敗:
let mut s = String::from("hello"); let r1 = &mut s; let r2 = &mut s; println!("{}",r1); println!("{}",r2);
錯誤如下:
error[E0499]: cannot borrow `s` as mutable more than once at a time --> src/main.rs:125:14 | 124 | let r1 = &mut s; | ------ first mutable borrow occurs here 125 | let r2 = &mut s; | ^^^^^^ second mutable borrow occurs here 126 | println!("{}",r1); | -- first borrow later used here
這個限制允許可變性,不過是以一種受限制的方式允計。對於我們的Rust新手經常難以適應這一點,因為大部分語言中變量任何時候都是可變的。
這個限制的好處是Rust可以在編譯時就避免數據競爭。數據競爭(data race)類似於競態條件,它可由這三個行為造成:
- 兩個或更多指針同時訪問同一數據。
- 至少有一個指針被用來寫入數據。
- 沒有同步數據訪問的機制。
數據競爭會導致未定義行為,難以在運行時追蹤,並且難以論斷和修復;Rust避免了這種情況的發生,因為它甚至不會編譯存在數據競爭的代碼。
fn main() { let mut s = String::from("hello");{ let r1 = &mut s; } // r1 在這里離開了作用域,所以我們完全可以創建一個新的引用 let r2 = &mut s;
類似的規則也存在於同時修改用可變與不可變引用中。
let mut s = String::from("hello"); let r1 = &s; // 沒問題 let r2 = &s; // 沒問題 let r3 = &mut s; // 大問題 println!("{}, {}, and {}", r1, r2, r3);
錯誤如下:
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable --> src/main.rs:126:14 | 124 | let r1 = &s; // 沒問題 | -- immutable borrow occurs here 125 | let r2 = &s; // 沒問題 126 | let r3 = &mut s; // 大問題 | ^^^^^^ mutable borrow occurs here 127 | println!("{}, {}, and {}", r1, r2, r3); | -- immutable borrow later used here
注意一個引用的作用域從聲明的地方開始一直持續到最后一次使用為止。例如,因為最后一次使用不可變引用在聲明可變引用之前,所以如下代碼是可以編譯的:
let mut s = String::from("hello"); let r1 = &s; // 沒問題 let r2 = &s; // 沒問題 println!("{}, {}", r1, r2); //此位置之后,r1和r2不再使用 let r3 = &mut s; // 沒問題 println!("{}", r3);
不可變引用r1和r2的作用域在println!最后一次使用后結束,這也是創建可變引用r3的地方。它們的作用域沒有重疊,所以代碼是可以編譯的。
盡管這些錯誤有時使人沮喪,但請牢記這是Rust編譯器在提前指出一個潛在的bug(在編譯時而不是在運行時)並精准顯示問題所在。這樣你就不必去跟蹤為何數據並不是你想象中的那樣。
懸垂引用(Dangling References)
在具有指針的語言中,很容易通過釋放內存時保留指向它的指針而錯誤地生成一個懸垂指針(dangling pointer),所謂懸垂指針是其指向的內存可能已經被分配給其它持有者。相比之下,在Rust中編譯器確保引用永遠也不會變成懸垂狀態:當你擁有一些數據的引用,編譯器確保數據不會在其引用之前離開作用域。
讓我們嘗試創建一個懸垂引用,Rust會通過一個編譯時錯誤來避免:
fn main(){ let references_to_nothing = dangle(); } fn dangle() -> &String { let s = String::from("hello"); &s }
這里是錯誤:
error[E0106]: missing lifetime specifier --> src/main.rs:138:16 | 138 | fn dangle() -> &String { | ^ expected named lifetime parameter | = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from help: consider using the `'static` lifetime | 138 | fn dangle() -> &'static String { | ^^^^^^^^
錯誤信息引用了一個功能:生命周期(lifetimes)。如果我們不理會生命周期部分,錯誤信息中確實包含了為什么這段代碼有問題的關鍵信息:
this function's return type contains a borrowed value, but there is no value for it to be borrowed from
讓我們仔細看看我們的dangle代碼的每一步到底發生了什么:
fn dangle() -> &String {//返回一個字符串的引用 let s = String::from("hello");//s是一個新字符串 &s//返回字符串s的引用 }//這里s離開作用域並被丟棄。其內存被釋放。 //危險!
因為s是在dangle函數內創建的,當dangle的代碼執行完畢后,s將被釋放。不過我們嘗試返回它的引用。這意味着這個引用會指向一個無效的String,這可是不對的。Rust不會允許我們這么做。
這里解決方法是直接返回String:
fn dangle() -> String { let s = String::from("hello"); s }
這樣就沒有任何錯誤了。所有權被移動出去,所以沒有值被釋放。
引用的規則
讓我們概括一下之前對引用的討論:
- 在任意給定時間,要么只能有一個可變引用,要么只能有多個不可變引用。
- 引用必須總是有效的。