iT邦幫忙

2024 iThome 鐵人賽

DAY 13
0
Software Development

螃蟹幼幼班:Rust 入門指南系列 第 13

Day13 - 所有權(二):借用

  • 分享至 

  • xImage
  •  

回顧:所有權轉移

昨天我們介紹了所有權的概念,以及某些型別在賦值給另一個變數時會發生所有權轉移(move)的情況。讓我們再看看還有哪些情況會發生所有權轉移。

函數參數的所有權轉移

把變數傳進函數也會讓所有權轉移,所以下面的程式會編譯錯誤。

fn print_with_smile(mut t: String) { // t 是擁有者
    t.push_str("😊");
    println!("{t}");
} // 離開作用域,t 對應記憶體釋放

fn main() {
    let s = String::from("hello");
    print_with_smile(s); // 所有權給出去了

    println!("s: {s}"); // s 沒有所有權,編譯失敗
}
$ cargo run
error[E0382]: borrow of moved value: `s`
  --> src/main.rs:15:18
   |
12 |     let s = String::from("hello");
   |         - move occurs because `s` has type `String`, which does not implement the `Copy` trait
13 |     print_with_smile(s);
   |                      - value moved here
14 |
15 |     println!("s: {s}");
   |                  ^^^ value borrowed here after move
   |
note: consider changing this parameter type in function `print_with_smile` to borrow instead if owning the value isn't necessary
  --> src/main.rs:1:28
   |
1  | fn print_with_smile(mut t: String) {
   |    ----------------        ^^^^^^ this parameter takes ownership of the value
   |    |
   |    in this function
   = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
   |
13 |     print_with_smile(s.clone());
   |                       ++++++++

For more information about this error, try `rustc --explain E0382`.

函數返回值的所有權轉移

在函數裡面回傳值,那所有權就會轉移到接收該值的變數。
我們把程式稍微修改一下,把所有權轉移回去,這樣就可以正確編譯了。

fn add_smile(mut t: String) -> String {
    t.push_str("😊");
    println!("{t}");
    t // 所有權轉移給外面接收的變數
} // 因為 t 所有權已經被轉移了,離開作用域也不會釋放記憶體

fn main() {
    let mut s = String::from("hello");
    s = add_smile(s); // s 有回傳值的所有權

    println!("s: {s}");
}

這樣可以把原本所有權的規則定義的更明確:

變數的所有權在賦值給其他變數就會移動。當擁有資料的變數離開作用域,並且該變數有數值的所有權,那數值的記憶體就會被釋放。

借用

接著回到今天的主題:借用。
前面提到的情況其實就是需要借用的情況:我們想保留變數的所有權(因為後續還要用),同時讓函數使用這個變數時,我們前面是把所有權轉移回去來處理,可是如果這個變數不只一個函數要使用怎麼辦?
例如像這樣:

fn add_smile(mut t: String) -> String {
    t.push_str("😊");
    println!("{t}");
    t
}

fn do_nothing(t: String) -> String {
    t
}

fn main() {
    let mut s = String::from("hello");
    s = do_nothing(s);
    s = add_smile(s);
    println!("s: {s}");
}

為了讓 s 最後還擁有所有權,每次都要把傳進函數的參數再回傳回來,這樣變成原本不用回傳值的函數也要把數值回傳了,如果這個函數有複數的參數,又同時需要回傳這個函數產生的資料,可能就要另外訂一個結構同時把原始參數和函數結果回傳,但這樣很麻煩而且會變得很混亂。

fn process_strings(t1: String, t2: String) -> (String, String, String) {
    let t3 = format!("{}{}", t1, t2); // 將 t1 和 t2 合併成 t3

    (t1, t2, t3)
}

不可變參考

而解決的這個情境的方式就是:借用(borrow),在 Rust 中,借用指的是允許其他部分的代碼暫時使用一個值而不獲得其所有權。
以書舉例,Rust 借用的概念就是,我只是暫時把書借給你,書還是我的,你看完之後要還我。
而這個借用概念在 Rust 裡是通過參考來實現的。

實際上的程式碼會長這樣:

fn count_len(t: &String) {
    println!("The length of the string is: {}", t.len());
} // t 沒有對應數值的所有權,離開作用域也不會去刪除

fn main() {
    let s = String::from("hello");
    count_len(&s); // 只是借用,所有權還是在 s 身上
    println!("s: {s}");
}

在型別的前面加上 & 代表參考,也就是允許使用這個值,但不擁有它的所有權,所以即使在 count_len 的作用域結束也不會把這個值的記憶體釋放,要等到外面 main 的作用域結束後才會被釋放掉,這樣就可以避免前述每次呼叫函數都需要把這個函數後還需要使用的參數回傳。

有加 & 和沒加的型別就算原本是同一種,在 Rust 也被認定是不同的型別,只要傳錯型別編譯的時候就會報錯,型別定義好就不用擔心寫錯。

fn count_len(t: &String) {
    println!("The length of the string is: {}", t.len());
}

fn main() {
    let s = String::from("hello");
    count_len(s);
    println!("s: {s}");
}
$ cargo run
error[E0308]: mismatched types
  --> src/main.rs:27:15
   |
27 |     count_len(s);
   |     --------- ^ expected `&String`, found `String`
   |     |
   |     arguments to this function are incorrect
   |
note: function defined here
  --> src/main.rs:21:4
   |
21 | fn count_len(t: &String) {
   |    ^^^^^^^^^ ----------
help: consider borrowing here
   |
27 |     count_len(&s);
   |               +

For more information about this error, try `rustc --explain E0308`.

可變參考

另一種是需要做筆記的情況,借用的數值能被改動嗎?答案是可以,不過能不能在上面做筆記是我一開始借你的時候就講好的。
如果直接在內部修改的話會直接報錯,擁有者沒說可以修改當然不能自己亂改。

fn add_smile(t: &String) {
    t.push_str("😊");
}

fn main() {
    let s = String::from("hello");
    add_smile(&s);
    println!("s: {s}");
}
$ cargo run
error[E0596]: cannot borrow `*t` as mutable, as it is behind a `&` reference
  --> src/main.rs:19:5
   |
19 |     t.push_str("😊");
   |     ^ `t` is a `&` reference, so the data it refers to cannot be borrowed as mutable
   |
help: consider changing this to be a mutable reference
   |
18 | fn add_smile(t: &mut String) {
   |                  +++

For more information about this error, try `rustc --explain E0596`.

所以我們要修改幾個地方:

fn add_smile(t: &mut String) { // 函數簽名要明確告訴外部要傳入的是 reference 而且數值要可以被修改
    t.push_str("😊");
}

fn main() {
    let mut s = String::from("hello"); // 變數宣告一開始就要是可變的
    add_smile(&mut s); // 傳入還要再確認是數值可變的 reference type
    println!("s: {s}");
}
  • 宣告 s 要用mut
  • 函數簽名也要定義型別是&mut
  • 呼叫函數的時候傳進去也要符合定義的型別&mut

這三個地方有任一地方不符合就會報錯,會判定成無效的操作。

參考規則

再來討論的是借給多少人有限制嗎?
根據情況不同,數量也會不同。
簡單來說規則是這樣:

  • 可變的引用同時間只可以借一個人
fn main() {
    let mut s = String::from("hello");
    let r1 = &mut s;
    let r2 = &mut s;
    add_smile(r1);
    add_smile(r2);
    println!("s: {s}");
}
$ cargo run
error[E0499]: cannot borrow `s` as mutable more than once at a time
  --> src/main.rs:25:14
   |
24 |     let r1 = &mut s;
   |              ------ first mutable borrow occurs here
25 |     let r2 = &mut s;
   |              ^^^^^^ second mutable borrow occurs here
26 |     add_smile(r1);
   |               -- first borrow later used here

For more information about this error, try `rustc --explain E0499`.
  • 針對同一個數值的所有權不能同時有可變和不可變的引用
fn main() {
    let mut s = String::from("hello");
    let r1 = &mut s;
    let r2 = &s;
    add_smile(r1);
    count_len(r2);
    println!("s: {s}");
}
error[E0502]: cannot borrow `s` as immutable because it is also borrowed as mutable
  --> src/main.rs:25:14
   |
24 |     let r1 = &mut s;
   |              ------ mutable borrow occurs here
25 |     let r2 = &s;
   |              ^^ immutable borrow occurs here
26 |     add_smile(r1);
   |               -- mutable borrow later used here

For more information about this error, try `rustc --explain E0502`.

不過只要改一下順序就可以正常編譯了,這是因為所謂的同時指的是引用的作用域重疊的部分,引用的作用域和變數的作用域不同,引用的作用域指的是他被宣告到他最後一次被使用。
在下面的範例中,因為把 add_smile(r1) 往前移了,r1 和 r2 的作用域就沒有重疊,也就不算同時有可變和不可變的引用了。

fn main() {
    let mut s = String::from("hello");
    let r1 = &mut s; // r1 引用作用域開始
    add_smile(r1);   // r1 引用作用域結束
    let r2 = &s;     // r2 引用作用域開始
    count_len(r2);   // r2 引用作用域結束
    println!("s: {s}");
}
  • 不可變的引用同時間可以借給多人。既然保證沒有人會去變動資料,那多少人是不是同一個時間讀,又或是讀的順序就都不會影響結果了。
fn count_len(t: &String) {
    println!("The length of the string is: {}", t.len());
}

fn print_with_smile(t: &String) {
    println!("{t}😊");
}

fn main() {
    let s = String::from("hello");
    let r1 = &s;
    let r2 = &s;
    print_with_smile(r1); // hello😊
    count_len(r2); // The length of the string is: 5
    println!("s: {s}"); // s: hello
}

這些規則是 Rust 為了在編譯時就避免資料競爭(data races)的情況。
所謂的資料競爭是由三種行為產生的:

  • 同時有兩個以上的指標存取同個資料。
  • 至少有一個指標在寫入資料。
  • 沒有針對資料的同步存取機制。

這種錯誤會造成會造成未定義行為,很難被發現也很難重現,測試也變得很困難。
大部分面對資料競爭的解決方案是用鎖,或是用原子操作等等,Rust 還會直接在編譯階段就拒絕可能會造成資料競爭的程式碼,最小化風險,不過在多執行緒的情況下仍然無法完全避免風險,還是需要其他機制。

迷途指標

上面介紹借用有會把參考傳來傳去,Rust 透過編譯器檢查會保證這些參考一定不是迷途參考:它能確保參考對應的資料一定還是有效的。
迷途指標(dangling pointer)是指當資源已經被釋放但指標卻還留著,這樣的指標指向的地方很可能被別人拿去用,這時候再來用的話就會有問題。

如果我們嘗試寫出一段會有迷途指標的程式碼,Rust 編譯會檢查到並且報錯。

fn main() {
    let reference_to_nothing = dangle();
    println!("{reference_to_nothing}");
}

fn dangle() -> &String {
    let s = String::from("hello"); // s 有所有權

    &s // 外部拿的指標對應的記憶體會被釋放
} // s 離開作用域, 釋放記憶體
$ cargo run
error[E0106]: missing lifetime specifier
  --> src/main.rs:65:16
   |
65 | 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, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
   |
65 | fn dangle() -> &'static String {
   |                 +++++++
help: instead, you are more likely to want to return an owned value
   |
65 - fn dangle() -> &String {
65 + fn dangle() -> String {
   |

error[E0515]: cannot return reference to local variable `s`
  --> src/main.rs:68:5
   |
68 |     &s
   |     ^^ returns a reference to data owned by the current function

Some errors have detailed explanations: E0106, E0515.
For more information about an error, try `rustc --explain E0106`.
error: could not compile `ownership` (bin "ownership") due to 2 previous errors

有兩個錯誤,第一個簡單來說是另外一個檢查機制,Rust 透過分析程式碼中參考的生命週期(lifetime),確保參考不會在無效時被使用,第二個主要描述不能把會變成空指標的參考回傳,修改的方式就是把 s 回傳,那所有權就轉移出去,s 記憶體也就不會釋放,就避免拿到迷途指標的狀況了。

fn main() {
    let reference_to_nothing = dangle();
    println!("{reference_to_nothing}"); // hello
}

fn dangle() -> String {
    let s = String::from("hello");

    s
}

結語

總結參考規則:

  1. 可變參考只能有一個,但不可變參考可以有多個。
  2. 參考永遠有效,不會出現空指標。

今天介紹了 Rust 中的借用概念,並通過具體範例說明如何透過參考來實現借用。
透過參考的使用限制,Rust 在提供變數彈性使用的同時,也確保了數據的安全性。這些檢查與 Rust 的設計理念一致,所有的檢查都在編譯階段完成,避免了運行時的崩潰和不可預期的行為,充分體現了 Rust 對編譯期安全的重視。

此外,借用允許程式碼在不轉移所有權的情況下暫時使用某個值,從而有效避免了頻繁複製數據和所有權轉移所帶來的性能損耗。


上一篇
Day12 - 所有權(一):基礎認識
下一篇
Day14 - 所有權(三):切片
系列文
螃蟹幼幼班:Rust 入門指南30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言