Rust 的錯誤大致分為兩種主要的種類, recoverable 和 unrecoverable ,對於 recoverable errors 例如找不到對應檔案,最有可能的處理方式是回報該問題並且重新嘗試該操作,對於 unrecoverable error 來說,那總是代表 bugs 的特徵,此情況會立即將程式停止。
panic!
macro實際上有兩種方式來造成 panic ,一種是做了某些造成整個程式 panic 的操作例如嘗試存取超過 array 範圍的記憶體空間,或者是我們自行呼叫 panic!
macro 。
不管是哪種方式,這些 panics 都會回傳一個錯誤訊息然後 unwind ,之後清空 stack 然後結束程式。
預設來說當 panic 發生時程式會自動開始 unwind ,代表 Rust 會從尾部走回到 stack 頭部並清空所有函式當中使用的資料,但這樣的 walking 成本昂貴,因此 Rust 提供另一個機制稱為 aborting ,會直接將程式終止而不會 clean up 。
至於程式原本使用的記憶體資源則會交給作業系統來清除,如果你寫出的程式最後編譯的 binaries 非常小,則我們可以從 unwinding 改成 aborting ,透過 panic = 'abort'
在 Cargo.toml
檔案當中。
panic!
backtrace將系統的環境變數 RUST_BACKTRACE
開啟時,當 panic 發生就會印出一系列錯誤訊息代表此 panic 如何發生的,這可以幫我們更好的 debug 。
Result
多數的錯誤都不需要直接把整個程式停止,相反的我們可以針對那些錯誤作出一些處理。此處我們介紹 Result
enum ,定義如下
enum Result<T, E> {
Ok(T),
Err(E),
}
T
和 E
為 generic type parameters ,此處只要知道 T
代表成功時會回傳的資料型別, E
代表錯誤時回傳的資料型別。以嘗試打開檔案為例
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
}
此處 File::open
回傳的即是一個 Result<T, E>
,當成功時會回傳 std::fs::File
,失敗時則是回傳 std::io::Error
,因此我們可以利用 match
來針對不同的情況進行處理
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => panic!("Problem opening the file: {error:?}"),
};
}
我們甚至可以再針對錯誤情況進行細分,同樣利用 match
如下
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {e:?}"),
},
other_error => {
panic!("Problem opening the file: {other_error:?}");
}
},
};
}
在 File::open
操作失敗時回傳的資料型態是 io::Error
,是一個由標準函式庫定義的結構體,此結構體有一個 kind
method ,我們可以使用它來取得 io::ErrorKind
的值, io::ErrorKind
是用來區分不同種類的錯誤的資料型別,特別是從 io
operation 得到的。
unwrap
and expect
match
多數時候很有用,不過可能造成可讀性上的問題,有時候也無法達成我們的需求。針對 Result<T, E>
,它自己有許多 helper methods ,例如 unwrap
method ,是一個類似 match
expression 的 shortcut method ,若 Result
回傳的是 Ok
的資料型別,則 unwrap
會回傳 Ok
當中的資料值,若是 Err
則會呼叫 panic!
macro 。
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt").unwrap();
}
另外還有一個 method expect
,它作用和 unwrap
相同但是此處我們可以自己指定錯誤訊息。
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt").expect("hello.txt should be included in this project");
}
首先我們看到以下的程式碼
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let username_file_result = File::open("hello.txt");
let mut username_file = match username_file_result {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut username = String::new();
match username_file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(e) => Err(e),
}
}
此函式會回傳一個 Result<String, io::Error>
資料型態的變數,若此函式成功執行沒有任何錯誤,則 caller 會得到一個持有 String
value 的 Ok
變數,若有任何錯誤則會得到一個持有 io::Error
的 Err
變數。
可以注意到在第一個 Err(e)
處理時,是直接回傳該 Err(e)
而非呼叫 panic!
macro 。
這種 propagation errors 的手法在 Rust 當中相當常見,也提供了一個 operation ?
來使這件事變得更容易。
?
operator : a shortcut for propagating errors我們看到以下程式碼
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username_file = File::open("hello.txt")?;
let mut username = String::new();
username_file.read_to_string(&mut username)?;
Ok(username)
}
此處 ?
的作用和 match
expression 幾乎完全相同,如果程式能順利完成則最後一個 Ok(username)
會作為回傳值,不過 match
和 ?
operator 依舊有不同,從 ?
operator 呼叫得到的錯誤值會經過 from
function ,定義在標準函式庫當中的 From
trait ,負責用來將資料值進行轉型。當 ?
operator 呼叫 from
function 時,接收到的 error type 會被轉為當前函式想要回傳的 error type 。
?
operator 使得函式的實作可以被簡化,例如以下的例子可以將程式碼透過把 method call 串連起來而簡化。
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username = String::new();
File::open("hello.txt")?.read_to_string(&mut username)?;
Ok(username)
}
此函式的作用和上述都相同,只是提供了一個更為簡潔的寫法。特別注意 ?
operator 只能在函式回傳 Result<T, E>
, Option
或 FromResidual
時使用,若直接在非回傳 Result<T, E>
的函式當中呼叫,則會獲得編譯錯誤。
如果把 ?
用在 Option<T>
上面作用也和 Result<T, E>
差不多,會在得到 None
時提前回傳,若是 Some
則會一路傳遞下去。
此處要特別注意的是我們可以在一個會回傳 Result
的函式實作當中搭配 ?
呼叫另一個回傳 Result
的函式,或者是在回傳 Option
的函式實作當中搭配 ?
呼叫另一個回傳 Option
的函式,但不能混著用,因為 ?
無法自動幫我們把 Result
和 Option
進行轉型。
到目前為止我們使用的 main
函式都回傳 Result<(), E>
,實際上 main
也可以修改為回傳 Result<(), E>
,例如以下程式碼
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let greeting_file = File::open("hello.txt")?;
Ok(())
}
此處 Box<dyn Error>
是一個 trait object ,目前我們可以短暫的把此型別認知為 "any kind of error" 。
利用 generic data types 來定義 function signatures 或者 structs ,如此一來我們可以利用不同的 data types 來建立他們。
假設我們有兩個函式,一個會找出 vector 當中最大的數字,一個找出 vector 當中最大的字元
fn largest_i32(list: &[i32]) -> &i32 {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn largest_char(list: &[char]) -> &char {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
兩個函式的行為其實一模一樣,差別只有傳入與回傳的資料型別的差異,此處我們就可以利用 generic type 來消除這類重複行為的函式,函式定義如下
fn largest<T>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
我們解讀這段函式定義如下:函式 largest
是一個在 T
之上的 generic ,而參數 list
則是一個包含 T
的 slice 。此處我們還要注意到由於有利用到 >
operator ,因此我們需要限制 T
有實作 std::cmp::PartialOrd
此 trait 。
例如我們可以定義一個點的結構體如下
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
}
此處的結構體定義 Point<T>
告訴我們 Point<T>
是一個 generic over some type T
,而 x, y
為相同的資料型別,如果我們在建立 instance 時做類似以下的事使得 x, y
資料型態不同,編譯會出錯
let wont_work = Point { x: 5, y: 4.0 };
此處就不多做介紹,之前提過的 Option<T>
和 Result<T, E>
便是最好的例子。
我們也可以針對 generic struct 或 enums 定義 generic method ,例如以下
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
我們針對 Point<T>
定義了一個 method 叫做 x()
並且會回傳成員 x
的 reference 。我們也可以針對某個特定的 concrete type 定義 method 例如
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
如此一來只有 Point<f32>
有 distance_from_origin
這個 method ,其他 Point<T>
的 instance 只要 T
不是 f32
則不會有這個 method 。
generic method 還有其他活用的方法,例如我們可以將成員混搭
struct Point<X1, Y1> {
x: X1,
y: Y1,
}
impl<X1, Y1> Point<X1, Y1> {
fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
Point {
x: self.x,
y: other.y,
}
}
}
此處我們用 X1, Y1
來定義 impl
因為它們需要符合 struct definition ,而 X2, Y2
則是定義在 fn mixup
後面,它們只跟 method 有關。
Rust 在編譯時期運用 monomorphization 使得利用 genrics 的程式碼效能不會因此下降。 Monomorphization 是在編譯時期把特定的型別填入 generics type 當中的過程。編譯過程當中編譯器做的事和我們做的事剛好相反,它找到 generic code 被呼叫的地方並把當中的 concrete type 填入前面的 generic type definition 產生程式碼。因此在執行時期非常有效率並不會增加 overhead 。
Traits 和其他程式語言當中定義的 interface 十分類似,但有些不同。它針對某個特定的資料型別定義功能並且該功能是可以和其他資料型別共享的,共享的行為可以抽象化的表示。我們可以利用 trait bounds 。
若不同型別間有相同的 methods ,則不同型別共享相同的行為。 Trait definition 是一個將 methods signatures 集合在一起來定義某個特定且必要行為的方法。
以下舉例有個結構體存有數種不同的文字訊息,其中 NewsArticle
存有新聞訊息,而 Tweet
則是保有至多 280 字元的 tweet 訊息,包含 metadata 表示它是否是新的推文。
我們希望有一個 aggregator 可以顯示每筆資料的總結,不管是存在 NewsArticle
或 Tweet
。為了做到這件事,每個型別都需要一個 summary ,也需要一個 summarize
method 讓這些 instance 可以呼叫。於是我們可以定義一個 public Summary
trait 來表示這個行為。
pub trait Summary {
fn summarize(&self) -> String;
}
其中 summarize
method 即是用來描述這些型別行為的 trait 。所有實作此 trait 的型別都需要提供自己實作此 method 的方法。編譯器會強迫每個有這個 Summary
trait 的型別都有定義自己的 summarize
method 。
接下來我們針對這兩種不同的資料型別, NewsArticle
和 Tweet
分別實作 summarize
method 。
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
定義型別的 trait 和定義普通的 method 相同,只是在 impl
後方要加上該型別的名稱。對於 trait 來說有個限制存在,就是只有當至少一個 trait 或該 type 對於我們的 crate 來說是 local 時我們才能實作該 trait 。例如我們可以在 Tweet
當中實作標準函式庫的 Display
trait ,因為 Tweet
是我們的 aggregator
crate 的一部分。我們也可以在 Vec<T>
上實作 Summary
trait ,因為 Summary
trait 對於 aggregator
crate 而言是 local 的。但我們不能在 aggregator
crate 當中對 Vec<T>
實作 Display
trait ,這樣的限制稱為 coherence ,或者稱為 orphan rule ,這樣的限制保證其他人不能入侵你的程式碼,同樣的你也不行。
有時候我們不想針對每個型別都要特別寫出該 trait 的實作,此時我們可以幫該 method 定義一個預設行為,如果我們真的需要實作它再寫出來,此時我們的實作會覆寫過原本的預設行為。同樣以上一段的 summarize
為例。
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
我們可以利用 trait 作為函式參數藉以確保傳入該函式的參數擁有此 trait ,例如以下
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
此處我們並沒有對 item
定義任何明確的資料型別,而是利用 impl
keyword 和對應的 trait name ,表示此函式接受所有有實作該 trait 的資料型別。不過實際上 impl Trait
這樣的標記方式是一個語法糖,原本的形式應該呈現如下的程式碼,稱為 trait bound
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
這種表示法在多個參數都想利用 impl Trait
時可以更簡潔的表達,若使用 impl Trait
可能長得像以下
pub fn notify(item1: &impl Summary, item2: &impl Summary) {
更改後可以更為簡潔
pub fn notify<T: Summary>(item1: &T, item2: &T) {
此處有個限制是我們呼叫 notify
時, item1
和 item2
的實際型別必須相同。
我們不只可以指定一種 trait ,兩種甚至三種以上都可以。
pub fn notify(item: &(impl Summary + Display)) {
利用 trait bounds 則會如下
pub fn notify<T: Summary + Display>(item: &T) {
但 trait bounds 依舊有它的缺點存在,如果每個參數的 trait bound 不同會讓整個函式定義變得很冗長,例如以下
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
我們可以利用 where
來解決這個問題
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
我們可以利用 impl Trait
標記來代表回傳的資料型態是一個有實作此 trait 的資料型別。
fn returns_summarizable() -> impl Summary {
Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
}
}
這個能力再 closures 和 iterators 當中非常實用,因為 closures 和 iterators 會建立一些只有編譯器知道的資料型別或者是非常長的型別定義。 impl Trait
讓我們的函式定義可以回傳某些有實作 Iterator
trait 的資料型態,而不用把所有 type 寫出來。
但此處也有個限制,回傳的型別只能是固定的單一型態,如果跟著情況改變則不會通過編譯,例如以下例子
fn returns_summarizable(switch: bool) -> impl Summary {
if switch {
NewsArticle {
headline: String::from(
"Penguins win the Stanley Cup Championship!",
),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from(
"The Pittsburgh Penguins once again are the best \
hockey team in the NHL.",
),
}
} else {
Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
}
}
}
我們可以善用 trait bounds ,使得對於一個 generic types 來說,只有擁有某個 trait 實作的型別才會有特定的 method ,例如以下
use std::fmt::Display;
struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}
此處對於 generic type T
而言,只有實作出 Display
和 PartialOrd
trait 的資料型別才會有 cmd_display
這個 method 。
甚至我們還可以挑選有實作某些 trait 的資料型態才能實作另一些 trait !這在標準函式庫當中非常常見,稱為 blanket implementations 。舉例,在標準函式庫當中只有實作 Display
trait 的資料型別可以實作 ToString
trait 。
impl<T: Display> ToString for T {
Sized
,這代表該型別在編譯時期就具有已知的 constant size 。我們可以透過 ?Sized
來移除這個預設,例如
struct Bar<T: ?Sized>(T);
Lifetime 在 Rust 當中也是一種 generic ,是用來確保 lifetime 在我們需要的時期總是有效的。此處我們應該記得的其中一個重點是所以 reference 都具有 lifetime ,也就是該 reference 有效的 scope 。多數時間 lifetime 都是 implicit 且 inferred ,就像多數時間資料的型別也是 inferred 。只有當數種型態都有可能時我們才需特別寫出來。和這個情況類似,只有當 lifetime 可能有多種可能時我們才需要特別標記。
Rust 透過 generic lifetime parameters 來確保 references 在執行時期的 lifetime 是確定有效的。此處不會討論過多細節,會是一些基本的概念和操作。
lifetime 最主要的目的就是為了避免 dangling references 的產生,那會使得我們取得非預期的資料,考慮以下程式碼
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("r: {r}");
}
此程式不會通過編譯,原因我們首先可以看到它有一個 outer scope 和 inner scope ,變數 r
嘗試獲取 &x
後將它印出,但 x
會在 inner scope 結束時同時被釋放,此時 r
所代表的 reference 即是 dangling reference 。此處的問題是 x
doesn't live long enough ,不過 Rust 是如何判斷此程式是否無效?答案是透過 borrow checker 。
Borrow checker 是 Rust 編譯器的一個元件,它會比較所有 scope 來確認所有 borrow 操作都是有效的。如果把上一段程式碼加上一些標記如下
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {r}"); // |
} // ---------+
此處 'a
表示 r
的 lifetime 而 'b
表示 x
的 lifetime 。我們可以看到 'b
的長度明顯小於 'a
的長度,在編譯時編譯器會比較兩者的長度並發現 r
在它的 lifetime 當中嘗試 refer 到一個 lifetime 更短的 a
,於是此程式會編譯失敗。原因就是因為 'b
的長度比 'a
短,物件的 lifetime 長度比指向該物件的 reference 更短。
修改成以下函式後即可通過編譯
fn main() {
let x = 5; // ----------+-- 'b
// |
let r = &x; // --+-- 'a |
// | |
println!("r: {r}"); // | |
// --+ |
} // ----------+
此處 x
的 lifetime 'b
比指向它的 reference r
的lifetime 'a
更長,因此 r
這個 reference 在它的 lifetime 期間是保證有效的。
現在我們嘗試撰寫一個程式來判斷兩個字串誰比較長,並且利用一個函式 longest()
來協助我們完成,寫起來可能會像以下這樣
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
如果 longest()
的實作是這樣,那連編譯都無法通過,因為編譯器不知道是哪一個 reference , x
或 y
要被回傳,它會告素我們它需要 generic lifetime parameters 。當我們定義函式並且我們不知道到底會傳入什麼值,也當然不知道什麼值會被回傳,所以我們無法推斷被傳入的 reference 的實際 lifetime ,所以我們無法保證回傳的 reference 依然會是有效的。當然 borrow checker 也無法推斷,因為它不知道 x, y
和它的 return value 的關係為何,此處我們介紹 Lifetime annotations 來解決這個問題。
Lifetime annotations 無法影響變數實際 lifetime 長度,它的作用是讓多個 reference 之間可以互相描述它們各自的 lifetime 關係。通常我們使用 'a
代表 first lifetime annotation ,會放在 &
後面。
&i32 // a reference
&'a i32 // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime
單獨一個 lifetime annotation 沒有意義,因為 lifetime annotations 的用意是用來告訴 Rust 編譯器多個 reference 參數的 lifetime 和彼此之間的關聯。
將 lifetime annotation 引入函式定義當中,我們希望這個 annotation 能幫我們表示一個含義:只要所有參數都是有效的,則回傳的 reference 會是有效的。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
此段定義的含義是,對於一個 lifetime 'a
來說,函式會接收兩個參數,這兩個字串參數的 lifetime 都至少和 'a
相同,同時也告訴編譯器被回傳的字串的 lifetime 至少也和 'a
相同。此處還是再強調一次,利用這個 annotation 不代表我們更改了任何變數的 lifetime ,我們僅僅是告訴 borrow checker 我們不應該讓這個函式接收任何可能違反此限制的參數。函式 largest
並不需要知道 x, y
確切的 lifetime 時長,僅需要把 'a
替換成某些 scope 即可。
當我們實際呼叫 longest()
時,它的 lifetime 'a
會被替換成 x
和 y
的 lifetime 重疊之部分,也就是 'a
會得到一個和 x, y
兩者 lifetime 較小的那個相同的 lifetime 。而回傳值的有效期限也會和 x, y
lifetime 較小者相同。
為了驗證這件事我們寫兩段程式分別測試
fn main() {
let string1 = String::from("long string is long");
{
let string2 = String::from("xyz");
let result = longest(string1.as_str(), string2.as_str());
println!("The longest string is {result}");
}
}
這段程式當中 longest()
的兩個參數, string1, string2
lifetime 分別對應到 outer scope 和 inner scope 的長度,明顯是 string2
的 lifetime 較短,因此 'a
就會是 inner scope 的長度, result
的 lifetime 也會和 inner scope 相同,因此此段程式碼會編譯通過並且順利執行。再看到下一段程式碼
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is {result}");
}
如果編譯的話會出現錯誤,因為 result
reference 所指向的變數 lifetime 長度會和 string2
相同, println!
macro 嘗試在 inner scope 之外存取它因此是違法的。即便回傳的值是 string1
結果也是相同的。
定義 lifetime parameters 的方式和函式實際行為有關,例如如果 longest()
根本不進行比較而直接回傳 x
,那即使我們將函式定義如下
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
x
}
y
並沒有定義 lifetime parameters ,但依舊能編譯並順利執行,因為這個函式回傳值跟 y
根本沒有關聯。當函式回傳的值是一個 reference 時,回傳型態的 lifetime parameter 必須至少和其中一個參數的 lifetime parameter 相同,若沒有的話它必須和某個函式當中建立的變數 lifetime 相同,但如此一來就會產生 dangling reference ,因為當此函式結束後該變數 lifetime 也結束。如果想做這件事,就直接把回傳值改為該資料,把其中的資料值 moved 到函式的 caller 當中,由 caller 負責釋放該變數。
如果想在結構體當中持有 reference 也可以,但必須寫出 lifetime annotation 。
struct ImportantExcerpt<'a> {
part: &'a str,
}
此處的 'a
lifetime annotation 告訴編譯器 ImportantExcerpt
的 instance 的 lifetime 不能比它的成員 part
這個 reference 指向的變數的 lifetime 還長。我們可以簡單理解為,成員若是 reference ,則該 reference 指向的資料之 lifetime 必須比該 struct 實體的 lifetime 還長。
考慮以下程式碼
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[..]
}
此處我們也是回傳一個 reference ,但為何不需要寫出 lifetime annotation ?這是有歷史因素的,原本舊版的 Rust 是需要把這段程式寫出 lifetime annotation 的,但後來發現這類程式總是會在特定情況進入相同的 lifetime ,行為變得可預期,於是 Rust 把它加入編譯器檢查的機制當中,讓 borrow checker 可以在這些情境下自行推斷 lifetime 而不需要特別寫出 lifetime annotation 。
這樣被嵌入在 Rust compiler 當中,進行 reference analysis 的模式稱為 lifetime elision rules 。它們是一些編譯器需要考慮的特別情況,若你的程式碼符合這些情境則不需要寫出 lifetime annotations 。
elision rules 並不會提供完整的推斷,如果編譯器嘗試套用這些情境與規則時發現依舊有模糊地帶,它會直接回傳錯誤並告訴開發者需要把 lifetime annotation 寫清楚。
對於一個函式而言,參數的 lifetime 稱為 input lifetimes ,而回傳值的 lifetime 稱為 output lifetimes 。
elision rules 目前有三個,如果三個 rules 都檢查後還是有模糊地帶,則開發者就需要把 lifetime annotation 寫清楚。
fn foo<'a, 'b>(x: &'a i32, y: &'b i32);
。fn foo<'a>(x: &'a i32) -> &'a i32
。&self
或 &mut self
,則 self
的 lifetime 會被指派給所有的 output lifetime parameters 。現在假裝自己是編譯器,記住上面三個規則並套用來分析以下函式定義
fn first_word(s: &str) -> &str {
一開始先嘗試第一個規則,每個 input parameter 都指派一個 lifetime parameter ,於是函式變的像以下形式
fn first_word<'a>(s: &'a str) -> &str {
第二個規則也能套用,因為我們只有一個 input parameters ,因此函式又被轉變為以下
fn first_word<'a>(s: &'a str) -> &'a str {
於是函式定義當中所有 reference 都具有 lifetime annotations ,所以編譯器可以繼續進行分析。再分析下一個例子
fn longest(x: &str, y: &str) -> &str {
先將第一個規則套上
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str{
先在第二個規則不適用,因為有多個 input parameters ,第三個也不適用,因為它不是一個 method ,所以到這裡為止 output parameters 的 lifetime 還是無法被確定,因此編譯器就會跳出錯誤告訴我們這個函式原型需要加上 lifetime annotation 。
其中一個特別的 lifetime 是 'static
,加上此標注的變數 lifetime 是整個程式的 lifetime 。所有的 string literals 都有 'static
lifetime ,像以下
let s: &'static str = "I have a static lifetime.";
此字串的文字會直接被存在程式的 binaries 當中。
關於 lifetime 的細節遠不止於此,想了解更多應該再去研讀 Rust Reference 。
函式和 closures 之間的差異有幾點,首先 closures 不需要像函式一樣標註參數或回傳值的型別, closures 並非用來展開一個介面給使用者,它們被存在變數之中並且可以不用命名就使用它們,並且直接揭露給使用者使用。
編譯器可以自行推斷 closures 的參數與回傳值型別,當然我們也可以加上額外的標注來使 closure 更清晰且在編譯時期讓編譯器更輕鬆,例如以下例子
let expensive_closure = |num: u32| -> u32 {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
以下我們定義一個函式讓參數加一,同樣定義一個 closure 進行一樣的行為,並且比較兩種實作不同之處。
fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;
第一個版本是函式的實作,第二個版本是 closure 並且完整的標註,第三個則移除了 closure definition 的型別標注,第四個連括號都移除了,如果實作行為當中的運算多於一行就不能把括號省略。
以下展示一個 closure 實際應用的例子
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5);
如果把這段程式碼拿去編譯會得到錯誤,因為編譯器會先預設該 closure 變數型態為 String
,但我們第二次呼叫卻使用整數型態。
Closures 獲取環境變數的方式有三種,同樣直接對應到函式取得參數的三種方式
對於 closure 來說,編譯器會自行決定要使用哪一種,基於它們行為的實作。例如以下程式碼當中, closure 只負責將變數印出來,所以它使用 immutable reference 。
fn main() {
let list = vec![1, 2, 3];
println!("Before defining the closure: {list:?}");
let only_borrows = || println!("From closure: {list:?}");
println!("Before calling closure: {list:?}");
only_borrows();
println!("After calling closure: {list:?}");
}
此範例展示了我們可以把 closure 和變數綁在一起,之後透過呼叫此變數的名稱加上小括號,像是函式一樣使用它。此處的另一個重點是因為該 closure 是 immutable reference ,因此我們在 closure 當中利用 list
後依舊可以存取它。
之後我們改變 closure body 並且添加一個元素到 list
vector 當中, closure 就會捕捉一個 mutable reference 。
fn main() {
let mut list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");
let mut borrows_mutably = || list.push(7);
borrows_mutably();
println!("After calling closure: {list:?}");
}
注意此處的另一個細節是在 closure definition 和第一個 closure call 之前,不可以出現任何其他對於 list
的 borrow ,因為在 closure 當中已經有一個 mutable reference 。
通常 closure 的參數不需要取得 ownership ,只有一種特殊情況,也就是該 closure 用來建立一個新的執行緒,而送入的資料應該被新的執行緒持有。
use std::thread;
fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");
thread::spawn(move || println!("From thread: {list:?}"))
.join()
.unwrap();
}
此處就利用了 move
keyword 來將參數的 ownership 轉移到 closure 當中,使得新的執行緒可以佔有該參數。
Fn
Traitsclosure 取得定義環境當中變數的 reference 或 ownership 後,它的 body 當中定義了要對 reference 進行何種操作, closure body 能做的事如下,將取得的變數值 move out of the closure ,或者 mutate the captured value 。
closure body 如果取得與處理取得的變數會影響 closure 實作的 traits ,而 traits 正是函式或 struct 用來定義何種 closure 可以使用。 closure 通常會自動實作一個、兩個或全部三個 Fn
traits 。
FnOnce
trait 代表該 closure 能被呼叫一次,所有 closure 都會實作這個 trait ,將取得的變數值 move out of body 的 closure 則是只會實作這個 trait 。FnMut
應用在所有不會把變數 move out of body 的 closure ,但它可能對獲取的變數做變化,這些 closure 可以被多次呼叫。Fn
應用在那些不會將變數 move out of body 也不會更改變數內容的 closure ,還有那些不會抓取環境變數的 closure 。這些 closure 都可以被多次呼叫,並且可以被 concurrently 的呼叫。我們嘗試觀察並解析以下程式碼實作
impl<T> Option<T> {
pub fn unwrap_or_else<F>(self, f: F) -> T
where
F: FnOnce() -> T
{
match self {
Some(x) => x,
None => f(),
}
}
}
T
是一個代表 Some
當中變數型態的 generic type ,同時也是函式 unwrap_or_else
的回傳值型態,另外可以注意到還有另一個 generic type parameter F
,它也是其中一個變數 f
的型態。其中它的 trait bound 是 FnOnce() -> T
,代表 F
最多被呼叫一次並且不會接收參數,回傳的資料型態是 T
。
接著嘗試解析一個標準函式庫的 method sort_by_key
,並試圖理解它和 unwrap_or_else
的差異以及為何它使用 FnMut
trait bound ,該 closure 接收 slice 當中 current item 的 reference ,並且回傳一個型別為 K
的可排序值。例如以下的程式將 Rectangle
依照 width
從小排到大。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
list.sort_by_key(|r| r.width);
println!("{list:#?}");
}
而 sort_by_key
會接收 FnMut
trait 定義的 closure 是因為它會被多次呼叫,每一個 slice 當中的 item 都會呼叫一次,而 |r| r.width
closure 不會進行 capture, mutate 或者 move 操作。
如果我們傳送一個只實作 FnOnce
trait 的 closure 並試圖把它傳送進入 sort_by_key
當中,編譯器不會通過此種操作。
let mut sort_operations = vec![];
let value = String::from("closure called");
list.sort_by_key(|r| {
sort_operations.push(value);
r.width
});
println!("{list:#?}");
}
由於這個 closure 會把變數的 ownership 移出 closure ,若我們把程式碼改為以下就可以通過編譯
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
let mut num_sort_operations = 0;
list.sort_by_key(|r| {
num_sort_operations += 1;
r.width
});
println!("{list:#?}, sorted in {num_sort_operations} operations");
}
Iterator 的作用是用來走訪每個 item 並判斷此序列是否結束了。在 Rust 當中的 iterator 性質是 lazy ,意思是直到我們呼叫 method 來使用 iterator 時它才會有作用。例如以下程式,即便我們建立一個針對 v1
vector 的 iterator ,但目前為止實際上什麼都沒做
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
我們可以利用此 iterator 來把 vector 當中所有元素印出來
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
for val in v1_iter {
println!("Got: {val}");
}
iterator 提供給我們一個優雅並安全的方式來走訪一系列的資料,相較於自己使用 indexing 並冒著可能存取到非法空間的風險,用 iterator 可以幫我們避免這些可能。以下我們學習如何做出 iterator 以及它如何達成這些功能
Iterator
trait and the next
method在標準函式庫當中所有的 iterators 都會實作一個叫做 Iterator
的 trait 。
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
此處的 type Item
和 Self::Item
用處是在定義此 trait 的 associated type ,此處我們只需要知道這代表在實作 Iterator
trait 時也同時定義資料型別 Item
,而這個 Item
型別正是 next
method 的回傳型別。換句話說, Item
即是 iterator 回傳的資料型別。
Iterator
trait 只需要我們實作一個 method 也就是 next
,它會回傳一個包裝在 Some
當中的物件,並且當 iterator 走完時會回傳 None
。
並且 iterator 應該是 mutable 的,因為呼叫 next
method 時會改變 iterator 的內部狀態。我們稱這樣的操作為 consume ,我們不需要自己將 iterator 定義為 mutable ,因為在 for loop 當中它會取得 iterator 的 ownership 並且把它變為 mutable 。
同時我們要注意到從 next
取得的是一個 immutable reference 。而 iter
method 產生的是一個 iterator over immutable reference 。若我們想建立的 iterator 可以取得 v1
的 ownership 並回傳該值,相較於呼叫 iter
,我們應該呼叫 into_iter
。同樣的如果我們走訪 mutable references ,我們也應該呼叫 iter_mut
。
呼叫 next
method 的 method 稱為 consuming adaptors ,因為呼叫他們時會將一個 iterator 用盡。其中一個例子是 sum
method ,它會取得 iterator 的 ownership 並透過重複呼叫 next
來走訪所有物件。用法如以下
fn iterator_sum() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
let total: i32 = v1_iter.sum();
assert_eq!(total, 6);
}
在使用 sum
method 之後, v1_iter
就失效了因為 sum
method 會取走它的 ownership 。
我們也有不會 consume iterator 的 method ,稱為 Iterator adaptors 。它們會透過改變原本 iterator 的某些部分而產生新的 iterators 。
例如有個 iterator adaptor method 稱為 map
,它會接收一個 closure 並在造訪每個 item 時呼叫, map
method 會回傳指向更改過的 item 的 iterator 。
let v1: Vec<i32> = vec![1, 2, 3];
v1.iter().map(|x| x + 1);
但如果把此段程式碼拿去編譯會得到錯誤訊息,告訴我們該 closure 從來不會被呼叫,因為 iterator adaptors 的性質為 lazy ,我們需要自行 consume 這些 iterator 。
因此我們可以使用 collect
method ,它會 consume iterator 並收集所有回傳值。
let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
由於 iterator adaptors 的 lazy 性質,我們需要呼叫 cosuming adaptor 來取得 iterator adaptors 的回傳值。
許多 iterator adaptors 的參數都是 closure ,我們定義 closure 也經常來當成 iterator adaptors 的參數因為它們可以獲取 environment 變數。
本文利用 The Adventures of Sherlock Holmes by Sir Arthur Conan Doyle 的全部內容作為 benchmark 來測試 loops 和 iterator 之間的差異,而 iterator 的性能更好。
Iterator 是 Rust 當中其中一個 zero_cost abstractions ,也就是該抽象實作在執行時期不會造成任何多餘的成本,更精確的說我們可以引用 Bjarne Stroustrup 在 Foundation of C++ 當中的話
zero-overhead principle: What you don't use, you don't pay for. And futhur: What you do use, you couldn't hand code any better.
查看以下例子,是一段從 audio decoder 取出的程式碼,其中的 decoding algorithm 使用線性預測來透過之前的樣本數據來預判未來可以能數據。當中利用了 iterator chain 來對三個變數進行運算
buffer
: a slice of datacoefficients
: an array of 12qlp_shift
: an amount by which to shift datalet buffer: &mut [i32];
let coefficients: [i64; 12];
let qlp_shift: i16;
for i in 12..buffer.len() {
let prediction = coefficients.iter()
.zip(&buffer[i - 12..i])
.map(|(&c, &s)| c * s as i64)
.sum::<i64>() >> qlp_shift;
let delta = buffer[i];
buffer[i] = prediction as i32 + delta;
}
這段程式碼利用迴圈走訪 coefficients
當中的 12 個元素並利用 zip
method 將係數值和在 buffer
當中的 12 個值搭配,之後對於每個配對,我們都將值相乘並把結果全部相加,再右移 qlp_shift
個位元。
此處我們利用一個 iterator ,兩個 iterator adaptors 並 consume 一個值。經過 Rust 編譯器編譯後得到的組合語言不會包含任何迴圈,它知道有 12 次迴圈,因此會將迴圈展開。並將所有 coefficient
存在 register 當中,在執行時期不會有任何針對 array 的邊界檢查。
pointer 代表的是一個攜帶某塊記憶體位址的變數,是一個 general concept 。在 Rust 當中最常見的 pointer 便是 reference ,利用 &
來表示對某個值進行 borrow 。它們沒有除此以外的其他功能,且沒有 overhead 。
smart pointers 則是在有 pointer 的功能以外,還有其他 metadata 或其他功能。在 Rust 的標準函式庫當中定義了許多的 smart pointers ,以下會介紹幾個。
在 Rust 當中 reference 和 smart pointer 有個顯著的差異, reference 只有 borrow data ,而 smart pointers 在許多情況是 own 他們指向的資料。
事實上我們之前學習過的 String
和 Vec<T>
都是 smart pointers ,通常 smart pointers 都是利用 structs 來實作,同時和尋常的 struct 不同的是它們會另外實作 Deref
和 Drop
這兩個 traits , Deref
trait 使得 smart pointer 的實體可以表現得像一個 reference ,所以我們的程式可以處理 reference 和 smart pointers 。 Drop
trait 則讓我們可以處理 smart pointer 指向的資料離開它們的 scope 時該如何處理。
Box<T>
to point to data on the heapBox<T>
是已知最直觀的 smart pointer 。它使開發者可以將資料存在 heap 當中而非 stack 當中。僅有指向 heap data 的 pointer 會留在 stack 當中。
Box 沒有性能上的 overhead ,但同時它們也不提供多餘的功能,通常以下幾種情境比較常用到
Box<T>
to store data on the heap首先我們要了解 syntax 和如何跟 Box<T>
當中的值互動。以下展示了如何利用 box 將一個 i32
的值存在 heap 當中
fn main() {
let b = Box::new(5);
println!("b = {b}");
}
此處定義了一個變數 b
,值是一個指向 5
的 Box
,而 5
會被放在 heap 上。當 box 離開它的 scope 時, b
會被 deallocated ,同時在 heap 當中的資料也會被 deallocate 。
recursive type 代表的是該型別的值可以包含和自己型別相同的值。但這樣的型別資料大小在編譯時期無法得知,因為理論上 recursive type 的 nesting value 可能無限的擴充下去。由於 Box 具有已知的大小,因此我們可以透過在 recursive type 定義當中加入 Box 來使 recurisve type 可運作。
以下為以 cons list 為例,這是一種在 functional programming language 常見的資料型態。整體概念除了 recursion 都是很好理解的。
cons list 最初來自 Lisp programming language ,由巢狀 pairs 組成,在 Lisp programming language 當中這即是它的 linked list 。例如以下包含 1, 2, 3 的 cons list 。
(1, (2, (3, Nil)))
每個 cons list 當中的 item 由兩個元素組成,當前 item 的值和下一個 item 。串列當中最後一個元素只會包含自己的值,下一個就是 Nil
。 cons list 透過遞迴呼叫 cons
function 來構成。在 Rust 當中多數情況我們會用 Vec<T>
而非 cons list 。此處僅是為了理解便利所以用 cons list 。
首先定義一個 enum ,此段程式還無法編譯,因為 List
還沒有一個已知的大小
enum List {
Cons(i32, List),
Nil,
}
利用這個 List
type 來儲存 1, 2, 3 會看起來像以下
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}
注意此處的 Nil
代表的是 non-recursive variant 而非 null 或 invalid 的概念。如果嘗試編譯則會得到錯誤,編譯器會告訴我們這個型別有 infinite size 。為了解決它,首先我們要理解 Rust 如何決定一個型別需要多少空間。
拿之前的 enum Message
為例
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
為了決定一個 Message
需要多少記憶體空間, Rust 會找到需要該 enum 當中需要最多空間的 variant ,在此例子當中 Message::Quit
不需要任何空間, Message::Move
至少需要能儲存兩個 i32
的空間。以此類推找出佔用最多空間的 variant 。
用相同的邏輯再回到 cons list 當中, Rust 試圖判斷 enum List
當中佔用空間最多的 variant ,首先計算 Cons
variant ,它佔用一個 i32
和一個 List
,之後又要進而計算當中的 List
佔用多少空間…無限循環下去。
Box<T>
to Get a Recursive Type with a Known Size在上述程式碼的編譯錯誤當中, Rust 還會給予以下建議
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
2 | Cons(i32, Box<List>),
| ++++ +
在這個建議當中, "indirection" 代表與其直接儲存一個值,不如儲存一個指向該值的指標。
由於 Box<T>
是一個指標, Rust 會知道 Box<T>
需要多少空間,指標佔有的空間大小不會因為指向什麼資料而改變。這代表與其直接放入另一個 List
值,不如放一個 Box<T>
在 Cons 的 variant 當中。 Box<T>
會指向另一個 List value ,而該 List
value 會在 heap 上而非在 Cons
variant 當中。概念上兩個實作相同,但實現的細節上,我們將 List
放在彼此之間,而非彼此之內。
於是我們可以將 List
的實作變為以下形式
enum List {
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}
如此一來就能通過編譯。編譯器此時也不會將 enum List
解讀為一個需要無限記憶體大小的型別,而是能確認 Cons
的大小是一個 i32
加上一個 Box<List>
。
到此處可以注意到 Box 提供的僅僅只有 indirection 和 heap allocation 的功能,並沒有其他特別的作用。同時它也有實作 Deref
trait ,使得程式對待 Box 的方式和對待其他 references 相同。當 Box<T>
離開它的 scope 後,在 heap 當中存放資料的區段會由於 Drop
trait 的實作而被清空。
Deref
trait實作 Deref
trait 的好處是可以讓我們客製化 dereference operator *
的行為。如此一來 smart pointer 可以被和平常的 reference 一樣對待。處理 reference 的程式同樣可以拿來處理 smart pointer 。
一個平常的 reference 即是一種指標的型別, dereference 可以視為跟隨 reference 指向的資料的操作。
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}
Box<T>
like a reference我們可以把上述程式修改為以下
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
可以觀察到僅需要更改一行程式碼,把 y
變為 Box<T>
的實體即可。
我們透過模仿 Box<T>
來定義屬於自己的 smart pointer ,首先 Box<T>
原始定義是一個 tuple struct with one element ,因此我們也將 MyBox<T>
定義為相同的型別,並且定義它的 new
函式。
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
如果此時我們將原本比較 x, y
值的時程式碼變更為使用 MyBox<T>
會發生什麼事呢?
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
顯然地會發生編譯錯誤,因為編譯器不知道如何對 MyBox
進行 dereference 。為了允許這個行為,我們需要自行實作 Deref
trait 。
Deref
trait像前述章節所提及,要實作一個 trait ,我們需要實作出該 trait 所有要求的 method , Deref
trait 由標準函式庫提供,要求我們實作一個 method 也稱為 deref
,會對 self
進行 borrow 並且回傳一個指向 inner data 的 reference 。以下是我們對 MyBox<T>
實作 Deref
的程式碼
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
程式碼當中, type Target = T;
定義了 Deref
trait 使用的 associated type 。和定義 generic parameter 有點不同,會在之後介紹。
忘記 tuple 實作的可以回去複習,在此處 self.0
會取得 tuple struct 當中第一個值,之後回傳指向它的 reference 。
此時當我們的程式碼運行到 *y
時, Rust 實際上運行以下的程式碼
*(y.deref())
Rust 會將 *
operator 替換成呼叫 deref
method 以及一個 plain dereference 如此一來我們就不需要額外呼叫 deref
method 。
此時可能有人有疑問, y.deref()
不就回傳了 reference 指向的值嗎?為何還需要在外面加上 *
?這依然是必要的,這和 ownership system 有關。若 deref
method 直接回傳該值,而不是該值的 reference ,則該值會被 moved out of self
。而我們並不想取得 MyBox<T>
的 inner value 。
Deref Coercion 會將指向某個實作 Deref
trait 的型別的 reference 轉為指向其他型別的 reference 。例如將 &String
轉為 &str
。這個行為是允許的是因為 String
有實作一個回傳 &str
的 Deref trait 。
當我們將某個型別當作函式參數傳入時,若該型別和原本函式定義的參數型別不同,此時只要該型別有實作 Deref
trait 則 Deref coercion 會自動發生。一連串呼叫 deref
method 的操作會將我們提供的資料型別轉為參數所需的資料型別。
Deref coercion 也讓我們可以撰寫能夠同屎處理 reference 和 smart pointer 的程式。
範例如下,若原本的函式是將傳進來的 str
印出。
fn hello(name: &str) {
println!("Hello, {name}!");
}
我們可以透過例如 hello("Rust")
這類方式來呼叫此函式,而 Deref coercion 則讓我們甚至可以傳送一個 MyBox<String>
進行該函式。
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
此處雖然 &m
的型別是 a reference to MyBox<String>
,但 Rust 可以透過呼叫 deref
將 &MyBox<String>
轉為 &String
。標準函式庫當中有在 String
上實作 Deref
使得它可以將 String
轉為 string slice 。因此 Rust 會再度呼叫 Deref
將 &String
轉為 &str
。
如果沒有 deref coercion ,則我們的程式需要變成以下的形式
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
(*m)
將 MyBox<String>
轉為 String
,之後 [..]
將 String
轉為 string slice ,最後透過 &
取得 reference 。
如果某個型別有實作 Deref
trait ,則 Rust 在使用該型別時會檢查並盡量使用 Deref::deref
來使參數型別和傳入型別吻合。而實際上要呼叫幾次 Deref::deref
會在編譯時期就決定好,因此 deref coercion 不會造成執行時期的負擔。
Deref
trait 可以將 immutable references 的 *
operator 給覆寫,而 DerefMut
則可以對 mutable reference 做相同的事。
Rust 在以下三種情形會進行 deref coercion
&T
送入 &U
: 呼叫 T: Deref<Target=U>
&mut T
送入 &mut U
: 呼叫 T: DerefMut<Target=U>
&mut T
送入 &U
: 呼叫 T: Deref<Target=U>
可以特別注意的是第三個情況, Rust 會將 mutable reference 轉為 immutable ,但反過來則是不可能的,這是由於 borrowing rules 的規範,若我們有某個資料的 mutable reference ,則該 mutable reference 必須是該資料唯一的 reference 。因此若把 immutable reference 轉為 mutable reference 可能打破這個規範。
Drop
TraitSmart pointer 的 Drop
trait 可以讓我們規範在該變數不在有效 scope 當中時該做什麼事。 Rust 會在編譯時期自動將這些釋放資源的程式碼加入我們的實作當中,因此我們不用在程式當中自己寫出釋放資源的操作。
至於到底該做什麼就端看 Drop
trait 當中的實作, Drop
trait 要求我們需要實作一個稱為 drop
的 method 。例如以下的範例
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer {
data: String::from("my stuff"),
};
let d = CustomSmartPointer {
data: String::from("other stuff"),
};
println!("CustomSmartPointers created.");
}
std::mem::drop
有時候我們希望進行 cleanup 的時機會比起原本 drop
被呼叫的時機更早。其中一個例子是我們利用 smart pointer 來管理 locks 時,我們可能會希望直接呼叫 drop
method 來釋放該 lock 使得其他部分的程式碼可以嘗試取得該 lock 。但 Rust 並不允許我們手動呼叫 drop
method ,因此我們需要利用標準函式庫當中的 std::mem::drop
。
Rust 不讓我們自行呼叫 drop
的原因是為了避免 double free error 。由於我們不能取消 Rust 自動呼叫 drop
method 的行為,也不能自行呼叫 drop
method ,因此我們需要 std::mem::drop
。
例如以下方法
fn main() {
let c = CustomSmartPointer {
data: String::from("some data"),
};
println!("CustomSmartPointer created.");
drop(c);
println!("CustomSmartPointer dropped before the end of main.");
}
若經過編譯並執行會得到以下內容
$ cargo run
Compiling drop-example v0.1.0 (file:///projects/drop-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/drop-example`
CustomSmartPointer created.
Dropping CustomSmartPointer with data `some data`!
CustomSmartPointer dropped before the end of main.
擁有 Drop
trait 的好處有很多個,在 Drop
trait 搭配 Rust ownership system 時, Rust 會自行對資料進行 cleanup 。再來是我們不需要擔心會將還在被使用的資料清除,因為 ownership system 會保證還在被使用的 reference 是有效的,同時保證 drop
只會在該資料不再被使用時會被呼叫一次。
Rc<T>
, the Reference Counted Smart Pointer多數情況, ownership 都是很清楚的,哪個變數擁有哪個值是很明確的。但有些情況一個值可能有數個 owners 。例如 graph data structure ,許多個邊可能同時指向同一個 node 。而該 node 在概念上是同時被許多個邊持有的。該 node 需要等到沒有邊指向自己時才能被清除。
為了啟用 multiple ownership ,我們需要明確的使用 Rc<T>
,也就是 reference counting 的縮寫。 Rc<T>
會追蹤某個值的 reference count 來將判斷該值是否還在被使用。只有當該值的 reference count 為 0 時,它才能被清除。
使用 Rc<T>
的時機是當我們想要在 heap 上配置空間給我們的資料,而程式當中有數個地方都可能用到它,但在編譯時期不知道哪個部分會最後使用該資料。如果我們能在編譯時期知道這件事的話,可以將該部分指派為該資料的 owner ,我們熟悉的 ownership system 也能負責處理它。
Rc<T>
只有在 single-threaded 情境能被使用, multithreaded 程式需要其他的方式處理。
Rc<T>
to Share data假設我們希望建構出一個如下方的鍵結串列
簡單分析可以得知, a, b, c 三個鍵結串列共享 5->10 這個串列。
如果用先前的 Cons list 和 Box<T>
實作如下
enum List {
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
let b = Cons(3, Box::new(a));
let c = Cons(4, Box::new(a));
}
嘗試編譯會得到錯誤
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0382]: use of moved value: `a`
--> src/main.rs:11:30
|
9 | let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
| - move occurs because `a` has type `List`, which does not implement the `Copy` trait
10 | let b = Cons(3, Box::new(a));
| - value moved here
11 | let c = Cons(4, Box::new(a));
| ^ value used here after move
For more information about this error, try `rustc --explain E0382`.
error: could not compile `cons-list` (bin "cons-list") due to 1 previous error
這個錯誤的原因是一開始 a
持有建立的 Cons list ,接著 b
將 a
當中資料的 ownership 轉移給自己,此時 a
就失效了,但我們又嘗試在建立 c
時使用 a
,因此無法通過編譯。
此時我們如果把 List
定義當中的 Box<T>
改為 Rc<T>
,則每個 Cons
variant 都會持有一個值以及一個指向 List
的 Rc<T>
。當我們建立 b
時,它會將 a
持有的 Rc<List>
複製一份給自己,而不是將 a
的 ownership 拿走,因此會增加 reference 的數量,並且 a
, b
會同時對該資料享有 ownership 。每次呼叫 Rc::clone
時都會增加 Rc<List>
當中 reference count 的值。直到 reference count 變為零,該值才會被清除。
enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::rc::Rc;
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
let b = Cons(3, Rc::clone(&a));
let c = Cons(4, Rc::clone(&a));
}
由於 Rc<T>
不在 prelude 當中,我們需要特別寫出 use std::rc::Rc;
來使用它。在此情況當中, Rc::clone
不會對該資料進行 deep copy ( clone
會 )。呼叫 Rc::clone
只會增加 reference count ,和 deep copy 相比耗費的時間很少。
Rc<T>
Increases the Reference countRc::strong_count()
可以取得該值當前的 reference count 。同時也有 Rc::weak_count()
,但它的使用場景不同。
Rc<T>
透過 immutable references 使得我們可以透過 read-only 的方式將資料在程式不同區塊共享。
RefCell<T>
and the Interior Mutability PatternInterior Mutability 是 Rust 當中的一種 Design Pattern ,使得我們可以在某個資料有 immutable reference 時依舊可以改變它。通常因為 borrowing rules 的關係這樣的行為是被禁止的。而改變資料時,我們需要利用 unsafe
關鍵字來暫時繞過 Rust 的規則。 Unsafe code 告素編譯器我們已經自己檢查了,編譯器不用幫我們檢查。
只有當我們確定 borrowing rules 將在執行時期被遵守時,我們才可以使用該型別來使用 interior mutability 。 unsafe
code 會被包在一個安全的 API 當中,而 outer type 依舊為 immutable 。
RefCell<T>
和 Rc<T>
不同, RefCell<T>
代表它持有資料的 single ownership ,所以它和 Box<T>
的具體差異是什麼?
可以先回憶 borrowing rules 的內容
使用 Box<T>
時, borrwoing rules 在編譯時期就會被確保,而 RefCell<T>
則是執行時期才會確保 borrowing rules 。使用 references 時違反 borrowing rules ,程式會編譯失敗,而使用 RefCell<T>
時,違反 borrowing rules ,程式會 panic 並離開。
多數情況在編譯時期就檢查 borrowing rules 會是最理想的,也是 Rust 預設的行為。而在執行時期才檢查 borrowing rules 也是有它的好處,也就是某些開發者確定是 memory-safe 但卻無法通過編譯器編譯的情況可以被允許。
由於 static analysis ,例如 Rust 編譯器,是 inherently conservative 的,程式的某些特徵是無法被檢查到的,最有名的例子便是 Halting Problem 。
在這些無法被檢查的情況當中, Rust 編譯器無法確認程式碼是否遵守 ownership rules 因此它會拒絕讓這些程式通過編譯,因此我們稱 Rust 編譯器為保守的。而 RefCell<T>
在開發者自身很確定程式有遵守 borrowing rules 但編譯器無法確認時,就可以讓程式通過編譯。
和 Rc<T>
相同, RefCell<T>
同樣只能應用在 single-threaded 的場景,若嘗試在 multithreaded program 使用會噴出編譯錯誤。
以下是關於如何選擇 Box<T>
, Rc<T>
, RefCell<T>
快速的重點提示
Rc<T>
使一份資料可以有數個持有者。 Box<T>
, RefCell<T>
都只能有 single ownerBox<T>
允許編譯時期檢查 immutable borrow 和 mutable borrow 。 Rc<T>
編譯時期檢查只允許檢查 immutable borrows 。 RefCell<T>
對於 immutable borrow 和 mutable borrow 的檢查都在執行時期。RefCell<T>
在執行時期檢查 mutable borrows ,我們可以改變 RefCell<T>
當中的值,即時 RefCell<T>
原本設為 immutable改變一個 immutable value 當中的值稱為 interior mutability pattern 。
由於 borrowing rule 的限制,我們不能對一個 immutable value 進行 mutable borrow ,以下面的例子來看,它不會通過編譯
fn main() {
let x = 5;
let y = &mut x;
}
儘管如此,有些情景會需要一個 value 在它自身的 method 當中為 mutable ,但對於其他程式而言是 immutable 。如此一來在該值 method 外部的程式碼都不能對該值進行更動。使用 RefCell<T>
是一個擁有 interior mutability 的方法,但 RefCell<T>
無法完全繞過 borrowing rules , borrow checker 在編譯時期會允許 interior mutability ,但如果在執行時期違反此規則,會得到 panic!
而非編譯錯誤。
以下來看幾個 RefCell<T>
適合使用的場景
在測試時有些情況我們會讓一個型別來取代另一個型別,為了觀察特定的行為以及確認它的實作正確性。這個 placeholder type 稱為 test double 。可以想像是拍電影時的替身演員, test doubles 在測試當中擔任的角色就像如此。 Mock objects 為 test doubles 當中的特別種類,用來記錄測試過程發生的事,我們可以藉此判斷實作是否正確。
Rust 的標準函式庫當中並沒有 mock object 這類的型態,不過我們依然可以自行實作一個 struct 並擁有和 mock object 相同的功能。
以下測試的情景為,建立一個可以追蹤某個值和最大值之間差距的函式庫,用處例如可以用來追蹤使用者最多可以使用某個 API 幾次。
我們的函式庫只需要提供追蹤和最大值的差距這樣的功能,以及哪些訊息該在何時被傳送。而使用我們函式庫的應用程式應該自行實作傳送訊息的機制,不管是在應用程式中印出訊息、傳送 email 或者是傳送簡訊。函式庫不用知道應用程式實作的細節,僅僅需要實作稱為 Messenger
的 trait 。
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
上述實作當中可以看見 Messenger
trait 當中有個 method 稱為 send
,傳入該 method 的參數是一個 self
的 immutable reference 和一個訊息的字串。該 trait 是我們的 mock object 需要實作的 interface 。另外就是我們想要測試 LimitTracker
當中 set_value
method 的行為。該 method 會吃進一個稱為 value
的參數,但並不會回傳任何東西,因此我們無法做 assertion 。我們希望有個方法可以判斷當我們建立一個 LimitTracker
和一個實作 Messenger
trait 的型別,和一個特定的 max
值時,我們傳送不同的 value
時,該 messenger 會傳送對應的訊息。
我們需要的 mock object 不需要傳送 email 或簡訊,僅僅需要追蹤在 send
被呼叫時被傳送的訊息內容。我們需要做的就是實作一個該 mock object 的實體,並且建立一個使用該 mock object 的 LimitTracker
,之後呼叫 set_value
method 並檢查 mock object 得到的訊息是否是我們所預期的。以下是一個可能的實作,儘管 borrow checker 不會讓他通過編譯
#[cfg(test)]
mod tests {
use super::*;
struct MockMessenger {
sent_messages: Vec<String>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: vec![],
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.len(), 1);
}
}
該測試程式利用 Vec
來追蹤傳送的訊息,同時定義一個 associated function new
用來建立新的 MockMessenger
。之後為 MockMessenger
實作一個 Messenger
trait 如此一來就可以把 MockMessenger
指派給 LimitTracker
。在 send
method 的實作當中,我們將傳送的訊息存入 MockMessenger
的陣列當中。
但如果嘗試編譯會出現以下錯誤
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
--> src/lib.rs:58:13
|
58 | self.sent_messages.push(String::from(message));
| ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference
|
2 | fn send(&mut self, msg: &str);
| ~~~~~~~~~
For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker` (lib test) due to 1 previous error
由於 send
method 取得的參數是一個 self
的 immutable reference ,因此我們無法更改 MockMessenger
來追蹤訊息。由於原本 Messenger
當中 send
method 的形式就是取得 self
的 immutable reference ,因此我們無法更改它成為 mutable reference 。
此時 interior mutability 就能派上用場,將 sent_messages
存在 RefCell<T>
當中,之後 send
method 就可以改變 sent_messages
來將訊息存入。
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.borrow_mut().push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
// --snip--
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
這個版本的實作當中, sent_messages
的型別從 Vec<String>
變成 RefCell<Vec<String>>
。在 send
method 的實作當中,第一個參數依然是 self
的 immutable reference 。不過我們可以透過在 RefCell<Vec<String>>
上呼叫 borrow_mut
來取得 RefCell<Vec<String>>
的 mutable reference ,也就是一個 vector 。
最後在進行 assertion 時,我們呼叫 borrow
來取得陣列的 immutable reference 。
RefCell<T>
我們利用 &
, &mut
來建立 immutable 和 mutable references 。而處理 RefCell<T>
時我們則是透過呼叫 borrow
和 borrow_mut
methods ,它們都屬於 RefCell<T>
的 safe API 。 borrow
method 會回傳一個型別為 Ref<T>
的 smart pointer ,而 borrow_mut
會回傳一個型別為 RefMut<T>
的 smart pointer 。兩種型別都有實作 Deref
,所以處理它們的方式可以和普通的 reference 相同。
RefCell<T>
會追蹤當前有多少個 Ref<T>
和 RefMut<T>
smart pointers 為有效的。每次呼叫 borrow
時 RefCell<T>
都會增加 immutable reference 的數量,當 Ref<T>
因為離開有效範圍而失效時,該計數會減一。而 RefCell<T>
和編譯時期的 borrow rules 相同,可以同時有數個 immutable borrows 或者一個 mutable borrow 。
如果違反此規定,則 RefCell<T>
會在執行時期產生 panic ,例如以下的程式碼
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
let mut one_borrow = self.sent_messages.borrow_mut();
let mut two_borrow = self.sent_messages.borrow_mut();
one_borrow.push(String::from(message));
two_borrow.push(String::from(message));
}
}
one_borrow
和 two_borrow
的作用範圍是相同的,而這不被允許,因為一個時間點只能有一個 mutable reference ,因此該程式碼雖然會通過編譯,但執行時會產生以下錯誤
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
Running unittests src/lib.rs (target/debug/deps/limit_tracker-e599811fa246dbde)
running 1 test
test tests::it_sends_an_over_75_percent_warning_message ... FAILED
failures:
---- tests::it_sends_an_over_75_percent_warning_message stdout ----
thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at src/lib.rs:60:53:
already borrowed: BorrowMutError
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::it_sends_an_over_75_percent_warning_message
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
注意到錯誤訊息當中的 already borrowed: BorrowMutError
,這就是 RefCell<T>
在執行時期處理 borrowing rules 的方式。
Rc<T>
and RefCell<T>
通常使用 RefCell<T>
都會伴隨使用 Rc<T>
, Rc<T>
的特色是使得一份資料可以被多個持有者持有,但它只給予 immutable access ,但如果有一個 Rc<T>
裡面包著一個 RefCell<T>
,則我們可以讓一份資料在同時有多個 owners 時又能被修改。
拿之前的 cons list 為例,我們利用 Rc<T>
使得一個節點可以有多個持有者。而由於 Rc<T>
持有的是 immutable values ,一但建立鍵結串列後我們就無法改變其中的值。若加上 RefCell<T>
就能做到這件事
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let value = Rc::new(RefCell::new(5));
let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));
let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));
*value.borrow_mut() += 10;
println!("a after = {a:?}");
println!("b after = {b:?}");
println!("c after = {c:?}");
}
此處的 value
是一個 Rc<RefCell<i32>>
的實體,之後我們建立的串列 a
持有 value
。此處進行 Rc::clone
的原因是為了讓 value
和 a
都持有該值,而非將 ownership 從 value
轉移給 a
。
這段程式碼編譯並執行的結果如下
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.63s
Running `target/debug/cons-list`
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))
Rust 對於記憶體管理的嚴格程度使得產生 memory leak 相較其他語言較為困難,但並不保證不會發生 memory leak ,在 Rust 當中, memory leak 是 memory safe 的。通過 Rc<T>
和 RefCell<T>
就有可能發生,當 items 之間互相 refer 並形成一個 cycle 時,會造成每個 item 的 reference count 永遠不可能達到 0 ,因此記憶體永遠不被釋放。
以下我們建立一個 List
enum 以及一個 tail
method
use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
enum List {
Cons(i32, RefCell<Rc<List>>),
Nil,
}
impl List {
fn tail(&self) -> Option<&RefCell<Rc<List>>> {
match self {
Cons(_, item) => Some(item),
Nil => None,
}
}
}
fn main() {}
此處 RefCell<Rc<List>>
代表的是我們具有改變 Cons
list 指向的下一個節點的能力, tail
method 則是提供存取 Cons
variant 的第二個 item 的能力。
接下來實作 main function 當中的內容,建立一個鍵結串列在 a
, b
當中,接下來讓 b
指向 a
,再改變 a
指向的鍵結串列為 b
,形成一個 reference cycle 。
fn main() {
let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
println!("a initial rc count = {}", Rc::strong_count(&a));
println!("a next item = {:?}", a.tail());
let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));
println!("a rc count after b creation = {}", Rc::strong_count(&a));
println!("b initial rc count = {}", Rc::strong_count(&b));
println!("b next item = {:?}", b.tail());
if let Some(link) = a.tail() {
*link.borrow_mut() = Rc::clone(&b);
}
println!("b rc count after changing a = {}", Rc::strong_count(&b));
println!("a rc count after changing a = {}", Rc::strong_count(&a));
// Uncomment the next line to see that we have a cycle;
// it will overflow the stack
// println!("a next item = {:?}", a.tail());
}
如果編譯此段程式碼,可以看到以下輸出
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.53s
Running `target/debug/cons-list`
a initial rc count = 1
a next item = Some(RefCell { value: Nil })
a rc count after b creation = 2
b initial rc count = 1
b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
b rc count after changing a = 2
a rc count after changing a = 2
此時我們的鍵結串列長的如下圖
當 main function 結束時,首先 Rust 會把 b
給 drop ,此時 b
原本指向的 Rc<List>
的 reference count 會從 2 變為 1 。所以該 Rc<List>
在 heap 上佔有的記憶體並不會被釋放,對於 a
來說也一樣,所以 list 配置的記憶體空間永遠不會被釋放。
Rc<T>
into a Weak<T>
目前為止我們了解 Rc::clone
會增加 Rc<T>
的 strong_count
,而 Rc<T>
物件只會在 strong_count
變為 0 時才會被清除。如果利用 Rc::downgrade
則可以對 Rc<T>
實體建立 weak reference 。
Strong reference 代表的是如何共享一個 Rc<T>
實體的 ownership ,而 weak reference 不代表 ownership relationship ,它的 count 也不會影響 Rc<T>
實體是否被清除與否,一旦 strong reference count 變為 0 ,即時 weak reference 行程 cycle 也會立刻被打斷。
呼叫 Rc::downgrade
後會得到一個型別為 Weak<T>
的 smart pointer ,同時也將 weak_count
增加 1 ( strong_count
不會被影響 )。
由於 Weak<T>
reference 指向的值可能已經被 drop 了,因此在使用時我們需要自行確保該值還是有效的。透過在 Weak<T>
實體上呼叫 upgrade
,我們可以得到一個 Option<Rc<T>>
。如果 Rc<T>
的值還沒被釋放,則我們會得到 Some
,若已經被釋放則會得到 None
。而且 Rust 會自己處理不同的情況。
Node
with Child Nodes首先建立一個結構稱為 Node
,自己擁有一個 i32
型別的值,同時有指向自己 children 的 reference 。
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Node {
value: i32,
children: RefCell<Vec<Rc<Node>>>,
}
我們希望每個 Node
都可以持有自己的 children ,並且這些 ownership 是可以被共享的,如此一來就可以有直接存取樹當中每個 Node
的能力。
因此我們將 children 定義為 Rc<Node>
並將它們放在 Vec<T>
當中。同時我們希望可以改變 children 的組成,因此我們將 Vec<Rc<Node>>
放在 RefCell<T>
當中。
接下來就可以利用我們的 struct definition 來建構實體,首先建立一個稱為 leaf
的實體,另一個 branch
則是將 leaf
作為自己的 children 。
fn main() {
let leaf = Rc::new(Node {
value: 3,
children: RefCell::new(vec![]),
});
let branch = Rc::new(Node {
value: 5,
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
}
注意到我們對 leaf
進行 clone ,因此目前 leaf
有兩個 owners ,分別是 leaf
和 branch
。同時我們希望 leaf
也可以存取到自己的親代節點也就是 branch
,因此我們進行以下變更。
此處的困難點是如果我們將 parent
field 的型別設為 Rc<T>
,則我們會製造出 reference cycle 造成 memory leak 。
仔細思考 parent 和 children 的關係, parent 應該持有它的 children ,而當 parent 被 drop 時,它的 children 也應該被 drop 。但 child 不應該持有自己的 parent ,如果 child 被 drop , parent 應該還是存在,此時就適合使用 weak reference 。
因此與其將 parent
設為 Rc<T>
型別,我們將它設為 Weak<T>
,更精確的說是 RefCell<Weak<Node>>
。
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
如此一來 children node 可以存取到自己的親代節點,但不會持有它。
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}
此處建立 leaf
節點的形式和前段很像,但這次多了 parent
field ,一開始 parent field 沒有東西,因此我們放一個空的 Weak<Node>
reference instance 在那。此時若我們嘗試利用 upgrade
method 取得 leaf
的親代節點,我們會得到 None
value 。
建立 branch
節點時同樣會在 parent
field 擺放一個 Weak<Node>
,之後我們可以將 leaf
的 parent 指向 branch
。我們在原本 leaf.parent
上呼叫 borrow_mut
method 之後呼叫 Rc::downgrade
來建立一個指向 branch
的 Weak<Node>
reference 。
(TODO: The Rustonnomicon)