2020/3/9

rust04 Ownership

ownership 是 rust 中特別的功能,因為 rust 沒有 garbage collection 的功能,因此要注意 ownership 的運作方式:borrowing, slices, 並瞭解 rust 如何在記憶體中存放資料。


What is Ownership?


所有的程式都必須要管理如何使用機器的記憶體。第一種使用 GC 機制,裡面內建一個自動處理的排程,不斷地自動回收不被使用的記憶體,第二種是交給 Programmer 自己配置與釋放記憶體。Rust 選擇第三種,利用 a system of ownership 所有權系統管理記憶體。編譯器在編譯時,會進行規則的檢查,而在執行時,所有權系統完全不會拖慢程式的速度(swift 也是使用這個方式,實作了 Automatic Reference Counting ARC)。


Stack and Heap


在很多程式語言中,不需要考慮資料是存在 Heap 或是 Stack,但 Rust 要注意 Stack 或是 Heap 會影響到程式語言的行為。


Stack 是 Last In First Out 的順序,有 Push 資料,Pop 資料的動作。


Stack 的 IO 很快,資料存取的位置一直在 Stack 的頂端,不需要尋找存放或讀取資料的位置。另外 Stack 裡面的資料都必須要佔用已知且固定的大小。


在編譯程式時,大小未知或是可能會改變的資料,要儲存在 Heap,Heap 是混亂的,當儲存資料前,必須要要求一塊固定大小的空間。OS 在 heap 找到一塊夠大的空間,標記為已使用,並回傳 pointer。這個動作稱為 allocating on the heap。因為 pointer 大小已知且固定,可以將 pointer 存放在 stack,但要取得實際資料的內容時,還是要透過 pointer 取得資料。


因 heap 要透過 pointer 存取,會比 stack 慢。OS 在處理比較近(stack)的兩份資料時,會比處理比較遠的 heap 較快。


在程式呼叫一個函數時,傳給函數的值(可能是指到 heap 的 pointer)以及函數的 local 變數,都會被 push 到 stack,當函數結束就會被移出 stack。


Ownership 系統要處理的事:追蹤那個 code 正在使用 heap 的哪些資料,最大限度減少 heap 上重複的資料數量,清理不在被使用的資料,確保不會耗用記憶體空間。因此 ownership 系統就是用來管理 heap 的資料。


Ownership Rules


  1. Each value in Rust has a variable that’s called its owner. 每一個值的背後都有一個稱為 owner 的變數
  2. There can only be one owner at a time. 每一個值都只有一個 owner
  3. When the owner goes out of scope, the value will be dropped. 當 owner(也就是變數) 離開作用域 scope 後,這個值就會被丟棄。

Variable Scope


{                      // s 在這裡無效, 它尚未宣告
    let s = "hello";   // 從此處起,s 是有效的

    // 可以使用 s
}                      // 此作用域已結束,s 不再有效

String 類別


一般的字串 literal 會被直接 hard coded 到程式裡面,雖然方便,但因為是不可變的,不適合在某些狀況使用,有時一開始會不知道字串的值,例如從 stdin 取得輸入的字串。 Rust 有第二種字串類別 String,這種類別會被配置到 heap,可儲存在編譯時期未知的字串。


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

s.push_str(", world!"); // push_str() 增加後面的字串

println!("{}", s); // 列印

String 可變但是 "string literal" 卻不可改變的原因,在於兩種類別處理記憶體的方式不同。


Memory and Allocation


string literal 在編譯時就已經知道內容,故內容會直接編碼到最後的執行檔中,這種方式可快速存取。String 用來處理可變長度的字串,因為可變,就需要在 heap 配置一塊編譯時未知大小的記憶體來存放內容。


  • 要在執行時,向 OS 取得 memory
  • 當使用完 String 後,要有將記憶體退還給 OS 的方法

第一步是當我們呼叫 String::from 時,就會取得 memory。但第二步,在有 GC 的語言中,GC 會自動處理。如果沒有 GC,就要在程式中,自己釋放記憶體,就是 allocate 後的 free,過早或是忘記 free 都會造成 bug。


Rust 是第三種機制,當變數離開 scope 後,就會被自動釋放。


{
    let s = String::from("hello"); // 從此處起,s 是有效的

    // 使用 s
}                                  // 此作用域已結束,
                                   // s 不再有效

當 s 離開 scope,Rust 會呼叫一個特殊的函數 drop,也就是在 } 之前,呼叫 drop


C++ 這種 item 在生命週期結束時,釋放資源的模式稱為 Resource Acquisition Is Initialization (RAII)


當有多個變數同時在 heap 使用 memory 時,就會有一些特殊的狀況


變數跟資料的互動方式 (一):move


let x = 5;
let y = x;

x, y 都是 5,所以有兩個 5 放入 stack 中


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

雖然程式碼跟上面類似,但實際上並不是複製一份。


首先 s1 的 pointer 會指向 heap 的記憶體位置,另外記錄長度及容量。



當 s1 賦值給 s2,並沒有複製 heap 的資料,而是複製了 s1 的 pointer, len, capacity 到 s2



如果用類似 string literal 的做法,複製了 heap 的資料,會造成記憶體的消耗,效能低落。


如果 s1, s2 離開 scope,這時候要釋放記憶體,會發生 s1, s2 做了兩次釋放 double free 的動作,兩次釋放會造成記憶體損壞,也就是安全漏洞。


為確保記憶體安全,在以下這種狀況,當 s1 指派給 s2 後,就不能再使用 s1 了,因為 rust 認為 s1 已經無效。以下這樣的程式碼,無法被執行。


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

println!("{}, world!", s1);

warning: unused variable: `s2`
 --> src/main.rs:3:9
  |
3 |     let s2 = s1;
  |         ^^ help: consider prefixing with an underscore: `_s2`
  |
  = note: #[warn(unused_variables)] on by default

error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:28
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `std::string::String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |
5 |     println!("{}, world!", s1);
  |                            ^^ value borrowed here after move

這種做法相對於 deep copy,比較像是 shallow copy,但因為 rust 讓第一個變數無效了,也就是用了 move 的動作,這不同於 shallow copy。因為只有 s2 有效,因此離開 scope,就只需要處理 s2 的釋放。



變數跟資料的互動方式 (二):clone


如果確實需要 deep copy 某個 String 在 heap 上的資料,可以使用 clone


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

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

記憶體的狀況會是這樣



Stack-Only Data: Copy


這邊沒有呼叫 clone,但 x 還是可以使用。原因是整數這種已知大小的資料,是儲存在 stack,而 stack 的資料是直接 copy。


let x = 5;
let y = x;

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

rust 有一種稱為 Copy trait 的特殊註解,可用在這邊。如果某一個類別有 Copy trait,舊的變數賦值給其他變數後,還可以持續使用。


rust 不允許實作了 Drop trait 的類別使用 Copy trait。如果對其值離開 scope 需要做特殊處理的類別,使用 Copy 註解,就會發生編譯錯誤。


可查看該類別的文件,檢查是不是有 Copy trait。但有個規則,任何簡單的 scalar 的組合,都是可以 Copy,不需要配置記憶體或是某個資源的類別,是可以 Copy


  • 所有整數類別 ex: u32
  • bool 也就是 true, false
  • 所有浮點數類別 ex: f64
  • 字元 chat
  • tuple,且裡面的元素都是可以 Copy 的時候 ex: (i32, i32)

Ownership and Functions


將值傳給函數,在語義上跟給變數賦值類似。向函數傳遞值,可能會被移動 move 或複製 copy


fn main() {
    let s = String::from("hello");  // s 進入作用域

    takes_ownership(s);             // s 的值移動到函數裡 ...
    // ... 所以到這裡 s 不再有效

    let x = 5;                      // x 進入作用域

    makes_copy(x);                  // x 應該移動函數裡,
    // 但 i32 是 Copy 的,所以在後面可繼續使用 x

} // 這裡, x 先移出了作用域,然後是 s。但因為 s 的值已被 move,所以不會有特殊操作

fn takes_ownership(some_string: String) { // some_string 進入作用域
    println!("{}", some_string);
} // 這裡,some_string 離開作用域並呼叫 `drop` 方法。佔用的內存被釋放

fn makes_copy(some_integer: i32) { // some_integer 進入作用域
    println!("{}", some_integer);
} // 這裡,some_integer 離開作用域。不會有特殊操作

Return Values and Scope


return value 也可以轉移 ownership


fn main() {
    let s1 = gives_ownership();         // gives_ownership 將回傳值
    // 移給 s1

    let s2 = String::from("hello");     // s2 進入作用域

    let s3 = takes_and_gives_back(s2);  // s2 被移動到
    // takes_and_gives_back 中, 
    // 它也將回傳值移給 s3
} // 這裡, s3 離開作用域並被丟棄。s2 也離開作用域,但已被 move,所以什麼也不會發生。s1 移出作用域並被丟棄

fn gives_ownership() -> String {             // gives_ownership 將返回值移動給
    // 調用它的函數

    let some_string = String::from("hello"); // some_string 進入作用域.

    some_string                              // 回傳 some_string 並移出給調用的函數
}

// takes_and_gives_back 將傳入字符串並回傳該值
fn takes_and_gives_back(a_string: String) -> String { // a_string 進入作用域

    a_string  // 回傳 a_string 並移出給調用的函數
}

變數的 ownership 遵循相同的模式:賦值給另一個變數,就會 move。當持有 heap 資料的變數離開 scope,就會被 drop,除非該資料被移動給另一個變數,轉移了 ownership


如果想要將某個變數傳給一個函數,但不讓該函數獲取 ownership,簡單的做法就是將該變數在函數回傳的時候,利用 tuple 一併傳回來。


fn main() {
    let s1 = String::from("hello");

    let (s2, len) = calculate_length(s1);

    println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();

    (s, length)
}

不過這種做法有點麻煩,rust 提供另一個機制:引用 references


References 引用 and Borrowing 借用


傳給函數的是 &s1,函數定義中是 &String, & 就是引用,可允許使用值,但不獲取其 ownership


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 是對 String 的引用
    s.len()
} // 這裡,s 離開了作用域。但因為它並不擁有引用值的所有權,所以不會發生什麼問題


這裡將獲取 Reference 作為函數參數的動作稱為 Borrowing 借用。借用得來的值,無法被修改,會發生編譯錯誤。


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

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}

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 std::string::String`
8 |     some_string.push_str(", world");
  |     ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable

Note: 就 C 語言來說 &s 代表該記憶體的位址的值,呼叫函數時,只是將記憶體的位址,傳入函數,函數內再透過 pointer 取得字串的 pointer, len , capacity


Mutable References


上一個程式的問題,可以用 mutable reference 解決


&s 改為&mut s


&String 改為 &mut String


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

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

但 mutale reference 有個限制:特定 scope 中的特定資料,有且只能有一個 mutable reference。


這是錯誤的程式碼


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

let r1 = &mut s;
let r2 = &mut s;

println!("{}, {}", r1, r2);

編譯錯誤


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

只要離開 scope,就可以建立一個新的 mutable reference


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

{
    let r1 = &mut s;

} // r1 在這裡離開了作用域,所以我們完全可以創建一個新的引用

let r2 = &mut s;

這種限制,可在編譯其避免 data race,data race 由以下行為發生


  • 兩個或更多 pointer 同時存取同一個資料
  • 至少有一個 pointer 用來寫入資料
  • 沒有同步存取資料的機制

data race 會造成問題,且在執行期很難追蹤跟修復,rust 直接禁止這種行為發生。


在不可變跟可變引用混用時,也會發生同樣的問題


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

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    let r3 = &mut s; // BIG PROBLEM

    println!("{}, {}, and {}", r1, r2, r3);
}

Dangling Reference


在有支援 pointer 的程式語言中,會因為釋放 memory 後,由原本保留的 pointer 取得錯誤的資料,也就是 dangling pointer,rust compiler 可確保 reference 永遠不會發生 dangling reference 的狀況。


fn main() {
    let reference_to_nothing = dangle();
}

// 回傳一個字串的 reference
fn dangle() -> &String {
    let s = String::from("hello");

    &s
} // s 在離開 scope 時,會被丟棄,記憶體被釋放,但回傳了 s 的 reference,會造成存取的問題

編譯錯誤


error[E0106]: missing lifetime specifier
 --> src/main.rs:6:16
  |
6 | fn dangle() -> &String {
  |                ^ help: consider giving it a 'static lifetime: `&'static`
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from

解決方式是修改成直接回傳 String


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

    s
} // s 的 ownership 會被 move 出去

The Rules of References


  • 在任意時間點,都只能有一個 mutable reference,要不然就是有多個 immutable refrences
  • references 必須永遠 valid

The Slice Type


另一種沒有 ownership 的資料類別是 slice。slice 可讓我們引用一個 collection 中的連續元素,而不是直接使用整個 collection


sample: 寫一個函數,參數為 String,回傳該 String 找到的第一個 word。如果該 String 中沒有空白,整個字串就是一個 word,也就是要回傳整個 String。


首先嘗試回傳 word 結尾的索引


fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    // 將 s 轉換為 byte array,然後用 iter 逐個 char 檢查是不是空白
    // 當遇到第一個空白,就回傳 index i
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }
    // 整個 string 都沒有空白,就回傳字串長度
    s.len()
}

如果 String 在呼叫 first_word 後,又呼叫了 clear(),該字串會被清空,但 index 卻沒有同時改變。另外這個 function 沒有辦法處理一個開頭就有空白字元的字串。


String Slices


String slice 是 String 中一部分值的引用 [start..end],是由 start 開始,直到 end,但不包含 end 的 range。


如果是 [start..=end],就有包含 end


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

let hello = &s[0..5];
let world = &s[6..11];

let hello = &s[0..=4];
let world = &s[6..=10];

這是 let world = &s[6..11]; let world = &s[6..=10];的狀況



如果開始是 0 可以省略不寫,如果最後面沒寫,就代表是字串結尾


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

let slice = &s[0..2];
let slice = &s[..2];

let len = s.len();

let slice = &s[3..len];
let slice = &s[3..];

// 以下這兩個是一樣的
let slice = &s[0..len];
let slice = &s[..];

slice range 的索引必須要在有效的 UTF-8 字元邊界內,如果要從 multi-byte 文字中間,建立 slice,會發生錯誤。


剛剛的 first_word 就要改用 slice 回傳,對 slice 字串,呼叫 clear,會發生編譯錯誤。原因是 borrowing 的規則,如果有某個值的可變引用時,就不能再取得另一個可變引用。


fn main() {
    let s = String::from("this is a test");
    let s2 = first_word(&s);

        // compile error!
    //s2.clear();

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

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

String Literals Are Slices


let s = "Hello, world!";

這是 string literal,s 實際上的類別是 &str ,是指向程式特定位置的 slice。因此 string literal 是不可變的, &str 是不可變引用。


String Slices as Parameters


剛剛的 first_word


fn first_word(s: &String) -> &str {     

實際上如果改為 &str,就可以對 String&str 使用相同的函數,讓函數更通用


fn first_word(s: &str) -> &str {

fn main() {
    let my_string = String::from("hello world");

    // first_word 中傳入 `String` 的 slice
    let word1 = first_word(&my_string[..]);

    let my_string_literal = "hello world";

    // first_word 中傳入字符串字面值的 slice
    let word2 = first_word(&my_string_literal[..]);

    // 因為 string literal 就等於 string slice
    // 不使用 slice 語法這樣寫也可以
    let word3 = first_word(my_string_literal);
}

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

其他類別的 slice


string slice 是針對 string,而 array 也有提供 slice


let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];

References


The Rust Programming Language


中文版


中文版 2

沒有留言:

張貼留言