2020年6月29日

rust19_Advanced Features

這邊是特定狀況下會遇到的 rust 功能,但很少遇到

  • Unsafe Rust: 用於需要捨棄 Rust 的某些保證,由 programmer 自己負責手動維持這些保證的時候
  • Advanced traits: 與 trait 相關的關聯類別,默認類別參數,完全限定語法(fully qualified syntax),supertraits 和 newtype 模式
  • Advanced types: 更多關於 newtype 模式的內容,類別別名,never 類別和動態大小類別
  • Advanced functions and closures: 函數指針和回傳closure
  • Macros: 在編譯時定義更多 code 的方式

Unsafe Rust

到目前為止,討論的 rust 都會在編譯時強制執行 memory safe 檢查與保證。但 rust 還隱藏另一個語言,不會強制執行 memory safe 保證,稱為 unsafe rust

unsafe rust 存在的原因,是因為靜態分析是保守的,當 compiler 能提供 code 的保證時,拒絕會比接受某些無效程式要好。這表示有時候 code 是合法的,但被拒絕。在這種情況下,可用 unsafe rust 告訴 compiler 這個程式是正確的。

另一個原因是,底層硬體的不安全性。如果 rust 不支援不安全的 operation,就無法完成某些工作,因為 rust 需要跟 OS 溝通。

unsafe superpowers

可用 unsafe 關鍵字來切換到 unsafe rust,然後就能開始一段 unsafe code,有四種可以在 unsafe rust 執行,但不能用在 safe rust 的操作:

  • Dereference a raw pointer
  • 呼叫 unsafe function or method
  • 存取或修改 a mutable static variable
  • 實作 an unsafe trait

unsafe 並不會關閉 borrow checker 或禁用其他 rust 安全檢查,如果在 unsafe code 使用引用,仍會被檢查。unsafe 只提供那四個不會被 compiler 檢查的 memory safe 功能。

unsafe 不代表該區塊的 code 就一定是危險並存在 memory safe 問題,其作用是由 programmer 確保該 code 會用有效方式使用 memory。

錯誤還是有可能會發生,但發生 memory safe 問題時,就知道一定是在 unsafe 區塊。

盡可能隔離 unsafe code,可封裝到一個安全的抽象並提供安全 API。standard library 的一部分被實現為,在被評審過 unsafe code 的安全抽象。這樣的封裝,可確保使用安全抽象進行 unsafe code。

Dereference a raw pointer

chap 4 的 dangling pointer 部分提到,compiler 會確保引用永遠有效。unsafe rust 有兩個稱為 raw pointer 的類似引用的新類別。跟引用一樣,raw pointer 是可變或不可變的,分別寫作 *const T*mut T。這裡的 * 不是 dereference,他是類別名稱的一部分,在 raw pointer 的 context 中,immutable 就表示 pointer 被 dereference 後,不能直接賦值。

raw pointer 跟 reference & smart pointer 的差別:

  • 允許忽略 borrowing rules,對同一個記憶體位置,可以有 immutable 及 mutable pointer 或多個 mutable pointers
  • 不保證會指向有效的 memory
  • 可以是 null
  • 沒有實作任何自動清理的功能

透過去除 rust 強制的保證,可放棄安全保證,以換取性能,或使用另一個語言或硬體 API 的能力。

從引用可同時產生 immutable 及 mutable raw pointer

let mut num = 5;

let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;

可在安全code 中產生 raw pointer,但不能在 unsafe code 中,dereference raw pointer

as 可將 immutable/mutable reference 轉換為對應的 raw pointer。因為是從安全的 reference 產生 raw pointer,所以可知道這些 raw pointer 是有效的,但不能對所有 raw pointer 都做這樣的假設。

以下是產生一個指向任意 memory address 的 raw pointer,嘗試使用任意的 memory address 是未定義的行為,該位址可能完全沒有任何資料,compiler 可能會優化這個 memory acesss,或是出現 segmentation fault。

let address = 0x012345usize;
let r = address as *const i32;

以下是在 unsafe code 中,dereference raw pointer

let mut num = 5;

let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;

unsafe {
    println!("r1 is: {}", *r1);
    println!("r2 is: {}", *r2);
}

產生 raw pointer 不會造成問題,只有存取其指向的值,才有可能遇到無效的值

上例同時產生了指向相同位址 num 的 raw pointers: *const i32*mut i32 。但如果嘗試產生 num 的不可變及可變引用,會因為 rust 所有權規則不允許擁有可變引用,同時擁有不可變引用,而無法編譯。

透過 raw pointer 可產生同一個位址的可變 pointer 及不可變 pointer,要注意透過可變 pointer 修改資料時,可能會造成 race condition。

raw pointer 主要用在呼叫 C 語言 API。另外是處理 borrow checker 無法理解的安全抽象,以下先介紹不安全函數。

呼叫 unsafe function or method

呼叫 unsafe function/method 跟一般 function 一樣,開頭有個 unsafe

以下是沒有做任何操作的 unsafe function 例子,要將 dangerous 呼叫插入 unsafe 區塊

unsafe fn dangerous() {}

unsafe {
    dangerous();
}

產生 unsafe code 的安全抽象

雖函數包含 unsafe code 不代表整個函數都是 unsafe。常見的方法是將 unsafe code 封裝到安全函數中。例如 std library 裡面的 split_at_mut,裡面需要一些 unsafe code。該函數定義於可變 slice 之上,可將該 slice 從給定的索引開始,分成兩個 slice,使用安全的 split_at_mut 方式如下:

let mut v = vec![1, 2, 3, 4, 5, 6];

let r = &mut v[..];

let (a, b) = r.split_at_mut(3);

assert_eq!(a, &mut [1, 2, 3]);
assert_eq!(b, &mut [4, 5, 6]);

但該函數無法透過安全 rust 實作。以下是失敗的程式,無法編譯

fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = slice.len();

    assert!(mid <= len);

    (&mut slice[..mid],
     &mut slice[mid..])
}

函數首先獲取 slice 的長度,然後通過檢查參數是否小於或等於這個長度來確認參數所給定的索引位於 slice 當中。該 assert 意味著如果傳入的索引比要分割的 slice 的索引更大,此函數在嘗試使用這個索引前 panic。後來我們在一個 tuple 中回傳兩個可變的 slice:一個從原始 slice 的開頭直到 mid 索引,另一個從 mid 直到原 slice 的結尾。

編譯錯誤

error[E0499]: cannot borrow `*slice` as mutable more than once at a time
 -->
  |
6 |     (&mut slice[..mid],
  |           ----- first mutable borrow occurs here
7 |      &mut slice[mid..])
  |           ^^^^^ second mutable borrow occurs here
8 | }
  | - first borrow ends here

rust 的 borrow check 無法理解要借用 slice 的兩個不同部分,它只知道我們借用了同一個 slice 兩次。

但其實可借用 slice 的不同部分,因為兩個 slice 不會重疊,但 rust compiler 無法理解這一段程式。現在就是要用 unsafe code 的時候

這邊使用 unsafe code 實作這個功能

use std::slice;

fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = slice.len();
    let ptr = slice.as_mut_ptr();

    assert!(mid <= len);

    unsafe {
        (slice::from_raw_parts_mut(ptr, mid),
         slice::from_raw_parts_mut(ptr.offset(mid as isize), len - mid))
    }
}

slice 是一個 pointer,並帶有該 slice 的長度。可用 len 方法獲取 slice 的長度,使用 as_mut_ptr 方法取得 slice 的 raw pointer。因為有一個 i32 值的可變 slice,as_mut_ptr 回傳一個 *mut i32 類別的raw pointer,存在 ptr 變數中。保持索引 mid 位於 slice 中的 assert。

接著是不安全代碼:slice::from_raw_parts_mut 函數獲取一個 raw pointer 和一個長度來產生一個 slice。這裡使用此函數從 ptr 中產生了一個有 mid 個項的 slice。之後在 ptr 上呼叫 offset 方法並使用 mid 作為參數來獲取一個從 mid 開始的raw pointer,並以 mid 之後項的數量為長度產生一個 slice。

slice::from_raw_parts_mut 函數是不安全的因為它獲取一個raw pointer,並必須確信這個指針是有效的。raw pointer 的 offset 方法也是不安全的,因為其必須確信此地址偏移量也是有效的指針。

必須將 slice::from_raw_parts_mutoffset 放入 unsafe 塊中以便能呼叫它們。通過觀察代碼,和增加 mid 必然小於等於 len 的斷言,我們可以說 unsafe 塊中所有的 raw pointer 將是有效的 slice 中資料的指針。這是一個可以接受的 unsafe 的用法。

不需要將 split_at_mut 函數的結果標記為 unsafe,並可以在安全 Rust 中調用此函數。我們創建了一個 unsafe code 的安全抽象,該 code 以一種安全的方式使用了 unsafe 代碼,因為其只從這個函數訪問的資料中產生了有效的指針。


slice::from_raw_parts_mut 在使用 slice 時很有可能會 crash,這段代碼獲取任意內存地址並產生了一個長為一萬的 slice

use std::slice;

let address = 0x01234usize;
let r = address as *mut i32;

let slice : &[i32] = unsafe {
    slice::from_raw_parts_mut(r, 10000)
};

我們並不擁有這個任意地址的memory,也不能保證這段代碼產生的 slice 包含有效的 i32 值。試圖使用猜測為有效的 slice 會導致未定義的行為。如果我們沒有注意將 address 向 4(字節)對齊(i32 的對齊方式),那麼甚至呼叫 slice::from_raw_parts_mut 已經是為定義行為了 —— slice 必須總是對齊的,即使它沒有被使用(哪怕甚至為空)。


使用 extern 呼叫外部 API

extern 用來產生及使用 Foeign Function Interface, FFI。

以下是使用 C standard library abs function 的例子。extern 在 rust 永遠是 unsafe 的。

extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!("Absolute value of -3 according to C: {}", abs(-3));
    }
}

extern "C" 塊中,列出了我們希望能夠呼叫的另一個語言中的外部函數的簽名和名稱。"C" 部分定義了外部函數所使用的應用程序接口application binary interface,ABI), ABI 定義了如何在 assembly language 層呼叫此函數。"C" ABI 是最常見的,並遵循 C 語言的 ABI。

從其他程式語言呼叫 rust

也可以用 extern 產生讓其他語言呼叫 rust function 的介面。在 fn 關鍵字前面增加 extern,並指定所用到的 ABI,再增加 #[no_mangle] 註解告訴 compiler 不要 mangle 這個函數的名稱。

mangling 發生在 compiler 修改函數名稱的時候,這會增加用在其他編譯過程的資訊,但會讓名稱較難閱讀,不同語言的 compiler 都會用不同方式 mangle 函數名稱,為使 rust fn 能讓其他語言呼叫,必須禁用 rust name mangling。

以下實例,當編譯為動態 lib,並連接到 C 語言,就能從 C 呼叫 call_from_c,使用 extern 不需要 unsafe

#[no_mangle]
pub extern "C" fn call_from_c() {
    println!("Just called a Rust function from C!");
}

存取或修改 a mutable static variable

到目前為止,一直避免討論 global variable,但 rust 支援這個功能,但對 ownership 規則來說,是有問題的。如果有兩個 threads 使用相同的 mutable global variable,可能會造成 race condition。

gloabl variable 在 rust 被稱為 static variable,以下是一個 string slice 的 static variable

static HELLO_WORLD: &str = "Hello, world!";

fn main() {
    println!("name is: {}", HELLO_WORLD);
}

通常靜態變數的名稱採用 SCREAMING_SNAKE_CASE 寫法,並 必須 標註變數的類別,在這個例子中是 &'static str。靜態變數只能儲存擁有 'static 生命週期的引用,這表示 Rust compiler 可以自己計算出其生命週期而無需直接標註。使用不可變靜態變數是安全的。

常數與不可變靜態變數可能看起來很類似,不過一個微妙的區別是靜態變數中的值有一個固定的內存地址。使用這個值總是會在相同的地址。另外,常數則允許在任何被用到的時候複製其數據。

常數與靜態變數的另一個區別在於靜態變數可以是可變的。訪問和修改可變靜態變數都是 不安全的。以下例子是如何宣告、訪問和修改名為 COUNTER 的可變靜態變數:

static mut COUNTER: u32 = 0;

fn add_to_count(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}

fn main() {
    add_to_count(3);

    unsafe {
        println!("COUNTER: {}", COUNTER);
    }
}

就像一般變數一樣,我們使用 mut 來指定可變性。任何讀寫 COUNTER 的 code 都必須位於 unsafe 塊中。這段 code 可以編譯並列印出 COUNTER: 3,因為這是單線程的。擁有多個線程使用 COUNTER 則可能導致數據競爭。

擁有可以全局訪問的可變數據,難以保證不存在數據競爭,這就是為何 Rust 認為可變靜態變數是不安全的。任何可能的情況,請優先使用第十六章討論的並發技術和線程安全智能指針,這樣編譯器就能檢測不同線程間的資料存取是安全的。

實作 an unsafe trait

當至少有一個方法中包含編譯器不能驗證的 invariant 時,trait 是不安全的。可以在 trait 之前增加 unsafe 關鍵字將 trait 聲明為 unsafe,同時 trait 的實現也必須標記為 unsafe

unsafe trait Foo {
    // methods go here
}

unsafe impl Foo for i32 {
    // method implementations go here
}

透過 unsafe impl,我們將支援編譯器所不能驗證的 invariants。

chap 16 “使用 SyncSend trait 的可擴展並發” 部分中的 SyncSend 標記 trait,編譯器會自動為完全由 SendSync 類別組成的類別自動實現他們。如果實作了一個包含一些不是 SendSync 的類別,比如 raw pointer,並希望將此類別標記為 SendSync,則必須使用 unsafe。Rust 不能驗證我們的類別保證可以安全的跨線程發送或在多線程間使用,所以需要我們自己進行檢查並通過 unsafe 表明。

Advanced Traits

在 trait 定義中,以 associated types 指定為 placeholder types

associated types 是一種將 type placeholder 跟 trait 建立關係的方法,這樣就能在 trait function 定義中使用這些 placeholder types。trait 的實作者,可在 type 指定具體的類別,並用於特殊的 function 實作。可定義一個使用多個類別的 trait,一直要到實作這個 trait 以前,都不需要確切知道使用了哪些類別。

associated type 在本章是比較常見的功能,但比其他章節的內容少見。

一個帶有 associated types 的 trait 的例子是標準庫提供的 Iterator trait。它有一個叫做 Item 的associated type 來替代遍歷的值的類別。chap 13的 “Iterator trait 和 next 方法” 部分曾提到過 Iterator trait 的定義:

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}

Item 是一個佔位類別,同時 next 方法定義表明它返回 Option<Self::Item> 類別的值。這個 trait 的實作者會指定 Item 的具體類別,但不管指定為何種類別,next 方法都會回傳一個包含了此具體類別值的 Option

關聯類別看起來類似泛型的概念,因為它允許定義一個函數而不指定其可以處理的類別。那麼為什麼要使用關聯類別呢?讓我們通過一個在 chap 13 中出現的 Counter struct 上實作 Iterator trait 的例子來檢視其中的區別。在示例 13-21 中,指定了 Item 的類別為 u32

impl Iterator for Counter {
    // 指定 Item 的類別為 u32
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        // --snip--

那麼為什麼 Iterator trait 不這樣定義呢?

pub trait Iterator<T> {
    fn next(&mut self) -> Option<T>;
}

區別在於使用泛型時,則不得不在每一個實現中標註類別。這是因為我們也可以實現為 Iterator<String> for Counter,或任何其他類別,這樣就可以有多個 CounterIterator 的實作。

換句話說,當 trait 有泛型參數時,可以多次實現這個 trait,每次需改變泛型參數的具體類別。接著當使用 Counternext 方法時,必須提供類別註解來表明希望使用 Iterator 的哪一個實現。

通過關聯類別,則無需標註類別因為不能多次實現這個 trait。如果是使用關聯類別的定義,我們只能選擇一次 Item 會是什麼類別,因為只能有一個 impl Iterator for Counter。當呼叫 Counternext 時不必每次指定我們需要 u32 值的 iterator。

Default Generic Type Parameters and Operator Overloading

當使用泛型參數時,可以為泛型指定一個預設的具體類別。如果默認類別就足夠的話,這消除了為具體類別實現 trait 的需要。為泛型類別指定默認類別的語法是在宣告泛型類別時使用 <PlaceholderType=ConcreteType>

這種情況的一個非常好的例子是用於運算符重載。運算符重載Operator overloading)是指在特定情況下自定義運算符(比如 +)的行為。

Rust 並不允許產生自定義運算符或重載任意運算符,不過 std::ops 中所列出的運算符和相應的 trait 可以透過實現運算符相關 trait 來重載。例如,以下展示了如何在 Point struct 上實現 Add trait 來重載 + 運算符,這樣就可以將兩個 Point 相加了:

use std::ops::Add;

#[derive(Debug, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

// 實作 Add trait 重載 Point instance 的 + 運算符
impl Add for Point {
    type Output = Point;

    // add 將兩個 Point 的 x 值和 y 值分別相加來產生一個新的 Point
    // Add trait 有一個叫做 Output 的關聯類別,它用來決定 add 方法的回傳值類別。
    fn add(self, other: Point) -> Point {
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

fn main() {
    assert_eq!(Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
               Point { x: 3, y: 3 });
}

這是 Add trait 的定義

trait Add<RHS=Self> {
    type Output;

    fn add(self, rhs: RHS) -> Self::Output;
}

這是一個帶有一個方法和一個關聯類別的 trait。比較陌生的部分是尖括號中的 RHS=Self:這個語法叫做 默認類別參數default type parameters)。RHS 是一個泛型類別參數(“right hand side” 的縮寫),它用於定義 add 方法中的 rhs 參數。如果實作 Add trait 時不指定 RHS 的具體類別,RHS 的類別將是默認的 Self 類別,也就是在其上實現 Add 的類別。

當為 Point 實作 Add 時,使用了默認的 RHS,因為我們希望將兩個 Point instance 相加。讓我們看看一個實作 Add trait 時希望自定義 RHS 類別而不是使用默認類別的例子

use std::ops::Add;

// 有兩個存放不同單元值的 structs: Millimeters 和 Meters
// 能夠將毫米值與米值相加,並讓 Add 的實作正確處理轉換
// 在 Millimeters 上實作 Add,以便能夠將 Millimeters 與 Meters 相加
struct Millimeters(u32);
struct Meters(u32);

// 指定 impl Add<Meters> 來設定 RHS 參數的值而不是使用默認的 Self
impl Add<Meters> for Millimeters {
    type Output = Millimeters;

    fn add(self, other: Meters) -> Millimeters {
        Millimeters(self.0 + (other.0 * 1000))
    }
}

默認參數類別主要用於如下兩個方面:

  • 擴展類別而不破壞現有代碼。
  • 在大部分用戶都不需要的特定情況下自訂。

standard library 的 Add trait 就是一個第二個目的例子:大部分時候你會將兩個相似的類別相加,不過它提供了自訂額外行為的能力。

Add trait 定義中使用默認類別參數意味著大部分時候無需指定額外的參數。換句話說,不需要任何實作樣板,這樣使用 trait 就更容易了。

第一個目的很類似,但過程是反過來的:如果需要為現有 trait 增加類別參數,為其提供一個默認類別將允許我們在不破壞現有實現代碼的基礎上擴展 trait 的功能。

Fully Qualified Syntax for Disambiguation: Calling Methods with the Same Name 完全限定語法用以消除歧義:呼叫相同名稱的methods

Rust 不能避免一個 trait 與另一個 trait 擁有相同名稱的方法,也不能阻止為某一個類別同時實現這兩個 trait。甚至有可能直接在類別上實現開始已經有的同名方法!

不過,當呼叫這些同名方法時,需要告訴 Rust 我們希望使用哪一個。以下這裡定義了 trait PilotWizard 都擁有方法 fly。接著在一個本身已經實作了名為 fly 方法的類別 Human 上實現這兩個 trait。每一個 fly 方法都進行了不同的操作:

// 兩個 trait 定義為擁有 fly 方法,並在直接定義有 fly 方法的 Human 類別上實作這兩個 trait
trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}


fn main() {
    let person = Human;
    // 呼叫 Human instance 的 fly 是呼叫類別的 method
    person.fly();

    // 在方法名稱前面,指定 trait 名稱,告訴 Rust 希望呼叫哪個 fly 實現
    Pilot::fly(&person);
    Wizard::fly(&person);
}

執行結果

*waving arms furiously*
This is your captain speaking.
Up!

關聯函數是 trait 的一部分,但沒有 self 參數。當同一作用域的兩個類別實現了同一 trait,Rust 就不能計算出我們期望的是哪一個類別,除非使用 完全限定語法fully qualified syntax)。例如,Animal trait 來說,它有關聯函數 baby_nameDog 實作了 Animal,同時有關聯函數 baby_name 直接定義於 Dog 之上:

// 個帶有關聯函數的 trait 和一個帶有同名關聯函數並實作了此 trait 的類別 Dog
trait Animal {
    fn baby_name() -> String;
}

struct Dog;

impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

fn main() {
    // 在 main 呼叫 Dog::baby_name 函數,它直接呼叫了定義於 Dog 之上的關聯函數。
    println!("A baby dog is called a {}", Dog::baby_name());

    // 指定 trait 名稱  沒有用
    // 因為 Animal::baby_name 是關聯函數而不是方法,因此它沒有 self 參數,Rust 無法計算出所需的是哪一個 Animal::baby_name 實現
    //println!("A baby dog is called a {}", Animal::baby_name());

    // 使用 完全限定語法,在尖括號中向 Rust 提供了類別註解
    println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}

完全限定語法的定義:

<Type as Trait>::function(receiver_if_method, next_arg, ...);

對於關聯函數,因沒有一個 receiver,故只會有其他參數的列表。可以選擇在任何函數或方法呼叫處使用完全限定語法。但允許省略任何 Rust 能夠從程序中的其他信息中計算出的部分。只有當存在多個同名實現而 Rust 需要幫助以便知道我們希望呼叫哪個實現時,才需要使用這個較為冗長的語法。

Using Supertraits to Require One Trait’s Functionality Within Another Trait 在另一個 trait 使用父 trait,以使用另一個 trait 的功能

有時我們可能會需要某個 trait 使用另一個 trait 的功能。在這種情況下,需要實作一個能夠依賴相關trait的 trait。這個所需的 trait 是我們實現的 trait 的 父 traitsupertrait)。

例如我們希望產生一個帶有 outline_print 方法的 trait OutlinePrint,它會列印出帶有星號框的值。也就是說,如果 Point 實現了 Display 並返回 (x, y),呼叫以 1 作為 x3 作為 yPoint 實例的 outline_print 會顯示如下:

**********
*        *
* (1, 3) *
*        *
**********

outline_print 的實作中,因為希望能夠使用 Display trait 的功能,則需要說明 OutlinePrint 只能用於同時也實現了 Display 並提供了 OutlinePrint 需要的功能的類別。可以通過在 trait 定義中指定 OutlinePrint: Display 來做到這一點。這類似於為 trait 增加 trait bound。以下是一個 OutlinePrint trait 的實作:

use std::fmt;

trait OutlinePrint: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {} *", output);
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}

因為指定了 OutlinePrint 需要 Display trait,則可以在 outline_print 中使用 to_string, 其會為任何實現 Display 的類別自動實現。如果不在 trait 名後增加 : Display 並嘗試在 outline_print 中使用 to_string,則會得到一個錯誤說在當前作用域中沒有找到用於 &Self 類別的方法 to_string

如果嘗試在一個沒有實現 Display 的類別上實現 OutlinePrint 會得到一個錯誤說 Display 是必須的而未被實現

struct Point {
    x: i32,
    y: i32,
}

impl OutlinePrint for Point {}

編譯錯誤

error[E0277]: the trait bound `Point: std::fmt::Display` is not satisfied
  --> src/main.rs:20:6
   |
20 | impl OutlinePrint for Point {}
   |      ^^^^^^^^^^^^ `Point` cannot be formatted with the default formatter;
try using `:?` instead if you are using a format string
   |
   = help: the trait `std::fmt::Display` is not implemented for `Point`

一旦在 Point 上實現 Display 並滿足 OutlinePrint 要求的限制,那麼在 Point 上實現 OutlinePrint trait 將能成功編譯,並可以在 Point 實例上呼叫 outline_print 來顯示位於星號框中的點的值。

use std::fmt;

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

使用 Newtype Pattern 在 external type 實作 external traits

在 chap10 的 “為類別實作 trait” 部分,我們提到了孤兒規則(orphan rule),它說明只要 trait 或類別對於當前 crate 是本地的話就可以在此類別上實現該 trait。一個繞開這個限制的方法是使用 newtype pattern,它涉及到在一個 tuple struct 中產生一個新類別(chap 5 “用沒有命名欄位的 tuple struct 來產生不同的類別” 部分介紹了元組結構體)。

這個tuple struct 帶有一個欄位作為希望實現 trait 的類別的簡單封裝。接著這個封裝類別對於 crate 是本地的,這樣就可以在這個封裝上實現 trait。“Newtype” 是一個源自 Haskell 編程語言的概念。使用這個模式沒有執行期的性能懲罰,這個封裝類別在編譯時就被省略了。

例如,如果想要在 Vec<T> 上實現 Display,而 orphan rule 阻止我們直接這麼做,因為 Displaytrait 和 Vec<T> 都定義於我們的 crate 之外。可以產生一個包含 Vec<T> 實例的 Wrapper struct,接著可以在 Wrapper 上實現 Display 並使用 Vec<T> 的值:

use std::fmt;

// 產生 Wrapper 類別封裝 Vec<String> 以便能夠實作 Display
struct Wrapper(Vec<String>);

impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

fn main() {
    let w = Wrapper(vec![String::from("hello"), String::from("world")]);
    println!("w = {}", w);
}

Display 的實作使用 self.0 來訪問其內部的 Vec<T>,因為 Wrapper 是 tuple struct 而 Vec<T>是 struct,位於索引 0 的項。接著就可以使用 WrapperDisplay 的功能了。

此方法的缺點是,因為 Wrapper 是一個新類別,它沒有定義於其值之上的方法;必須直接在 Wrapper 上實現 Vec<T> 的所有方法,這樣就可以代理到self.0 上 —— 這就允許我們完全像 Vec<T> 那樣對待 Wrapper。如果希望新類別擁有其內部類別的每一個方法,為封裝類別實現 Deref trait(chap 15 “通過 Deref trait 將智能指針當作常規引用處理” 部分討論過)並返回其內部類別是一種解決方案。如果不希望封裝類別擁有所有內部類別的方法 —— 比如為了限制封裝類別的行為 —— 則必須只自行實現所需的方法。

上面便是 newtype 模式如何與 trait 結合使用的;還有一個不涉及 trait 的實用模式。現在讓我們將話題的焦點轉移到一些與 Rust 類別系統交互的高級方法上來吧。

Advanced Types

從一個關於為什麼 newtype patern 與類別一樣有用的更寬泛的討論開始。接著會轉向 type aliases,一個類似於 newtype 但有著稍微不同的語義的功能。我們還會討論 ! 類別和動態大小類別。

為了類別安全和抽象而使用 newtype pattern

newtype pattern 還可以用於一些其他我們還未討論的功能,包括靜態的確保某值不被混淆,和用來表示一個值的單元。實際上剛剛已經有一個這樣的例子:MillimetersMeters structs 都在 newtype 中封裝了 u32 值。如果撰寫了一個有 Millimeters 類別參數的函數,不小心使用 Meters 或普通的 u32 值來呼叫該函數的程序是不能編譯的。

另一個 newtype pattern 的應用在於抽象掉一些類別的實現細節:例如,封裝類別可以暴露出與直接使用其內部私有類別時所不同的公有 API,以便限制其功能。

newtype 也可以隱藏其內部的泛型類別。例如,可以提供一個封裝了 HashMap<i32, String>People 類別,用來儲存人名以及相應的 ID。使用 People 的 code 只需呼叫提供的公有 API 即可,比如向 People 集合增加名字的方法,這樣這些 code 就無需知道在內部我們將一個 i32ID 賦予了這個名字了。newtype pattern 是一種實現 chap17 “封裝隱藏了實現細節” 部分所討論的隱藏實現細節的封裝的輕量級方法。

以 type aliases 產生 type synonyms 類別同義詞

連同 newtype pattern,Rust 還提供了聲明 type alias ,使用 type 關鍵字來給予現有類別另一個名字。例如,可以像這樣產生 i32 的別名 Kilometers

// Kilometers 是 i32 的 同義詞 synonym
type Kilometers = i32;

let x: i32 = 5;
let y: Kilometers = 5;

// 因為 Kilometers 是 i32 的別名,他們是同一類別,可以將 i32 與 Kilometers 相加,也可以將 Kilometers 傳遞給獲取 i32 參數的函數
println!("x + y = {}", x + y);

type alias 的主要用途是減少重複。例如,可能會有這樣很長的類別:Box<dyn Fn() + Send + 'static>

let f: Box<dyn Fn() + Send + 'static> = Box::new(|| println!("hi"));

fn takes_long_type(f: Box<dyn Fn() + Send + 'static>) {
    // --snip--
}

fn returns_long_type() -> Box<dyn Fn() + Send + 'static> {
    // --snip--
}

類別別名通過減少項目中重複代碼的數量來使其更加易於控制。這裡我們為這個冗長的類別引入了一個叫做 Thunk 的別名,這樣就可以將所有使用這個類別的地方替換為更短的 Thunk

type Thunk = Box<dyn Fn() + Send + 'static>;

let f: Thunk = Box::new(|| println!("hi"));

fn takes_long_type(f: Thunk) {
    // --snip--
}

fn returns_long_type() -> Thunk {
    // --snip--
}

type alias 也經常與 Result<T, E> 結合使用來減少重複。考慮一下標準庫中的 std::io 模塊。I/O 操作通常會返回一個 Result<T, E>,因為這些操作可能會失敗。標準庫中的 std::io::Error 結構代表了所有可能的 I/O 錯誤。std::io 中大部分函數會返回 Result<T, E>,其中 Estd::io::Error,比如 Write trait 中的這些函數:

use std::io::Error;
use std::fmt;

pub trait Write {
    fn write(&mut self, buf: &[u8]) -> Result<usize, Error>;
    fn flush(&mut self) -> Result<(), Error>;

    fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>;
    fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<(), Error>;
}

這裡出現了很多的 Result<..., Error>。為此,std::io 有這個 type alias:

type Result<T> = Result<T, std::io::Error>;

因為這位於 std::io 中,可用的完全限定的別名是 std::io::Result<T> —— 也就是說,Result<T, E>E 放入了 std::io::ErrorWrite trait 中的函數最終看起來像這樣:

pub trait Write {
    fn write(&mut self, buf: &[u8]) -> Result<usize>;
    fn flush(&mut self) -> Result<()>;

    fn write_all(&mut self, buf: &[u8]) -> Result<()>;
    fn write_fmt(&mut self, fmt: Arguments) -> Result<()>;
}

類別別名有兩個優點:易於編寫 在整個 std::io 中提供了一致的接口。因為這是一個別名,它只是另一個 Result<T, E>,這意味著可以在其上使用 Result<T, E> 的任何方法,以及像 ? 這樣的特殊語法。

不會 return 的 never type

Rust 有一個叫做 ! 的特殊類別。在類別理論術語中,它被稱為 empty type,因為它沒有值。我們更傾向於稱之為 never type。這個名字描述了它的作用:在從不 return 的函數時候充當 return value。例如:

fn bar() -> ! {
    // --snip--
}

“函數 bar 從不 return",而從不返回的函數被稱為 發散函數diverging functions)。因無法產生 ! 類別的值,所以 bar 也不可能返回值。

不過一個不能產生的類別有什麼用呢?如果你回想一下示例 2-5 中的代碼,曾經有一些看起來像這樣的代碼:

// match 語句和一個以 continue 結束的分支
let guess: u32 = match guess.trim().parse() {
    Ok(num) => num,
    Err(_) => continue,
};

當時我們忽略了一些細節。在chap 6 “match 控制流運算符” 部分,我們學習了 match 的分支必須返回相同的類別。如下code不能運作:

let guess = match guess.trim().parse() {
    Ok(_) => 5,
    Err(_) => "hello",
}

這裡的 guess 必須既是 interger 也是 字符串,而 Rust 要求 guess 只能是一個類別。那麼 continue返回了什麼呢?為什麼示例 19-34 中會允許一個分支返回 u32 而另一個分支卻以 continue 結束呢?

continue 的值是 !。也就是說,當 Rust 要計算 guess 的類別時,它查看這兩個分支。前者是 u32 值,而後者是 ! 值。因為 ! 並沒有一個值,Rust 決定 guess 的類別型是 u32

描述 ! 的行為的正式方式是 never type,可以強轉為任何其他類別。允許 match 的分支以 continue 結束是因為 continue 並不真正返回一個值;相反它把控制權交回上層循環,所以在 Err 的情況,事實上並未對 guess 賦值。


never type 的另一個用途是 panic!。還記得 Option<T> 上的 unwrap 函數嗎?它產生一個值或 panic。這裡是它的定義:

impl<T> Option<T> {
    pub fn unwrap(self) -> T {
        match self {
            Some(val) => val,
            None => panic!("called `Option::unwrap()` on a `None` value"),
        }
    }
}

Rust 知道 valT 類別,panic!! 類別,所以整個 match 表達式的結果是 T 類別。這能工作是因為 panic! 並不產生一個值;它會終止程序。對於 None 的情況,unwrap 並不返回一個值,所以這些 code 是有效。


最後一個有著 ! 類別的表達式是 loop

print!("forever ");

loop {
    print!("and ever ");
}

循環永遠也不結束,所以此表達式的值是 !。但是如果引入 break 這就不為真了,因為循環在執行到 break 後就會終止。

動態大小的類別 與 Sized Trait

Rust 需要知道應該為特定類別的值分配多少空間這樣的資訊,其類別系統的有個特殊功能:這就是 動態大小類別dynamically sized types)的概念。這有時被稱為 “DST” 或 “unsized types”,這些類別允許我們處理只有在運行時才知道大小的類別。

讓我們深入研究一個貫穿本書都在使用的動態大小類別的細節:str。沒錯,不是 &str,而是 str 本身。str 是一個 DST;直到運行時我們都不知道字符串有多長。因為直到執行時都無法知道大小,也就是不能創建 str 類別的變數,也不能獲取 str 類別的參數。以下這些 code,他們無法運作:

let s1: str = "Hello there!";
let s2: str = "How's it going?";

Rust 需要知道應該為特定類別的值分配多少memory,同時所有同一類別的值必須使用相同數量的內存。如果允許編寫這樣的code,也就意味著這兩個 str 需要佔用完全相同大小的空間,不過它們有著不同的長度。這也就是為什麼不可能創建一個存放動態大小類別的變數的原因。

那麼該怎麼辦呢?你已經知道了這種問題的答案:s1s2 的類別是 &str 而不是 str。如果你回想第四章 “string slice” 部分,slice 儲存了開始位置和 slice 的長度。

所以雖然 &T 是一個儲存了 T 所在的內存位置的單個值,&str 則是 兩個 值:str 的地址和其長度。這樣,&str 就有了一個在編譯時可以知道的大小:它是 usize 長度的兩倍。

也就是說,我們會知道 &str 的大小,而無論其引用的string是多長。這是 Rust 中動態大小類別的用法:他們有一些額外的 metadata 來儲存動態大小的資訊。這引出了動態大小類別的黃金規則:必須將動態大小類別的值置於某種指針之後。

可以將 str 與所有類別的指針結合:比如 Box<str>Rc<str>。事實上,之前我們已經見過了,不過是這裡是另一個動態大小類別:trait。

每一個 trait 都是一個可以透過 trait 名稱來引用的動態大小類別。在chap17 “為使用不同類別的值而設計的 trait object” 部分,我們提到了為了將 trait 用於 trait object,必須將他們放在指針之後,比如 &TraitBox<Trait>Rc<Trait> 也可以)。trait 之所以是動態大小類別的是因為只有這樣才能使用它。

為了處理 dynamically sized types DST,Rust 有一個特定的 trait 用來決定一個類別的大小是否在編譯時可知:這就是 Sizedtrait。這個 trait 自動為編譯器提供在編譯時就知道大小的類別實現。另外,Rust 隱式的為每一個泛型函數增加了 Sized bound。也就是說,對於以下泛型函數定義:

fn generic<T>(t: T) {
    // --snip--
}

實際上被當作如下處理:

fn generic<T: Sized>(t: T) {
    // --snip--
}

泛型函數預設只能用於在編譯時已知大小的類別。然而可以使用如下特殊語法來放寬這個限制:

fn generic<T: ?Sized>(t: &T) {
    // --snip--
}

?Sized trait bound 與 Sized 相對;也就是說,它可以讀作 “T 可能是也可能不是 Sized 的”。這個語法只能用於 Sized ,而不能用於其他 trait。

另外注意我們將 t 參數的類別從 T 變為了 &T:因為其類別可能不是 Sized 的,所以需要將其置於某種指針之後。在這個例子中選擇了引用。

接下來,讓我們討論一下函數和閉包!

Advanced Functions and Closures

function pointers and returning closures

function pointers

我們討論過了如何向函數傳遞 closure;也可以向函數傳遞一般函數!這在我們希望傳遞已經定義的函數而不是重新定義 closure 作為參數是很有用。通過函數指針允許我們使用函數作為另一個函數的參數。函數的類別是 fn (使用小寫的 “f” )以免與 Fn 閉包 trait 相混淆。fn 被稱為 函數指針function pointer)。指定參數為函數指針的語法類似於閉包:

src/main.rs

fn add_one(x: i32) -> i32 {
    x + 1
}

// 使用 `fn` 類別接受函數指針作為參數
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
    f(arg) + f(arg)
}

fn main() {
    let answer = do_twice(add_one, 5);

    println!("The answer is: {}", answer);
}

這會印出 The answer is: 12do_twice 中的 f 被指定為一個接受一個 i32 參數並返回 i32fn。接著就可以在 do_twice 函數體中呼叫 f。在 main 中,可以將函數名 add_one 作為第一個參數傳遞給 do_twice

不同於閉包,fn 是一個類別而不是一個 trait,所以直接指定 fn 作為參數而不是宣告一個帶有 Fn作為 trait bound 的泛型參數。

函數指針實現了三種 closure trait(FnFnMutFnOnce),所以總是可以在呼叫期望閉包的函數時傳遞函數指針作為參數。傾向於編寫使用泛型和閉包 trait 的函數,這樣它就能接受函數或閉包作為參數。

一個只期望接受 fn 而不接受閉包的情況的例子是,與不存在閉包的外部 code 溝通時:C 語言的函數可以接受函數作為參數,但 C 語言沒有閉包。

另一個既可以使用內聯定義的閉包又可以使用命名函數的例子,讓我們看看一個 map 的應用。使用 map 函數將一個數字 vector 轉換為一個字符串 vector,就可以使用閉包,比如這樣:

let list_of_numbers = vec![1, 2, 3];
let list_of_strings: Vec<String> = list_of_numbers
    .iter()
    .map(|i| i.to_string())
    .collect();

或者可以將函數作為 map 的參數來代替閉包,像是這樣:

let list_of_numbers = vec![1, 2, 3];
let list_of_strings: Vec<String> = list_of_numbers
    .iter()
    .map(ToString::to_string)
    .collect();

注意這裡必須使用 “高級 trait” 部分講到的完全限定語法,因為存在多個叫做 to_string 的函數;這裡使用了定義於 ToString trait 的 to_string 函數,標準庫為所有實現了 Display 的類別實現了這個 trait。


另一個實用的模式暴露了元組結構體和元組結構體枚舉成員的實現細節。這些項使用 () 作為初始化語法,這看起來就像函數呼叫,同時它們確實被實現為回傳由參數構造的實例的函數。它們也被稱為實現了閉包 trait 的函數指針,並可以採用類似以下的方式呼叫:

enum Status {
    Value(u32),
    Stop,
}

let list_of_statuses: Vec<Status> =
    (0u32..20)
    .map(Status::Value)
    .collect();

有些人傾向於函數風格,有些人喜歡閉包。這兩種形式最終都會產生同樣的代碼,所以請使用對你來說更明白的形式吧。

returning closures

閉包表現為 trait,這意味著不能直接回傳 closure。對於大部分需要返回 trait 的情況,可以使用實現了期望回傳 trait 的具體類別來替代函數的返回值。但是這不能用於閉包,因為他們沒有一個可返回的具體類別;例如不允許使用函數指針 fn 作為返回值類別。

這段 code 嘗試直接返回閉包,它並不能編譯:

fn returns_closure() -> Fn(i32) -> i32 {
    |x| x + 1
}

編譯器給出的錯誤是:

error[E0277]: the trait bound `std::ops::Fn(i32) -> i32 + 'static:
std::marker::Sized` is not satisfied
 -->
  |
1 | fn returns_closure() -> Fn(i32) -> i32 {
  |                         ^^^^^^^^^^^^^^ `std::ops::Fn(i32) -> i32 + 'static`
  does not have a constant size known at compile-time
  |
  = help: the trait `std::marker::Sized` is not implemented for
  `std::ops::Fn(i32) -> i32 + 'static`
  = note: the return type of a function must have a statically known size

錯誤又一次指向了 Sized trait!Rust 並不知道需要多少空間來儲存閉包。不過我們在上一部分見過這種情況的解決辦法:可以使用 trait 對象:

fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
    Box::new(|x| x + 1)
}

這段code可以編譯。關於 trait 對象的更多內容,請回顧第 chap17 的 “為使用不同類別的值而設計的 trait 對象” 部分。

Macros

我們已經用過像 println! 這樣的 macro 了,不過還沒完全探索什麼是巨集以及它是如何工作的。巨集Macro)是 rust 的功能:

  • Declarative macro

    使用 macro_rules!

  • 三種 procedural macros

    • 自訂 #[derive] macros

      ​ 特定 code 增加 derive 屬性,用在 structs 與 enums

    • Attribute-like macros

      ​ 自訂屬性,可用在任意 items

    • Function-like macros

      ​ 看起來像 function calls,但用在作為參數的 tokens

什麼已經有了函數還需要巨集呢?

巨集和函數的差異

metaprogramming 對於減少大量編寫和維護的代碼是非常有用的,它也扮演了函數的角色。巨集有一些函數所沒有的附加能力。

一個函數標籤必須宣告函數參數個數和類別。巨集只接受一個可變參數:用一個參數呼叫 println!("hello") 或用兩個參數呼叫 println!("hello {}", name)

巨集可以在編譯器翻譯代碼前展開,例如,巨集可以在一個給定類別上實現 trait 。因為函數是在執行時被呼叫,同時 trait 需要在執行時實現,所以函數跟巨集不同。

實現一個巨集而不是函數的消極面是,巨集定義要比函數定義更複雜,因為你要編寫生成 Rust 代碼的 Rust 代碼。由於這樣的間接性,巨集定義通常要比函數定義更難閱讀、理解以及維護。

巨集和函數的最後一個重要的區別是:在呼叫巨集 之前 必須定義並將其引入作用域,而函數則可以在任何地方定義和呼叫。

使用 macro_rules! 的 declarative macros 用於 general mataprogramming

可使用 macro_rules! 來定義巨集。讓我們透過查看 vec! 巨集定義來探索,如何使用 macro_rules!結構。chap 8 說明了如何使用 vec! 巨集來生成一個給定值的 vector。例如,下面的巨集用三個整數產生一個 vector :

let v: Vec<u32> = vec![1, 2, 3];

也可以使用 vec! 巨集來產生兩個整數的 vector 或五個 string slice 的 vector 。但卻無法使用函數做相同的事情,因為我們無法預先知道參數值的數量和類別。

以下展示了一個 vec! 稍微簡化的定義。

src/lib.rs

#[macro_export]
macro_rules! vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}

19-36: 一個 vec! 巨集定義的簡化版本

注意:標準庫中實際定義的 vec! 包括預分配適當量的內存的代碼。這部分為 code optimization,為了簡化,此處並沒有包含在內。


無論何時導入定義了巨集的包,#[macro_export] annotation 應該是可用的。 如果沒有該註解,這個巨集不能被引入作用域。

接著使用 macro_rules! 和巨集名稱開始巨集定義,且所定義的巨集 不帶 感嘆號。名字後跟大括號表示巨集 body,在該例中巨集名稱是 vec

vec! 巨集的結構和 match 表達式的結構類似。此處有一個單邊模式 ( $( $x:expr ),* ) ,後跟 => 以及和模式相關的代碼塊。如果模式匹配,該相關代碼塊將被執行。假設這只是這個巨集中的模式,且只有一個有效匹配,其他任何匹配都是錯誤的。更複雜的巨集會有多個 arm。

巨集定義中有效模式語法和在 chap18 提及的模式語法是不同的,因為巨集模式所匹配的是 Rust 代碼結構而不是值。回過頭來檢查下 D-1 中模式片段什麼意思。對於全部的巨集模式語法,請查閱 macros 文件

首先,一對括號包含了全部模式。接下來是後跟一對括號的美元符號( $ ),其通過替代代碼捕獲了符合括號內模式的值。$() 內則是 $x:expr ,其匹配 Rust 的任意表達式或給定 $x 名字的表達式。

$() 之後的逗號說明一個逗號分隔符可以有選擇的出現代碼之後,這段代碼與在 $() 中所捕獲的代碼相匹配。逗號之後的 * 說明該模式匹配零個或多個 * 之前的任何模式。

當以 vec![1, 2, 3]; 呼叫巨集時,$x 模式與三個表達式 123 進行了三次匹配。

現在讓我們來看看這個出現在與此單邊模式相關的代碼塊中的模式:在 $()* 部分中所生成的 temp_vec.push() 為在匹配到模式中的 $() 每一部分而生成。$x 由每個與之相匹配的表達式所替換。當以 vec![1, 2, 3]; 調用該巨集時,替換該巨集調用所生成的代碼會是下面這樣:

let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec

我們已經定義了一個巨集,其可以接收任意數量和類別的參數,同時可以生成能夠創建包含指定元素的 vector 的代碼。

macro_rules! 中有一些奇怪的地方。將來會有第二種採用 macro 關鍵字的 declarative macro,其工作方式類似但修復了這些極端情況。在未來的更新之後,macro_rules! 就會被 deprecated。在此基礎之上,同時鑑於大多數 Rust 程序員 使用 巨集而非 編寫 巨集的事實,此處不再深入探討 macro_rules!

請查閱文件或其他資源,如 “The Little Book of Rust Macros” 來更多地瞭解如何寫巨集。

從屬性產生 code 的 procedural macros

prcedural macro 運作起來類似 function (a type of procedure),以一些 code 為 input,執行這些 code,產生一些 code 為 output,而不像是 declarative macros 的方法:pattern matching 並替代為其他 code。

有三種 procedural macros,不過它們的工作方式都類似。其一,其定義必須位於一種特殊類別的屬於它們自己的 crate 中。這麼做出於複雜的技術原因,將來我們希望能夠消除這些限制。

其二,使用這些巨集需採用類似 19-37 的形式,其中 some_attribute 是一個使用特定巨集的佔位符。

src/lib.rs

use proc_macro;

#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}

19-37: 一個使用 prcedural macro 的例子

prcedural macro 包含一個函數,這也是其得名的原因:“過程” 是 “函數” 的同義詞。那麼為何不叫 “函數巨集” 呢?因為有一個 prcedural macro 是 “類函數” 的,叫成函數會產生混亂。

無論如何,定義 prcedural macro 的函數接受一個 TokenStream 作為輸入並產生一個 TokenStream 作為輸出。這也就是巨集的核心:巨集所處理的源代碼組成了輸入 TokenStream,同時巨集生成的代碼是輸出 TokenStream。最後,函數上有一個屬性;這個屬性表明過程巨集的類別。在同一 crate 中可以有多種的過程巨集。

考慮到這些巨集是如此類似,我們會從自訂派生巨集開始。接著會解釋與其他形式巨集的微小區別。

如何撰寫自訂 derive 巨集

以下產生 hello_macro crate,定義 HelloMacro trait,有一個名稱為 hello_macro 的 associated function。接下來,不讓使用者對每個類別都實作 HelloMacro trait,而是提供一個 procedural macro,讓使用者可用 #[derive(HelloMacro)] annotate 類別,並取得 hello_macro function 的預設實作。該實作會列印 Hello, Macro! My name is TypeName!,其中 TypeName 會換成類別名稱。換句話說,我們會做一個 crate,讓 user 可寫下列 code:

src/main.rs

use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;

#[derive(HelloMacro)]
struct Pancakes;

fn main() {
    Pancakes::hello_macro();
}

19-38: crate user 所寫的能夠使用過程式巨集的代碼

執行該代碼將會列印 Hello, Macro! My name is Pancakes!

第一步是產生一個 lib crate:

$ cargo new hello_macro --lib

接下來,定義 HelloMacro trait 以及其關聯函數:

src/lib.rs

pub trait HelloMacro {
    fn hello_macro();
}

現在有了一個包含函數的 trait 。此時,crate 用戶可以實作該 trait 以達到其期望的功能:

use hello_macro::HelloMacro;

struct Pancakes;

impl HelloMacro for Pancakes {
    fn hello_macro() {
        println!("Hello, Macro! My name is Pancakes!");
    }
}

fn main() {
    Pancakes::hello_macro();
}

然而,他們需要為每一個他們想使用 hello_macro 的類別編寫實現的代碼塊。我們希望為省略這些工作。

另外,我們也無法為 hello_macro 函數提供一個能夠列印實現了該 trait 的類別的名字的默認實現:Rust 沒有 reflection 的能力,因此其無法在執行時獲取類別名稱。我們需要一個在運行時生成 code 的巨集。

下一步是定義過程式巨集。在編寫本部分時,過程式巨集必須在其自己的 crate 內。該限制在未來可能被取消。

structuring crate 和其中巨集的慣例如下:對於一個 foo 的包來說,一個 custom derive procedural macro crate 的包被稱為 foo_derive 。在 hello_macro 項目中產生名為 hello_macro_derive 的包。

$ cargo new hello_macro_derive --lib

由於兩個 crate 緊密相關,因此在 hello_macro 包的目錄下產生過程式巨集的 crate。如果改變在 hello_macro 中定義的 trait ,同時也必須改變在 hello_macro_derive 中實現的過程式巨集。

這兩個 crate 需要分別發佈,編程人員如果使用這些包,則需要同時添加這兩個依賴並將其引入作用域。我們也可以只用 hello_macro 包而將 hello_macro_derive 作為一個依賴,並重新導出過程式巨集的代碼。但我們組織項目的方式使編程人員使用 hello_macro 成為可能,即使他們無需 derive 的功能。

需要將 hello_macro_derive 聲明為一個過程巨集的 crate。同時也需要 synquote crate 中的功能,正如註釋中所說,需要將其加到依賴中。為 hello_macro_derive 將下面的代碼加入到 Cargo.toml 文件中。

hellomacroderive/Cargo.toml

[lib]
proc-macro = true

[dependencies]
syn = "0.14.4"
quote = "0.6.3"

為定義一個過程式巨集,請將 19-39 中的 code 放在 hello_macro_derive crate 的 src/lib.rs 文件裡面。注意這段代碼在我們增加 impl_hello_macro 函數的定義之前是無法編譯的。

hellomacroderive/src/lib.rs

在 Rust 1.31.0 時,還需要寫 extern crate ,請查看 https://github.com/rust-lang/rust/issues/54418 https://github.com/rust-lang/rust/pull/54658 https://github.com/rust-lang/rust/issues/55599

extern crate proc_macro;

use crate::proc_macro::TokenStream;
use quote::quote;
use syn;

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // 產生 Rust 代碼所代表的語法樹  以便可以進行操作
    let ast = syn::parse(input).unwrap();

    // 產生 trait 實現
    impl_hello_macro(&ast)
}

19-39: most procedural macro crates 處理 Rust 代碼時所需的代碼

注意在 19-39 中分離函數的方式,這將和你幾乎所見到或創建的每一個過程巨集都一樣,因為這讓編寫一個過程式巨集更加方便。在 impl_hello_macro 被呼叫的地方所選擇做的什麼依賴於該過程式巨集的目的而有所不同。

現在,我們已經引入了 三個新的 crate:proc_macrosynquote 。Rust 自帶 proc_macro crate,因此無需將其加到 Cargo.toml 文件的依賴中。proc_macro crate 是編譯器用來讀取和操作我們 Rust code 的 API。syn crate 將字符串中的 Rust 代碼解析成為一個可以操作的資料結構。quote 則將 syn 解析的資料結構反過來傳入到 Rust 代碼中。這些 crate 讓解析任何我們所要處理的 Rust 代碼變得更簡單:為 Rust 編寫整個的解析器並不是一件簡單的工作。

當用戶在一個類別上指定 #[derive(HelloMacro)] 時,將會呼叫hello_macro_derive 函數。原因在於我們已經使用 proc_macro_derive 及其指定名稱對 hello_macro_derive 函數進行了註解:HelloMacro ,其匹配到 trait 名,這是大多數過程巨集遵循的習慣。

該函數首先將來自 TokenStreaminput 轉換為一個我們可以解釋和操作的資料結構。這正是 syn 派上用場的地方。syn 中的 parse_derive_input 函數獲取一個 TokenStream 並返回一個表示解析出 Rust 代碼的 DeriveInput 結構。示例 19-40 展示了從字符串 struct Pancakes; 中解析出來的 DeriveInput 結構的相關部分:

DeriveInput {
    // --snip--

    ident: Ident {
        ident: "Pancakes",
        span: #0 bytes(95..103)
    },
    data: Struct(
        DataStruct {
            struct_token: Struct,
            fields: Unit,
            semi_token: Some(
                Semi
            )
        }
    )
}

19-40: 解析示例 19-38 中帶有巨集屬性的代碼時得到的 DeriveInput 實例

該 struct 的欄位展示了我們解析的 Rust 代碼是一個類單元結構體,其 ident( identifier,表示名字)為 Pancakes。該結構裡面有更多字段描述了所有類別的 Rust 代碼,查閱 synDeriveInput 的文檔 以獲取更多信息。

此時,尚未定義 impl_hello_macro 函數,其用於構建所要包含在內的 Rust 新代碼。但在此之前,注意其輸出也是 TokenStream。所返回的 TokenStream 會被加到我們的 crate 用戶所寫的代碼中,因此,當用戶編譯他們的 crate 時,他們會獲取到我們所提供的額外功能。

你可能也注意到了,當呼叫 parse_derive_inputparse 失敗時。在錯誤時 panic 對過程巨集來說是必須的,因為 proc_macro_derive 函數必須返回 TokenStream 而不是 Result,以此來符合過程巨集的 API。這裡選擇用 unwrap 來簡化了這個例子;在產生的 code 中,則應該通過 panic!expect 來提供關於發生何種錯誤的更加明確的錯誤信息。

現在我們有了將註解的 Rust code 從 TokenStream 轉換為 DeriveInput 實例的代碼,讓我們來產生在註解類別上實現 HelloMacro trait 的 code,如 19-41 所示。

hellomacroderive/src/lib.rs

fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
    let name = &ast.ident;
    let gen = quote! {
        impl HelloMacro for #name {
            fn hello_macro() {
                println!("Hello, Macro! My name is {}", stringify!(#name));
            }
        }
    };
    gen.into()
}

19-41: 使用解析過的 Rust code 實現 HelloMacro trait

我們得到一個包含以 ast.ident 作為註解類別名字(標識符)的 Ident 結構體實例。19-40 中的結構表明當 impl_hello_macro 函數運行於 19-38 中的代碼上時 ident 字段的值是 "Pancakes"。因此,19-41 中 name 變數會包含一個 Ident 結構的實例,當列印時,會是字符串 "Pancakes",也就是19-38 中結構的名稱。

quote! 讓我們可以編寫希望返回的 Rust diamagnetic。quote! 巨集執行的直接結果並不是編譯器所期望的並需要轉換為 TokenStream。為此需要調用 into 方法,它會消費這個中間表示(intermediate representation,IR)並返回所需的 TokenStream 類別值。

這個巨集也提供了一些模板機制;我們可以寫 #name ,然後 quote! 會以 名為 name 的變數值來替換它。你甚至可以做些與這個正則巨集任務類似的重複事情。查閱 quote crate 的文件 來獲取詳盡的介紹。

我們期望我們的過程式巨集能夠為通過 #name 獲取到的用戶註解類別生成 HelloMacro trait 的實現。該 trait 的實現有一個函數 hello_macro ,其函數體包括了我們期望提供的功能:打印 Hello, Macro! My name is 和註解的類別名。

此處所使用的 stringify! 為 Rust 內置巨集。其接收一個 Rust 表達式,如 1 + 2 , 然後在編譯時將表達式轉換為一個字符串常量,如 "1 + 2" 。這與 format!println! 是不同的,它計算表達式並將結果轉換為 String 。有一種可能的情況是,所輸入的 #name 可能是一個需要打印的表達式,因此我們用 stringify!stringify! 編譯時也保留了一份將 #name 轉換為字符串之後的內存分配。

此時,cargo build 應該都能成功編譯 hello_macrohello_macro_derive 。我們將這些 crate 連接到 19-38 的代碼中來看看過程巨集的行為!在 projects 目錄下用 cargo new pancakes命令產生一個二進制項目。需要將 hello_macrohello_macro_derive 作為依賴加到 pancakes 包的 Cargo.toml 文件中去。如果你正將 hello_macrohello_macro_derive 的版本發佈到 https://crates.io/ 上,其應為正規依賴;如果不是,則可以像下面這樣將其指定為 path 依賴:

[dependencies]
hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }

把 19-38 中的代碼放在 src/main.rs ,然後執行 cargo run:其應該打印 Hello, Macro! My name is Pancakes!。其包含了該過程巨集中 HelloMacro trait 的實現,而無需 pancakes crate 實現它;#[derive(HelloMacro)] 增加了該 trait 實現。

接下來,讓我們探索一下其他的過程巨集與自定義派生巨集有何區別。

Attribute-like macros

類似自訂derive macros,但不是用 derive 屬性產生 code,而是讓我們產生新的屬性。這很彈性:dervice 只能用在 structs 及 enums,但 attributes 可用在其他 items,例如 functions。這邊是使用 attribute-like macro 的例子:有一個名稱為 route 的屬性,可 annotates functions,用在 web application framework

#[route(GET, "/")]
fn index() {

#[route] 屬性將由框架本身定義為一個過程巨集。其巨集定義的函數簽名看起來像這樣:

#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {

這裡有兩個 TokenStream 類別的參數;第一個用於屬性內容本身,也就是 GET, "/" 部分。第二個是屬性所標記的項,在本例中,是 fn index() {} 和剩下的函數體。

除此之外,類屬性巨集與自定義派生巨集工作方式一致:創建 proc-macro crate 類別的 crate 並實現希望生成代碼的函數!

Function-like macros

function-like macro 類似 funcation call,跟 macro_rules! macros 類似,比 function 更有彈性,可有任意數量的參數。只能用 match-like 語法定義 macro_rules! ,但是 funcation-like macros 接受一個 TokenStream 參數,並自訂使用該參數的 rust code。以下是使用 sql! macro 的 function-like macro 的 code。

let sql = sql!(SELECT * FROM posts WHERE id=1);

這個巨集會解析其中的 SQL 語句並檢查其是否是句法正確的。該巨集應該被定義為如此:

#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {

這類似於 custom derive macro 的簽名:獲取括號中的 token,並回傳希望生成的 code

References

The Rust Programming Language

中文版

中文版 2

2020年6月22日

rust18_Pattern Matching

pattern 是 rust 的特殊語法,用來匹配類別的 struct,結合 match expression 及其他 struct 可提供更多程式 control flow 的支配權。Pattern 由以下內容組成

  • Literals
  • Destructured arrays, enums, structs, or tuples
  • Variables
  • Wildcards
  • Placeholders

這些元件就是要處理的 data,接著可用匹配值決定程式是否有正確的資料,運作特定部分的 code

透過一些值與 pattern 比較來使用它們,如果匹配,就做對應處理。

以下討論 pattern 適用處,refutableirrefutable 模式的區別,及不同類別的 pattern 語法

所有可能用到 Pattern 的地方

match Arms

match 表達式由 match 關鍵字、用於匹配的值和一個或多個分支構成,這些分支包含一個模式和在值匹配分支的模式時運行的表達式:

match VALUE {
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
}

match 表達式必須是 窮盡exhaustive)的,所有可能的值都必須被考慮到。

有一個特定的模式 _ 可以匹配所有情況,不過它從不綁定任何變數。這在例如希望忽略任何未指定值的情況很有用。本章之後的 “在模式中忽略值” 部分會詳細介紹 _ 模式的更多細節。

Conditional if let Expression

chap6 討論過 if let 及如何撰寫只關心一個狀況的match 語法的簡寫。if let 可對應一個 optional 帶有 code 的 else,在 if let pattern 不匹配時運作。

可以組合併匹配 if letelse ifelse if let 表達式。相比 match表達式一次只能將一個值與模式比較提供了更多靈活性;一系列 if letelse ifelse if let 分支並不要求其條件相互關聯。

fn main() {
    let favorite_color: Option<&str> = None;
    let is_tuesday = false;
    let age: Result<u8, _> = "34".parse();

    if let Some(color) = favorite_color {
        println!("Using your favorite color, {}, as the background", color);
    } else if is_tuesday {
        println!("Tuesday is green day!");
    } else if let Ok(age) = age {
        if age > 30 {
            println!("Using purple as the background color");
        } else {
            println!("Using orange as the background color");
        }
    } else {
        println!("Using blue as the background color");
    }
}

注意 if let 也可以像 match 分支那樣引入 shadowed 變數:if let Ok(age) = age 引入了一個新的 shadowed 變數 age,它包含 Ok 成員中的值。這意味著 if age > 30 條件需要位於這個代碼塊內部;不能將兩個條件組合為 if let Ok(age) = age && age > 30,因為我們希望與 30 進行比較的被覆蓋的 age 直到大括號開始的新作用域才是有效的。

if let 表達式的缺點在於無法用 compiler 檢查窮盡性,而 match 表達式有做檢查。如果去掉最後的 else 塊而遺漏處理一些情況,編譯器也不會警告這類可能的邏輯錯誤。

while let Conditional Loop

只要 stack.pop() 回傳 Some 就列印

let mut stack = Vec::new();

stack.push(1);
stack.push(2);
stack.push(3);

while let Some(top) = stack.pop() {
    println!("{}", top);
}

for Loop

for 可以獲取一個模式,例如在 for 循環中使用模式來解構元組

enumerate 方法產生一個迭代器,得到一個值和其在迭代器中的索引,他們位於一個元組中。第一個 enumerate 呼叫會產生元組 (0, 'a')。當這個值匹配模式 (index, value)index將會是 0 而 value 將會是 'a',並印出第一行輸出。

let v = vec!['a', 'b', 'c'];

for (index, value) in v.iter().enumerate() {
    println!("{} is at index {}", value, index);
}

let statement

let x = 5; 其實就是 pattern,也就是 let PATTERN = EXPRESSION;

這個模式實際上等於 “將任何值綁定到變數 x,不管值是什麼”

// 將一個 tuple 與模式匹配
let (x, y, z) = (1, 2, 3);

function parameters

函數參數也可以是模式,x 部分就是一個模式

fn foo(x: i32) {
    // 代碼
}

ex:

fn print_coordinates(&(x, y): &(i32, i32)) {
    println!("Current location: ({}, {})", x, y);
}

fn main() {
    let point = (3, 5);
    print_coordinates(&point);
}

closure 類似 function,故也可以在 closure parameter 使用 pattern

在一些地方,模式必須是 irrefutable 的,必須匹配所提供的任何值。在另一些情況,他們則可以是 refutable 的。

Refutability: 是否 pattern 會匹配失敗

模式有兩種形式:refutable(可反駁的)和 irrefutable(不可反駁的)。能匹配任何傳遞的可能值的模式被稱為是 不可反駁的irrefutable)。例如 let x = 5; 語句中的 x,因為 x 可以匹配任何值所以不可能會失敗。

對某些可能的值進行匹配會失敗的模式被稱為是 可反駁的refutable)。一個這樣的例子便是 if let Some(x) = a_value 表達式中的 Some(x);如果變量 a_value 中的值是 None 而不是 Some,那麼 Some(x) 模式不能匹配。

let 語句、 函數參數和 for 循環只能接受不可反駁的模式,因為通過不匹配的值程序無法進行有意義的工作。if letwhile let 表達式被限制為只能接受可反駁的模式,因為根據定義就是要處理可能的失敗:條件表達式的功能就是根據成功或失敗執行不同的操作。

通常無需擔心可反駁和不可反駁模式的區別,不過確實需要熟悉可反駁性的概念,這樣當在錯誤信息中看到時就知道如何應對。遇到這些情況,根據代碼行為的意圖,需要修改模式或者使用模式的結構。

一個嘗試在 Rust 要求不可反駁模式的地方使用可反駁模式以及相反情況的例子

let Some(x) = some_option_value;

會發生編譯錯誤

error[E0005]: refutable pattern in local binding: `None` not covered
 -->
  |
3 | let Some(x) = some_option_value;
  |     ^^^^^^^ pattern `None` not covered

為了修復在需要不可反駁模式的地方使用可反駁模式的情況,可以修改使用模式的代碼:不同於使用 let,可以使用 if let

if let Some(x) = some_option_value {
    println!("{}", x);
}

如果為 if let 提供了一個總是會匹配的模式,嘗試把不可反駁模式用到 if let 上,比如以下的 x,則會出錯:

if let x = 5 {
    println!("{}", x);
};

會發生編譯錯誤

error[E0162]: irrefutable if-let pattern
 --> <anon>:2:8
  |
2 | if let x = 5 {
  |        ^ irrefutable pattern

匹配分支必須使用可反駁模式,除了最後一個分支需要使用能匹配任何剩餘值的不可反駁模式。

Pattern Syntax

以下列出所有 pattern 的有效語法

matching literals

let x = 1;

match x {
    1 => println!("one"),
    2 => println!("two"),
    3 => println!("three"),
    _ => println!("anything"),
}

matching named variables

Named variables 是 irrefutable(不可反駁的)可 match any value

match 會開始一個新作用域,match 表達式中作為模式的一部分聲明的變數會覆蓋 match 結構之外的同名變數,與所有變數一樣。

fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        // y 是 shaowed variable,跟上面的 y 無關,可匹配任何 Some 裡面的 value
        // 當 scope 結束,y 的作用域就結束
        Some(y) => println!("Matched, y = {:?}", y),
        _ => println!("Default case, x = {:?}", x),
    }

    // 這裡的 y 還是一樣是 10
    println!("at the end: x = {:?}, y = {:?}", x, y);
}

multiple patterns

match 表達式中,可以使用 | 語法匹配多個模式,它代表 or 的意思。

let x = 1;

match x {
    // 符合 1 或 2
    1 | 2 => println!("one or two"),
    3 => println!("three"),
    _ => println!("anything"),
}

matching rangers of values with

... 語法允許你匹配一個閉區間範圍內的值。

let x = 5;

match x {
    // 如果 x 是 1、2、3、4 或 5,就會匹配
    1 ... 5 => println!("one through five"),
    _ => println!("something else"),
}

rangers 範圍只允許用於數字或 char

let x = 'c';

match x {
    'a' ... 'j' => println!("early ASCII letter"),
    'k' ... 'z' => println!("late ASCII letter"),
    _ => println!("something else"),
}

destructing to break apart values

可以使用模式來解構 structs、enums、tuples 和引用,以便使用這些值的不同部分。

  • destructing structs

    可以通過帶有模式的 let 語句將其分解:

    struct Point {
        x: i32,
        y: i32,
    }
    
    fn main() {
        let p = Point { x: 0, y: 7 };
    
        let Point { x: a, y: b } = p;
        assert_eq!(0, a);
        assert_eq!(7, b);
    }

    這個例子展示了模式中的變數名不必與結構體中的欄位一致。不過通常希望變數名與欄位名一致以便於理解變數來自於哪些欄位。

    因為變數名匹配欄位名是常見的,同時因為 let Point { x: x, y: y } = p; 包含了很多重複,所以對於匹配欄位的模式存在簡寫:只需列出結構體欄位的名稱,則模式產生的變數會有相同的名稱。

    struct Point {
        x: i32,
        y: i32,
    }
    
    fn main() {
        let p = Point { x: 0, y: 7 };
    
        let Point { x, y } = p;
        assert_eq!(0, x);
        assert_eq!(7, y);
    }

    也可以在部分struct 模式中使用 literal 進行解析,而不是為所有的欄位產生變數。

    fn main() {
        let p = Point { x: 0, y: 7 };
    
        match p {
            // x 軸
            Point { x, y: 0 } => println!("On the x axis at {}", x),
            // y 軸
            Point { x: 0, y } => println!("On the y axis at {}", y),
            Point { x, y } => println!("On neither axis: ({}, {})", x, y),
        }
    }
  • destructing enums

    enum Message {
        Quit,
        Move { x: i32, y: i32 },
        Write(String),
        ChangeColor(i32, i32, i32),
    }
    
    fn main() {
        let msg = Message::ChangeColor(0, 160, 255);
    
        match msg {
            // Message::Quit 這樣沒有任何資料的enum,不能進一步解構其值。只能匹配其 literal Message::Quit,因此模式中沒有任何變數
            Message::Quit => {
                println!("The Quit variant has no data to destructure.")
            },
            // 使用大括號並列出欄位變數,以便將其分解以供此分支的代碼使用
            Message::Move { x, y } => {
                println!(
                    "Move in the x direction {} and in the y direction {}",
                    x,
                    y
                );
            }
            Message::Write(text) => println!("Text message: {}", text),
            Message::ChangeColor(r, g, b) => {
                println!(
                    "Change the color to red {}, green {}, and blue {}",
                    r,
                    g,
                    b
                )
            }
        }
    }
  • destructing reference

    當模式所匹配的值中包含引用時,需要解構引用之中的值,這可以通過在模式中指定 & 做到。這讓我們得到一個包含引用所指向資料的變數,而不是包含引用的變數。這個技術在通過迭代器遍歷引用時,要使用 closure 中的值而不是其引用時非常有用。

    遍歷一個 vector 中的 Point 實例的引用,並同時解構引用和其中的 struct 以方便對 xy 值進行計算

    let points = vec![
        Point { x: 0, y: 0 },
        Point { x: 1, y: 5 },
        Point { x: 10, y: -3 },
    ];
    
    let sum_of_squares: i32 = points
        .iter()
        .map(|&Point { x, y }| x * x + y * y)
        .sum();
  • destructing nested structs and enums

    以匹配嵌套的 enum

    enum Color {
       Rgb(i32, i32, i32),
       Hsv(i32, i32, i32)
    }
    
    enum Message {
        Quit,
        Move { x: i32, y: i32 },
        Write(String),
        ChangeColor(Color),
    }
    
    fn main() {
        let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));
    
        match msg {
            Message::ChangeColor(Color::Rgb(r, g, b)) => {
                println!(
                    "Change the color to red {}, green {}, and blue {}",
                    r,
                    g,
                    b
                )     
            },
            Message::ChangeColor(Color::Hsv(h, s, v)) => {
                println!(
                    "Change the color to hue {}, saturation {}, and value {}",
                    h,
                    s,
                    v
                )
            }
            _ => ()
        }
    }
  • destructing nested structs and tuples

    struct 和 tuple 嵌套在 tuple 中

    let ((feet, inches), Point {x, y}) = ((3, 10), Point { x: 3, y: -10 });

Ignoring values in a pattern

有時忽略模式中的一些值是有用的,例如 match 中最後捕獲全部情況的分支實際上沒有做任何事,但是它確實對所有剩餘情況負責。有一些簡單的方法可以忽略模式中全部或部分值:使用 _ 模式,使用一個以下劃線開始的名稱,或者使用 ..忽略所剩部分的值。

  • ignoring an entire value with _

    _ 也可以用在函數的參數上

    fn foo(_: i32, y: i32) {
        println!("This code only uses the y parameter: {}", y);
    }
    
    fn main() {
        foo(3, 4);
    }
  • ignoring parts of a value with a nested _

    只需要測試部分值但在期望運行的代碼部分中沒有使用它們時,也可以在另一個模式內部使用 _來只忽略部分值。

    let mut setting_value = Some(5);
    let new_setting_value = Some(10);
    
    match (setting_value, new_setting_value) {
        // 不需要 Some 中的值時
        (Some(_), Some(_)) => {
            println!("Can't overwrite an existing customized value");
        }
        _ => {
            setting_value = new_setting_value;
        }
    }

    也可以在一個模式中的多處使用 underline 來忽略特定值

    let numbers = (2, 4, 8, 16, 32);
    
    match numbers {
        (first, _, third, _, fifth) => {
            println!("Some numbers: {}, {}, {}", first, third, fifth)
        },
    }
  • ignoring an unused variable by starting its name with _

    產生了一個變量卻不在任何地方使用它,Rust 會給你一個警告,因為這可能會是個 bug。但是有時是有用的,例如你正在設計原型或剛剛開始一個 project。這時你希望告訴 Rust 不要警告未使用的變量,為此可以用 underline 作為變數名的開頭。

    fn main() {
        let _x = 5;
        let y = 10;
    }

    只使用 _ 和使用以下劃線開頭的名稱有些微妙的不同:比如 _x 仍會將值綁定到變數,而 _則完全不會綁定。以下會得到一個編譯錯誤,因為 s 的值仍然會移動進 _s,並阻止我們再次使用 s

    let s = Some(String::from("Hello!"));
    
    if let Some(_s) = s {
        println!("found a string");
    }
    
    println!("{:?}", s);

    要改寫為

    let s = Some(String::from("Hello!"));
    
    if let Some(_) = s {
        println!("found a string");
    }
    
    println!("{:?}", s);
  • ignoring remaining parts of a value with ..

    對於有多個部分的值,可以使用 .. 語法來只使用部分並忽略其它值,同時避免不得不對每一個忽略值列出下劃線。.. 模式會忽略模式中剩餘的任何沒有顯式匹配的值部分。以下有一個 Point 存放了三維空間中的坐標。在 match 表達式中,我們希望只操作 x 座標並忽略 yz 的值:

    struct Point {
        x: i32,
        y: i32,
        z: i32,
    }
    
    let origin = Point { x: 0, y: 0, z: 0 };
    
    match origin {
        // 忽略 Point 中除 x 以外的 fields
        Point { x, .. } => println!("x is {}", x),
    }
    
    fn main() {
        let numbers = (2, 4, 8, 16, 32);
    
        match numbers {
            // 只匹配 tuple 中的第一個和最後一個值並忽略掉所有其它值
            (first, .., last) => {
                println!("Some numbers: {}, {}", first, last);
            },
        }
    }

    如果期望匹配和忽略的值是不明確的,Rust 會給編譯錯誤。

    fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (.., second, ..) => {
            println!("Some numbers: {}", second)
        },
    }
}

Extra Conditionals with Match Guards

匹配守衛match guard)是一個指定與 match 分支模式之後的額外 if 條件,它也必須被滿足才能選擇此分支。匹配守衛用於表達比單獨的模式所能允許的更為複雜的情況。

let num = Some(4);

match num {
    // 判斷 x 是否 < 5
    Some(x) if x < 5 => println!("less than five: {}", x),
    Some(x) => println!("{}", x),
    None => (),
}

可以使用match guard來解決模式中 shadowed 變數的問題,那裡 match 表達式的模式中產生了一個變數而不是使用 match 之外的同名變數。新變數就代表不能夠測試外部變數的值。可使用匹配守衛修復這個問題:

fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        // 用 matching guard 測試跟外部變數是否相等
        // n == y 沒有產生新變數,這裡的 y 就是外部的 y
        Some(n) if n == y => println!("Matched, n = {:?}", n),
        _ => println!("Default case, x = {:?}", x),
    }

    println!("at the end: x = {:?}, y = {:?}", x, y);
}
let x = 4;
let y = false;

match x {
    // 也可以使用 或 運算符 | 來指定多個模式,同時 match guard 的條件會作用域所有的模式
    4 | 5 | 6 if y => println!("yes"),
    _ => println!("no"),
}

@ Bindings

at@)讓我們在產生一個存放值的變數的同時測試其值是否匹配模式。

enum Message {
    Hello { id: i32 },
}

let msg = Message::Hello { id: 5 };

match msg {
    // 測試 Message::Hello 的 id 字段是否位於 3...7 範圍內,同時也希望能綁定其值到 id_variable 中以便此分支相關聯的 code 可以使用它
    Message::Hello { id: id_variable @ 3...7 } => {
        println!("Found an id in range: {}", id_variable)
    },
    // 指定了一個範圍 10, 11, 12,但裡面不能使用 id,因為沒有將 id 儲存到另一個變數中
    Message::Hello { id: 10...12 } => {
        println!("Found an id in another range")
    },
    // 沒有範圍的變數
    Message::Hello { id } => {
        println!("Found some other id: {}", id)
    },
}

References

The Rust Programming Language

中文版

中文版 2

2020年6月15日

rust17_rust 的物件導向特性

以下討論 rust 如何提供物件導向的特性,並討論這樣做的優缺點

物件導向程式語言的特性

物件、封裝、繼承

物件包含資料及行為

rust 的 struct, enum 是儲存資料,而 impl 區塊提供 methods,雖然帶有 methods 的 struct, enum 不被稱為物件,但跟物件有相同的定義

封裝隱藏實作細節

唯一能跟物件溝通的方法,是透過 public API,封裝可讓我們在不修改 API 的狀況下,修改或重構物件的內部實作。

chap7 提到,可使用 pub 決定 module, 類別, function, method 是公有的,預設則是私有的。

例如 AveragedCollection 定義為 pub,其他 code 可使用這個 struct,但內部的 list, average 還是私有的

pub struct AveragedCollection {
    list: Vec<i32>,
    average: f64,
}

透過 impl 區塊,實作 add, remove, average

impl AveragedCollection {
    pub fn add(&mut self, value: i32) {
        self.list.push(value);
        self.update_average();
    }

    pub fn remove(&mut self) -> Option<i32> {
        let result = self.list.pop();
        match result {
            Some(value) => {
                self.update_average();
                Some(value)
            },
            None => None,
        }
    }

    pub fn average(&self) -> f64 {
        self.average
    }

    fn update_average(&mut self) {
        let total: i32 = self.list.iter().sum();
        self.average = total as f64 / self.list.len() as f64;
    }
}

公有方法 addremoveaverage 是修改 AveragedCollection 的唯一方式。當使用 add方法把一個元素加入到 list 或者使用 remove 方法來刪除時,這些方法的實現同時會呼叫私有的 update_average 方法來更新 average

listaverage 是私有的,所以沒有其他方式來使得外部的代碼直接向 list 增加或者刪除元素,否則 list 改變時可能會導致 average 不同步。average 方法回傳 average 的值,這使得外部的代碼只能讀取 average 而不能修改它。

因為我們已經封裝了 AveragedCollection 的細節,將來可以輕鬆改變類似資料結構這些方面的內容。例如,可以使用 HashSet 代替 Vec 作為 list 的類別。只要 addremoveaverage 公有函數的簽名保持不變,使用 AveragedCollection 的code就無需改變。相反如果使得 list 為公有,就未必都會如此了: HashSetVec 使用不同的方法增加或移除項,所以如果要想直接修改 list 的話,外部的code不得不做出修改。

如果封裝是一個語言被認為是面向OO語言所必要的方面的話,那麼 Rust 滿足這個要求。在code中不同的部分使用 pub 可以封裝其實作細節。

以 Type System 進行繼承,並提供共用程式

繼承Inheritance)是一個很多程式語言都提供的機制,一個物件可以定義為繼承另一個物件的定義,這使其可以獲得父物件的資料和行為,而無需重新定義。

如果一個語言必須有繼承才能被稱為面向對象語言的話,那麼 Rust 就不是物件導向。

rust 無法定義一個 struct 繼承父 struct 的成員和方法。然而,Rust 也提供了其他的解決方案。

選擇繼承有兩個主要的原因。第一個是為了重用代碼:一旦為一個類別實作了特定 methods,繼承可以對一個不同的類別重用這個實作。

Rust 可以使用默認 trait 方法實作來進行共享,這類似於父類別有一個方法的實現,而通過繼承子類別也擁有這個方法的實作。當實作 trait 時也可以選擇覆蓋的預設實作,這類似於子類別覆蓋從父類別繼承的方法。

第二個使用繼承的原因與類別系統有關:子類別可以用於父類別被使用的地方。這也被稱為 多態polymorphism),這意味著如果多種對象共享特定的屬性,則可以相互替代使用。

很多人將多態(Polymorphism)視為繼承的同義詞。不過它是一個可以用於多種類別的code的更一般化的概念。對於繼承來說,這些類別通常是子類別。 Rust 利用泛型來支援多個不同類別的抽象,並通過 trait bounds 加強對這些類別所必須提供的內容的限制。這有時被稱為 bounded parametric polymorphism

近來繼承在很多語言中失寵了,因為其時常帶有共享多於所需的代碼的風險。子類別不應總是共享其父類別的所有特徵,但是繼承卻始終如此。如此會使程序設計更為不靈活,並引入無意義的類別方法,或由於方法實際並不適用於子類別而造成錯誤的可能性。某些語言還只允許子類別繼承一個父類別,進一步限制了程序設計的靈活性。

因為這些原因,Rust 選擇使用 trait 替代繼承。讓我們看一下 Rust 中的 trait 是如何實現多態的。

利用 trait objects 實作不同類別的值

chap 8 提到了 vector 只能儲存相同類別元素的限制。替代方案是定義一個 enum 來儲存 integer, float, string。這表示可在每個單元儲存不同類別的資料。

有時候會想擴充有效的類別集合,以 GUI 為例,他會呼叫 list 的每一個 draw method,把資料畫在 UI 上。如果要實作 gui crate,會有 Button, TextField,但 gui libray 的 user 會希望產生自訂可以繪製在畫面上的類別,例如 Image, SelectBox

實作 library 時,不會知道所有 user 需要用到的類別,只知道 gui 要記錄不同類別的值,且要對每一個值呼叫 draw。

定義一般行為的 trait

為了實作 gui,要定義一個 Draw trait,其中包含 draw method,另外定義一個存放 trait object 的 vector,trait object 會指向實作了指定 trait 的類別 instance

可使用 trait object 替代泛型或其他類別。rust 就會在編譯時,確保該值會實作 trait object 的 trait

rust 刻意不將 struct, enum 稱為物件,在 struct, enum 中,資料跟 impl 行為是分開的。

trait object 會將資料跟行為結合,更接近物件的定義,但 trait object 不同於傳統的 object,因為不能向 trait object 增加資料,trait object 的作用,是將一般通用行為抽象化。

泛型類別參數一次只能替代一個具體類型,而 trait 對象則允許在執行時替代為多種具體類別。

使用 trait 對象的方法,一個 Screen 實例可以存放一個既能包含 Box<Button>,也能包含 Box<TextField>Vec

// 定義一個帶有 draw 方法的 trait Draw
pub trait Draw {
    fn draw(&self);
}

// 一個 Screen struct的定義,它帶有 components,儲存實作了 Draw trait 的 trait 對象的 vector
// 這個 vector 的類別是 Box<Draw>
pub struct Screen {
    pub components: Vec<Box<dyn Draw>>,
}

// 在 Screen 上實作一個 run 方法,該方法在每個 component 上呼叫 draw
impl Screen {
    pub fn run(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }
}

//////////

// 實作 Draw trait 的 Button
pub struct Button {
    pub width: u32,
    pub height: u32,
    pub label: String,
}

impl Draw for Button {
    fn draw(&self) {
        // 實際繪製按鈕的代碼
    }
}
// 一個使用 gui 的 crate 中,在 SelectBox 上實作 Draw trait
use gui::Draw;

struct SelectBox {
    width: u32,
    height: u32,
    options: Vec<String>,
}

impl Draw for SelectBox {
    fn draw(&self) {
        // code to actually draw a select box
    }
}

use gui::{Screen, Button};

fn main() {
    let screen = Screen {
        components: vec![
            Box::new(SelectBox {
                width: 75,
                height: 10,
                options: vec![
                    String::from("Yes"),
                    String::from("Maybe"),
                    String::from("No")
                ],
            }),
            Box::new(Button {
                width: 50,
                height: 10,
                label: String::from("OK"),
            }),
        ],
    };

    screen.run();
}

只關心值所反映的資訊而不是其具體類別 —— 類似於動態語言中稱為 duck typing 的概念:如果它走起來像一隻鴨子,叫起來像一隻鴨子,那麼它就是一隻鴨子

trait object 執行 dynamic dispatch

回憶一下 chap 10 討論過的,當對泛型使用 trait bound 時編譯器所進行單態化處理:編譯器為每一個被泛型類型參數代替的具體類別產生了非泛型的函數和方法實作。單態化所產生的代碼進行 靜態分發static dispatch)。靜態分發發生於編譯器在編譯時就知道呼叫了什麼方法的時候。這與 動態分發dynamic dispatch)相對,這時編譯器在編譯時無法知道呼叫了什麼方法。在動態分發的情況下,編譯器會產生可以在執行時判斷呼叫了什麼方法的code。

當使用 trait object 時,Rust 必須使用動態分發。編譯器無法知道所有可能用於 trait object 代碼的類別,所以它也不知道應該呼叫哪個類別的哪個方法實作。為此,Rust 在執行時使用 trait object 中的pointer 來判斷要呼叫哪個方法。動態分發不讓編譯器有選擇的內聯方法代碼,這會相應的禁用一些優化。儘管在編寫示例 17-5和可以支持示例 17-9 中的代碼的過程中確實獲得了額外的靈活性,但仍然需要權衡取捨。

trait object 要求 object safety

只有 對象安全object safe)的 trait 才可以組成 trait object。圍繞所有使得 trait object safe 的屬性存在一些複雜的規則,不過在實現中,只涉及到兩條規則。如果一個 trait 中所有的方法有如下屬性時,則該 trait 是 object safe 的:

  • 回傳值類別不是 Self
  • 方法沒有任何泛型類別參數

Self 關鍵字是我們要實現 trait 或方法的類別的別名。object safe 對於 trait 對象是必須的,因為一旦有了 trait object,就不再知道實現該 trait 的具體類別是什麼了。如果 trait 方法回傳具體的 Self 類別,但是 trait object 沒有了其真正的類型,那麼該方法不可能使用已經不知道的原始具體類別。同理對於泛型類別參數來說,當使用 trait 時其會放入具體的類別參數:此具體類別變成了實現該 trait 的類別的一部分。當使用 trait object 時其具體類別被抹去了,故無從得知放入泛型參數類別的類別是什麼。

一個 trait 的方法不是 object safe 的例子是標準庫中的 Clone trait。Clone trait 的 clone 方法的參數簽名看起來像這樣:

pub trait Clone {
    fn clone(&self) -> Self;
}

String 實作了 Clone trait,當在 String 呼叫 clone 方法時會得到一個 String instance。類似的,當呼叫 Vec 實例的 clone 方法會得到一個 Vec instance。clone 的簽名需要知道什麼類型會代替 Self,因為這是它的回傳值。

如果嘗試做一些違反有關 trait object 的object safe 規則的事情,編譯器會提示你。例如,如果嘗試實現示例 17-4 中的 Screen struct 來存放實作了 Clone trait 而不是 Draw trait 的類型,像這樣:

pub struct Screen {
    pub components: Vec<Box<dyn Clone>>,
}

將會得到如下錯誤:

error[E0038]: the trait `std::clone::Clone` cannot be made into an object
 --> src/lib.rs:2:5
  |
2 |     pub components: Vec<Box<dyn Clone>>,
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::clone::Clone` cannot be
made into an object
  |
  = note: the trait cannot require that `Self : Sized`

這表示不能以這種方式使用此 trait 作為 trait object。

實作物件導向設計模式

state pattern 是有一個值有某些內部狀態,就是 state object,該值的行為隨著內部狀態而改變。state object 有共享的機制,在 rust 是用 struct, trait 實作,而不是 object 與繼承。每一個 state object代表負責自己的行為與需要改變為另一個狀態的規則。擁有這個 state object 不需要知道,不同狀態的行為及怎麼轉換狀態。

使用 state pattern 表示當程式業務邏輯需求改變時,不需要改變使用者的狀態或值。只需要修改 state object 裡面的 code 來改變規則,或增加更多 state object。

ex: 增量式發布 blog 的 work flow

  1. 產生一個空白的 blog draft
  2. 完成 draft,就要求審核
  3. draft 通過審核,就會被發布
  4. 被發佈的 blog 內容會被列印出來

無法對 blog 進行任何修改,在審核通過前,blog 要保持未發佈的狀態。

這是使用 blog crate 的實作

use blog::Post;

fn main() {
    // 用 new 產生新的 blog draft
    let mut post = Post::new();
    // 編輯 draft 內容
    post.add_text("I ate a salad for lunch today");
    assert_eq!("", post.content());
    // 要求審核
    post.request_review();
    assert_eq!("", post.content());
    // 審核通過,取得 content 時才會有完整的內容
    post.approve();
    assert_eq!("I ate a salad for lunch today", post.content());
}

定義 Post 並產生 draft state 的 blog instance

公有的 Post,與私有的 State trait

lib.rs

pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    pub fn new() -> Post {
        Post {
            // 在 state 記錄 Option 類別的 trait object:  Box<State>
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }
}

trait State {}

struct Draft {}

impl State for Draft {}

State trait 定義了所有不同狀態的 blog 所共享的行為,接下來 DraftPendingReviewPublished 狀態都會實作 State 。現在這個 trait 並沒有任何方法,一開始只定義 Draft狀態因為這是我們希望 blog 的初始狀態。

當產生新的 Post 時,我們將其 state 設定為一個存放了 BoxSome 值。這個 Box 指向一個 Draft struct 新實例。這確保了無論何時新建一個 Post 實例,它都會從草案開始。因為 Poststate 是私有的,也就無法產生任何其他狀態的 Post 了!。Post::new 函數中將 content 設定為新的空 String

存放 blog content

希望有一個 method 可以產生 blog content

post.add_text("I ate a salad for lunch today");

因此就先在 impl Post 增加這個 fn,因為需要改變 add_textPost,故獲取一個 self 的可變引用,接下來用content 中的 Stringpush_str 並傳送 text 參數,然後存到 content

impl Post {
    // --snip--
    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }
}

確保 draft post 的 content 是空的

blog draft 必須要在 content method 回傳空白字串,暫時先固定回傳 ""

impl Post {
    // --snip--
    pub fn content(&self) -> &str {
        ""
    }
}

審核 blog 並改變狀態

將其狀態由 Draft 改為 PendingReview

impl Post {
    // --snip--
    pub fn request_review(&mut self) {
        if let Some(s) = self.state.take() {
            // 將 state 透過 request_review 改為 PendingReview
            self.state = Some(s.request_review())
        }
    }
}

trait State {
    // 不同於使用 self、 &self 或者 &mut self 作為方法的第一個參數,這裡使用了 self: Box<Self>
    // 這個表示這個方法,只對這個類型的 Box 有效。這個語法獲取了 Box<Self> 的所有權,使老狀態無效化以便 Post 的狀態值可以將自身轉換為新狀態。
    fn request_review(self: Box<Self>) -> Box<dyn State>;
}

struct Draft {}

impl State for Draft {
    // 產生新的 PendingReview state
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        Box::new(PendingReview {})
    }
}

struct PendingReview {}

impl State for PendingReview {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

改變 content 行為的 approve fn

approve 方法將與 request_review 方法類似:它會將 state 設定為審核通過時應處於的狀態

impl Post {
    // --snip--
    pub fn approve(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.approve())
        }
    }
}

trait State {
    fn request_review(self: Box<Self>) -> Box<dyn State>;
    fn approve(self: Box<Self>) -> Box<dyn State>;
}

struct Draft {}

impl State for Draft {
    // --snip--
    // 沒有作用,回傳 self
    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

struct PendingReview {}

impl State for PendingReview {
    // --snip--
    // 回傳 Published state
    fn approve(self: Box<Self>) -> Box<dyn State> {
        Box::new(Published {})
    }
}

struct Published {}

impl State for Published {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    // 沒有作用,回傳 self
    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

為 State trait 增加 content fn

trait State {
    // --snip--
    fn content<'a>(&self, post: &'a Post) -> &'a str {
        ""
    }
}

// --snip--
struct Published {}

impl State for Published {
    // --snip--
    fn content<'a>(&self, post: &'a Post) -> &'a str {
        &post.content
    }
}

完整程式:

pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }

    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }

    pub fn content(&self) -> &str {
        self.state.as_ref().unwrap().content(&self)
    }

    pub fn request_review(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.request_review())
        }
    }

    pub fn approve(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.approve())
        }
    }
}


trait State {
    fn request_review(self: Box<Self>) -> Box<dyn State>;

    fn approve(self: Box<Self>) -> Box<dyn State>;

    fn content<'a>(&self, _post: &'a Post) -> &'a str {
        ""
    }
}

struct Draft {}

impl State for Draft {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        Box::new(PendingReview {})
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }
}

struct PendingReview {}

impl State for PendingReview {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        Box::new(Published {})
    }
}

struct Published {}

impl State for Published {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn content<'a>(&self, post: &'a Post) -> &'a str {
        &post.content
    }
}

fn main() {
    let mut post = Post::new();

    post.add_text("I ate a salad for lunch today");
    assert_eq!("", post.content());

    post.request_review();
    assert_eq!("", post.content());

    post.approve();
    assert_eq!("I ate a salad for lunch today", post.content());
}

Trade-offs of the State Pattern

如果不用 state pattern 實作,會需要在 Post, main 裡使用很多 match 語法,檢查 blog state 並改變行為。

對於 state pattern 來說,Post fn 與使用 Post 的 code 都不需要 match 語法,增加新狀態也只需要增加新的 struct,並實作 trait methods

可再擴充以下功能

  • 只允許 blog 在 Draft state 修改 content
  • 增加 reject 將 state 由 PendingReview 改回 Draft
  • 將狀態改為 Published 以前,要呼叫兩次 approve

state pattern 的一個缺點是,因為實作了狀態轉換,讓狀態之間有相關性,例如想在 PendingReviewPublished 之間增加 Scheduled 狀態,就要修改 PendingReview

另一個缺點是重複邏輯的 code。可嘗試在 State trait 回傳 selfrequest_reviewapprove 增加預設實作,但會違反 object safe 特性,因為 trait 不知道 self 的具體資料型別。

另一個重複是 Post 裡面的 request_reviewapprove 很類似,都是設定 state 為新的值。這部分可用 macro 解決

以物件導向方法實作,並沒有用到 rust 的優勢,以下修改程式,將無效狀態與狀態改變,轉換為編譯錯誤。

將 States, Behaviors 編碼為類別

不同於完全封裝狀態及狀態轉移,將狀態編碼為類別,rust 的類別檢查就能處理為編譯期的錯誤

Post 初始化 Post::new 時為 DraftPost,但 DraftPost 沒有 content method,如果嘗試呼叫 content 就會發生編譯錯誤

pub struct Post {
    content: String,
}

pub struct DraftPost {
    content: String,
}

impl Post {
    pub fn new() -> DraftPost {
        DraftPost {
            content: String::new(),
        }
    }

    pub fn content(&self) -> &str {
        &self.content
    }
}

impl DraftPost {
    pub fn add_text(&mut self, text: &str) {
        self.content.push_str(text);
    }
}

PostDraftPost 都有一個私有的 content 來儲存 blog content。這些 struct 不再有 state,因為我們將類別編碼為 struct 的類別。Post 將代表發佈的 blog,它有一個返回 contentcontent 方法。

仍然有一個 Post::new 函數,不過不同於返回 Post 實例,它回傳 DraftPost 。不可能產生一個 Post instance,因為 content 是私有的同時沒有任何函數返回 Post

DraftPost 上定義了一個 add_text 方法,這樣就可以像之前那樣向 content 增加文本,不過注意 DraftPost 並沒有定義 content 方法!如此現在程序確保了所有 blog 都從草案開始,同時DraftPost沒有任何可供展示的內容。任何繞過這些限制的嘗試都會產生編譯錯誤。

以類別轉換實作狀態轉移

增加另一個struct PendingReviewPost 來實現這個限制,在 DraftPost 上定義 request_review 方法來返回 PendingReviewPost,並在 PendingReviewPost 上定義 approve 方法來返回 Post

impl DraftPost {
    // --snip--

    pub fn request_review(self) -> PendingReviewPost {
        PendingReviewPost {
            content: self.content,
        }
    }
}

pub struct PendingReviewPost {
    content: String,
}

impl PendingReviewPost {
    pub fn approve(self) -> Post {
        Post {
            content: self.content,
        }
    }
}

request_reviewapprove 方法獲取 self 的所有權,因此會消費 DraftPostPendingReviewPost instance,並分別轉換為 PendingReviewPost 和發佈的 Post。在呼叫 request_review 之後就不會遺留任何 DraftPost 實例,後者同理。

PendingReviewPost 並沒有定義 content 方法,所以嘗試讀取其內容會導致編譯錯誤,DraftPost 同理。因為唯一得到定義了 content 方法的 Post 實例的途徑是呼叫 PendingReviewPostapprove 方法,而得到 PendingReviewPost 的唯一辦法是呼叫 DraftPostrequest_review 方法,現在我們就將發博文的工作流編碼進了類別系統。

因為 request_reviewapprove 返回新 instance 而不是修改被呼叫的 struct,所以我們需要增加更多的 let post = 覆蓋賦值來保存返回的實例。也不再能 assert 草案和等待審核的 blog content 為空字符串了

use blog::Post;

fn main() {
    let mut post = Post::new();

    post.add_text("I ate a salad for lunch today");

    let post = post.request_review();

    let post = post.approve();

    assert_eq!("I ate a salad for lunch today", post.content());
}

References

The Rust Programming Language

中文版

中文版 2

2020年6月8日

rust16 Concurrency

安全高速處理 concurrent programming 是 rust 的另一個主要目標。

concurrent programming 就是程式不同部分獨立執行

parallel programming 就是程式不同部分同時執行

因為在 context 中處理這些問題很容易出錯,rust 想解決這個問題

owner 與 類別系統是解決 memory leak 與 concurrency 的有效工具,使得 rust 很多 concurrency error 都變成編譯期的錯誤。因此我們可在開發時,就解決問題,可稱為 fearless concurrency。

(以下說到 concurrency 同時有兩個意思:concurrent 及 parallel programming)

Erlang 有著優雅的消息傳遞並發功能,但只有模糊不清的在線程間共享狀態的方法。高階語言只實現可能解決方案的子集合是合理的策略,因為高級語言強調犧牲一些控制,換取更高的抽象性。

  • 如何產生 thread
  • message passing concurrency,其中 channel 用來在 thread 之間傳遞資料
  • shared state concurrency,其中多個 threads 可存取同一片資料
  • SyncSend trait,可讓 rust 的 concurrency 擴充到自訂的的與 std library 提供的類別中

以 Thread 同時執行程式

大部分 OS 將程式運作於一個 process 中,而 OS 負責管理 process。程式內部也可以有多個同時運作的獨立部分,稱為 threads

將程式拆分多個 threads 可改善效能,但也會增加複雜性。因 theads 同時運作,無法保證不同 thread 間程式的運作順序,導致發生以下問題

  • race condition: 多個 thread 以不同順序存取資料或資源
  • deadlock: 兩個 threads 互相等待對方停止使用資源,而無法繼續運作
  • 只會發生在特定情況,且無法重現與修復的 bug

rust 嘗試解決使用 thread 的負面影響。

不同程式語言用不同方法實作 thread,很多 OS 提供了產生 thread 的 API。由 programming language 呼叫 OS API 產生 thread 的方法也稱為 1:1,一個 OS thread 對應一個 language thread

有些語言自己提供特殊的 thread,被稱為 green thread,使用 green thread 的語言會在不同數量的 OS thread context 中執行它們。因此 green thread model 被稱為 M:N model,M 個 green thread 對應 N 個 OS thread

對 rust 來說,最重要的取捨是 runtime support。runtime 在不同 context 有不同含意。

在目前的 context 中,runtime 代表 binary file 中包含語言自己提供的 code。這些 code 可大可小,但所有 non-assembly language 都有一定數量的 runtime code。更小的 runtime code 有較少功能,但 binary 也比較小,更容易在更多 context 中跟其他語言整合。雖然很多語言覺得增加 runtime code 換取功能是可接受的,但 rust 目標要做到幾乎沒有 runtime code,保持高效能且要能呼叫 C 語言。

rust 不使用 green thread,採用 1:1 thread model,有其他 crate 實現了 M:N thread model,那些 thread 犧牲了效能,但換來更好的 thread control 與更低的 context switching cost

spawn 產生 thead

呼叫 thread::spawn 並傳遞 closure 參數,用以產生 thread,該 closure 中包含希望在新 thread 中運作的 code

use std::thread;
use std::time::Duration;

fn main() {
    thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {} from the spawned thread!", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    // main thread 列印其他資料
    // 當 main thread 結束,整個程式都會結束
    for i in 1..5 {
        println!("hi number {} from the main thread!", i);
        thread::sleep(Duration::from_millis(1));
    }
}

thread::sleep 強制 thread 停止執行一小段時間,這會允許其他不同的線程運行。這些線程可能會輪流運行,不過並不保證如此:這跟操作系統如何切換thread 有關。即使新建的線程 code 列印直到 i 等於 9 ,但它在主線程結束之前也只列印到了 5。

hi number 1 from the spawned thread!
hi number 1 from the main thread!
hi number 2 from the spawned thread!
hi number 2 from the main thread!
hi number 3 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the main thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!

使用 join 等待所有 thread 結束

由於 main thread 結束,所有 thread 就會被中止

可透過 thread::spawn 的回傳值,儲存到變數中,用來修復 thread 沒有被完全執行的問題。

thread::spawn 的回傳值類別是 JoinHandleJoinHandle 是一個擁有所有權的值,當對其呼叫 join 方法時,它會等待其線程結束。

use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {} from the spawned thread!", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        println!("hi number {} from the main thread!", i);
        thread::sleep(Duration::from_millis(1));
    }

    // handle 的 join 會 blocking main thread 直到 handle 所代表的線程結束
    handle.join().unwrap();
}

如果把 join 提到 for 前面, main thread 會先等待 new thread 完成,再繼續 main thread,所以列印就不會交錯出現

use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {} from the spawned thread!", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    handle.join().unwrap();

    for i in 1..5 {
        println!("hi number {} from the main thread!", i);
        thread::sleep(Duration::from_millis(1));
    }
}

在 thread 使用 move closure

move closure 常常跟 thread::spawn 一起使用,可讓我們在一個 thread 使用另一個 thread 的資料。

chap 13 提到可在參數列表前,使用 move 強制 closure 獲取其使用環境值的所有權。這個方法在將 closure 傳遞給 new thread 以便將資料移動到 new thread 中最實用。

以下是一個嘗試在主線程中產生一個 vector 並用於新建線程的例子

closure 使用了 v,所以 closure 會捕獲 v 並使其成為 closure 環境的一部分。因為 thread::spawn 在一個新線程中執行這個 closure,所以可以在新線程中使用 v

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(|| {
        println!("Here's a vector: {:?}", v);
    });

    handle.join().unwrap();
}

但發生編譯錯誤

error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function
 --> src/main.rs:6:32
  |
6 |     let handle = thread::spawn(|| {
  |                                ^^ may outlive borrowed value `v`
7 |         println!("Here's a vector: {:?}", v);
  |                                           - `v` is borrowed here
  |
note: function requires argument type to outlive `'static`
 --> src/main.rs:6:18
  |
6 |       let handle = thread::spawn(|| {
  |  __________________^
7 | |         println!("Here's a vector: {:?}", v);
8 | |     });
  | |______^
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
  |
6 |     let handle = thread::spawn(move || {
  |                                ^^^^^^^

Rust 會 推斷 如何捕獲 v,因為 println! 只需要 v 的引用,closure 嘗試借用 v。然而這有一個問題:Rust 不知道這個新線程會執行多久,所以無法知道 v 的引用是否一直有效。

以下是 v 的引用有可能不再有效的狀況

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(|| {
        println!("Here's a vector: {:?}", v);
    });

    drop(v); // oh no!

    handle.join().unwrap();
}

透過在閉包之前增加 move 關鍵字,我們強制 closure 獲取其使用的值的所有權,而不是任由 Rust 推斷它應該借用值。

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(move || {
        println!("Here's a vector: {:?}", v);
    });

    handle.join().unwrap();
}

一旦 move v 的所有權,就無法在 main thread 中再使用 v 了,也就是不能在 main thread 將 v 丟棄 drop(v);

使用 Message Passing 在 Thread 間傳遞資料

確保安全的 concurrency 方法是 message passing。Go 的口號:Do not communicate by sharing memory; instead, share memory by communicating.

rust 實現 messaging passing 的方法是 channel,可想像為一條河,將小船放在河中。

channel 有兩個部分:transmitter, receiver。

範例

use std::thread;
use std::sync::mpsc;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let val = String::from("hi");
        tx.send(val).unwrap();
    });
}

使用 mpsc::channel 函數產生一個新的通道;mpsc多個生產者,單個消費者multiple producer, single consumer)的縮寫。換句話說,一個 channel 可以有多個產生值的發送端,但只能有一個接收端。


可在 main thread 接收 channel 的值

use std::thread;
use std::sync::mpsc;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let val = String::from("hi");
        tx.send(val).unwrap();
    });

    let received = rx.recv().unwrap();
    println!("Got: {}", received);
}

接收端有 recvtry_recv 兩種 method,recv 會 blocking 程式直到接收到值: Result<T, E>,當 channel 被關閉,recv 會收到 error

try_recv 不會 blocking 程式,會立刻回傳 Result<T,E>: Ok 表示有資料,Err 表示沒有資料。

Channels and Ownership Transference

ownership 規則在訊息傳遞中,有助於編寫安全的 concurrency code,避免錯誤。

以下嘗試在 new thread 中發送 val 後再使用它,結果編譯時就發生錯誤,一但把值發送到另一個 thread,就不能再使用它。

use std::thread;
use std::sync::mpsc;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let val = String::from("hi");
        tx.send(val).unwrap();
        println!("val is {}", val);
    });

    let received = rx.recv().unwrap();
    println!("Got: {}", received);
}

編譯錯誤

error[E0382]: borrow of moved value: `val`
  --> src/main.rs:10:31
   |
8  |         let val = String::from("hi");
   |             --- move occurs because `val` has type `std::string::String`, which does not implement the `Copy` trait
9  |         tx.send(val).unwrap();
   |                 --- value moved here
10 |         println!("val is {}", val);
   |                               ^^^ value borrowed here after move

發送多個值

main thread 中沒有任何暫停的 code,而是等待 channel 被關閉,會自動結束 for 迴圈

use std::thread;
use std::sync::mpsc;
use std::time::Duration;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let vals = vec![
            String::from("hi"),
            String::from("from"),
            String::from("the"),
            String::from("thread"),
        ];

        for val in vals {
            tx.send(val).unwrap();
            // 每次發送後暫停 1s
            thread::sleep(Duration::from_secs(1));
        }
    });

    for received in rx {
        println!("Got: {}", received);
    }
}

利用 cloning transmitter 產生多個 producers

mpsc 是 multiple producer, single consumer 的縮寫,可透過 clone 發送端,產生多個 producer

use std::thread;
use std::sync::mpsc;
use std::time::Duration;

fn main() {
    let (tx, rx) = mpsc::channel();

    // 以 clone 產生另一個 tx,在不同 thread 使用不同的 tx
    let tx1 = mpsc::Sender::clone(&tx);
    thread::spawn(move || {
        let vals = vec![
            String::from("1 hi"),
            String::from("1 from"),
            String::from("1 the"),
            String::from("1 thread"),
        ];

        for val in vals {
            tx1.send(val).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });

    thread::spawn(move || {
        let vals = vec![
            String::from("2 more"),
            String::from("2 messages"),
            String::from("2 for"),
            String::from("2 you"),
        ];

        for val in vals {
            tx.send(val).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });

    for received in rx {
        println!("Got: {}", received);
    }
}

執行結果

Got: 1 hi
Got: 2 more
Got: 1 from
Got: 2 messages
Got: 2 for
Got: 1 the
Got: 1 thread
Got: 2 you

Shared-State Concurrency

雖然 message passing 是處理 concurrency 很好的方法,但不是唯一一種方法。

channel 都是單所有權,因為一旦將一個值傳送到通道中,將無法再使用這個值。共享內存有多所有權:多個線程可以同時使用相同的memory。rust 以 mutex 達到這個功能。

以 mutexes 一次允許一個 thread 存取資料

mutex 就是 mutual exclusion 的縮寫

為了存取 mutex 的資料,thread 必須先取得 lock,lock 是 mutex 中一部分資料結構,會記錄誰有該資料的使用權。換句話說, mutex 是利用 locking system 保護資料。

mutex 很難使用,因此很多人還是使用 channel,要記住兩個原則:

  1. 在使用資料前,要先取得 lock
  2. 用完 mutex 保護的資料後,要 release lock

不過 rust 的類別及 ownership 系統,讓我們不會在 locking, unlocking 出錯

Mutex<T>的 API

從 single thread context 使用 mutex 開始測試

use std::sync::Mutex;

fn main() {
    // 用 new 產生 mutex
    let m = Mutex::new(5);

    {
        // 用 lock 獲取鎖,使用資料,這是 blocking function call
        let mut num = m.lock().unwrap();
        *num = 6;
    }

    println!("m = {:?}", m);
}

如果另一個線程擁有鎖,並且那個線程 panic 了,則呼叫 lock 會失敗。在這種情況下,沒人能夠再獲取鎖,所以這裡選擇 unwrap 並在遇到這種情況時使線程 panic。

Mutex<T> 是一個智能指針。lock 會回傳一個叫做 MutexGuard 的智能指針。這個智能指針實現了 Deref 來指向其內部數據;其也提供了一個 Drop實現當 MutexGuard 離開作用域時自動釋放鎖,這正發生內部作用域的結尾。我們不會忘記釋放鎖並阻塞互斥器為其它線程,因為鎖的釋放是自動發生的。

丟棄了鎖之後,可以列印出互斥器的值,並發現能夠將其內部的 i32 改為 6。

在多個 threads 之間分享 Mutex<T>

以下嘗試用 Mutex<T> 在十個線程中對同一個計數器值加一,這樣計數器將從 0 變為 10。注意,接下來的幾個例子會出現編譯錯誤

use std::sync::Mutex;
use std::thread;

fn main() {
    // 產生 counter 變數來存放內含 i32 的 Mutex<T>
    let counter = Mutex::new(0);
    let mut handles = vec![];

    // 產生 10 個 threads,所有 thread 使用相同的 closure
    // closure 會呼叫 lock 獲取 Mutex<T> 的 lock,然後 +1
    // 離開 scope 時,會自動 release lock
    for _ in 0..10 {
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();

            *num += 1;
        });
        handles.push(handle);
    }

    // 以 join 等待所有 thread 結束
    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}

編譯錯誤

error[E0382]: use of moved value: `counter`
  --> src/main.rs:12:36
   |
6  |     let counter = Mutex::new(0);
   |         ------- move occurs because `counter` has type `std::sync::Mutex<i32>`, which does not implement the `Copy` trait
...
12 |         let handle = thread::spawn(move || {
   |                                    ^^^^^^^ value moved into closure here, in previous iteration of loop
13 |             let mut num = counter.lock().unwrap();
   |                           ------- use occurs due to use in closure

訊息是說, counter 被移入了 closure,但這是我們需要的功能,卻發生編譯錯誤


接下來嘗試簡化程式,只產生兩個 threads

use std::sync::Mutex;
use std::thread;

fn main() {
    let counter = Mutex::new(0);
    let mut handles = vec![];

    let handle = thread::spawn(move || {
        let mut num = counter.lock().unwrap();

        *num += 1;
    });
    handles.push(handle);

    let handle2 = thread::spawn(move || {
        let mut num2 = counter.lock().unwrap();

        *num2 += 1;
    });
    handles.push(handle2);

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}

編譯錯誤

error[E0382]: use of moved value: `counter`
  --> src/main.rs:15:33
   |
5  |     let counter = Mutex::new(0);
   |         ------- move occurs because `counter` has type `std::sync::Mutex<i32>`, which does not implement the `Copy` trait
...
8  |     let handle = thread::spawn(move || {
   |                                ------- value moved into closure here
9  |         let mut num = counter.lock().unwrap();
   |                       ------- variable moved due to use in closure
...
15 |     let handle2 = thread::spawn(move || {
   |                                 ^^^^^^^ value used here after move
16 |         let mut num2 = counter.lock().unwrap();
   |                        ------- use occurs due to use in closure

error[E0382]: borrow of moved value: `counter`
  --> src/main.rs:26:29
   |
5  |     let counter = Mutex::new(0);
   |         ------- move occurs because `counter` has type `std::sync::Mutex<i32>`, which does not implement the `Copy` trait
...
15 |     let handle2 = thread::spawn(move || {
   |                                 ------- value moved into closure here
16 |         let mut num2 = counter.lock().unwrap();
   |                        ------- variable moved due to use in closure
...
26 |     println!("Result: {}", *counter.lock().unwrap());
   |                             ^^^^^^^ value borrowed here after move

第一個錯誤為 counter 被移入 handle 的 thread closure 中,因此無法在第二個 thread 呼叫 lock

rust 明確說明,無法將 counter 的 ownership 移動到多個 threads 裡面,接下來透過 chap15 的 multiple-ownership 方法解決

在 multiple threads 使用 multiple ownership

chap 15 用 smart pointer Rc<T> 產生引用計數的值,以便擁有多個 owner。

嘗試將 Mutex<T> 封裝到 Rc<T>,並在 ownership 移入 thread 之前,clone Rc<T>

use std::rc::Rc;
use std::sync::Mutex;
use std::thread;

fn main() {
    let counter = Rc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Rc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();

            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}

再次發生編譯錯誤

error[E0277]: `std::rc::Rc<std::sync::Mutex<i32>>` cannot be sent between threads safely
  --> src/main.rs:11:22
   |
11 |         let handle = thread::spawn(move || {
   |                      ^^^^^^^^^^^^^ `std::rc::Rc<std::sync::Mutex<i32>>` cannot be sent between threads safely
   |
   = help: within `[closure@src/main.rs:11:36: 15:10 counter:std::rc::Rc<std::sync::Mutex<i32>>]`, the trait `std::marker::Send` is not implemented for `std::rc::Rc<std::sync::Mutex<i32>>`
   = note: required because it appears within the type `[closure@src/main.rs:11:36: 15:10 counter:std::rc::Rc<std::sync::Mutex<i32>>]`
   = note: required by `std::thread::spawn`

Rc<T> 並不能安全的在線程間共享。當 Rc<T> 管理引用計數時,它必須在每一個 clone 呼叫時增加計數,並在每一個克隆被丟棄時減少計數。Rc<T> 並沒有使用任何 concurrency 處理,來確保改變計數的操作不會被其他線程打斷。在計數出錯時可能會導致詭異的 bug,比如可能會造成 memory leak,或在使用結束之前就丟棄一個值。我們所需要的是一個完全類似 Rc<T>,又以一種線程安全的方式改變引用計數的類型。

Atomic Reference Counting with Arc<T>

Arc<T> 是類似 Rc<T> 但又能安全用在 multi-thread 環境的類別。這是一個 atomically reference counted 類別,可查閱 std::sync::atomic 文件得到更多資訊

因為 Arc<T> 是thread-safe,所以性能較差,因此不是所有原始類別都是 atomic。

回到剛剛的例子,將 Arc<T> 替換掉 Rc<T> 就可以編譯與執行

use std::sync::{Mutex, Arc};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();

            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}

RefCell<T>/Rc<T>Mutex<T>/Arc<T> 的相似處

因為 counter 不可變,但可獲取內部值的可變引用,這表示 Mutex<T> 提供內部可變性,類似 Cell 類別。

使用 RefCell<T> 可改變 Rc<T> 的內容,同樣可使用 Mutex<T> 改變 Arc<T> 的內容

另一個要注意的是,rust 無法避免使用 Mutex<T> 全部的邏輯錯誤。在 chap15 使用 Rc<T> 可能造成引用循環,兩個 Rc<T> 互相引用,就會發生 memory leak。

同理 Mutex<T> 也有可能造成 deadlock,如果一個操作要鎖定兩個資源,而兩個 thread 各持有一個 lock,就會發生 deadlock

可參閱 Mutex<T>MutexGuard 的文件說明

以 Sync 與 Send Trait 擴充 Concurrency

rust 的 concurrency 處理,在程式語言層面來說,不需要知道太多 concurrency 處理的問題。

不過有兩個 concurrency 概念是內嵌於程式語言中:std::marker 中的 SyncSend trait。

Send 可在 threads 之間轉移 ownership

Send 標記 trait 表示類別的所有權可以在線程間傳遞。幾乎所有的 Rust 類型都有實作 Send ,不過有一些例外,包括 Rc<T>:這是不能 Send 的,因為如果clone了 Rc<T> 的值並嘗試將clone的所有權轉移到另一個線程,這兩個線程都可能同時更新引用計數。因此,Rc<T> 用於單線程,這時不需要為擁有線程安全的引用計數而付出性能代價。

Rust 類別系統和 trait bound 確保永遠也不會意外的將不安全的 Rc<T> 在線程間發送。當嘗試在示例 16-14 中這麼做的時候,會得到錯誤 the trait Send is not implemented for Rc<Mutex<i32>>。而使用標記為 SendArc<T>時,就沒有問題了。

任何完全由 Send 的類型組成的類型也會自動被標記為 Send。也就是說,幾乎所有基本類型都是 Send 的,除了 chap19 將會討論的 raw pointer。

Sync 可允許多個 threads 使用資料

Sync 標記 trait 表是一個實作了 Sync 的類別,可以安全的在多個線程中擁有其值的引用。換一種方式來說,對於任意類型 T,如果 &TT 的引用)是 Send 的話 T 就是 Sync 的,這意味著其引用就可以安全的發送到另一個線程。類似於 Send 的情況,基本類別是 Sync 的,完全由 Sync 的類別組成的類別也是 Sync 的。

智能指針 Rc<T> 也不是 Sync 的,出於其不是 Send 相同的原因。RefCell<T>Cell<T> 系列類別不是 Sync 的。RefCell<T> 在運行時所進行的借用檢查也不是線程安全的。

Mutex<T>Sync 的,正如 “在線程間共享 Mutex<T>” 部分所講的它可以被用來在多線程中共享使用。

直接實作 SendSync 是不安全的

通常並不需要實作 SendSync trait,因為由 SendSync 的類別組成的類別,自動就是 SendSync 的。因為他們是標記 trait,甚至都不需要實現任何方法。他們只是用來加強並發相關的不可變性的。

實作這些標記 trait 涉及到編寫不安全的 Rust 代碼, chap 19 將會講述具體的方法;當前重要的是,在產生新的由不是 SendSync 的部分構成的並發類別時需要多加小心,以確保維持其安全保證。The Nomicon 中有更多關於這些保證以及如何維持他們的資訊。

Summary

因為 Rust 本身很少有處理並發的內容,有很多的並發方案都由 crate 實現。他們比標準庫要發展的更快;請在網上搜索當前最新的用於多線程場景的 crate。

Rust 提供了用於消息傳遞的通道,和像 Mutex<T>Arc<T> 這樣可以安全的用於並發上下文的智能指針。類別系統和借用檢查器會確保這些場景中的代碼,不會出現 race condition 和無效的引用。一旦代碼可以編譯了,我們就可以堅信這些代碼可以正確的運行於多線程環境,而不會出現其他語言中經常出現的那些難以追蹤的 bug。

References

The Rust Programming Language

中文版

中文版 2