2020/06/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 適用處,refutable 與 irrefutable 模式的區別,及不同類別的 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 let、else if 和 else if let 表達式。相比 match表達式一次只能將一個值與模式比較提供了更多靈活性;一系列 if let、else if、else 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 let 和 while 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 以方便對 x 和 y 值進行計算

    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 座標並忽略 y和 z 的值:

    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/06/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;
    }
}

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

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

因為我們已經封裝了 AveragedCollection 的細節,將來可以輕鬆改變類似資料結構這些方面的內容。例如,可以使用 HashSet 代替 Vec 作為 list 的類別。只要 add、remove 和 average 公有函數的簽名保持不變,使用 AveragedCollection 的code就無需改變。相反如果使得 list 為公有,就未必都會如此了: HashSet 和 Vec 使用不同的方法增加或移除項,所以如果要想直接修改 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 所共享的行為,接下來 Draft、PendingReview 和 Published 狀態都會實作 State 。現在這個 trait 並沒有任何方法,一開始只定義 Draft狀態因為這是我們希望 blog 的初始狀態。

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

存放 blog content

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

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

因此就先在 impl Post 增加這個 fn,因為需要改變 add_text 的 Post,故獲取一個 self 的可變引用,接下來用content 中的 String 的 push_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 的一個缺點是,因為實作了狀態轉換,讓狀態之間有相關性,例如想在 PendingReview 與 Published 之間增加 Scheduled 狀態,就要修改 PendingReview

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

另一個重複是 Post 裡面的 request_review 與 approve 很類似,都是設定 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);
    }
}

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

仍然有一個 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_review 和 approve 方法獲取 self 的所有權,因此會消費 DraftPost 和 PendingReviewPost instance,並分別轉換為 PendingReviewPost 和發佈的 Post。在呼叫 request_review 之後就不會遺留任何 DraftPost 實例,後者同理。

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

因為 request_review 和 approve 返回新 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/06/08

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 可存取同一片資料
  • Sync 與 Send 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 的回傳值類別是 JoinHandle。JoinHandle 是一個擁有所有權的值,當對其呼叫 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);
}

接收端有 recv 與 try_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 中的 Sync 和 Send 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>>。而使用標記為 Send 的 Arc<T>時,就沒有問題了。

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

Sync 可允許多個 threads 使用資料

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

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

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

直接實作 Send 與 Sync 是不安全的

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

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

Summary

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

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

References

The Rust Programming Language

中文版

中文版 2