Rust 引用與借用

2023-03-22 15:09 更新
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 展示了一張示意圖。

trpl04-05

圖 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è)行為造成:

  • 兩個(gè)或更多指針同時(shí)訪問(wèn)同一數(shù)據(jù)。
  • 至少有一個(gè)指針被用來(lái)寫(xiě)入數(shù)據(jù)。
  • 沒(méi)有同步數(shù)據(jù)訪問(wèn)的機(jī)制。

數(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ù)并不是你想象中的那樣。

懸垂引用(Dangling References)

在具有指針的語(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)有值被釋放。

引用的規(guī)則

讓我們概括一下之前對(duì)引用的討論:

  • 在任意給定時(shí)間,要么 只能有一個(gè)可變引用,要么 只能有多個(gè)不可變引用。
  • 引用必須總是有效的。

接下來(lái),我們來(lái)看看另一種不同類型的引用:slice。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)