iT邦幫忙

2022 iThome 鐵人賽

DAY 6
0

環境

OS: Windows 10
Editor: Visual Studio Code
Rust version: 1.63.0

簡述

不同語言有著不同的記憶體管理方式,就我所碰過的語言,大多分為兩種:

  1. 開發者自己配置與釋放記憶體,像是CC++
  2. 交由垃圾回收(Garbage Collection, GC)機制處理,像是C#Golang

Rust則是由所有權(Ownership) 機制來管理記憶體。

文件對Rust的所有權機制做了簡單的概括:

  1. Rust 中每個數值都會有一個變數作為它的擁有者(owner)
  2. 同時間只能有一個擁有者。
  3. 當擁有者離開作用域時,數值就會被丟棄。

關於第三點

當擁有者離開作用域時,數值就會被丟棄。

指的作用域是Scope {}的意思,整段是除了描述所有權外,還包含了生命週期(Life Time) 的概念。例如:

fn main() {
    let s = "hello"; // s 誕生
    println!("{}", s);
} // s 死亡

或是一個更明顯的例子:

fn main() {
    let x = {
        let y = 6; // y誕生
        y + 1
    }; // y死亡
    println!("x is {}", x);
}

但這應該不是絕對的,例如許多語言中有static之類的關鍵字,可以延長或是在運行期間全程存在的作法,留個問題在這邊,之後關注到生命週期的章節再來分析這件事。

Move

首先,試試看這個範例

let x = 5;
let y = x;
println!("x is {}, y is {}", x, y);
// output:
//    x is 5, y is 5

如同許多程式語言一樣,基本型別是會進行複製(Copy),但下面就不太一樣了:

let s1 = String::from("hello");
let s2 = s1;
println!("s1 is {}, s2 is {}", s1, s2);

編譯過後,compiler會給錯誤:

Compiling basic v0.1.0 (D:\projects\rust_learning\basic)
error[E0382]: borrow of moved value: `s1`
 --> src\main.rs:4:36
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |     println!("s1 is {}, s2 is {}", s1, s2);
  |                                    ^^ value borrowed here after move
  |
  = note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info)

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

如果有其他語言經驗的話,會知道字串(String),通常會想要它可以隨意增減長度與內容,所以不太可能直接複製整塊記憶體,畢竟在編譯時也不可能知道完整大小。

而在有GC機制語言的中,s1s2會同時指向一塊記憶體,直到不再去使用這個字串。

而在這裡,可以很清楚知道Rust中所有權的意思了。原本分配再給s1的內容,所有權都歸於s2了,當要再使用s1的時候就會是無效的了。

當然,也可以依照compiler的指示,使用clone,複製一整份內容給s2

let s1 = String::from("hello");
let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2);

References

接下來,看函式對所有權機制的處理,以下範例是會編譯失敗的:

fn simple_print(s: String) { // s的所有權交給了這裡的函式
    println!("{}", s);
} // s離開scope,釋放掉

fn print_len(s: String) {
    println!("string length is {}", s.len());
}

fn main() {
    let s = String::from("hello");
    simple_print(s);
    print_len(s); // 錯誤,s已經釋放掉了
}

但這裡就有一個問題,假如之後許多功能,都需要用到某個變數,但在離開作用域(scope)之後就會釋放掉了,這導致如果遵循所有權的規定,我們必須這樣寫:

fn simple_print(s: String) -> String { // s的所有權交給了這裡的函式
    println!("{}", s);
    s // 用完了,還回去
}

fn print_len(s: String) {
    println!("string length is {}", s.len());
}

fn main() {
    let s = String::from("hello");
    let s = simple_print(s);
    print_len(s);
}

這樣就會變得非常不直覺且奇怪。

於是在這裡開始介紹對參數引用(references),

引用

在Rust中,&這個表示引用,我們修改一下上面的範例:

fn simple_print(s: &String) {
    // s的所有權交給了這裡的函式
    println!("{}", s);
}

fn print_len(s: &String) {
    println!("string length is {}", s.len());
}

fn main() {
    let s = String::from("hello");
    simple_print(&s);
    print_len(&s);
}

我們在函式參數列中,對要引用的參數的型別前面加上&,並且呼叫函式的時候,對要引用的參數加上&表示:「這個變數借給你用(Borrrowing)」。

可變引用

接下去看下一個範例,假設我要借某個字串,然後修改其中的內容:

fn simple_print(s: &String) {
    println!("{}", s);
}

fn print_len(s: &String) {
    println!("string length is {}", s.len());
}

fn add_world(s: &String) {
    s.push_str(", world");
}

fn main() {
    let s = String::from("hello");
    add_world(&s);
    simple_print(&s);
    print_len(&s);
}

編譯之後,給出錯誤

 Compiling basic v0.1.0 (D:\projects\rust_learning\basic)
error[E0596]: cannot borrow `*s` as mutable, as it is behind a `&` reference
  --> src\main.rs:11:5
   |
10 | fn add_world(s: &String) {
   |                 ------- help: consider changing this to be a mutable reference: `&mut String`
11 |     s.push_str(", world");
   |     ^^^^^^^^^^^^^^^^^^^^^ `s` is a `&` reference, so the data it refers to cannot be borrowed as mutable

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

編譯器建議我們加上mut,而這個就是可變引用

fn simple_print(s: &String) {
    println!("{}", s);
}

fn print_len(s: &String) {
    println!("string length is {}", s.len());
}

fn add_world(s: &mut String) {
    s.push_str(", world");
}

fn main() {
    let mut s = String::from("hello"); // 記得這裡要改為mut
    add_world(&mut s);
    simple_print(&s);
    print_len(&s);
}

這樣compiler就不會跟我們抱怨了。

可變引用的限制

假如是引用的話(不去動原始資料的那種),是可以一次借給多個人的

// 這個編譯會過
let s = String::from("hello");
let x = &s;
let y = &s;

println!("x is {}, y is {}", x, y);

但如果是可變引用的話,是不可以的

let mut s = String::from("hello");
let x = &mut s;
let y = &mut s;

可變引用一次限制一個人使用,這個限制很大程度在編譯期就預防了資料競爭(data races),既同一時間不同地方寫入同一個變數。

但如果限制作用域,是可行的,因為在別的作用域結束之後,變數的借用就結束了:

fn simple_print(s: &String) {
    println!("{}", s);
}

fn add_world(s: &mut String) {
    s.push_str(", world");
}

fn add_exclamation_mark(s: &mut String) {
    s.push_str("!");
}

fn main() {
    let mut s = String::from("hello");
    add_world(&mut s);
    add_exclamation_mark(&mut s);
    simple_print(&s);
}

再來也不可以同時對同一個變數作引用可變引用,因為這有可能讓引用突然被借去改變了值

let mut s = String::from("hello");

let s1 = &s;
let s2 = &s;
let s3 = &mut s;

println!("{}, {} and {}", s1, s2, s3);

但也是一樣,直到借出去的被借完之後,在換人借就可以了

let mut s = String::from("hello");

let s1 = &s;
let s2 = &s;
println!("{}, {}", s1, s2); // s 被 s1, s2借去用了

// s1 跟 s2不再借用s了

let s3 = &mut s; // s3 借 s 拿去用
println!("{}", s3);

Reference


上一篇
[Rust] 函式 (Function)
下一篇
[Rust] 字串與string literal
系列文
嘗試30天學「不」會Rust18
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言