ch04-02-references-and-borrowing.md
commit d82136cbdc91c598ef9997493aa577b1a349565e
示例 4-5 中的元組代碼有這樣一個問題:我們必須將 String
返回給調(diào)用函數(shù),以便在調(diào)用 calculate_length
后仍能使用 String
,因為 String
被移動到了 calculate_length
內(nèi)。相反我們可以提供一個 String
值的引用(reference)。引用(reference)像一個指針,因為它是一個地址,我們可以由此訪問儲存于該地址的屬于其他變量的數(shù)據(jù)。 與指針不同,引用確保指向某個特定類型的有效值。
下面是如何定義并使用一個(新的)calculate_length
函數(shù),它以一個對象的引用作為參數(shù)而不是獲取值的所有權(quán):
文件名: src/main.rs
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
首先,注意變量聲明和函數(shù)返回值中的所有元組代碼都消失了。其次,注意我們傳遞 &s1
給 calculate_length
,同時在函數(shù)定義中,我們獲取 &String
而不是 String
。這些 & 符號就是 引用,它們允許你使用值但不獲取其所有權(quán)。圖 4-5 展示了一張示意圖。
圖 4-5:&String s
指向 String s1
示意圖
注意:與使用 ??
&
?? 引用相反的操作是 解引用(dereferencing),它使用解引用運算符,?*
?。我們將會在第八章遇到一些解引用運算符,并在第十五章詳細討論解引用。
仔細看看這個函數(shù)調(diào)用:
let s1 = String::from("hello");
let len = calculate_length(&s1);
&s1
語法讓我們創(chuàng)建一個 指向 值 s1
的引用,但是并不擁有它。因為并不擁有這個值,所以當引用停止使用時,它所指向的值也不會被丟棄。
同理,函數(shù)簽名使用 &
來表明參數(shù) s
的類型是一個引用。讓我們增加一些解釋性的注釋:
fn calculate_length(s: &String) -> usize { // s是String的引用
s.len()
} // 這里,s 離開了作用域。但因為它并不擁有引用值的所有權(quán),
// 所以什么也不會發(fā)生
變量 s
有效的作用域與函數(shù)參數(shù)的作用域一樣,不過當 s
停止使用時并不丟棄引用指向的數(shù)據(jù),因為 s
并沒有所有權(quán)。當函數(shù)使用引用而不是實際值作為參數(shù),無需返回值來交還所有權(quán),因為就不曾擁有所有權(quán)。
我們將創(chuàng)建一個引用的行為稱為 借用(borrowing)。正如現(xiàn)實生活中,如果一個人擁有某樣東西,你可以從他那里借來。當你使用完畢,必須還回去。我們并不擁有它。
如果我們嘗試修改借用的變量呢?嘗試示例 4-6 中的代碼。劇透:這行不通!
文件名: src/main.rs
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
示例 4-6:嘗試修改借用的值
這里是錯誤:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
--> src/main.rs:8:5
|
7 | fn change(some_string: &String) {
| ------- help: consider changing this to be a mutable reference: `&mut String`
8 | some_string.push_str(", world");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership` due to previous error
正如變量默認是不可變的,引用也一樣。(默認)不允許修改引用的值。
我們通過一個小調(diào)整就能修復示例 4-6 代碼中的錯誤,允許我們修改一個借用的值,這就是 可變引用(mutable reference):
文件名: src/main.rs
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
首先,我們必須將 s
改為 mut
。然后在調(diào)用 change
函數(shù)的地方創(chuàng)建一個可變引用 &mut s
,并更新函數(shù)簽名以接受一個可變引用 some_string: &mut String
。這就非常清楚地表明,change
函數(shù)將改變它所借用的值。
可變引用有一個很大的限制:如果你有一個對該變量的可變引用,你就不能再創(chuàng)建對該變量的引用。這些嘗試創(chuàng)建兩個 s
的可變引用的代碼會失?。?br>
文件名: src/main.rs
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
錯誤如下:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> src/main.rs:5:14
|
4 | let r1 = &mut s;
| ------ first mutable borrow occurs here
5 | let r2 = &mut s;
| ^^^^^^ second mutable borrow occurs here
6 |
7 | println!("{}, {}", r1, r2);
| -- first borrow later used here
For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership` due to previous error
這個報錯說這段代碼是無效的,因為我們不能在同一時間多次將 s
作為可變變量借用。第一個可變的借入在 r1
中,并且必須持續(xù)到在 println!
中使用它,但是在那個可變引用的創(chuàng)建和它的使用之間,我們又嘗試在 r2
中創(chuàng)建另一個可變引用,該引用借用與 r1
相同的數(shù)據(jù)。
這一限制以一種非常小心謹慎的方式允許可變性,防止同一時間對同一數(shù)據(jù)存在多個可變引用。新 Rustacean 們經(jīng)常難以適應這一點,因為大部分語言中變量任何時候都是可變的。這個限制的好處是 Rust 可以在編譯時就避免數(shù)據(jù)競爭。數(shù)據(jù)競爭(data race)類似于競態(tài)條件,它可由這三個行為造成:
數(shù)據(jù)競爭會導致未定義行為,難以在運行時追蹤,并且難以診斷和修復;Rust 避免了這種情況的發(fā)生,因為它甚至不會編譯存在數(shù)據(jù)競爭的代碼!
一如既往,可以使用大括號來創(chuàng)建一個新的作用域,以允許擁有多個可變引用,只是不能 同時 擁有:
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 在這里離開了作用域,所以我們完全可以創(chuàng)建一個新的引用
let r2 = &mut s;
Rust 在同時使用可變與不可變引用時也采用的類似的規(guī)則。這些代碼會導致一個錯誤:
let mut s = String::from("hello");
let r1 = &s; // 沒問題
let r2 = &s; // 沒問題
let r3 = &mut s; // 大問題
println!("{}, {}, and {}", r1, r2, r3);
錯誤如下:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:6:14
|
4 | let r1 = &s; // no problem
| -- immutable borrow occurs here
5 | let r2 = &s; // no problem
6 | let r3 = &mut s; // BIG PROBLEM
| ^^^^^^ mutable borrow occurs here
7 |
8 | println!("{}, {}, and {}", r1, r2, r3);
| -- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` due to previous error
哇哦!我們 也 不能在擁有不可變引用的同時擁有可變引用。
不可變引用的用戶可不希望在他們的眼皮底下值就被意外的改變了!然而,多個不可變引用是可以的,因為沒有哪個只能讀取數(shù)據(jù)的人有能力影響其他人讀取到的數(shù)據(jù)。
注意一個引用的作用域從聲明的地方開始一直持續(xù)到最后一次使用為止。例如,因為最后一次使用不可變引用(println!
),發(fā)生在聲明可變引用之前,所以如下代碼是可以編譯的:
let mut s = String::from("hello");
let r1 = &s; // 沒問題
let r2 = &s; // 沒問題
println!("{} and {}", r1, r2);
// 此位置之后 r1 和 r2 不再使用
let r3 = &mut s; // 沒問題
println!("{}", r3);
不可變引用 r1
和 r2
的作用域在 println!
最后一次使用之后結(jié)束,這也是創(chuàng)建可變引用 r3
的地方。它們的作用域沒有重疊,所以代碼是可以編譯的。編譯器在作用域結(jié)束之前判斷不再使用的引用的能力被稱為 非詞法作用域生命周期(Non-Lexical Lifetimes,簡稱 NLL)。你可以在 The Edition Guide 中閱讀更多關(guān)于它的信息。
盡管這些錯誤有時使人沮喪,但請牢記這是 Rust 編譯器在提前指出一個潛在的 bug(在編譯時而不是在運行時)并精準顯示問題所在。這樣你就不必去跟蹤為何數(shù)據(jù)并不是你想象中的那樣。
在具有指針的語言中,很容易通過釋放內(nèi)存時保留指向它的指針而錯誤地生成一個 懸垂指針(dangling pointer),所謂懸垂指針是其指向的內(nèi)存可能已經(jīng)被分配給其它持有者。相比之下,在 Rust 中編譯器確保引用永遠也不會變成懸垂狀態(tài):當你擁有一些數(shù)據(jù)的引用,編譯器確保數(shù)據(jù)不會在其引用之前離開作用域。
讓我們嘗試創(chuàng)建一個懸垂引用,Rust 會通過一個編譯時錯誤來避免:
文件名: src/main.rs
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
這里是錯誤:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
--> src/main.rs:5:16
|
5 | 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
|
5 | fn dangle() -> &'static String {
| ~~~~~~~~
For more information about this error, try `rustc --explain E0106`.
error: could not compile `ownership` due to previous error
錯誤信息引用了一個我們還未介紹的功能:生命周期(lifetimes)。第十章會詳細介紹生命周期。不過,如果你不理會生命周期部分,錯誤信息中確實包含了為什么這段代碼有問題的關(guān)鍵信息:
this function's return type contains a borrowed value, but there is no value
for it to be borrowed from
讓我們仔細看看我們的 dangle
代碼的每一步到底發(fā)生了什么:
文件名: src/main.rs
fn dangle() -> &String { // dangle 返回一個字符串的引用
let s = String::from("hello"); // s 是一個新字符串
&s // 返回字符串 s 的引用
} // 這里 s 離開作用域并被丟棄。其內(nèi)存被釋放。
// 危險!
因為 s
是在 dangle
函數(shù)內(nèi)創(chuàng)建的,當 dangle
的代碼執(zhí)行完畢后,s
將被釋放。不過我們嘗試返回它的引用。這意味著這個引用會指向一個無效的 String
,這可不對!Rust 不會允許我們這么做。
這里的解決方法是直接返回 String
:
fn no_dangle() -> String {
let s = String::from("hello");
s
}
這樣就沒有任何錯誤了。所有權(quán)被移動出去,所以沒有值被釋放。
讓我們概括一下之前對引用的討論:
接下來,我們來看看另一種不同類型的引用:slice。
更多建議: