ch04-02-references-and-borrowing.md
commit d82136cbdc91c598ef9997493aa577b1a349565e
示例 4-5 中的元組代碼有這樣一個(gè)問(wèn)題:我們必須將 String
返回給調(diào)用函數(shù),以便在調(diào)用 calculate_length
后仍能使用 String
,因?yàn)?nbsp;String
被移動(dòng)到了 calculate_length
內(nèi)。相反我們可以提供一個(gè) String
值的引用(reference)。引用(reference)像一個(gè)指針,因?yàn)樗且粋€(gè)地址,我們可以由此訪問(wèn)儲(chǔ)存于該地址的屬于其他變量的數(shù)據(jù)。 與指針不同,引用確保指向某個(gè)特定類型的有效值。
下面是如何定義并使用一個(gè)(新的)calculate_length
函數(shù),它以一個(gè)對(duì)象的引用作為參數(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í)在函數(shù)定義中,我們獲取 &String
而不是 String
。這些 & 符號(hào)就是 引用,它們?cè)试S你使用值但不獲取其所有權(quán)。圖 4-5 展示了一張示意圖。
圖 4-5:&String s
指向 String s1
示意圖
注意:與使用 ??
&
?? 引用相反的操作是 解引用(dereferencing),它使用解引用運(yùn)算符,?*
?。我們將會(huì)在第八章遇到一些解引用運(yùn)算符,并在第十五章詳細(xì)討論解引用。
仔細(xì)看看這個(gè)函數(shù)調(diào)用:
let s1 = String::from("hello");
let len = calculate_length(&s1);
&s1
語(yǔ)法讓我們創(chuàng)建一個(gè) 指向 值 s1
的引用,但是并不擁有它。因?yàn)椴⒉粨碛羞@個(gè)值,所以當(dāng)引用停止使用時(shí),它所指向的值也不會(huì)被丟棄。
同理,函數(shù)簽名使用 &
來(lái)表明參數(shù) s
的類型是一個(gè)引用。讓我們?cè)黾右恍┙忉屝缘淖⑨專?br>
fn calculate_length(s: &String) -> usize { // s是String的引用
s.len()
} // 這里,s 離開(kāi)了作用域。但因?yàn)樗⒉粨碛幸弥档乃袡?quán),
// 所以什么也不會(huì)發(fā)生
變量 s
有效的作用域與函數(shù)參數(shù)的作用域一樣,不過(guò)當(dāng) s
停止使用時(shí)并不丟棄引用指向的數(shù)據(jù),因?yàn)?nbsp;s
并沒(méi)有所有權(quán)。當(dāng)函數(shù)使用引用而不是實(shí)際值作為參數(shù),無(wú)需返回值來(lái)交還所有權(quán),因?yàn)榫筒辉鴵碛兴袡?quán)。
我們將創(chuàng)建一個(gè)引用的行為稱為 借用(borrowing)。正如現(xiàn)實(shí)生活中,如果一個(gè)人擁有某樣?xùn)|西,你可以從他那里借來(lái)。當(dāng)你使用完畢,必須還回去。我們并不擁有它。
如果我們嘗試修改借用的變量呢?嘗試示例 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:嘗試修改借用的值
這里是錯(cuò)誤:
$ 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
正如變量默認(rèn)是不可變的,引用也一樣。(默認(rèn))不允許修改引用的值。
我們通過(guò)一個(gè)小調(diào)整就能修復(fù)示例 4-6 代碼中的錯(cuò)誤,允許我們修改一個(gè)借用的值,這就是 可變引用(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)建一個(gè)可變引用 &mut s
,并更新函數(shù)簽名以接受一個(gè)可變引用 some_string: &mut String
。這就非常清楚地表明,change
函數(shù)將改變它所借用的值。
可變引用有一個(gè)很大的限制:如果你有一個(gè)對(duì)該變量的可變引用,你就不能再創(chuàng)建對(duì)該變量的引用。這些嘗試創(chuàng)建兩個(gè) s
的可變引用的代碼會(huì)失?。?br>
文件名: src/main.rs
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
錯(cuò)誤如下:
$ 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
這個(gè)報(bào)錯(cuò)說(shuō)這段代碼是無(wú)效的,因?yàn)槲覀儾荒茉谕粫r(shí)間多次將 s
作為可變變量借用。第一個(gè)可變的借入在 r1
中,并且必須持續(xù)到在 println!
中使用它,但是在那個(gè)可變引用的創(chuàng)建和它的使用之間,我們又嘗試在 r2
中創(chuàng)建另一個(gè)可變引用,該引用借用與 r1
相同的數(shù)據(jù)。
這一限制以一種非常小心謹(jǐn)慎的方式允許可變性,防止同一時(shí)間對(duì)同一數(shù)據(jù)存在多個(gè)可變引用。新 Rustacean 們經(jīng)常難以適應(yīng)這一點(diǎn),因?yàn)榇蟛糠终Z(yǔ)言中變量任何時(shí)候都是可變的。這個(gè)限制的好處是 Rust 可以在編譯時(shí)就避免數(shù)據(jù)競(jìng)爭(zhēng)。數(shù)據(jù)競(jìng)爭(zhēng)(data race)類似于競(jìng)態(tài)條件,它可由這三個(gè)行為造成:
數(shù)據(jù)競(jìng)爭(zhēng)會(huì)導(dǎo)致未定義行為,難以在運(yùn)行時(shí)追蹤,并且難以診斷和修復(fù);Rust 避免了這種情況的發(fā)生,因?yàn)樗踔敛粫?huì)編譯存在數(shù)據(jù)競(jìng)爭(zhēng)的代碼!
一如既往,可以使用大括號(hào)來(lái)創(chuàng)建一個(gè)新的作用域,以允許擁有多個(gè)可變引用,只是不能 同時(shí) 擁有:
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 在這里離開(kāi)了作用域,所以我們完全可以創(chuàng)建一個(gè)新的引用
let r2 = &mut s;
Rust 在同時(shí)使用可變與不可變引用時(shí)也采用的類似的規(guī)則。這些代碼會(huì)導(dǎo)致一個(gè)錯(cuò)誤:
let mut s = String::from("hello");
let r1 = &s; // 沒(méi)問(wèn)題
let r2 = &s; // 沒(méi)問(wèn)題
let r3 = &mut s; // 大問(wèn)題
println!("{}, {}, and {}", r1, r2, r3);
錯(cuò)誤如下:
$ 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í)擁有可變引用。
不可變引用的用戶可不希望在他們的眼皮底下值就被意外的改變了!然而,多個(gè)不可變引用是可以的,因?yàn)闆](méi)有哪個(gè)只能讀取數(shù)據(jù)的人有能力影響其他人讀取到的數(shù)據(jù)。
注意一個(gè)引用的作用域從聲明的地方開(kāi)始一直持續(xù)到最后一次使用為止。例如,因?yàn)樽詈笠淮问褂貌豢勺円茫?code>println!),發(fā)生在聲明可變引用之前,所以如下代碼是可以編譯的:
let mut s = String::from("hello");
let r1 = &s; // 沒(méi)問(wèn)題
let r2 = &s; // 沒(méi)問(wèn)題
println!("{} and {}", r1, r2);
// 此位置之后 r1 和 r2 不再使用
let r3 = &mut s; // 沒(méi)問(wèn)題
println!("{}", r3);
不可變引用 r1
和 r2
的作用域在 println!
最后一次使用之后結(jié)束,這也是創(chuàng)建可變引用 r3
的地方。它們的作用域沒(méi)有重疊,所以代碼是可以編譯的。編譯器在作用域結(jié)束之前判斷不再使用的引用的能力被稱為 非詞法作用域生命周期(Non-Lexical Lifetimes,簡(jiǎn)稱 NLL)。你可以在 The Edition Guide 中閱讀更多關(guān)于它的信息。
盡管這些錯(cuò)誤有時(shí)使人沮喪,但請(qǐng)牢記這是 Rust 編譯器在提前指出一個(gè)潛在的 bug(在編譯時(shí)而不是在運(yùn)行時(shí))并精準(zhǔn)顯示問(wèn)題所在。這樣你就不必去跟蹤為何數(shù)據(jù)并不是你想象中的那樣。
在具有指針的語(yǔ)言中,很容易通過(guò)釋放內(nèi)存時(shí)保留指向它的指針而錯(cuò)誤地生成一個(gè) 懸垂指針(dangling pointer),所謂懸垂指針是其指向的內(nèi)存可能已經(jīng)被分配給其它持有者。相比之下,在 Rust 中編譯器確保引用永遠(yuǎn)也不會(huì)變成懸垂?fàn)顟B(tài):當(dāng)你擁有一些數(shù)據(jù)的引用,編譯器確保數(shù)據(jù)不會(huì)在其引用之前離開(kāi)作用域。
讓我們嘗試創(chuàng)建一個(gè)懸垂引用,Rust 會(huì)通過(guò)一個(gè)編譯時(shí)錯(cuò)誤來(lái)避免:
文件名: src/main.rs
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
這里是錯(cuò)誤:
$ 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
錯(cuò)誤信息引用了一個(gè)我們還未介紹的功能:生命周期(lifetimes)。第十章會(huì)詳細(xì)介紹生命周期。不過(guò),如果你不理會(huì)生命周期部分,錯(cuò)誤信息中確實(shí)包含了為什么這段代碼有問(wèn)題的關(guān)鍵信息:
this function's return type contains a borrowed value, but there is no value
for it to be borrowed from
讓我們仔細(xì)看看我們的 dangle
代碼的每一步到底發(fā)生了什么:
文件名: src/main.rs
fn dangle() -> &String { // dangle 返回一個(gè)字符串的引用
let s = String::from("hello"); // s 是一個(gè)新字符串
&s // 返回字符串 s 的引用
} // 這里 s 離開(kāi)作用域并被丟棄。其內(nèi)存被釋放。
// 危險(xiǎn)!
因?yàn)?nbsp;s
是在 dangle
函數(shù)內(nèi)創(chuàng)建的,當(dāng) dangle
的代碼執(zhí)行完畢后,s
將被釋放。不過(guò)我們嘗試返回它的引用。這意味著這個(gè)引用會(huì)指向一個(gè)無(wú)效的 String
,這可不對(duì)!Rust 不會(huì)允許我們這么做。
這里的解決方法是直接返回 String
:
fn no_dangle() -> String {
let s = String::from("hello");
s
}
這樣就沒(méi)有任何錯(cuò)誤了。所有權(quán)被移動(dòng)出去,所以沒(méi)有值被釋放。
讓我們概括一下之前對(duì)引用的討論:
接下來(lái),我們來(lái)看看另一種不同類型的引用:slice。
更多建議: