2020年5月24日

rust15 Smart Pointers

pointer 是儲存記憶體位址的變數,會指向其他資料。rust 最常見的 pointer 是 chap4 介紹的引用 reference。利用 & 參考到指向的值,沒有特殊功能,也沒有額外開銷,所以最常用。

smart pointers 是另一種資料結構,類似 pointer 但有額外的 metadata 及功能。smart pointer 概念起源於 C++ 及其他語言。本章會討論 reference counting smart pointer,允許該資料有多個使用者。reference counting smart pointer 會記錄有幾個使用者,在沒有任何使用者時,會清除資料。

rust 的 reference 與 smart pointer 的差異是,smart pointer 擁有 指向的資料。

chap 8 的 StringVec<T> 都是 smart pointer。因為可修改這些資料,且帶有 metadata (ex: 容量) 與額外的功能 (ex: String 是 UTF-8 編碼)

smart pointer 通常用 struct 實作。另外實作了 DerefDrop trait。Deref trait 可讓 smart pointer 表現得像引用。Drop trait 可自訂當 smart pointer 離開 scope 時的程式。

smart pointer 是 rust 常用的通用設計模式。以下會討論到

  • Box<T> 可在 heap 配值
  • Rc<T> 一個引用計數類別,該資料可以有多個使用者
  • Ref<T>RefMut<T> ,透過 RefCell<T> 存取,是在執行期而不是編譯時執行借用規則的類別

還會討論 內部可變性 interior mutability 模式,可讓不可變的類別暴露出可改變內部值的 API

引用循環 reference cycles 會如何 leak memory,要如何避免

使用 Box 指向 heap 的資料

Box<T> 是最簡單直接的 smart pointer。可將值放在 heap 而不是 stack,存在 stack 的是指向 heap 的 pointer。除了資料存在 heap 非 stack 以外,沒有性能損失。多用於以下場景:

  • 有一個在邊一時不知道大小的類別,又想在需要確切大小的 context 中使用該類別時

    會在後面 "以 Boxes 實作遞迴類別" 這個部分說明

  • 有大量資料,並希望確保資料不被 Copy 的狀況下移轉所有權時

    轉移大量資料的 ownership 會花很多時間,因為 Copy 的關係,為改善效能可使用 box

  • 希望擁有一個值,並只關心他的類別是否實現了特定 trait,而不是具體類別時

    稱為 trait object, chap17 會說明

使用 Box<T> 在 heap 儲存資料

定義變數 b,是儲存到 heap 的 Box,當 b 在 main 結束時,會被釋放,釋放過程作用於 box 本身以及在 heap 的資料

fn main() {
    let b = Box::new(5);
    println!("b = {}", b);
}

將一個值單獨放在 heap 沒有什麼意義,以下是一個只能用 box 定義的類別的例子

以 Boxes 實作遞迴類別

rust 需要在編譯時知道類別會佔用多少空間,recursive type 遞迴類別是一種類別是編譯時無法知道大小的類別,其值的一部分可以是相同類別的另一個值。在 recursive type 使用 box,就可解決不知道大小的問題。

cons list 是 functional language 常見的類別,用來說明 recursive type

cons list 源自 Lisp,cons (construct function) 利用兩個參數來產生一個新的 list,通常一個是值,另一個是 list。將 x 與 y 連接,就表示產生一個新的容器,把 x 放在新容器的開頭,後面是舊容器 y。

cons list 每一項都包含兩個元素,當前的值與下一項,當最後一項是 Nil 就表示沒有下一項。Nil 跟 chap 6 代表無效或缺少的值的 null nil 不同。

cons list 在 rust 並不常用,比較常用的是 Vec<T>

//定義一個代表 i32 值的 cons list 的enum
enum List {
    Cons(i32, List),
    Nil,
}

// 使用 List 枚舉儲存列表 1, 2, 3
use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1, Cons(2, Cons(3, Nil)));
}

編譯錯誤:List 的一個成員被定義為是遞迴:它直接存放了另一個相同類別的值。這表示 Rust 無法計算為了存放 List 值到底需要多少空間

error[E0072]: recursive type `List` has infinite size
 --> src/main.rs:2:1
  |
2 | enum List {
  | ^^^^^^^^^ recursive type has infinite size
3 |     Cons(i32, List),
  |               ---- recursive without indirection
  |
  = help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to make `List` representable

error: aborting due to previous error

計算非遞回類別的大小

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

當 Rust 需要知道要為 Message 值分配多少空間時,它可以檢查每一個成員並發現 Message::Quit並不需要任何空間,Message::Move 需要兩個 i32 值的空間,依此類推。因此,Message 值所需的空間等於儲存其最大成員的空間大小。

當 Rust 編譯器檢查 List 這樣的遞迴類別時,編譯器嘗試計算出儲存一個 List enum 需要多少內存,並開始檢查 Cons 成員,那麼 Cons 需要的空間等於 i32 的大小加上 List 的大小。為了計算 List 需要多少內存,它檢查其成員,從 Cons 成員開始。Cons成員儲存了一個 i32 值和一個List值,這樣的計算將無限進行下去

使用 Box<T> 給遞迴類別一個已知的大小

因為 Box<T> 是一個 pointer,我們總是知道它需要多少空間:指針的大小並不會根據其指向的數據量而改變。這意味著可以將 Box 放入 Cons 成員中而不是直接存放另一個 List 值。Box 會指向另一個位於堆上的 List 值,而不是存放在 Cons 成員中。概念上,我們仍然有一個通過在其中 “存放” 其他列表創建的列表,不過現在實現這個概念的方式更像是一個項接著另一項,而不是一項包含另一項。

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1,
        Box::new(Cons(2,
            Box::new(Cons(3,
                Box::new(Nil))))));
}

Cons 將會需要一個 i32 的大小加上儲存 box 指針資料的空間。Nil 成員不儲存值,所以它比 Cons 成員需要更少的空間。現在我們知道了任何 List 值最多需要一個 i32 加上 box 指針數據的大小。使用 box ,打破了無限遞迴的連鎖,這樣編譯器就能夠計算出儲存 List 值需要的大小了。

box 只提供間接儲存與 heap 配置,沒有其他特殊功能。Box<T> 類型是一個 smart pointer,因為它實現了 Deref trait,它允許 Box<T> 值被當作引用對待。當 Box<T> 值離開作用域時,由於 Box<T> 類型 Drop trait 的實現,box 所指向的heap data也會被清除。

透過 Deref trait 以 regular reference 的方式使用 smart pointer

實作 Deref trait 可允許 overload dereference operator * ,這樣就可以用傳統的 reference 方式使用 smart pointer

以下會定義一個 MyBox<T> 類別,類似 Box<T> 但不會在 heap 儲存資料,用來說明 dereference operator 如何運作

透過 rereference operator 追蹤 pointer 的值

以下產生一個 i32 的引用,並用 dereference operator 使用該值

fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

像引用一樣使用 Box<T>

y 為一個指向 x 值的 box 實例,而不是指向 x 值的引用。

fn main() {
    let x = 5;
    let y = Box::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

自訂 smart pointer

struct MyBox<T>(T);

impl<T> MyBox<T> {
    // 獲取一個 T 類型的參數並回傳一個存放傳入值的 MyBox 實例
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn main() {
    let x = 5;
    let y = MyBox::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

編譯錯誤

error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
  --> src/main.rs:15:19
   |
15 |     assert_eq!(5, *y);
   |                   ^^

error: aborting due to previous error

為了啟用 * 運算符的解引用功能,需要實現 Deref trait。

實作 Deref trait

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

use std::ops::Deref;

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}

fn main() {
    let x = 5;
    let y = MyBox::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

deref 中寫入了 &self.0,這樣 deref 回傳了希望通過 * 運算符訪問的值的引用。main 函數中對 MyBox<T> 值的 * ,現在可以編譯並能通過 assert 檢查了。

*y 實際上是 *(y.deref())

function, method 的 implicit Deref Coercions

deref coercions 是 rust 實作 function, method 傳遞參數的方法。它會將實作了 Deref 類別的 reference 轉換為 Deref 能夠從原始類別轉換而來的類別的 reference。Deref coercion converts a reference to a type that implements Deref into a reference to a type that Deref can convert the original type into.

deref coercions 會自動發生,這時候會有一系列的 deref method 被呼叫,將我們提供的類別,轉換成參數所需要的類別。

deref coercions 讓 rust programmer 編寫 function, method 呼叫時,不需要增加過多的 &* 的引用與解引用。

因為 deref coercion,可使用 MyBox<String> 的引用呼叫 hello

fn hello(name: &str) {
    println!("Hello, {}!", name);
}

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&m);
}

這裡使用 &m 呼叫 hello 函數,其為 MyBox<String> 值的引用。因為 MyBox<T>實現了 Deref trait,Rust 可以通過 deref&MyBox<String> 變為 &String。標準庫中提供了 String 上的 Deref 實現,其會返回字符串 slice,這可以在 Deref 的 API 文檔中看到。Rust 再次呼叫 deref&String 變為 &str,這就符合 hello 函數的定義了。

如果沒有 deref coercion,就得用這種方式撰寫

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&(*m)[..]);
}

(*m)MyBox<String> 解引用為 String。接著 &[..] 獲取了整個 String 的字符串 slice 來匹配 hello 的簽名。

deref coercion 如何跟 mutability 交互運作

類似使用 Deref trait overload 不可變引用的 * operator, rust 提供 DerefMut trait 用於重載可變引用的 * operator

rust 在發現類別跟 trait 實作滿足三種情況時,會進行 deref coercion

  • T: Deref<Target=U> 時,從 &T&U
  • T: DerefMut<Target=U> 時,從 &mut T&mut U
  • T: Deref<Target=U> 時,從 &mut T&U

前兩個除了可變性以外,都是一樣的,第一種情況代表,如果有 &T,而 T 實作了回傳 U 類別的 Deref ,則可直接得到 &U。第二種情況表示,對可變引用也有相同的結果

第三種比較特別,rust 也能將可變引用強制轉為不可變引用,但不可變引用"不能"轉為可變引用。根據借用規則,可變引用必須是這些資料的唯一引用,否則會無法編譯。將可變引用轉為不可變引用也沒關係。將不可變引用轉為可變引用,則需要限制資料只能有一個不可變引用,但就無法遵循借用規則,因此 rust 無法將不可變引用轉換為可變引用。

透過 Drop trait 在 cleanup 時執行程式

smart pointer 次要的是 Drop trait,可在值要離開 scope 時,執行一些程式。可為所有類別提供 Drop trait 的實作,這些 code 可以用來釋放檔案或網路連結的資源。smart pointer 的 Drop 一定會被用到。例如 Box<T> 自訂了 Drop 用來釋放 box 指向的 heap。

其他程式語言必須要自行在使用 smart pointer instance 後,自己呼叫清理 memory 或資源的程式碼。rust 會在 compiler 自動插入這些離開 scope 被執行的code

指定在值離開 scope 就執行的 code 的方法就是實作 Drop trait。會需要實作 drop method,它會取得一個 self 可變引用。

在drop 呼叫 println! 觀察drop 的行為

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let c = CustomSmartPointer { data: String::from("my stuff") };
    let d = CustomSmartPointer { data: String::from("other stuff") };
    println!("CustomSmartPointers created.");
}

Drop trait 包含在 prelude,不需要 use

執行結果

CustomSmartPointers created.
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!

利用 std::mem::drop 提早丟棄值

rust 限制不能直接呼叫 drop,但有時需要提早清理某些值。ex: 使用 smart pointer 管理 locks 時,可能會需要強制呼叫 drop 釋放 lock,但 rust 有限制,故需要改用 std::mem::drop

fn main() {
    let c = CustomSmartPointer { data: String::from("some data") };
    println!("CustomSmartPointer created.");

    // 編譯錯誤
    c.drop();
    println!("CustomSmartPointer dropped before the end of main.");
}
error[E0040]: explicit use of destructor method
  --> src/main.rs:14:7
   |
14 |     c.drop();
   |       ^^^^ explicit destructor calls not allowed

error: aborting due to previous error

改使用 prelude 裡面的 std::mem::drop

fn main() {
    let c = CustomSmartPointer { data: String::from("some data") };
    println!("CustomSmartPointer created.");
    drop(c);
    println!("CustomSmartPointer dropped before the end of main.");
}

執行結果

CustomSmartPointer created.
Dropping CustomSmartPointer with data `some data`!
CustomSmartPointer dropped before the end of main.

不需擔心 drop 正在使用的值,因為 rust compiler 能夠檢查出來

ownership 系統可確保 drop 只在值不被使用時,被呼叫一次

Rc Reference Counted Smart Pointer

為了多所有權,rust 有 Rc<T> 的類別,稱為 reference counting。引用計數可記錄一個值被引用的次數,確認該值是否仍被使用,如果是 0,就可以被清理。

Rc<T> 用在當我們希望在 heap 配置一些資料,讓程式多個地方讀取,但無法再編一時,就知道程式哪一個部分最後結束,不使用它。注意 Rc<T> 只能用在單一線程的狀況。

chat16 會討論多線程如何進行 引用計數。

利用 Rc<T> 共享資料

想產生兩個 (b,c) 共享第三個 list (a) 所有權的 list

這種方式不能運作

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let a = Cons(5,
        Box::new(Cons(10,
            Box::new(Nil))));
    let b = Cons(3, Box::new(a));
    let c = Cons(4, Box::new(a));
}

編譯錯誤

error[E0382]: use of moved value: `a`
  --> src/main.rs:13:30
   |
9  |     let a = Cons(5,
   |         - move occurs because `a` has type `List`, which does not implement the `Copy` trait
...
12 |     let b = Cons(3, Box::new(a));
   |                              - value moved here
13 |     let c = Cons(4, Box::new(a));
   |                              ^ value used here after move

因為 Cons 擁有儲存的資料,產生 b 時,a 被移動到 b,b 擁有 a,再用 a 產生 c 就會發生編譯錯誤

改用 Rc<List> ,產生 b 時,不會獲取 a,這邊會複製 a 裡面的 Rc,將引用計數由 1 增加為 2。產生 c 時,引用計數由 2 增加為 3。每次呼叫 Rc::clone,Rc 中的資料的引用計數,都會增加。

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a));
    let c = Cons(4, Rc::clone(&a));
}

必須 use std::rc::Rc;因為不在 prelude 裡面。

也可呼叫 a.clone(),而不是 Rc::clone(&a),但 rust 習慣使用 Rc::clone。因為 Rc::clone 不像大部分類別的 clone 會進行 deep copy

cloning Rc<T> 會增加 reference count

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    // a 中 Rc<List> 的初始引用計數為一,接著每次調用 clone,計數會增加一
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("count after creating a = {}", Rc::strong_count(&a));
    let b = Cons(3, Rc::clone(&a));
    println!("count after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, Rc::clone(&a));
        println!("count after creating c = {}", Rc::strong_count(&a));
    }
    // Drop trait 的實現,當 Rc<T> 值離開作用域時自動減少引用計數
    println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}

執行結果

count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2

Rc<T> 允許通過不可變引用,讓程式的多個部分讀取數據。如果 Rc<T> 也允許多個可變引用,則會違反第四章討論的借用規則之一:相同位置的多個可變借用可能造成資料競爭和不一致。不過可以修改資料是非常有用的!在下一部分,我們將討論內部可變性模式和 RefCell<T> 類型,它可以與 Rc<T> 結合使用來處理不可變性的限制。

RefCell 及 interior mutability pattern

interior mutability pattern 是 rust 的一個 design pattern。可在有不可變引用時,改變資料,通常這是借用規則中不允許的。為了改變資料,要在資料結構上,使用 unsafe 模糊 rust 既有的可變性及借用歸則 (chap19)。當可確保程式碼在運作時,會遵守借用規則,即使編譯器不能確認的狀況,可以選用內部可變性模式的類別。相關的 unsafe code 會被封裝到安全的 API 中,但外部類別不變。

利用 RefCell<T> 在執行時檢查借用規則

不同於 Rc<T>RefCell<T> 代表其資料的唯一的所有權。讓 RefCell<T> 不同於像 Box<T> 這樣的類別的原因是:回憶一下第四章所學的借用規則

  1. 在任意給定時間,只能擁有一個可變引用或任意數量的不可變引用 之一(而不是全部)。
  2. 引用必須永遠有效

對於引用和 Box<T>,借用規則的不可變性作用於編譯時期。對於 RefCell<T>,這些不可變性作用於 執行時期。對於引用,如果違反這些規則,會得到一個編譯錯誤。而對於 RefCell<T>,如果違反這些規則程式會 panic 並 exit。

在編譯時檢查借用規則的優勢是這些錯誤將在開發過程的早期被捕獲,同時對沒有執行時的性能影響,因為所有的分析都提前完成了。在編譯時檢查借用規則是大部分情況的最佳選擇,這也正是其為何是 Rust 的默認行為。

相反在執行時檢查借用規則的好處則是允許出現特定 memory-safe 的場景,而它們是不允許在編譯時被檢查。靜態分析,正如 Rust 編譯器,是天生保守的。程式的一些屬性則不可能通過分析代碼發現:其中最著名的就是 停機問題(Halting Problem)

因為有時候無法進行分析,如果 Rust 編譯器不能通過所有權規則編譯,它可能會拒絕一個正確的程式;從這種角度考慮它是保守的。如果 Rust 接受不正確的程序,那麼用戶也就不會相信 Rust 所做的保證了。然而,如果 Rust 拒絕正確的程序,雖然會給程序員帶來不便,但不會帶來災難。RefCell<T> 正是用於當你確信程式碼遵守借用規則,而編譯器不能理解和確定的時候。

類似於 Rc<T>RefCell<T> 只能用於單線程場景。如果嘗試在多線程上下文中使用RefCell<T>,會得到一個編譯錯誤。chap 16 會介紹如何在多線程程序中使用 RefCell<T> 的功能。

如下為選擇 Box<T>Rc<T>RefCell<T> 的理由:

  • Rc<T> 允許相同資料有多個所有者;Box<T>RefCell<T> 有單一所有者。
  • Box<T> 允許在編譯時執行不可變或可變借用檢查;Rc<T>僅允許在編譯時執行不可變借用檢查;RefCell<T> 允許在運行時執行不可變或可變借用檢查。
  • 因為 RefCell<T> 允許在執行時進行可變借用檢查,所以我們可以在即使 RefCell<T> 自身是不可變的情況下修改其內部的值。

在不可變值內部改變值就是 內部可變性 模式。讓我們看看何時內部可變性是有用的,並討論這是如何成為可能的。

Interior Mutability 內部可變性: A Mutable Borrow to an Immutable Value 不可變值的可變借用

借用規則的一個推論是當有一個不可變值時,不能可變的借用它。例如,如下程式不能編譯:

fn main() {
    let x = 5;
    let y = &mut x;
}

然而,特定情況下在值的方法內部能夠修改自己是很有用的;而在其他程式碼中,值仍然是不可變。在該值方法外部的程式碼不能修改其值。RefCell<T> 是一個獲得內部可變性的方法。RefCell<T>並沒有完全繞開借用規則,編譯器中的借用檢查器允許內部可變性並相應的在執行時檢查借用規則。如果違反了這些規則,會得到 panic! 而不是編譯錯誤。

讓我們通過一個實際的例子來瞭解何處可以使用 RefCell<T> 來修改不可變值並看看為何這麼做是有意義的。

interior mutability 實例:mock object

test double (測試替身) 是一個通用程式概念,可在測試中,替代某個類別的類別。mock object 是特定類別的測試替身,可記錄測試過程中發生的事情,用以確認測試是正確的。

雖然 rust 沒有與其他程式語言中完全相同的物件,誒沒有在 std library 建立 mock object。我們可自行產生一個跟 mock object 有相同功能的 struct。

ex: 一個記錄某個值與最大值的差距的library,並根據當前值與最大值的差距來發送消息。這個 library 可以用來記錄用戶所允許的呼叫 API 次數限額。

該library只提供記錄與最大值的差距,以及何種情況發送什麼消息的功能。使用此庫的程式則期望提供實際發送消息的機制:程序可以選擇記錄一條消息、發送 email、發送短信等等。庫本身無需知道這些細節;只需實現其提供的 Messenger trait 即可。

記錄某個值與最大值差距的library,並根據此值的特定級別發出警告

lib.rs

pub trait Messenger {
    fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: 'a + Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
    where T: Messenger {
    pub fn new(messenger: &T, max: usize) -> LimitTracker<T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize) {
        self.value = value;

        let percentage_of_max = self.value as f64 / self.max as f64;

        if percentage_of_max >= 0.75 && percentage_of_max < 0.9 {
            self.messenger.send("Warning: You've used up over 75% of your quota!");
        } else if percentage_of_max >= 0.9 && percentage_of_max < 1.0 {
            self.messenger.send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quota!");
        }
    }
}

程式中一個重要部分是擁有一個方法 sendMessenger trait,它會獲取一個 self 的不可變引用和文字信息。這是我們的 mock 對象所需要擁有的 API interface。另一個重要的部分是我們需要測試 LimitTrackerset_value 方法的行為。可以改變傳遞的 value 參數的值,不過 set_value並沒有返回任何可供 assert 的值。也就是說,如果使用某個實現了 Messenger trait 的值和特定的 max 創建 LimitTracker,當傳遞不同 value 值時,消息發送者應被告知發送合適的消息。

我們所需的 mock object 是,呼叫 send 不同於實際發送 email 或短息,其只記錄信息被通知要發送了。可以產生一個 mock object instance,用其創建 LimitTracker,呼叫 LimitTrackerset_value方法,然後檢查 mock 對象是否有我們期望的消息。以下是展示了一個如此嘗試的 mock object 實作,不過借用檢查器並不允許這樣做:

lib.rs

#[cfg(test)]
mod tests {
    use super::*;

    struct MockMessenger {
        sent_messages: Vec<String>,
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger { sent_messages: vec![] }
        }
    }

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            self.sent_messages.push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        let mock_messenger = MockMessenger::new();
        let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

        limit_tracker.set_value(80);

        assert_eq!(mock_messenger.sent_messages.len(), 1);
    }
}

測試定義了一個 MockMessenger struct,其 sent_messages 字段為一個 String 值的 Vec用來記錄被告知發送的消息。我們還定義了一個關聯函數 new 以便於產生從空消息列表開始的 MockMessenger 值。接著為 MockMessenger 實現 Messenger trait 這樣就可以為 LimitTracker提供一個 MockMessenger。在 send 方法的定義中,獲取傳入的消息作為參數並儲存在 MockMessengersent_messages 列表中。

在測試中,我們測試了當 LimitTracker 被告知將 value 設置為超過 max 值 75% 的某個值。首先新建一個 MockMessenger,其從空消息列表開始。接著新建一個 LimitTracker 並傳遞新建 MockMessenger 的引用和 max 值 100。我們使用值 80 呼叫 LimitTrackerset_value 方法,這超過了 100 的 75%。接著 assert MockMessenger 中記錄的消息列表應該有一條消息。

error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
  --> src/lib.rs:52:13
   |
51 |         fn send(&self, message: &str) {
   |                 ----- help: consider changing this to be a mutable reference: `&mut self`
52 |             self.sent_messages.push(String::from(message));
   |             ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable

error: aborting due to previous error

不能修改 MockMessenger 來記錄消息,因為 send 方法獲取 self 的不可變引用。我們也不能參考錯誤文本的建議使用 &mut self 替代,因為這樣 send 的簽名就不符合 Messenger trait 定義中的簽名了(請隨意嘗試如此修改並看看會出現什麼錯誤信息)。

這正是內部可變性的用武之地!我們將通過 RefCell 來儲存 sent_messages,然而 send 將能夠修改 sent_messages 並儲存消息。

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::RefCell;

    struct MockMessenger {
        sent_messages: RefCell<Vec<String>>,
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger { sent_messages: RefCell::new(vec![]) }
        }
    }

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            self.sent_messages.borrow_mut().push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        let mock_messenger = MockMessenger::new();
        let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

        limit_tracker.set_value(80);
        assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
    }
}

現在 sent_messages 的類別是 RefCell<Vec<String>> 而不是 Vec<String>。在 new 函數中新建了一個 RefCell instance 替代空 vector。

對於 send 方法的實現,第一個參數仍為 self 的不可變借用,這是符合方法定義的。我們呼叫 self.sent_messagesRefCellborrow_mut 方法來獲取 RefCell 中值的可變引用,這是一個 vector。接著可以對 vector 的可變引用呼叫 push 以便記錄測試過程中看到的消息。

最後必須做出的修改位於斷言中:為了看到其內部 vector 中有多少個項,需要調用 RefCellborrow 以獲取 vector 的不可變引用。

在 runtime 以 RefCell<T> 追蹤借用

當產生不可變和可變引用時,我們分別使用 &&mut 語法。對於 RefCell<T> 來說,則是 borrowborrow_mut 方法,這屬於 RefCell<T> 安全 API 的一部分。

borrow 方法回傳 Ref 類別的 smart pointer,borrow_mut 方法返回 RefMut 類別的 smart pointer。這兩個類別都實現了 Deref 所以可以當作一般引用。

RefCell<T> 記錄當前有多少個活動的 Ref<T>RefMut<T> 智能指針。每次呼叫 borrowRefCell<T> 將活動的不可變借用計數加一。當 Ref 值離開作用域時,不可變借用計數減一。就像編譯時借用規則一樣,RefCell<T> 在任何時候只允許有多個不可變借用或一個可變借用。

如果我們嘗試違反這些規則,相比引用時的編譯時錯誤,RefCell<T> 的實現會在運行時 panic!。示例 15-23 展示了對示例 15-22 中 send 實現的修改,這裡我們故意嘗試在相同作用域產生兩個可變借用以便demo RefCell<T> 不允許我們在運行時這麼做:

impl Messenger for MockMessenger {
    fn send(&self, message: &str) {
        let mut one_borrow = self.sent_messages.borrow_mut();
        let mut two_borrow = self.sent_messages.borrow_mut();

        one_borrow.push(String::from(message));
        two_borrow.push(String::from(message));
    }
}
test tests::it_sends_an_over_75_percent_warning_message ... FAILED

failures:

---- tests::it_sends_an_over_75_percent_warning_message stdout ----
thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at 'already borrowed: BorrowMutError', src/libcore/result.rs:997:5
note: Run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

使用 RefCell 使得在只允許不可變值的上下文中編寫修改自身以記錄消息的 mock object 成為可能。雖然有取捨,但是我們可以選擇使用 RefCell<T> 來獲得比一般引用所能提供的更多的功能。

組合 Rc<T> and RefCell<T> 讓 mutable data 能有多個 owner

RefCell<T> 的一個常見用法是與 Rc<T> 結合。

Rc<T> 允許對相同數據有多個所有者,不過只能提供資料的不可變訪問。如果有一個儲存了 RefCell<T>Rc<T> 的話,就可以得到有多個所有者 並且 可以修改的值了!

例如,ex: 15-18 的 cons list 的例子中使用 Rc<T> 使得多個列表共享另一個列表的所有權。因為 Rc<T> 只存放不可變值,所以一旦創建了這些列表值後就不能修改。讓我們加入 RefCell<T> 來獲得修改列表中值的能力。示例 15-24 展示了通過在 Cons 定義中使用 RefCell<T>,我們就允許修改所有列表中的值了:

Rc<RefCell<i32>> 產生可以修改的 List

main.rs

#[derive(Debug)]
enum List {
    Cons(Rc<RefCell<i32>>, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;
use std::cell::RefCell;

fn main() {
    let value = Rc::new(RefCell::new(5));

    let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));

    let b = Cons(Rc::new(RefCell::new(6)), Rc::clone(&a));
    let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a));

    *value.borrow_mut() += 10;

    println!("a after = {:?}", a);
    println!("b after = {:?}", b);
    println!("c after = {:?}", c);
}

這裡產生了一個 Rc<RefCell<i32> instance 並儲存在變數 value 中以便之後直接訪問。接著在 a 中用包含 valueCons 成員產生了一個 List。需要 clone value 以便 avalue 都能擁有其內部值 5 的所有權,而不是將所有權從 value 移動到 a 或者讓 a 借用 value

我們將列表 a 封裝進了 Rc<T> 這樣當產生列表 bc 時,他們都可以引用 a,正如示例 15-18 一樣。

一旦產生了列表 abc,我們將 value 的值加 10。為此對 value 呼叫了 borrow_mut,這裡使用了第五章討論的自動解引用功能(“-> 運算符到哪去了?” 部分)來解引用 Rc<T> 以獲取其內部的 RefCell<T> 值。borrow_mut 方法回傳 RefMut<T> 智能指針,可以對其使用解引用運算符並修改其內部值。

當我們列印出 abc 時,可以看到他們都擁有修改後的值 15 而不是 5:

a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 6 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 10 }, Cons(RefCell { value: 15 }, Nil))

使用 RefCell<T>,我們可以擁有一個表面上不可變的 List,不過可以使用 RefCell<T> 中提供內部可變性的方法來在需要時修改數據。RefCell<T> 的執行時借用規則檢查也確實保護我們免於出現數據競爭,而且我們也能犧牲一些速度來換取資料結構的靈活性。

std library 中也有其他提供內部可變性的類型,比如 Cell<T>,它有些類似(RefCell<T>)除了相比提供內部值的引用,其值被拷貝進和拷貝出 Cell<T>。還有 Mutex<T>,其提供線程間安全的內部可變性,下一章並發會討論它的應用。請查看標準庫來獲取更多細節和不同類型之間的區別。

reference cycle 會造成 memory leak

Rust 的內存安全保證難以意外地製造永遠也不會被清理的內存(被稱為 內存洩露memory leak)),但並不是不可能。與在編譯時拒絕資料競爭不同, Rust 並不保證完全避免內存洩露,這意味著內存洩露在 Rust 被認為是內存安全的。這一點可以通過 Rc<T>RefCell<T> 看出:有可能會創建個個項之間相互引用的引用。這會造成內存洩露,因為每一項的引用計數將永遠也到不了 0,其值也永遠也不會被丟棄。

製造 reference cycle

一個存放 RefCell 的 cons list 定義,可以修改 Cons 成員所引用的資料

main.rs

use std::rc::Rc;
use std::cell::RefCell;
use crate::List::{Cons, Nil};

// 一個存放 RefCell 的 cons list 定義,可以修改 Cons 成員所引用的資料
// 能夠修改 Cons 成員所指向的 List
// 增加了一個 tail 方法來方便我們在有 Cons 成員的時候訪問其第二項
#[derive(Debug)]
enum List {
    Cons(i32, RefCell<Rc<List>>),
    Nil,
}

impl List {
    fn tail(&self) -> Option<&RefCell<Rc<List>>> {
        match self {
            Cons(_, item) => Some(item),
            Nil => None,
        }
    }
}

fn main() {
    // 在 a 產生了一個list,一個指向 a 裡面的 list 的 b list,接著修改 b 中的list指向 a 中的list,這會創建一個引用循環。
    let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));

    println!("a initial rc count = {}", Rc::strong_count(&a));
    println!("a next item = {:?}", a.tail());

    let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));

    println!("a rc count after b creation = {}", Rc::strong_count(&a));
    println!("b initial rc count = {}", Rc::strong_count(&b));
    println!("b next item = {:?}", b.tail());

    // 修改 a 使其指向 b 而不是 Nil
    // 用 tail 方法獲取 a 中 RefCell<Rc<List>> 的引用,並放入變數 link 中
    // 使用 RefCell<Rc<List>> 的 borrow_mut 方法將其值從存放 Nil 的 Rc 修改為 b 中的 Rc<List>
    if let Some(link) = a.tail() {
        *link.borrow_mut() = Rc::clone(&b);
    }

    println!("b rc count after changing a = {}", Rc::strong_count(&b));
    println!("a rc count after changing a = {}", Rc::strong_count(&a));

    // 取消如下行的註釋來觀察引用循環;
    // 這會導致 stack overflow
    println!("a next item = {:?}", a.tail());
}

如果沒有最後一行,正確的執行結果

a initial rc count = 1
a next item = Some(RefCell { value: Nil })
a rc count after b creation = 2
b initial rc count = 1
b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
b rc count after changing a = 2
a rc count after changing a = 2

最後一行發生的錯誤

thread 'main' has overflowed its stack
fatal runtime error: stack overflow
Abort trap: 6

Rc<T> 改為 Weak<T> 避免引用循環

呼叫 Rc::clone 會增加 Rc<T> instance 的 strong_count,和 Rc<T> instance 只在其 strong_count 為 0 時才會被清理。也可以呼叫 Rc::downgrade 並傳遞 Rc instance 的引用來產生其值的 弱引用weak reference)。

呼叫 Rc::downgrade 時會得到 Weak<T> 類型的智能指針。不同於將 Rc<T> 實例的 strong_count 加一,呼叫 Rc::downgrade 會將 weak_count加一。Rc<T> 類型使用 weak_count 來記錄其存在多少個 Weak<T> 引用,類似於 strong_count。其區別在於 weak_count 無需計數為 0 就能使 Rc 實例被清理。

強引用代表如何共享 Rc<T> instance 的所有權。弱引用並不代表所有權關係。他們不會造成引用循環,因為任何引入了弱引用的循環一旦所涉及的強引用計數為 0 就會被打破。

因為 Weak<T> 引用的值可能已經被丟棄了,為了使用 Weak<T> 所指向的值,我們必須確保其值仍然有效。為此可以呼叫 Weak<T> 實例的 upgrade 方法,這會回傳 Option<Rc<T>>。如果 Rc<T>值還未被丟棄則結果是 Some,如果 Rc<T> 已經被丟棄則結果是 None。因為 upgrade 回傳一個 Option<T>,Rust 會處理 SomeNone的情況,並且不會有一個無效的 pointer。

以下是一個例子,不同於使用一個某項只知道其下一項的列表,我們會產生一個某項知道其子項 父項的樹形結構。

產生 Tree Data Structure: 帶有 Child Nodes 的 Node

use std::rc::Rc;
use std::cell::RefCell;

#[derive(Debug)]
// Node 的存放擁有所有權的 i32 值和其子 Node 值引用的 struct
// 希望 Node 擁有其子節點,同時也希望通過變數來共享所有權,以便可以直接訪問 tree 的每一個 Node。為此 Vec<T> 的項的類別被定義為 Rc<Node>。我們還希望能修改其他節點的子節點,所以 children 中 Vec<Rc<Node>> 被放進了 RefCell<T>
struct Node {
    value: i32,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    // 產生一個叫做 leaf 的帶有值 3 且沒有子節點的 Node instance,和另一個帶有值 5 並以 leaf 作為子節點的 branch
    let leaf = Rc::new(Node {
        value: 3,
        children: RefCell::new(vec![]),
    });

    // 這裡 clone 了 leaf 中的 Rc<Node> 並儲存在了 branch 中,這表示 leaf 中的 Node 現在有兩個所有者:leaf和branch。
    // 可以透過 branch.children 從 branch 中獲得 leaf,不過無法從 leaf 到 branch。leaf 沒有到 branch 的引用且並不知道他們相互關聯。我們希望 leaf 知道 branch 是其父節點。
    let _branch = Rc::new(Node {
        value: 5,
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });
}

增加子到父節點的引用

為了使子節點知道其父節點,需要在 Node 定義中增加一個 parent。問題是 parent的類型應該是什麼。我們知道其不能包含 Rc<T>,因為這樣 leaf.parent 將會指向 branchbranch.children 會包含 leaf 的指針,這會形成引用循環,會造成其 strong_count 永遠也不會為 0.

現在換一種方式思考這個關係,父節點應該擁有其子節點:如果父節點被丟棄了,其子節點也應該被丟棄。然而子節點不應該擁有其父節點:如果丟棄子節點,其父節點應該依然存在。這正是弱引用的例子!

所以 parent 使用 Weak<T> 類別而不是 Rc<T>,具體來說是 RefCell<Weak<Node>>。現在 Node結構體定義看起來像這樣:

use std::rc::{Rc, Weak};
use std::cell::RefCell;

// 一個節點就能夠引用其父節點,但不擁有其父節點
#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    // leaf 擁有指向其父節點 branch 的 Weak 引用
    // leaf 一開始時沒有父節點,所以我們產生了一個空的 Weak 引用實例。
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    // 當嘗試使用 upgrade 方法獲取 leaf 的父節點引用時,會得到 None。如 println! 輸出所示:leaf parent = None
    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());

    // 產生 branch 節點時,其也會產生一個 Weak<Node> 引用,因為 branch 並沒有父節點。leaf 仍然作為 branch 的一個子節點。一旦在 branch 中有了 Node 實例,就可以修改 leaf 使其擁有指向父結節點的 Weak<Node> 引用。
    // 這裡使用了 leaf 中 parent 裡的 RefCell<Weak<Node>> 的 borrow_mut 方法,接著使用了 Rc::downgrade 函數來從 branch 中的 Rc 值產生了一個指向 branch 的 Weak<Node> 引用。
    let branch = Rc::new(Node {
        value: 5,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });

    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

    // 列印 leaf 的父節點時,這一次將會得到存放了 branch 的 Some 值

    // leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) }, children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) }, children: RefCell { value: [] } }] } })
    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}

觀察 strong_countweak_count 的變化

use std::rc::{Rc, Weak};
use std::cell::RefCell;

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    // 產生 leaf,其 Rc<Node> 的強引用計數為 1,弱引用計數為 0。
    // leaf strong = 1, weak = 0
    println!(
        "leaf strong = {}, weak = {}",
        Rc::strong_count(&leaf),
        Rc::weak_count(&leaf),
    );

    {
        // 在內部作用域中產生 branch 並與 leaf 相關聯,此時 branch 中 Rc<Node> 的強引用計數為 1,弱引用計數為 1(因為 leaf.parent 通過 Weak<Node> 指向 branch)
        let branch = Rc::new(Node {
            value: 5,
            parent: RefCell::new(Weak::new()),
            children: RefCell::new(vec![Rc::clone(&leaf)]),
        });

        *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

        // branch strong = 1, weak = 1
        println!(
            "branch strong = {}, weak = {}",
            Rc::strong_count(&branch),
            Rc::weak_count(&branch),
        );

        // 這裡 leaf 的強引用計數為 2,因為現在 branch 的 branch.children 中儲存了 leaf 的 Rc<Node> 的拷貝,不過弱引用計數仍然為 0
        // leaf strong = 2, weak = 0
        println!(
            "leaf strong = {}, weak = {}",
            Rc::strong_count(&leaf),
            Rc::weak_count(&leaf),
        );
    }

    // 當內部作用域結束時,branch 離開作用域,Rc<Node> 的強引用計數減少為 0,所以其 Node 被丟棄。來自 leaf.parent 的弱引用計數 1 與 Node 是否被丟棄無關,所以並沒有產生任何 memory leak
    // 在內部作用域結束後嘗試訪問 leaf 的父節點,會再次得到 None。在程序的結尾,leaf 中 Rc<Node> 的強引用計數為 1,弱引用計數為 0,因為現在 leaf 又是 Rc<Node> 唯一的引用了。
    // leaf parent = None
    // leaf strong = 1, weak = 0
    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
    println!(
        "leaf strong = {}, weak = {}",
        Rc::strong_count(&leaf),
        Rc::weak_count(&leaf),
    );
}

執行結果

leaf strong = 1, weak = 0
branch strong = 1, weak = 1
leaf strong = 2, weak = 0
leaf parent = None
leaf strong = 1, weak = 0

References

The Rust Programming Language

中文版

中文版 2

2020年5月18日

rust14 進一步了解 Cargo 及 Crates.io

cargo 除了 build, run, test 以外,還有更多功能

  • 以 release profile 自訂 build process
  • 發布 library 到 crates.io
  • 以 workspace 組織大型專案
  • 由 crates.io 安裝 binaries
  • 擴充 cargo 的自訂指令

以 release profile 自訂 build process

rust 的 release profile 是 pre-defined 可自訂的,帶有不同的 options,每一個 profile 都是獨立的

cargo 有兩個主要的 profile:執行 cargo build 使用 dev profile,執行 cargo build --release 使用 release profile。

當專案的 Cargo.toml 中沒有任何 [profile.*] 時,cargo 會使用預設 profiles 設定。

opt-level 控制 Rust 會對代碼進行何種程度的優化,數值是 0~3,可在 Cargo.toml 增加這幾行設定,修改 opt-level

Cargo.toml

[profile.dev]
opt-level = 0

[profile.release]
opt-level = 3

發布 library 到 crates.io

crates.io 用來分發 library 的 source code。

rust, cargo 有提供讓別人更容易找到與使用我們發布的 library 的功能

提供有用的註解

// 是 rust 的註解

/// 是 documentation comments,可產生 html 文件,主要是用來撰寫如何 "使用" 這個 crate 的說明。支援 Mardown 格式的註解

一開始是函數的說明,然後是如何使用該函數的範例

src/lib.rs

/// 將給定的數字加一
///
/// # Examples
///
/// ```
/// let five = 5;
///
/// assert_eq!(6, my_crate::add_one(5));
/// ```
pub fn add_one(x: i32) -> i32 {
    x + 1
}

cargo doc 會產生文件

cargo doc --open 會產生文件的 html 並打開 browser

常用的 documentation comments

  • Examples

    範例

  • Panics

    該函數可能會產生 panic!

  • Errors

    如果函數回傳 Result,這裡說明可能會出現的錯誤及錯誤訊息

  • Safety

    如果函數使用 unsafe ,希望呼叫函數者,支援確保 unsafe 區塊正常工作的不變條件 invariants

Documentation Comments as Tests

Examples 區塊說明如何使用library,而 cargo test 也會像測試一樣,執行該區塊的範例程式碼。如果程式被改變了,那麼測試也會發生問題。這可確保程式跟文件是同步的。

   Doc-tests my_crate

running 1 test
test src/lib.rs - add_one (line 5) ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Commenting Contained Items

//! 這是另一種文件註解,這是包含 items 的註解,通常用在 crate 根文件,也就是 src/lib.rs,或模塊的根文件為 crate 或模塊整體提供文件。

ex: 希望增加包含 add_one 函數的 my_crate crate 文件,可在 src/lib.rs 增加 //! 開頭的註解

//! # My Crate
//!
//! `my_crate` 是一個使得特定計算更方便的
//! 工具集合

/// 將給定的數字加一。
// --snip--

使用 pub use 匯出 public api

chap 7 介紹了使用 mod 將程式整合到 module 中,及如何使用 pub 將 item 變成公有的,及如何使用 use 將 item 引入作用域。

但開發的文件結構,可能會是多層級,不方便使用者查詢,也會討厭使用 use my_crate::some_module::another_module::UsefulType; 而不是 use my_crate::UsefulType; 來使用類別。

公有 API 結構,是發布 crate 要考慮的事情

即使原始檔的結構複雜,可用 pub use 重新匯出不同的 public api 結構

ex: 有一個 art library,裡面有兩個 enum: PrimaryColor, SecondaryColor 的模塊 kinds,及一個包含函數 mix 的模塊 utils

src/lib.rs

//! # Art
//!
//! 一個描述美術信息的庫。

pub mod kinds {
    /// 採用 RGB 色彩模式的主要顏色。
    pub enum PrimaryColor {
        Red,
        Yellow,
        Blue,
    }

    /// 採用 RGB 色彩模式的次要顏色。
    pub enum SecondaryColor {
        Orange,
        Green,
        Purple,
    }
}

pub mod utils {
    use crate::kinds::*;

    /// 等量的混合兩個主要顏色
    /// 來創建一個次要顏色。
    pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
        // --snip--
        SecondaryColor::Orange
    }
}

main.rs

use art::kinds::PrimaryColor;
use art::utils::mix;

fn main() {
    let red = PrimaryColor::Red;
    let yellow = PrimaryColor::Yellow;
    mix(red, yellow);
}

使用者要搞清楚 PrimaryColor 在 kinds 模塊,而 mix 在 utils 模塊

可以採用實例中的 art crate 並增加 pub use 語句來重導出項到頂層結構

lib.rs

//! # Art
//!
//! 一個描述美術信息的庫。

pub use kinds::PrimaryColor;
pub use kinds::SecondaryColor;
pub use utils::mix;

pub mod kinds {
    // --snip--
}

pub mod utils {
    // --snip--
}

可改用 pub use 的結構

use art::PrimaryColor;
use art::mix;

fn main() {
    // --snip--
}

建立 Crates.io 帳號

發布 crate 前,要先在 crates.io 註冊並獲取一個 API token

然後用 cargo login 登入

cargo login abcdefghijklmnopqrstuvwxyz012345

這個命令會通知 Cargo,你的 API token 並將其儲存在本地的 ~/.cargo/credentials 文件中。注意這個 token 是一個 秘密secret)且不應該與其他人共享。

發布 crate 前

在 crate 的 Cargo.toml 的 [package] 部分增加這個 crate 的 metadata

crate 需要一個唯一的名稱,另外要填寫關於該 crate 用途的描述和用戶可能在何種條款下使用該 crate 的 license,可使用 Linux 基金會的 Software Package Data Exchange SPDX

Cargo.toml

[package]
name = "guessing_game"
license = "MIT"

如果要用自訂的 license,可放到一個文件,並改用 license-file 指定該文件名稱

很多 Rust 社區成員選擇與 Rust 自身相同的 license,這是一個雙許可的 MIT OR Apache-2.0

[package]
name = "guessing_game"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]
description = "A fun game where you guess what number the computer has chosen."
license = "MIT OR Apache-2.0"

[dependencies]

發布到 Crates.io

因發布是 permanent 永久的,無法覆蓋舊版,也無法刪除。

cargo publish 用這個指令發布

新版就根據 語義化版本規則 來根據修改的類型決定下一個版本號,修改 version 的值,再 cargo publish 即可

使用 cargo yank 從 Crates.io 撤銷版本

雖不能刪除舊版 crate,但可阻止其他專案加入 dependencies 中

撤銷版本可阻止新專案依賴此版本的函式庫,但現存的依賴還是可以繼續使用。撤銷帶有 Cargo.lock 專案的依賴,不會發生問題,但新產生的 Cargo.lock 將不能使用已經被撤銷的版本。

cargo yank --vers 1.0.1 可撤銷版本 1.0.1

cargo yank --vers 1.0.1 --undo 允許再次依賴該版本

Cargo 的 workspace

如果 crate 持續擴大,可拆分成多個 library crate,cargo 提供 workspace 功能,可管理多個協同開發的 package

建立 workspace

workspace 是一系列共享同樣的 Cargo.lock 與輸出目錄的 package。

現在建立一個 workspace,有一個 binary project 及兩個 libraries,一個提供 addone,一個提供 addtwo function。

mkdir add
cd add

在 add 目錄產生 Cargo.toml 檔案。內容為

[workspace]

members = [
    "adder",
]

產生 adder binary crate

$ cargo new adder
     Created binary (application) `adder` package

現在可以用 cargo build 建構專案,目錄及檔案如下

├── Cargo.lock
├── Cargo.toml
├── adder
│   ├── Cargo.toml
│   └── src
│       └── main.rs
└── target

在 workspace 產生第二個 crate

修改 Cargo.toml

[workspace]

members = [
    "adder",
    "add-one",
]

產生 add-one library

$ cargo new add-one --lib
     Created library `add-one` package

現在的目錄及檔案為

├── Cargo.lock
├── Cargo.toml
├── add-one
│   ├── Cargo.toml
│   └── src
│       └── lib.rs
├── adder
│   ├── Cargo.toml
│   └── src
│       └── main.rs
└── target

add-one/src/lib.rs 增加一個 add_one 函數:

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

adder 依賴 crate add-one。首先需要在 adder/Cargo.toml 文件中增加 add-one 作為路徑依賴。工作空間中的 crate 不必相互依賴,所以仍需表明工作空間中 crate 的依賴關係。

adder/Cargo.toml

[dependencies]

add-one = { path = "../add-one" }

在 adder 使用 add_one

adder/src/main.rs

use add_one;

fn main() {
    let num = 10;
    println!("Hello, world! {} plus one is {}!", num, add_one::add_one(num));
}

build 專案

$ cargo build
   Compiling add-one v0.1.0 (/Users/charley/project/add/add-one)
   Compiling adder v0.1.0 (/Users/charley/project/add/adder)
    Finished dev [unoptimized + debuginfo] target(s) in 0.78s

為了在頂層 add 目錄執行二進制 crate,需要通過 -p 參數和包名稱來運行 cargo run 指定工作空間中我們希望使用的包

$ cargo run -p adder
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `target/debug/adder`
Hello, world! 10 plus one is 11!

在 workspace 依賴外部 crate

注意:工作空間只在根目錄有一個 Cargo.lock,而不是在每一個 crate 目錄都有 Cargo.lock

這確保了所有的 crate 都使用完全相同版本的依賴。如果在 Cargo.tomladd-one/Cargo.toml 中都增加 rand crate,則 Cargo 會將其都解析為同一版本並記錄到唯一的 Cargo.lock 中。使得工作空間中的所有 crate 都使用相同的依賴意味著其中的 crate 都是相容的。

add-one/Cargo.toml 中的 [dependencies] 部分增加 rand crate 以便能夠在 add-one crate 中使用 rand crate:

add-one/Cargo.toml

[dependencies]

rand = "0.3.14"

現在就可以在 add-one/src/lib.rs 中增加 use rand; 了,接著在 add 目錄運行 cargo build 構建整個工作空間就會引入並編譯 rand crate

$ cargo build
    Updating crates.io index
  Downloaded libc v0.2.60
   Compiling libc v0.2.60
   Compiling rand v0.4.6
   Compiling rand v0.3.23
   Compiling add-one v0.1.0 (/Users/charley/project/add/add-one)
   Compiling adder v0.1.0 (/Users/charley/project/add/adder)
    Finished dev [unoptimized + debuginfo] target(s) in 56.98s

頂層的 Cargo.lock 包含了 add-onerand 依賴的訊息。但即使 rand 被用於工作空間的某處,也不能在其他 crate 中使用它,除非也在他們的 Cargo.toml 中加入 rand

測試

add_one crate 中的 add_one::add_one 函數增加一個測試

add-one/src/lib.rs

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

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        assert_eq!(3, add_one(2));
    }
}
$ cargo test
   Compiling add-one v0.1.0 (/Users/charley/project/add/add-one)
   Compiling adder v0.1.0 (/Users/charley/project/add/adder)
    Finished dev [unoptimized + debuginfo] target(s) in 1.30s
     Running target/debug/deps/add_one-cbf892206c67cf71

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

     Running target/debug/deps/adder-0108381d26b7c5be

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests add-one

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

也可指定測試的 crate

$ cargo test -p add-one

由 crates.io 安裝 binaries

cargo install 用來本地安裝及使用 binary crate

binary crate 是在 crate 有 src/main.rs 或其他可執行程式,不同於本身不能執行的 library。通常 crate 的 README 有該 crate 是 library 或 binary 的資訊。

所有來自 cargo install 的 binary crate 都安裝到 rust 安裝根目錄的 bin folder 中。如果用 rustup.rs 安裝的 rust,且沒有自訂設定,目錄會是 $HOME/.cargo/bin

將該目錄新增到 $PATH 就可以執行程式

ex: ripgrep 用於搜索文件的 grep 的 Rust 實現

$ cargo install ripgrep
Updating registry `https://github.com/rust-lang/crates.io-index`
 Downloading ripgrep v0.3.2
 --snip--
   Compiling ripgrep v0.3.2
    Finished release [optimized + debuginfo] target(s) in 97.91 secs
  Installing ~/.cargo/bin/rg

擴充 cargo 的自訂指令

如果 $PATH 中有類似 cargo-something 的 binary file,就可以用 cargo something 來執行。

cargo --list 會列出所有自訂指令

References

The Rust Programming Language

中文版

中文版 2

2020年5月11日

rust 13 Functional Language Features: Iterators and Closures

函數式編程風格包含將函數作為參數值或其他函數的返回值、將函數賦值給變數以供之後執行等等。

rust 提供兩個 funcational language 的特性

  • 閉包(Closures),一個可以儲存在變數裡的類似函數的結構
  • 迭代器(Iterators),一種處理元素序列的方式

還有先前已經說明的 pattern matching 及 enum,也是受到 funcational language 的影響

Closures: 可獲取環境的 Anonymous Functions

closure 是可儲存變數或作為參數傳遞給其他函數的 anonymous function。可在某處產生 closure,在不同 context 中進行運算,closure 跟 function 不同,允許獲取呼叫者 scope 中的值。

利用 closure 產生抽象行為

假想情境:在一個利用 app 產生的自訂健身計畫的公司工作,後端以 rust 編寫,但產生健身計畫需要考慮很多參數,例如年齡、BMI、喜好、最近健身活動與自訂強度係數。我們希望在需要時呼叫計算的演算法,且計算所需時間很短,只希望呼叫一次,不讓 user 等太久。

先做一個假的計算函數 (simulatedexpensivecalculation),執行 2s,在 main 定義 generateworkout 參數,將業務邏輯寫在 generateworkout 裡面

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

fn simulated_expensive_calculation(intensity: u32) -> u32 {
    println!("calculating slowly...");
    thread::sleep(Duration::from_secs(2));
    intensity
}

// 根據輸入呼叫 simulated_expensive_calculation 函數來列印健身計畫
fn generate_workout(intensity: u32, random_number: u32) {
    if intensity < 25 {
        println!(
            "Today, do {} pushups!",
            simulated_expensive_calculation(intensity)
        );
        println!(
            "Next, do {} situps!",
            simulated_expensive_calculation(intensity)
        );
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                simulated_expensive_calculation(intensity)
            );
        }
    }
}

fn main() {
    // 實際的程式會從 app 前端獲取強度係數並使用 rand crate 來生成亂數,目前先訂為固定值
    let simulated_user_specified_value = 10;
    let simulated_random_number = 7;

    generate_workout(
        simulated_user_specified_value,
        simulated_random_number
    );
}

將會對 simulatedexpensivecalculation 進行修改,為了簡化更新步驟,要重構程式,只呼叫 simulatedexpensivecalculation 一次,並去掉目前多餘的兩次函數呼叫。

利用函數重構

將重複的 simulated_expensive_calculation 函數呼叫提取到一個變數中,並將結果儲存在變量 expensive_result

fn generate_workout(intensity: u32, random_number: u32) {
    let expensive_result =
        simulated_expensive_calculation(intensity);

    if intensity < 25 {
        println!(
            "Today, do {} pushups!",
            expensive_result
        );
        println!(
            "Next, do {} situps!",
            expensive_result
        );
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                expensive_result
            );
        }
    }
}

利用 closure 重構

定義一個 closure 並將其儲存在變數中,可以選擇將整個 simulated_expensive_calculation 函數體移動到這裡引入的 closure 中

let expensive_closure = |num| {
    println!("calculating slowly...");
    thread::sleep(Duration::from_secs(2));
    num
};

closure 定義以一對 | 開始,在裡面指定 closure 的參數,這種語法跟 smalltalk, ruby 類似,如果有多個參數,就 | param1, param2 | 這樣定義。

參數後面是 { } 的 closure body,如果裡面只有一行,可省略 {}。最後要加上 ;,因 closure body 最後是 num,跟函數一樣,就是回傳 num

let 表示 expensive_closure 是一個 anonymous function 的定義,不是呼叫 anonymous function 的回傳值。呼叫 closure 類似呼叫函數。

fn generate_workout(intensity: u32, random_number: u32) {
    let expensive_closure = |num| {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        num
    };

    if intensity < 25 {
        println!(
            "Today, do {} pushups!",
            expensive_closure(intensity)
        );
        println!(
            "Next, do {} situps!",
            expensive_closure(intensity)
        );
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                expensive_closure(intensity)
            );
        }
    }
}

closure 類別推斷與註解

首先討論一下為何閉包定義中和所涉及的 trait 中沒有類型註解

closure 並不要求像 fn 一樣在參數及回傳值上註明類別。closure 不同於 function,沒有暴露在外面的介面,不需要特別命名或給 library 使用。closure 通常很短,且只與對應任意較小的 context 中,在有限制的 context,compiler 可以自行推測參數及回傳值得的資料型別。

如果像要更明確的 code,還是可以加上類別註解

let expensive_closure = |num: u32| -> u32 {
    println!("calculating slowly...");
    thread::sleep(Duration::from_secs(2));
    num
};

closure 的語法很接近 function

fn  add_one_v1   (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x|             { x + 1 };   // 省略了類別註解
let add_one_v4 = |x|               x + 1  ;   // 省略 {}

closure 會自行推測類別,但如果呼叫兩次,但給了不同類別,會發生編譯 error

let example_closure = |x| x;

let s = example_closure(String::from("hello"));
let n = example_closure(5);

使用帶有泛型及 Fn trait 的 closure

改善多次呼叫 closure 的問題,可將結果儲存到變數中。

另外一種方法,是產生一個存放 closure 及呼叫 closure 結果的 struct,該 struct 只會在需要結果時,執行 closure 並儲存結果,這個 pattern 稱為 memorization 或 lazy evaluation

為了讓 struct 存放 closure,需要指定 closure 的類別,因為 struct 定義需要知道其每一個 item 的類別。每一個 closure instance 有其自己獨有的匿名類型:也就是說,即使兩個 closure 有相同的簽名,他們的類別仍然是不同的。為了定義使用 closure 的結構體、枚舉或函數參數,需要像第十章討論的那樣使用泛型和 trait bound。

Fn 系列 trait 由 standard library 提供。所有的 closure 都實現了 trait FnFnMutFnOnce 中的一個。在“閉包會捕獲其環境”部分我們會討論這些 trait 的區別;在這個例子中可以使用 Fn trait。

為了滿足 Fn trait bound 我們增加了代表 closure 所必須的參數和返回值類型的類別。在這個例子中,closure 有一個 u32 的參數並返回一個 u32,這樣所指定的 trait bound 就是 Fn(u32) -> u32

定義 Cacher,在 calculation 儲存 closure,在 value 存放 Option

struct Cacher<T>
    where T: Fn(u32) -> u32
{
    calculation: T,
    value: Option<u32>,
}

Cacher 的 fields 是私有的,因為我們希望 Cacher 管理這些值而不是直接改變他們。

impl<T> Cacher<T>
    where T: Fn(u32) -> u32
{
    fn new(calculation: T) -> Cacher<T> {
        Cacher {
            calculation,
            value: None,
        }
    }

    fn value(&mut self, arg: u32) -> u32 {
        match self.value {
            Some(v) => v,
            None => {
                let v = (self.calculation)(arg);
                self.value = Some(v);
                v
            },
        }
    }
}

Cacher::new 函數獲取一個泛型參數 T,它定義於 impl 塊上下文中並與 Cacher 結構體有著相同的 trait bound。Cacher::new 返回一個在 calculation 字段中存放了指定 closure 和在 value 字段中存放了 None 值的 Cacher 實例,因為我們還未執行 closure。

在需要 closure 的執行結果時,不同於直接呼叫 closure,它會呼叫 value 方法。這個方法會檢查 self.value 是否已經有了一個 Some 的結果值;如果有,它返回 Some 中的值並不會再次執行closure。

如果 self.valueNone,則會呼叫 self.calculation 中儲存的closure,將結果保存到 self.value 以便將來使用,並同時返回結果值。

在 generate_workout 中利用 Cacher

fn generate_workout(intensity: u32, random_number: u32) {
    let mut expensive_result = Cacher::new(|num| {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        num
    });

    if intensity < 25 {
        println!(
            "Today, do {} pushups!",
            expensive_result.value(intensity)
        );
        println!(
            "Next, do {} situps!",
            expensive_result.value(intensity)
        );
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                expensive_result.value(intensity)
            );
        }
    }
}

Cacher 的限制

目前 Cacher 的實現存在兩個小問題,這使得在不同上下文中復用變得很困難。

Cacher 假設對於 value 方法的任何 arg 參數值總是會返回相同的值。也就是說,這個 Cacher 的測試會失敗:

#[test]
fn call_with_different_values() {
    let mut c = Cacher::new(|a| a);

    let v1 = c.value(1);
    let v2 = c.value(2);

    assert_eq!(v2, 2);
}

可修改 Cacher 存放一個 hash map 而不是單獨一個值。hash map 的 key 將是傳遞進來的 arg值,而 value 則是對應 key 呼叫 closure 的結果值。相比之前檢查 self.value 直接是 Some 還是 None 值,現在 value 函數會在hash map 中尋找 arg,如果找到的話就返回其對應的值。如果不存在,Cacher 會呼叫 closure 並將結果值保存在 hash map 對應 arg 值的位置。

第二個問題是它的應用被限制為只接受獲取一個 u32 值並返回一個 u32 值的 closure。比如說,我們可能需要能夠緩存一個獲取字符串 slice 並返回 usize 值的 closure 的結果。請嘗試引入更多泛型參數來增加 Cacher 功能的靈活性。

以 closure 捕獲環境

closure 有另一個函數所沒有的功能:他們可以捕獲其環境並訪問其被定義的作用域的變數。

fn main() {
    let x = 4;

    // x 不是 equal_to_x 的參數,是跟 equal_to_x 相同 scope 的變數
    let equal_to_x = |z| z == x;

    let y = 4;

    assert!(equal_to_x(y));
}

當 closure 從環境中捕獲一個值,closure 會在閉包體中儲存這個值以供使用。這會使用 memory 並產生額外的開銷。

closure 可以通過三種方式捕獲其環境,他們直接對應函數的三種獲取參數的方式:獲取所有權,可變借用和不可變借用。這三種捕獲值的方式被編碼為如下三個 Fn trait:

  • FnOnce 使用從周圍作用域捕獲的變數,closure 周圍的作用域被稱為其 環境environment。為了使用捕獲到的變數,使用必須獲取其所有權並在定義閉包時將其移動進 closure。其名稱的 Once 部分代表了closure不能多次獲取相同變數的所有權的事實,所以它只能被呼叫一次。
  • FnMut 獲取可變的借用值,所以可以改變其環境
  • Fn 從其環境獲取不可變的借用值

產生 closure 時,Rust 根據其如何使用環境中變數來推斷我們希望如何引用環境。由於所有 closure 都可以被呼叫至少一次,所以所有 closure 都實現了 FnOnce 。那些並沒有移動被捕獲變數的所有權到 closure 內的closure也實現了 FnMut ,而不需要對被捕獲的變數進行可變訪問的closure也實現了 Fn。 在示例 13-12 中,equal_to_x closure 不可變的借用了 x(所以 equal_to_x 具有 Fn trait),因為 closure body 只需要讀取 x 的值。

如果你希望強制 closure 獲取其使用的環境值的所有權,可以在參數列表前使用 move 關鍵字。這個技巧在將 closure 傳遞給新線程以便將數據移動到新線程中時最實用。

fn main() {
    // 改為 vector,因為原本的整數,可被 copy 而不是 move
    let x = vec![1, 2, 3];

    let equal_to_x = move |z| z == x;

    // 因為 x 被移動到 closure,main 就不被允許使用 x
    // println!("can't use x here: {:?}", x);

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

    assert!(equal_to_x(y));
}

大部分需要指定一個 Fn 系列 trait bound 的時候,可以從 Fn 開始,而編譯器會根據 closure body 中的情況告訴你是否需要 FnMutFnOnce

用 iterator 處理元素序列

rust 的 iterator 是惰性 lazy 的,在使用前都不會有效果。例如,下面產生了一個 iterator,但在沒有使用前,沒有任何作用。

let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();

使用 iterator

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

let v1_iter = v1.iter();

for val in v1_iter {
    println!("Got: {}", val);
}

Iterator trait 和 next 方法

iterator 都實作了 Iterator trait

trait Iterator {
    type Item;

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

    // 此處省略了方法的預設實作
}

type ItemSelf::Item 是新的語法,定義了 trait 的關聯類別 associated type

Iterator trait 要求同時定義一個 Item 類別,該類別用在 next 的回傳值類別

next 一次回傳一個 item,封裝在 Some 裡面,當 iterator 結束時,會回傳 None

#[test]
fn iterator_demonstration() {
    let v1 = vec![1, 2, 3];

    let mut v1_iter = v1.iter();

    assert_eq!(v1_iter.next(), Some(&1));
    assert_eq!(v1_iter.next(), Some(&2));
    assert_eq!(v1_iter.next(), Some(&3));
    assert_eq!(v1_iter.next(), None);
}

注意 v1_iter 必須是可變的,在 iterator 上呼叫 next 方法會改變用來記錄序列位置的狀態。換句話說,代碼 消費(consume)了,或使用了迭代器。每一個 next 呼叫都會從 iterator 中消費一個項。使用 for 循環時不需要使 v1_iter 可變,因為 for 循環會獲取 v1_iter 的所有權並在後台使 v1_iter 可變。

next 呼叫中得到的值是 vector 的不可變引用。iter 方法生成一個不可變引用的 iterator。如果我們需要一個獲取 v1 所有權並返回擁有所有權的 iterator,可以呼叫 into_iter而不是 iter。如果我們希望迭代可變引用,則可以調用 iter_mut 而不是 iter

消費了 iterator 的方法

Iterator trait 有一系列不同的由標準庫提供默認實現的方法;你可以在 Iterator trait 的標準庫 API 文檔中找到所有這些方法。一些方法在其定義中呼叫了 next 方法,這也就是為什麼在實現 Iterator trait 時要求實現 next 方法的原因。

這些呼叫 next 方法的方法被稱為 消費適配器consuming adaptors),因為呼叫他們會消耗 iterator。ex: sum 方法獲取 iterator 的所有權並反覆呼叫 next 來遍歷迭代器,因而會消費迭代器。當其遍歷每一個項時,它將每一個項加總到一個總和並在迭代完成時回傳總和。

#[test]
fn iterator_sum() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();

    let total: i32 = v1_iter.sum();

    assert_eq!(total, 6);
}

呼叫 sum 之後不再允許使用 v1_iter 因為呼叫 sum 時它會獲取 iterator 的所有權。

產生其他 iterators 的方法

Iterator trait 中定義了另一類方法,被稱為 迭代器適配器iterator adaptors),可將當前 iterator 變為不同類型的 iterator。可鏈式呼叫多個迭代器適配器,但因為所有的迭代器都是惰性的,必須呼叫一個消費適配器方法以便獲取迭代器適配器呼叫的結果。

示例 13-17 展示了一個調用迭代器適配器方法 map 的例子,該 map 方法使用閉包來調用每個元素以生成新的迭代器。 這裡的閉包創建了一個新的迭代器,對其中 vector 中的每個元素都被加 1。不過這些代碼會產生一個警告:

let v1: Vec<i32> = vec![1, 2, 3];

v1.iter().map(|x| x + 1);
warning: unused `std::iter::Map` that must be used
 --> src/main.rs:5:2
  |
5 |     v1.iter().map(|x| x + 1);
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: #[warn(unused_must_use)] on by default
  = note: iterators are lazy and do nothing unless consumed

警告提醒了我們為什麼:iterator adaptors 是惰性的,而這裡我們需要消費迭代器。用 collect 將結果收集到一個資料結構中

let v1: Vec<i32> = vec![1, 2, 3];

let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();

assert_eq!(v2, vec![2, 3, 4]);

使用 closure 獲取環境

這是利用 filter iterator adaptor 與捕獲環境的 closure 的例子

iterator 的 filter 方法獲取一個使用 iterator 每一個項並返回 boolean 的 closure。如果closure返回 true,其值將會包含在 filter 提供的新迭代器中。如果 closure 返回 false,其值不會包含在結果迭代器中。

使用 filter 和一個捕獲環境中變量 shoe_size 的閉包,這樣閉包就可以遍歷一個 Shoe struct 集合以便只返回指定大小的鞋子:

#[derive(PartialEq, Debug)]
struct Shoe {
    size: u32,
    style: String,
}

fn shoes_in_my_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
    shoes.into_iter()
        .filter(|s| s.size == shoe_size)
        .collect()
}

#[test]
fn filters_by_size() {
    let shoes = vec![
        Shoe { size: 10, style: String::from("sneaker") },
        Shoe { size: 13, style: String::from("sandal") },
        Shoe { size: 10, style: String::from("boot") },
    ];

    let in_my_size = shoes_in_my_size(shoes, 10);

    assert_eq!(
        in_my_size,
        vec![
            Shoe { size: 10, style: String::from("sneaker") },
            Shoe { size: 10, style: String::from("boot") },
        ]
    );
}

shoes_in_my_size 函數獲取一個鞋子 vector 的所有權和一個鞋子大小作為參數。它回傳一個只包含指定大小鞋子的 vector。

shoes_in_my_size 函數呼叫了 into_iter 來創建一個獲取 vector 所有權的 iterator。呼叫 filter 將這個迭代器適配成一個只含有那些閉包返回 true 的元素的 new iterator。

closure 從環境中捕獲了 shoe_size 變數並使用其值與每一隻鞋的大小作比較,只保留指定大小的鞋子。最終,調用 collect 將迭代器適配器返回的值收集進一個 vector 並返回。

當呼叫 shoes_in_my_size 時,我們只會得到與指定值相同大小的鞋子。

實作 Iterator trait 建立自訂 iterator

在 vector 呼叫 iter, into_iteritem_mut 可產生 iterator,也可以用 standard library 中其他 collection 類別產生 iterator (ex: hash map)。也可以實作 Iterator trait 產生自訂 iterator,定義中唯一需要的方法是 next

ex: 一個只會從 1 數到 5 的 iterator 。首先,創建一個結構體來存放一些值,接著實現 Iterator trait 將這個結構體放入迭代器中並在此實現中使用其值。

//定義 Counter struct和一個 count 初值為 0 的 Counter 實例的 new 函數
struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

// 為 Counter 實現 Iterator trait,通過定義 next 方法來指定使用 iterator 時的行為
impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        self.count += 1;

        if self.count < 6 {
            Some(self.count)
        } else {
            None
        }
    }
}

#[test]
fn calling_next_directly() {
    let mut counter = Counter::new();

    assert_eq!(counter.next(), Some(1));
    assert_eq!(counter.next(), Some(2));
    assert_eq!(counter.next(), Some(3));
    assert_eq!(counter.next(), Some(4));
    assert_eq!(counter.next(), Some(5));
    assert_eq!(counter.next(), None);
}

通過定義 next 方法實現 Iterator trait,我們現在就可以使用任何標準庫定義的擁有默認實現的 Iterator trait 方法了,因為他們都使用了 next 方法的功能。

ex: 獲取 Counter 實例產生的值,將這些值與另一個 Counter 實例在省略了第一個值之後產生的值配對,將每一對值相乘,只保留那些可以被三整除的結果,然後將所有保留的結果相加

#[test]
fn using_other_iterator_trait_methods() {
    let sum: u32 = Counter::new().zip(Counter::new().skip(1))
                                 .map(|(a, b)| a * b)
                                 .filter(|x| x % 3 == 0)
                                 .sum();
    assert_eq!(18, sum);
}

注意: zip 只會產生 (1,2) (2,3) (3,4) (4,5) 沒有 (5, None)

改良 I/O project

使用 iterator 改進 Config:newsearch

原始程式 lib.rs

use std::error::Error;
use std::fs;
use std::env;

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    let results = if config.case_sensitive {
        search(&config.query, &contents)
    } else {
        search_case_insensitive(&config.query, &contents)
    };

    for line in results {
        println!("{}", line);
    }

    Ok(())
}

pub struct Config {
    pub query: String,
    pub filename: String,
    pub case_sensitive: bool,
}

impl Config {
    //  &'static str 是 string literal 的類別
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            // 異常時,會傳 Err
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        // 使用 env 環境變數
        let case_sensitive = env::var("CASE_INSENSITIVE").is_err();

        Ok(Config { query, filename, case_sensitive })
    }
}

// 需要在 search 的簽名中定義一個顯式生命週期 'a 並用於 contents 參數和返回值。
fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    // 用 lines 處理每一行資料,包含 query 時,就放到 results
    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

// 在比較查詢和每一行之前將他們都轉換為小寫
fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let query = query.to_lowercase();
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.to_lowercase().contains(&query) {
            results.push(line);
        }
    }

    results
}

// 使用 "duct" 作為這個測試中需要搜索的字符串。用來搜索的文本有三行,其中只有一行包含 "duct"。我們斷言 search 函數的返回值只包含期望的那一行。
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn case_sensitive() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(
            vec!["safe, fast, productive."],
            search(query, contents)
        );
    }

    #[test]
    fn case_insensitive() {
        let query = "rUsT";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";

        assert_eq!(
            vec!["Rust:", "Trust me."],
            search_case_insensitive(query, contents)
        );
    }
}

main.rs

use std::env;
use std::process;
use minigrep;
use minigrep::Config;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {}", err);
        process::exit(1);
    });

    if let Err(e) = minigrep::run(config) {
        eprintln!("Application error: {}", e);

        process::exit(1);
    }
}

利用 iterator 去掉 clone

Config 原本是用 clone 取得 command line 參數

impl Config {
    //  &'static str 是 string literal 的類別
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            // 異常時,會傳 Err
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        // 使用 env 環境變數
        let case_sensitive = env::var("CASE_INSENSITIVE").is_err();

        Ok(Config { query, filename, case_sensitive })
    }
}

可將 new 改為獲取一個有所有權的 iterator 為參數,而不是借用 slice

Config::new 獲取 iterator 的所有權,並不再使用 借用 的索引操作,就可將 iterator 的 String 移動到 Config 中

直接使用 env:args 的 iterator

修改 main,將 env::args 的返回值傳給 Config::new

fn main() {
    let config = Config::new(env::args()).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {}", err);
        process::exit(1);
    });

    // --snip--
}

更新 Config::new 接受一個 iterator,env::args 函數的標準庫文檔展示了其返回的迭代器類型是 std::env::Args。因為這裡需要獲取 args 的所有權且通過迭代改變 args,我們可以在 args 參數前指定 mut 關鍵字使其可變。

impl Config {
    pub fn new(mut args: std::env::Args) -> Result<Config, &'static str> {
        // --snip--

使用 Iterator trait 方法代替索引

Config::new 改為呼叫 args.next()

impl Config {
    pub fn new(mut args: std::env::Args) -> Result<Config, &'static str> {
        args.next();

        let query = match args.next() {
            Some(arg) => arg,
            None => return Err("Didn't get a query string"),
        };

        let filename = match args.next() {
            Some(arg) => arg,
            None => return Err("Didn't get a file name"),
        };

        let case_sensitive = env::var("CASE_INSENSITIVE").is_err();

        Ok(Config { query, filename, case_sensitive })
    }
}

利用 iterator adaptor 簡化程式

原本 search 的寫法是

fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    // 用 lines 處理每一行資料,包含 query 時,就放到 results
    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

改用iterator,可避免使用可變的 results vector,就能避免 concurrent 存取 results 的問題

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    contents.lines()
        .filter(|line| line.contains(query))
        .collect()
}

大部分 Rust programmer 傾向於使用 iterator,一旦你對不同 iterator 的工作方式有了感覺之後,迭代器可能會更容易理解。相比使用不同的循環並產生新 vector,iterator codes 更關注循環的目的。

比較 loops 與 iterators 的效能

用 iterator 會比 for 的程式速度快一點

iterator 在 rust 是 zero-cost abstraction,抽象但不會增加執行的 overhead

ex: 有三個變數:一個叫 buffer 的數據 slice、一個有 12 個元素的數組 coefficients、和一個代表位移位數的 qlp_shift 。為了計算 prediction 的值,這些代碼遍歷了 coefficients 中的 12 個值,使用 zip 方法將係數與 buffer 的前 12 個值組合在一起。接著將每一對值相乘,再將所有結果相加,然後將總和右移 qlp_shift 位。

let buffer: &mut [i32];
let coefficients: [i64; 12];
let qlp_shift: i16;

for i in 12..buffer.len() {
    let prediction = coefficients.iter()
                                 .zip(&buffer[i - 12..i])
                                 .map(|(&c, &s)| c * s as i64)
                                 .sum::<i64>() >> qlp_shift;
    let delta = buffer[i];
    buffer[i] = prediction as i32 + delta;
}

Rust 知道這裡會迭代 12 次,所以它會“展開”(unroll)循環。展開是一種移除循環程式開銷,並替換為每個迭代中的重複代碼的優化。

References

The Rust Programming Language

中文版

中文版 2

2020年5月4日

rust 適合開發 command line tool,這邊要開發一個 grep: Globally search a Regular Expression and Print。這邊會展示如何讓我們的命令行工具提中很多命令行工具中用到的終端功能。讀取環境變數來配置工具的行為。列印到標準錯誤控制流(stderr) 而不是標準輸出(stdout),這樣可以選擇將成功輸出重定向到文件中的同時仍然在 console 上顯示錯誤信息。

接受 command line 參數

建立新的 minigrep project

cargo new minigrep

Rust 標準庫提供函數 std::env::args,這個函數回傳一個傳遞給程序的命令行參數的 iterator。

use std::env;

fn main() {
    // 將 command line 參數收集到一個 vector 中並列印
    // Rust 中我們很少會需要註明類別,但 collect 是一個需要註明類別的函數,因為 Rust 不能推斷出你想要什麼類別的集合
    let args: Vec<String> = env::args().collect();
    println!("{:?}", args);
}

執行,第一個參數是程式的名稱

$ cargo run 1234 55
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `target/debug/minigrep 1234 55`
["target/debug/minigrep", "1234", "55"]

注意:args 函數和無效的 Unicode

std::env::args 在其任何參數包含無效 Unicode 字元時會 panic。如果你需要接受包含無效 Unicode 字元的參數,要用 std::env::args_os 代替。這個函數會傳回 OsString 而不是 StringOsString 在每個平台都不一樣而且比 String 值處理起來更為複雜。

將參數儲存到變數

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();

    // 程式的名稱 是 &args[0]
    let query = &args[1];
    let filename = &args[2];

    println!("Searching for {}", query);
    println!("In file {}", filename);
}

執行

$ cargo run test sample.txt
   Compiling minigrep v0.1.0 (/Users/charley/project/idea/rust/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.67s
     Running `target/debug/minigrep test sample.txt`
Searching for test
In file sample.txt

讀取檔案

要增加讀取由 filename 命令行參數指定檔案的功能

std::fs::read_to_string 處理 filename 參數,打開檔案,回傳包含其內容的 Result<String>

use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let query = &args[1];
    let filename = &args[2];

    println!("Searching for {}", query);
    println!("In file {}", filename);

    let contents = fs::read_to_string(filename)
        .expect("Something went wrong reading the file");

    println!("With text:\n{}", contents);
}

執行結果

$ cargo run the poem.txt
   Compiling minigrep v0.1.0 (/Users/charley/project/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.72s
     Running `target/debug/minigrep the poem.txt`
Searching for the
In file poem.txt
With text:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.

How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!

重構以改進 module 及錯誤處理

  • main 解析了參數並打開檔案,最好是分離這兩個功能。

  • search 跟 filename 是程式中的設定變數, f, contents 則是程式邏輯,最好將設定變數儲存到 struct,有更結構化的變數資訊。

  • 如果打開檔案失敗會用 expect 列印錯誤訊息,但只是 "file not found",沒有其他類型的錯誤訊息 ex: 權限錯誤

  • 不停地使用 expect 處理不同的錯誤,例如參數不夠,會得到 index out of bounds error,但問題不夠明確。 將所有錯誤訊息代碼集中,也能確保列印有意義的錯誤訊息。

分離 binary project 的程式邏輯

在 binary project 中,main 函數負責多個任務的組織問題。rust 社群提供分離 binary project 程式邏輯的 guideline。

  1. 將城市分成 main.rs, lib.rs,將邏輯放入 lib.rs
  2. 當 command line 解析邏輯較小時,可保留在 main.rs 中
  3. 當 command line 解析開始複雜時,同樣將其從 main.rs 提取到 lib.rs
  4. 保留在 main 函數中的責任應被限制為
    • 使用參數值呼叫 command line 解析邏輯
    • 設定
    • 呼叫 lib.rs 中的 run 函數
    • 如果 run 回傳錯誤,就處理錯誤

main.rs 負責程式運作,lib.rs 負責任務邏輯,因不能直接測試 main,將程式邏輯移動到 lib.rs 中,就可以進行測試。

提取參數解析器

改為呼叫新函數 parse_config

use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let (query, filename) = parse_config(&args);

    println!("Searching for {}", query);
    println!("In file {}", filename);

    let contents = fs::read_to_string(filename)
        .expect("Something went wrong reading the file");

    println!("With text:\n{}", contents);
}

fn parse_config(args: &[String]) -> (&str, &str) {
    let query = &args[1];
    let filename = &args[2];

    (query, filename)
}

組合設定值

將 query, filename 兩個值放入一個 struct 並給一個有意義的名字

use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = parse_config(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    let contents = fs::read_to_string(config.filename)
        .expect("Something went wrong reading the file");

    println!("With text:\n{}", contents);
}

struct Config {
    query: String,
    filename: String,
}

fn parse_config(args: &[String]) -> Config {
    let query = args[1].clone();
    let filename = args[2].clone();

    Config { query, filename }
}

main 中的 args 變量是參數值的owner並只允許 parse_config 函數借用他們,如果 Config 嘗試獲取 args 中值的所有權將違反 Rust 的借用規則。以 clone String 的方式,產生 Config instance

建立 Config 的 constructor

parse_config 從一個普通函數變為一個叫做 new 且與結構體關聯的函數。未來可以像 std library 中的 String 呼叫 String::new 一樣來產生 Config,將 parse_config 變為一個與 Config 關聯的 new 函數。

use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    let contents = fs::read_to_string(config.filename)
        .expect("Something went wrong reading the file");

    println!("With text:\n{}", contents);
}

struct Config {
    query: String,
    filename: String,
}

impl Config {
    fn new(args: &[String]) -> Config {
        let query = args[1].clone();
        let filename = args[2].clone();

        Config { query, filename }
    }
}

處理錯誤

直接執行時,會發生錯誤

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `target/debug/minigrep`
thread 'main' panicked at 'index out of bounds: the len is 1 but the index is 1', src/main.rs:25:21
note: Run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

在 new 裡面,進行參數數量檢查,並以 panic! 產生有意義的錯誤訊息

// --snip--
fn new(args: &[String]) -> Config {
    if args.len() < 3 {
        panic!("not enough arguments");
    }
    // --snip--

new 回傳 Result 而不是呼叫 panic!

可以改為回傳一個 Result 值,它在成功時會包含一個 Config 的實例,而在錯誤時會描述問題。

use std::env;
use std::fs;
use std::process;

fn main() {
    let args: Vec<String> = env::args().collect();

    // 呼叫 new,失敗時以 std::process::exit 直接停掉程式
    let config = Config::new(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {}", err);
        process::exit(1);
    });

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    let contents = fs::read_to_string(config.filename)
        .expect("Something went wrong reading the file");

    println!("With text:\n{}", contents);
}

struct Config {
    query: String,
    filename: String,
}

impl Config {
    //  &'static str 是 string literal 的類別
    fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            // 異常時,會傳 Err
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

執行結果

$ cargo run
   Compiling minigrep v0.1.0 (/Users/charley/project/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.74s
     Running `target/debug/minigrep`
Problem parsing arguments: not enough arguments

提取程式邏輯到 run 函數

fn main() {
    // --snip--

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    run(config);
}

fn run(config: Config) {
    let contents = fs::read_to_string(config.filename)
        .expect("something went wrong reading the file");

    println!("With text:\n{}", contents);
}

// --snip--

在 run 回傳錯誤

use std::env;
use std::fs;
use std::process;
use std::error::Error;

fn main() {
    let args: Vec<String> = env::args().collect();

    // 呼叫 new,失敗時以 std::process::exit 直接停掉程式
    let config = Config::new(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {}", err);
        process::exit(1);
    });

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    if let Err(e) = run(config) {
        println!("Application error: {}", e);

        process::exit(1);
    }
}

// 錯誤類別,使用了 trait 對象 Box<dyn Error>(在開頭使用了 use 語句將 std::error::Error 引入作用域)
fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    println!("With text:\n{}", contents);

    Ok(())
}

struct Config {
    query: String,
    filename: String,
}

impl Config {
    //  &'static str 是 string literal 的類別
    fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            // 異常時,會傳 Err
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

將程式碼拆分到 crate

src/lib.rs

use std::error::Error;
use std::fs;

// 錯誤類別,使用了 trait 對象 Box<dyn Error>(在開頭使用了 use 語句將 std::error::Error 引入作用域)
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    println!("With text:\n{}", contents);

    Ok(())
}

pub struct Config {
    pub query: String,
    pub filename: String,
}

impl Config {
    //  &'static str 是 string literal 的類別
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            // 異常時,會傳 Err
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

src/main.rs

use std::env;
use std::process;
use minigrep;
use minigrep::Config;

fn main() {
    let args: Vec<String> = env::args().collect();

    // 呼叫 new,失敗時以 std::process::exit 直接停掉程式
    let config = Config::new(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {}", err);
        process::exit(1);
    });

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    if let Err(e) = minigrep::run(config) {
        println!("Application error: {}", e);

        process::exit(1);
    }
}

測試

去掉 lib.rs, main.rs 的 println!

search 要 iterate 每一行資料,並檢查是否有包含 query

增加 search 的測試程式

use std::error::Error;
use std::fs;

// 錯誤類別,使用了 trait 對象 Box<dyn Error>(在開頭使用了 use 語句將 std::error::Error 引入作用域)
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    // println!("With text:\n{}", contents);
    // 呼叫 search
    for line in search(&config.query, &contents) {
        println!("{}", line);
    }

    Ok(())
}

pub struct Config {
    pub query: String,
    pub filename: String,
}

impl Config {
    //  &'static str 是 string literal 的類別
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            // 異常時,會傳 Err
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        Ok(Config { query, filename })
    }
}

// 需要在 search 的簽名中定義一個顯式生命週期 'a 並用於 contents 參數和返回值。
fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    // 用 lines 處理每一行資料,包含 query 時,就放到 results
    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

// 使用 "duct" 作為這個測試中需要搜索的字符串。用來搜索的文本有三行,其中只有一行包含 "duct"。我們斷言 search 函數的返回值只包含期望的那一行。
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(
            vec!["safe, fast, productive."],
            search(query, contents)
        );
    }
}

處理環境變數

增加 search_case_insensitive 的功能,並將會在設置了環境變量後呼叫它。

use std::error::Error;
use std::fs;
use std::env;

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    let results = if config.case_sensitive {
        search(&config.query, &contents)
    } else {
        search_case_insensitive(&config.query, &contents)
    };

    for line in results {
        println!("{}", line);
    }

    Ok(())
}

pub struct Config {
    pub query: String,
    pub filename: String,
    pub case_sensitive: bool,
}

impl Config {
    //  &'static str 是 string literal 的類別
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            // 異常時,會傳 Err
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let filename = args[2].clone();

        // 使用 env 環境變數
        let case_sensitive = env::var("CASE_INSENSITIVE").is_err();

        Ok(Config { query, filename, case_sensitive })
    }
}

// 需要在 search 的簽名中定義一個顯式生命週期 'a 並用於 contents 參數和返回值。
fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    // 用 lines 處理每一行資料,包含 query 時,就放到 results
    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

// 在比較查詢和每一行之前將他們都轉換為小寫
fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let query = query.to_lowercase();
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.to_lowercase().contains(&query) {
            results.push(line);
        }
    }

    results
}

// 使用 "duct" 作為這個測試中需要搜索的字符串。用來搜索的文本有三行,其中只有一行包含 "duct"。我們斷言 search 函數的返回值只包含期望的那一行。
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn case_sensitive() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(
            vec!["safe, fast, productive."],
            search(query, contents)
        );
    }

    #[test]
    fn case_insensitive() {
        let query = "rUsT";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";

        assert_eq!(
            vec!["Rust:", "Trust me."],
            search_case_insensitive(query, contents)
        );
    }
}

執行結果

$ export CASE_INSENSITIVE=1
$ cargo run the poem.txt
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `target/debug/minigrep the poem.txt`
Then there's a pair of us - don't tell!
They'd banish us, you know.
To tell your name the livelong day

將錯誤訊息轉到 stderr

目前都是用 println! 將輸出列印到 console,錯誤訊息可改為輸出到 stderr,這樣就能讓標準輸出 stdout 轉到一個檔案,而錯誤訊息還是列印到 console

檢查錯誤要寫到哪裡

執行程式時,可用 > 將 stdout 轉輸出到檔案中

$ cargo run > output.txt

將錯誤列印到 stderr

列印錯誤要改用 eprintln!

use std::env;
use std::process;
use minigrep;
use minigrep::Config;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {}", err);
        process::exit(1);
    });

    if let Err(e) = minigrep::run(config) {
        eprintln!("Application error: {}", e);

        process::exit(1);
    }
}

References

The Rust Programming Language

中文版

中文版 2