2020年4月6日

rust08 Common Collections

Collection 可包含多個值。跟內建的 array 與 tuple 不同,collection 是儲存在 heap,資料的大小可隨時異動。以下討論三個最常用的 collection

  • vector: 可逐項儲存數量可變的值
  • string: 是 char 的集合,也就是先前用過的 String
  • hash map: key - value pair。這是 map 的一種特殊實作的版本。

std library 提供的其他 collection 的文件

Storing Lists of Values with Vectors

vector 可儲存多個值,在記憶體中是一個接著一個排列。只能儲存相同類別的值。例如,適合儲存文件的逐行文字資料,或是購物車裡的商品價格。

建立新的 Vector

注意要加上 <i32> 資料類別,因為 compiler 無法判斷沒有任何值的 Vector 的資料型別。

let v: Vec<i32> = Vec::new();

vec! 是 Macro,因為已經有初始的值,compiler 就能推測出 v 的類別是 Vec<i32>

let v = vec![1, 2, 3];

更新 Vector

要能改變 v,必須宣告為 mut 可變

let mut v = Vec::new();

v.push(5);
v.push(6);
v.push(7);
v.push(8);

Dropping a Vector Drops Its Elements

當 vector 離開 scope 被丟棄時,裡面的內容也會被丟棄。

{
    let v = vec![1, 2, 3, 4];

    // 使用 v

} // v 離開 scope 並被丟棄

讀取 Vector 的 elements

用 index 或是 get method,index 的結果是 reference,get method 回傳的結果是 Option<&T>

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

// &v[2] 透過 index 取得第三個資料
let third: &i32 = &v[2];
println!("The third element is {}", third);

// 用 get method
match v.get(2) {
    Some(third) => println!("The third element is {}", third),
    None => println!("There is no third element."),
}

如果要取得超過長度的 index,就會在執行時發生 error,這部分錯誤無法在編譯時被發現。而 get 並不會 crash,而是回傳 None。

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

// thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 100'
let does_not_exist = &v[100];

// None
let does_not_exist = v.get(100);

當程式獲得一個有效的 reference,borrow checker 會處理 ownership 及 borrowing rules 確保 vector 的引用永遠有效。ex: 因為在相同作用域中同時存在可變和不可變引用的規則,無法在獲得了 vector 的第一個元素的不可變引用後,嘗試在 vector 末尾增加一個元素。

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

let first = &v[0];

v.push(6);

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

編譯時會發生錯誤

error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable

在 vector 的結尾增加新元素時,可能發生沒有足夠空間將所有所有元素依次相鄰存放,這時候會分配新記憶體並將舊的元素複製到新的空間中。這時,第一個元素的引用就指向了被釋放的記憶體。借用規則會阻止程式陷入這種狀況。

Iterating over the Values in a Vector

用 for

let v = vec![100, 32, 57];
for i in &v {
    println!("{}", i);
}

// 也可以取得可變引用,然後修改裡面的值
let mut v = vec![100, 32, 57];
for i in &mut v {
    *i += 50;
}

Using an Enum to Store Multiple Types

當需要在 vector 儲存不同類別的資料時,可以使用 enum。vector 裡面儲存的類別,就是相同的 enum

enum SpreadsheetCell {
    Int(i32),
    Float(f64),
    Text(String),
}

let row = vec![
    SpreadsheetCell::Int(3),
    SpreadsheetCell::Text(String::from("blue")),
    SpreadsheetCell::Float(10.12),
];

但如果一開始無法知道要儲存到 vector 的所有類別,就無法使用 enum,要改用 chap17 的 trait。

Storing UTF-8 Encoded Text with Strings

通常在 String 會遇到三個問題:rust 會確保找出所有可能的錯誤、String 資料結構比想像中複雜、UTF-8。

String 本身就是 collection of bytes,再外加一些 method 讓 bytes 可用 text 解譯。接下來會討論 String 跟其他 collections 的相異處,例如:因為人類跟機器解讀 String 方法不同,造成String 的 indexing 比較複雜。

What is a String?

rust 在核心中只有一種 string type: string slice str,通常會以引用的方式出現 &str。chap4 有討論過 string slices 就是 references 到某些存在別處的 UTF-8 encoded string data。例如 String literal,就是存在程式的 binary 中,因此也是 string slices。

rust std library 提供的 String type 是 growable, mutable, owned, UTF-8 encoded string type。當 Rustacean 談到 rust 的 "string" 時,通常是同時代表 String 以及 string slice &str 這兩個。雖然大部分都是關於 String,這兩個類別在 std 都被廣泛使用,同時他們都是 UTF-8 encoded。

std library 還有其他 string 類別,例如:OsString, OsStr, CString, CStr。還有其他 libray crate 提供更多 string。類別是以 String 或是 Str 結尾, 對應到 owned 及 borrowed variants。

Creating new String

    // 建立新的 String
    let mut s = String::new();

    let data = "initial contents";
    // to_string 可用在任何實現了 Display trait 的類別
    let s = data.to_string();

    // 用 string literal 的 to_string 產生 String
    let s = "initial contents".to_string();

    // 用 String::from 產生 String
    let s = String::from("initial contents");

    // 可以儲存 UTF-8 的字串
    let hello = String::from("السلام عليكم");
    let hello = String::from("Dobrý den");
    let hello = String::from("Hello");
    let hello = String::from("שָׁלוֹם");
    let hello = String::from("नमस्ते");
    let hello = String::from("こんにちは");
    let hello = String::from("안녕하세요");
    let hello = String::from("你好");
    let hello = String::from("Olá");
    let hello = String::from("Здравствуйте");
    let hello = String::from("Hola");

Updating a String

可使用 push_str, push 增加 string 內容

    // push_str 增加 string slice,但 push_str 不需要獲得該 string 的 ownership
    let mut s = String::from("foo");
    s.push_str("bar");

    // 如果 push_str 獲取了 s2 的 ownership,就會無法列印資料
    let mut s1 = String::from("foo");
    let s2 = "bar";
    s1.push_str(s2);
    println!("s2 is {}", s2);

    // 用 push 增加 char
    let mut s = String::from("lo");
    s.push('l');

使用 +format! 連接字串

    let s1 = String::from("Hello, ");
    let s2 = String::from("world!");
    let s3 = s1 + &s2; // 注意 s1 被移動了,不能繼續使用, s2 還可以繼續使用

    // + 的 函數定義類似這樣
    // fn add(self, s: &str) -> String {
    // &s2 是引用, add 只能將 String 及 &s 相加,不能將兩個 String 相加
    // 上面的 s2 會由 String 強制轉型 coerced 為 &str

    let s1 = String::from("tic");
    let s2 = String::from("tac");
    let s3 = String::from("toe");

    //let s = s1 + "-" + &s2 + "-" + &s3;
    // 可改用 format!,跟 println! 類似,且不會獲取 s2, s3 的 ownership
    let s = format!("{}-{}-{}", s1, s2, s3);

    println!("{}, {}, {}", s, s2, s3);

Indexing into Strings

如果用 index 語法取得 String 的一部分會發生錯誤,也就是說 rust 的 string 不支援 indexing

    let s1 = String::from("hello");
    let h = s1[0];

編譯錯誤

error[E0277]: the type `std::string::String` cannot be indexed by `{integer}`
 --> src/main.rs:5:13
  |
5 |     let h = s1[0];
  |             ^^^^^ `std::string::String` cannot be indexed by `{integer}`
  |
  = help: the trait `std::ops::Index<{integer}>` is not implemented for `std::string::String`

String 的實作方法

String 是一個 Vec<u8> 的封裝

    let len1 = String::from("Hola").len();
    // len 為 4

    let len2 = String::from("Здравствуйте").len();
    // len 為 24 不是 12,因為 unicode 每個字元需要 2 bytes

    println!("{}, {}", len1, len2);

因此 char 的 index 並不一定能對應到有效的 unicode。

假設 rust 可以這樣寫

let hello = "Здравствуйте";
let answer = &hello[0];

З 的第一個byte 為 208, 第二個是 151, answer 應該為 208,但 208 不是一個正常的 unicode。為了避免發生這樣的問題,rust 直接拒絕這樣的寫法,而是給我們編譯錯誤的訊息。

Bytes and Scalar Values and Grapheme Clusters

rust 有三種方式理解 string: bytes, scalar values, grapheme clusters (字形集合)

例如 印度語單詞 “नमस्ते” 存在 Vector 為

[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]

有 18 bytes,但從 unicode 來說,應該要是

['न', 'म', 'स', '्', 'त', 'े']

但第四與第六都不是字母,以 grapheme cluster 方式理解,應該為

["न", "म", "स्", "ते"]

另外因為 String 透過 index 取得 slice 的時間預期為 O(1),但 String 每次都必須要從開頭開始,無法確保O(1) 這樣的效能。

因此 rust 的 string 不支援 indexing

Slicing Strings

這是可能會造成程式 crash 的 method。可以用 [] 及 range 取得 string slice

    let hello = "Здравствуйте";

    // 這些字元都是 2 bytes
    // s 將會是 “Зд”
    let s = &hello[0..4];

    // 如果獲取 &hello[0..1] ,會造成程式 panic
    let s2 = &hello[0..1];
thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`', src/libcore/str/mod.rs:2027:5

Iterating over Strings

for c in "नमस्ते".chars() {
    println!("{}", c);
}
न
म
स
्
त
े

也可以轉換為 bytes

for b in "नमस्ते".bytes() {
    println!("{}", b);
}

Storing Keys with Associated Values in Hash Maps

HashMap<K, V> 透過 hashing function 決定如何將 key, value 放入 memory

Creating a New Hash Map

    // HashMap 沒有倍 prelude 自動引用
    use std::collections::HashMap;

    // 類似 vector,HashMap 的 key 為 String, value 為 i32
    let mut scores = HashMap::new();
    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);


    // 也可以用 vector 的 collect 產生 Hash Map
    let teams  = vec![String::from("Blue"), String::from("Yellow")];
    let initial_scores = vec![10, 50];

    // 先透過 zip 產生 tuple 的 vector,再呼叫 collect
    let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();
    println!("{:?}", scores);
    // {"Yellow": 50, "Blue": 10}

Hash Maps and Ownership

對於像 i32 實現 Copy trait 的類別,值可以複製到 Hash Map,但對於 String 有 ownership 的值,其值會因為 move 而轉職給 Hash Map

    use std::collections::HashMap;

    let field_name = String::from("Favorite color");
    let field_value = String::from("Blue");

    let mut map = HashMap::new();
    map.insert(field_name, field_value);
    // 這裡 field_name 和 field_value 不再有效,不能再使用

如果是用 reference,這些引用指向的值,雖 ownership 不會移動給 Hash Map,但必須在 map 有效時同樣有效。

存取 Hash Map 的 Values

    use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);

    let team_name = String::from("Blue");
    // get 會回傳 Option<V>,如果不存在會回傳 None
    let score = scores.get(&team_name);
    println!("{:?}", score);
    // Some(10)


    // use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);

    // 用 for iterate Hash Map
    for (key, value) in &scores {
        println!("{}: {}", key, value);
    }
    //Yellow: 50
    //Blue: 10

更新 Hash Map

覆蓋舊的 value: insert

    use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Blue"), 25);

    println!("{:?}", scores);
    // {"Blue": 25}

只在沒有 value 時 insert: entry

    use std::collections::HashMap;

    let mut scores = HashMap::new();
    scores.insert(String::from("Blue"), 10);

    scores.entry(String::from("Yellow")).or_insert(50);
    // 不會改成 50
    scores.entry(String::from("Blue")).or_insert(50);

    println!("{:?}", scores);
    // {"Yellow": 50, "Blue": 10}

根據舊的值,更新新值

例如記錄某個單詞出現幾次

    use std::collections::HashMap;

    let text = "hello world wonderful world";

    let mut map = HashMap::new();

    // or_insert 會回傳 &mut V,可變引用
    for word in text.split_whitespace() {
        let count = map.entry(word).or_insert(0);
        *count += 1;
    }

    println!("{:?}", map);
    // {"wonderful": 1, "hello": 1, "world": 2}

Hashing Functions

HashMap 預設使用 "cryptographically strong" hashing function,可防止 Denial of Service (DoS) 攻擊。這不是最快的演算法,但犧牲性能提高安全性。可利用 hasher 切換使用其他 hashing function。hasher 是實作 BuildHasher trait 的類別。crates.io 可找到其他常用的 hasher hashing function。

References

The Rust Programming Language

中文版

中文版 2

2020年3月30日

rust07 Packages, Crates, and Modules

程式都會遇到 scope 的問題,在某一個時間點,compiler 知道些變數,允許呼叫哪些函數,變數引用是什麼?

rust 有 module system

  • packages: 是 cargo 的功能,可建立、測試、分享 crate
  • crates 是一個 module 的樹狀結構,有 library 或 binary project
  • modules 和 use: 允許你控制 scope 及 privacy of paths
  • paths: 對 struct, function, module 命名的方法

Packages and Crate

  • crate 是 binary 或是 library
  • crate root 一份描述如何建構 create 的文件
  • 一個 Package 是一個或多個 crates
  • Package 透過 Cargo.toml 用來描述建構 crates

  • package 可包含 0 或 1 個 library crate,不能超過。可包含多個 binary crates。至少要包含一個 crate (library 或 binary 都可以)

cargo new 就是在產生一個新的 package

$ cargo new my-project
     Created binary (application) `my-project` package

cargo package 預設就是在 Cargo.toml 同一層的目錄中,有包含 src 目錄,且裡面有 main.rs 時, cargo 就知道這個 package 帶有一個跟 package 同名的 binary crate,且 src/main.rs 就是 crate root。另外如果 src/lib.rs,其 package 帶有同名的 library crate,且 src/lib.rs 是 crate root。 crate root 檔案將會傳給 rust 用來建構 library 或是 binary。

package 可包含 0 或 1 個 library crate,不能超過。可包含多個 binary crates。至少要包含一個 crate (library 或 binary 都可以)

如果 package 同時包含 src/main.rs 及 src/lib.rs,就是帶有兩個 crate: 一個同名 library,一個同名 binary。package 可帶有多個 binary crate,要將其檔案放在 src/bin 目錄,每個檔案都是一個獨立的 binary crate。

Defining Modules to Control Scope and Privacy

module system

  • module: 組織 code 與控制路徑私有性的方法
  • paths: 命名 items 的方法
  • use: 將某個 path 帶入 scope
  • pub: 開放 items,讓 items 變成公有
  • as: 將 item 引入 scope 時,用來 rename
  • external packages
  • glob operator: 將 module 的所有內容引入 scope
  • 將不同 module 分割到獨立的檔案中

module 可組織程式碼,將 crate 分組,增加 readability 與 reuse 便利性。module 也可以控制 privacy of items,item 可以是 public (可被 outside code 使用),或是 private,只是 internal implementation。

以下是 restaurant 的 library crate 範例

在一個餐廳可分兩個部分:front of house 及 back of house。front of house 包含 seating customers, 收單的 servers,payment,製作飲料的 bartenders。back of house 包含chef,在廚房燒菜,洗碗機,及負責管理的 managers。

首先建立一個新的 library

cargo new --lib restaurant

修改 src/lib.rs

mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}

        fn seat_at_table() {}
    }

    mod serving {
        fn take_order() {}

        fn serve_order() {}

        fn take_payment() {}
    }
}

modules 可包含其他 items 的定義:structs, enums, constants, traits, functions

module 可將相關的定義放在一起。

src/main.rs, src/lib.rs 分別稱為 crate roots

以上就是產生了這樣的 module tree。類似 file system 的目錄樹,另一個類似的地方,是需要用 path 來找到適當的 module 或 item。

crate
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment

Paths for Referring to an item in the Module Tree

rust 以 path 在module tree 中定位 item,如果要呼叫某個 function 就必須要知道他的 path

path 有兩種形式,兩種都有一或多個 identifiers,id 之間以 :: 區隔

  • absolute path: 從 crate root 開始,以 crate 開頭
  • relative path: 從目前的 module 開始,以 self, super 或目前的 module 的 id 開始
mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    front_of_house::hosting::add_to_waitlist();
}

在編譯時,會發生 error ,這是 privacy 的問題

error[E0603]: function `add_to_waitlist` is private
 --> src/lib.rs:9:37
  |
9 |     crate::front_of_house::hosting::add_to_waitlist();
  |                                     ^^^^^^^^^^^^^^^

error[E0603]: function `add_to_waitlist` is private
  --> src/lib.rs:12:30
   |
12 |     front_of_house::hosting::add_to_waitlist();
   |                              ^^^^^^^^^^^^^^^

rust 的所有 items: funcitons, methods, structs, enums, modules, constants 預設都是 private。在 parent module 的 items 不能使用 child modules 裡面的 private items。但 child modules 的 items 可以使用 ancestor modules 的 items。原因是 child modules 會 wrap & hide implementation details,但 child modules 可看到跟其定義相同的 context 的其他 items。

rust 會隱藏 inner implementation details,因此我們可知道如何在不影響 outer code 的狀況下,改變 inner code。可利用 pub keyword 讓 item 變成 public。

Exposing Paths with pub

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

// 因為 eat_at_restaurant 跟 front_of_house 是相同的 module,故可直接使用 front_of_house
pub fn eat_at_restaurant() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    front_of_house::hosting::add_to_waitlist();
}

Starting Relative Paths with super

super 類似 filesystem 的 .. 用途,代表 parent module。

backofhouse 跟 serve_order 在相同的 module

fn serve_order() {}

mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();

        // super 就等同於 crate
        super::serve_order();
    }

    fn cook_order() {}
}

Making Structs and Enums Public

如果在 struct 定義前面加上 pub,可讓 struct 變成 public,但 struct 裡面的欄位還是 private。必須單獨每一個欄位分開設定 pub

mod back_of_house {
    pub struct Breakfast {
        pub toast: String,
        seasonal_fruit: String,
    }

    // 為 Breakfast 增加 public method: summer
    impl Breakfast {
        pub fn summer(toast: &str) -> Breakfast {
            Breakfast {
                toast: String::from(toast),
                seasonal_fruit: String::from("peaches"),
            }
        }
    }
}

pub fn eat_at_restaurant() {
    // Order a breakfast in the summer with Rye toast 訂早餐
    let mut meal = back_of_house::Breakfast::summer("Rye");

    // Change our mind about what bread we'd like 修改餐點
    meal.toast = String::from("Wheat");
    println!("I'd like {} toast please", meal.toast);

    // The next line won't compile if we uncomment it; we're not allowed
    // to see or modify the seasonal fruit that comes with the meal
    // 無法修改 seasonal_fruit,因為該欄位是 private
    // meal.seasonal_fruit = String::from("blueberries");
}

對於將 enum 設定為 public,只需要在定義的地方加上 pub,就會讓所有 variants 都是 public

mod back_of_house {
    pub enum Appetizer {
        Soup,
        Salad,
    }
}

pub fn eat_at_restaurant() {
    let order1 = back_of_house::Appetizer::Soup;
    let order2 = back_of_house::Appetizer::Salad;
}

Bringing Paths info Scope with use

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

// 原本在使用 hosting,必須要透過 相對或絕對路徑
// 透過 use 宣告後,後面就可直接使用 hosting
use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

use 要使用相對路徑有一點點不同,路徑必須要用 self 開頭。不過目前實測,如果不寫 self 也沒有問題。

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

// self 也可以不寫
//use self::front_of_house::hosting;
use front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

Creating Idiomatic use Paths

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

// use 可以將 add_to_waitlist 引入,這樣後面的程式,就可以直接呼叫這個 fn
// 但這樣是不好的寫法
// 因為使用 add_to_waitlist 時,感覺 add_to_waitlist 是定義在這個 module 裡面
// 而上面 hosting::add_to_waitlist(); 這樣的呼叫方式
// 比較清楚知道,add_to_waitlist 是定義在其他地方
use crate::front_of_house::hosting::add_to_waitlist;

pub fn eat_at_restaurant() {
    add_to_waitlist();
    add_to_waitlist();
    add_to_waitlist();
}

當 use structs, enums, 其他 items 時,最好指定 full path。例如指定使用標準函式庫的 HashMap

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.insert(1, 2);
}

唯一的例外是,在 scope 中,使用了兩個相同名稱的 items,這時必須要加上 parent module,用來區分 items

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

fn function1() -> fmt::Result {
}

fn function2() -> io::Result<()> {
}

Providing New Names with as

使用相同名稱的 items 有另一種解決方法,就是用 as 加上替代的 new local name

use std::fmt::Result;
use std::io::Result as IoResult;

fn function1() -> Result {
}

fn function2() -> IoResult<()> {
}

Re-exporting Names with pub use

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

// 在 use 前面再加上 pub
// 就等於將 hosting 變成 public,外面的 code 也可以直接使用 hosting::add_to_waitlist()
pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

Re-exporting 在程式碼的 internal structure 跟外面呼叫你的 code 的方法不同時,會很有用。例如餐廳,營運者很容易了解 "front of house" "back of house" 的意義。但客戶可能不懂這兩個名詞,這時候,可以透過 pub use 調整結構。

Using External Packages

先前專案有使用 rand 這個 external package,是在 Cargo.toml 裡面加上 rand

[dependencies]
rand = "0.5.5"

加上 rand 的定義後,等於將 rand 帶入這個 package 的 scope。透過 use 引用 Rng

use rand::Rng;
fn main() {
    let secret_number = rand::thread_rng().gen_range(1, 101);
}

Rust community 將許多 library packages 都放在 https://crates.io

標準函式庫 std 也是一個 crate。但因為 std 是在 rust language 裡面,所以不需要另外在 Cargo.toml 裡面填寫到 dependencies 中。

Using Nested Paths to Clean Up Large use Lists

如果同時要使用兩個以上的 module,但 std:: 會一直重複。

use std::cmp::Ordering;
use std::io;

改用這個方式

use std::{cmp::Ordering, io};

另一個例子

use std::io;
use std::io::Write;

// 改為
use std::io::{self, Write};

The Glob Operator

如果想使用某個 path 的所有 public items,就用 *

use std::collections::*;

Separating Modules info Different Files

先前的範例都是將多個 module 定義在一個檔案中。可以將 module 分在不同檔案。

修改 src/lib.rs

// 定義在 src/front_of_house.rs
mod front_of_house;

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

src/frontofhouse.rs

pub mod hosting {
    pub fn add_to_waitlist() {}
}

可再進一步分離 hosting

修改 src/frontofhouse.rs

pub mod hosting;

新增 src/frontofhouse/hosting.rs

pub fn add_to_waitlist() {}

References

The Rust Programming Language

中文版

中文版 2

2020年3月23日

rust06 enums and pattern matching

enumerations 也稱為 enums,可列舉所有可能的值,定義一個新的類別。首先會了解如何定義 enums,以及資料編碼的方法。然後會了解一個特殊的 enum,就是 Option,代表不是某個值,就是另一個值。然後介紹 pattern match expression,最後是 if let,一個方便處理 enum 的方法。

rust 的 enum 類似 F#, OCaml, Haskell 的 algebraic data types

Defining an enum

假設要處理 IPv4, IPv6 兩種 IP Address,程式會遇到兩種 IP,故要用 enum 列出所有可能的值。

任何一個 IP,不是 IPv4 就是 IPv6,不能兩個都是,但 IPv4, IPv6 都是 IP Address。

enum IpAddrKind {
    V4,
    V6,
}

enum values

產生兩個不同成員的 instances,IpAddrKind::V4IpAddrKind::V6 都是 IpAddrKind 類別。

let four = IpAddrKind::V4;
let six = IpAddrKind::V6;   

可定義使用 IpAddrKind 的 function,並用 IpAddrKind::V4IpAddrKind::V6 呼叫該 function

fn route(ip_type: IpAddrKind) { }

route(IpAddrKind::V4);
route(IpAddrKind::V6);

另外用 struct 儲存實際的 IP Address

enum IpAddrKind {
    V4,
    V6,
}

struct IpAddr {
    kind: IpAddrKind,
    address: String,
}

let home = IpAddr {
    kind: IpAddrKind::V4,
    address: String::from("127.0.0.1"),
};

let loopback = IpAddr {
    kind: IpAddrKind::V6,
    address: String::from("::1"),
};

可以用另一種更簡潔的方式,表達相同的概念,就是直接將資料放入 enum,這樣就不需要 struct。

enum IpAddr {
    V4(String),
    V6(String),
}

let home = IpAddr::V4(String::from("127.0.0.1"));

let loopback = IpAddr::V6(String::from("::1"));

用 enum 代替 struct 還有其他優點:每個成員可以處理不同類與數量的資料。

例如將 IPv4 分成四個 0~255 的數字

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);

let loopback = IpAddr::V6(String::from("::1"));

標準函式庫提供了 IPv4, IPv6 的定義:

struct Ipv4Addr {
    // --snip--
}

struct Ipv6Addr {
    // --snip--
}

enum IpAddr {
    V4(Ipv4Addr),
    V6(Ipv6Addr),
}

可以將任意資料放入 enum 成員中,例如 String, 數字, structs,放入另一個 enum 也可以。

另一個範例,裡面有不同類別的值

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

這跟用多個不同的 struct 定義很類似,但 struct 會是不同的類別,不能像 enum message 一樣,可定義一個同時能處理這些類別的 function

struct QuitMessage; // 類單元結構體
struct MoveMessage {
    x: i32,
    y: i32,
}
struct WriteMessage(String); // 元組結構體
struct ChangeColorMessage(i32, i32, i32); // 元組結構體

enum 也可以用 impl 定義 method,這個範例是定義 call 這個 method

impl Message {
    fn call(&self) {
        // 在這裡定義方法body
    }
}

let m = Message::Write(String::from("hello"));
m.call();

The Option Enum and Its Advantages Over Null Values

Option 是標準函式庫定義的另一個 enum。代表有值或是沒有值。這也就是說,可以處理掉 Null 造成的問題。

rust 並沒有其他語言都有的 null value,在有 null 的程式語言中,變數通常是 null 或是 non-null

null 的發明者 Tony Hoare 在 2009年"Null References: The Billion Dollar Mistake" 演講裡面說,null 是一個 billion-dollar mistake,原本他是要設計一個可透過 compiler 自動檢查,保證所有引用的使用都是安全的。但他另外增加了 null reference,因為他很容易實作出來。但這是一個很容易發生的 error, vaulerabilities, system crash,也就是這四十多年造成了數十億美元的損失。

使用 null value 會造成 error,非常容易出現。

但 null 本身還是有意義的,代表目前無效或是不存在的值。

rust 沒有 null,但有提供 Option<T>,包含在 prelude 中,也不需要 Option:: 就能直接使用 SomeNone

enum Option<T> {
    Some(T),
    None,
}

宣告時,如果使用 None,必須明確定義類別,因為 compiler 不知道怎麼判斷類別。

let some_number = Some(5);
let some_string = Some("a string");

let absent_number: Option<i32> = None;

Option<T> 比 null 好的原因是資料類別檢查,另外不需要擔心變數是不是空值,只要不是 Option<T> 就不可能是空值。

例如無法將 Option<i8>i8 相加

let x: i8 = 5;
let y: Option<i8> = Some(5);

let sum = x + y;

可閱讀 Enum std::option::Option 文件,了解要如何使用 Option

例如用 match 處理 Some 與 None 兩種狀況

    match self {
        Option::Some(val) => val,
        Option::None =>
            panic!("called `Option::unwrap()` on a `None` value"),
    }

The match Control Flow Operator

match 可將 value 與一系列模式相比較匹配,模式可由 literal, 變數, wildcards 或其他內容組成。

match 可想像為硬幣分類器。valueincents 可取得硬幣的價值。

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u32 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        },
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

match 很像 if,但 if expression 要回傳 bool,而 match 可回傳任意類別。

match 會依照順序進行比較,會執行匹配的第一個值,回傳該值後面的 expression 回傳值。也可以用 {} 加上多行程式碼。

Patterns that Bind to Values

match 的另一個功能是綁定匹配模式的部分值,也就是從 enum 成員中提取值。

ex: 1999~2008,US 在每個州印刷了不同的 25 美分,但其他硬幣就沒有這種設計,將 State 這個資訊加入 enum 改變 Quarter。

Quarter 成員存放了一個 UsState 值的 Coin enum,匹配到 Coin::Quarter 時,state 將會綁定 25 美分硬幣所對應州的值。接著在那個分支的代碼中使用 state

#[derive(Debug)] // 這樣可以可以立刻看到州的名稱
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn value_in_cents(coin: Coin) -> u32 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("State quarter from {:?}!", state);
            25
        },
    }
}

呼叫 value_in_cents(Coin::Quarter(UsState::Alaska))時,coin 將是 Coin::Quarter(UsState::Alaska)。當將值與每個分支相比較時,直到遇到 Coin::Quarter(state)。這時,state 綁定的將會是值 UsState::Alaska。接著就可以在 println! 表達式中使用這個綁定了,像這樣就可以獲取 Coin 枚舉的 Quarter 成員中內部的州的值。

Matching with Option<T>

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i + 1),
    }
}

let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);

Matches Are Exhaustive

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        Some(i) => Some(i + 1),
    }
}

剛剛的 plus_one 如果忘了匹配 None,就會發生編譯錯誤

error[E0004]: non-exhaustive patterns: `None` not covered
 --> src/main.rs:2:11
  |
2 |     match x {
  |           ^ pattern `None` not covered

rust 的 matching 是 exhaustive 的,必須要窮舉,將所有可能性都列出來。

The _ Placeholder

如果真的不想列出所有可匹配的條件,可以用 _替代。

最後的 _ 會匹配所有上面沒有的值

let some_u8_value = 0u8;
match some_u8_value {
    1 => println!("one"),
    3 => println!("three"),
    5 => println!("five"),
    7 => println!("seven"),
    _ => (),
}

但這樣在只關心一種條件的狀況,語法就太多。因此 rust 提供了 if let

Concise Control Flow with if let

let some_u8_value = Some(0u8);
match some_u8_value {
    Some(3) => println!("three"),
    _ => (),
}

可以用 if let 替代,程式碼會比較少,但會失去 match 的 exhaustive 窮舉特性

if let Some(3) = some_u8_value {
    println!("three");
}

可以在 if let 再加上 else

let mut count = 0;
match coin {
    Coin::Quarter(state) => println!("State quarter from {:?}!", state),
    _ => count += 1,
}

上面的程式碼可以改寫為

let mut count = 0;
if let Coin::Quarter(state) = coin {
    println!("State quarter from {:?}!", state);
} else {
    count += 1;
}

References

The Rust Programming Language

中文版

中文版 2