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
- Each value in Rust has a variable that’s called its owner. 每一個值的背後都有一個稱為 owner 的變數
- There can only be one owner at a time. 每一個值都只有一個 owner
- 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];
沒有留言:
張貼留言