2020年7月6日

rust20_實作 multithread web server

  1. 學習一些 TCP 與 HTTP 知識
  2. 在 socket 上監聽 TCP connections
  3. 解析少量的 HTTP request
  4. 產生一個合適的 HTTP response
  5. 通過 thread pool 改善 server 的吞吐量

Building a Single-Threaded Web Server

產生 project heelo

$ cargo new hello
     Created binary (application) `hello` project
$ cd hello

修改 main.rs,監聽 TCP port 7878

use std::net::TcpListener;

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        println!("Connection established!");
    }
}

bind 類似 new,會回傳一個新的 TcpListener instance

bind 回傳 Result<T, E>,這代表綁定可能會失敗,例如,連接 80 端口需要管理員權限(非管理員用戶只能監聽大於 1024 的端口),所以如果不是管理員嘗試連接 80 端口,則會綁定失敗。

另外如果運行兩個此程序的實例這樣會有兩個程式監聽相同的port,綁定會失敗。因為我們是出於學習目的來編寫一個基礎的 server,將不用關心處理這類錯誤,使用 unwrap 在出現這些情況時直接停止程式。

TcpListenerincoming 方法回傳一個 iterator,它提供了一系列的 stream(更準確的說是 TcpStream 類型的流)。stream)代表一個客戶端和服務端之間打開的連線。連線connection)代表客戶端連接服務端、服務端生成 response 以及服務端關閉連接的全部 request / response 過程。為此,TcpStream 允許我們讀取它來查看客戶端發送了什麼,並可以撰寫 response。for 循環會依次處理每個連接並產生一系列的流供我們處理。

目前為止,處理流的過程包含 unwrap,如果出現任何錯誤會終止程序,如果沒有任何錯誤,則列印出信息。接下來我們將為成功的情況增加更多功能。當客戶端連接到服務端時 incoming方法回傳錯誤是可能的,因為我們實際上沒有遍歷連接,而是遍歷 連線嘗試connection attempts)。連線可能會因為很多原因不能成功,大部分是操作系統相關的。例如,很多系統限制同時打開的連接數;新連接嘗試產生錯誤,直到一些打開的連接關閉為止。

測試

在終端執行 cargo run,接著在 browser 中瀏覽 127.0.0.1:7878。瀏覽器會顯示出看起來無法連接的錯誤訊息,因為 server 目前並沒 response 任何資料。但是如果我們觀察 terminal,會發現當瀏覽器連接 server 時會印出一系列的訊息!

Connection established!
Connection established!
Connection established!

使用 ctrl-C 來停止程序。並在做出最新的修改之後執行 cargo run 重啟服務。

讀取 request

為了分離取得連線和接下來對連接的操作的相關內容,我們將開始一個新函數來處理連線。在這個新的 handle_connection 函數中,我們從 TCP 流中讀取資料並列印出來以便觀察瀏覽器發送過來的資料。

use std::io::prelude::*;
use std::net::TcpStream;
use std::net::TcpListener;

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        handle_connection(stream);
    }
}

// stream 參數是可變的。這是因為 TcpStream 在內部記錄了所返回的資料,內部狀態可能會改變
fn handle_connection(mut stream: TcpStream) {
    // 宣告一個 buffer 來存放讀取到的數據
    let mut buffer = [0; 512];

    // 將緩衝區中的字節轉換為字符串並打印出來。String::from_utf8_lossy 函數獲取一個 &[u8] 並產生一個 String
    // 函數名的 “lossy” 部分來源於當其遇到無效的 UTF-8 序列時的行為:它使用 �,U+FFFD REPLACEMENT CHARACTER,來代替無效序列。你可能會在緩衝區的剩餘部分看到這些替代字元
    stream.read(&mut buffer).unwrap();

    println!("Request: {}", String::from_utf8_lossy(&buffer[..]));
}

觀察 http request

Request: GET / HTTP/1.1
Host: 127.0.0.1:7878
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7,zh-CN;q=0.6,es;q=0.5,vi;q=0.4,de;q=0.3
DN

http 格式

Method Request-URI HTTP-Version CRLF
headers CRLF
message-body

第一行是 request line

/ 是 URI: Uniform Resource Identifier

CRLF序列 (CRLF代表 carriage return line feed,這是打字機時代的術語!)結束。

撰寫 response

HTTP-Version Status-Code Reason-Phrase CRLF
headers CRLF
message-body

第一行是 status line

回應 HTTP/1.1 200 OK\r\n\r\n

use std::io::prelude::*;
use std::net::TcpStream;
use std::net::TcpListener;

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        handle_connection(stream);
    }
}

fn handle_connection(mut stream: TcpStream) {
    let mut buffer = [0; 512];

    stream.read(&mut buffer).unwrap();

    let response = "HTTP/1.1 200 OK\r\n\r\n";

    stream.write(response.as_bytes()).unwrap();
    stream.flush().unwrap();
}

當在瀏覽器中加載 127.0.0.1:7878 時,會得到一個空頁面而不是錯誤。

回應 html

在項目根目錄創建一個新文件,hello.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Hello!</title>
  </head>
  <body>
    <h1>Hello!</h1>
    <p>Hi from Rust</p>
  </body>
</html>

修改 handle_connection 來讀取 HTML 文件

use std::fs;
// --snip--

fn handle_connection(mut stream: TcpStream) {
    let mut buffer = [0; 512];
    stream.read(&mut buffer).unwrap();

    let contents = fs::read_to_string("hello.html").unwrap();

    let response = format!("HTTP/1.1 200 OK\r\n\r\n{}", contents);

    stream.write(response.as_bytes()).unwrap();
    stream.flush().unwrap();
}

在 browser 就能看到 hello.html 網頁內容

validating request and selectively responding

目前會回傳固定的 html,修改為可判斷網址

fn handle_connection(mut stream: TcpStream) {
    let mut buffer = [0; 512];
    stream.read(&mut buffer).unwrap();

    let get = b"GET / HTTP/1.1\r\n";

    if buffer.starts_with(get) {
        let contents = fs::read_to_string("hello.html").unwrap();

        let response = format!("HTTP/1.1 200 OK\r\n\r\n{}", contents);

        stream.write(response.as_bytes()).unwrap();
        stream.flush().unwrap();
    } else {
        // 其他請求
        let status_line = "HTTP/1.1 404 NOT FOUND\r\n\r\n";
        let contents = fs::read_to_string("404.html").unwrap();

        let response = format!("{}{}", status_line, contents);

        stream.write(response.as_bytes()).unwrap();
        stream.flush().unwrap();
    }
}

匹配 requst GET / HTTP/1.1,並區分不同的 response

對於任何不是 / 的請求返回 404 狀態碼的 response 和錯誤頁面

404.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Hello!</title>
  </head>
  <body>
    <h1>Oops!</h1>
    <p>Sorry, I don't know what you're asking for.</p>
  </body>
</html>

refactoring

將這些區別分別提取到一行 ifelse 中,對狀態行和文件名變數賦值;然後在讀取文件和寫入 response 的代碼中無條件的使用這些變數。

fn handle_connection(mut stream: TcpStream) {
    let mut buffer = [0; 512];
    stream.read(&mut buffer).unwrap();

    let get = b"GET / HTTP/1.1\r\n";

    let (status_line, filename) = if buffer.starts_with(get) {
        ("HTTP/1.1 200 OK\r\n\r\n", "hello.html")
    } else {
        ("HTTP/1.1 404 NOT FOUND\r\n\r\n", "404.html")
    };

    let contents = fs::read_to_string(filename).unwrap();

    let response = format!("{}{}", status_line, contents);

    stream.write(response.as_bytes()).unwrap();
    stream.flush().unwrap();
}

Turn into a Multithreaded Server

目前 server 會依次處理每一個請求,如果 server 正接收越來越多的請求,會使性能越來越差。如果一個請求花費很長時間來處理,隨後而來的請求則不得不等待這個長請求結束。我們需要修復這種情況,不過首先讓我們實際嘗試重現一下這個問題。

模擬 slow request

在回應 /sleep 時會暫停 5s

use std::thread;
use std::time::Duration;
// --snip--

fn handle_connection(mut stream: TcpStream) {
    // --snip--

    let get = b"GET / HTTP/1.1\r\n";
    let sleep = b"GET /sleep HTTP/1.1\r\n";

    let (status_line, filename) = if buffer.starts_with(get) {
        ("HTTP/1.1 200 OK\r\n\r\n", "hello.html")
    } else if buffer.starts_with(sleep) {
        thread::sleep(Duration::from_secs(5));
        ("HTTP/1.1 200 OK\r\n\r\n", "hello.html")
    } else {
        ("HTTP/1.1 404 NOT FOUND\r\n\r\n", "404.html")
    };

    // --snip--
}

利用 thead pool 改善效能

線程池thread pool)是一組預先分配的等待或準備處理任務的 thread。當程式收到一個新任務,線程池中的一個線程會被分配任務,這個線程會離開並處理任務。其餘的線程則可用於處理在第一個線程處理任務的同時處理其他接收到的任務。當第一個線程處理完任務時,它會返回空閒線程池中等待處理新任務。線程池允許我們並發處理連接,增加 server 的吞吐量。

我們會將 pool 線程限制為較少的數量,以防拒絕服務(Denial of Service, DoS)攻擊;如果程序為每一個接收的請求都產生一個線程,某人向 server 發起千萬級的請求時會耗盡服務器的資源並導致所有請求的處理都被終止。

不同於分配無限的線程,線程池中將有固定數量的等待線程。當新進請求時,將請求發送到線程池中做處理。線程池會維護一個接收請求的隊列。每一個線程會從隊列中取出一個請求,處理請求,接著向對隊列索取另一個請求。通過這種設計,則可以並發處理 N 個請求,其中 N 為線程數。如果每一個線程都在響應慢請求,之後的請求仍然會阻塞隊列,不過相比之前增加了能處理的慢請求的數量。

這個設計僅僅是多種改善 web server 吞吐量的方法之一。其他可供探索的方法有 fork/join 模型和單線程異步 I/O 模型。

先為每一個 stream 分配了一個新線程進行處理

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        thread::spawn(|| {
            handle_connection(stream);
        });
    }
}

一個假想的 thread pool API

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    let pool = ThreadPool::new(4);

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        pool.execute(|| {
            handle_connection(stream);
        });
    }
}

使用 ThreadPool::new 來產生一個新的線程池,它有一個可配置的線程數的參數,在這裡是 ˋ。這樣在 for 循環中,pool.execute 有類似 thread::spawn 的 API,它獲取一個線程池運行於每一個流的閉包。pool.execute 需要實現為獲取閉包並傳遞給池中的線程。這段 code 還不能編譯,不過編譯器會告訴我們如何修復它。

建立 ThreadPool struct

src/lib.rs

pub struct ThreadPool;

impl ThreadPool {
    pub fn new(size: usize) -> ThreadPool {
        ThreadPool
    }
}

src/main.rs

use hello::ThreadPool;

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    let pool = ThreadPool::new(4);

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        pool.execute(|| {
            handle_connection(stream);
        });
    }
}

因為還沒有定義 execute method

$ cargo check
    Checking hello v0.1.0 (/Users/charley/project/idea/rust/hello)
error[E0599]: no method named `execute` found for type `hello::ThreadPool` in the current scope
  --> src/main.rs:17:14
   |
17 |         pool.execute(|| {
   |              ^^^^^^^

error: aborting due to previous error

我們會在 ThreadPool 上定義 execute 函數來獲取一個閉包參數。chap13 的 “使用帶有泛型和 Fn trait 的閉包” 部分,閉包作為參數時可以使用三個不同的 trait:FnFnMutFnOnce。我們需要決定這裡該使用哪種閉包。因最終需要實現的類似於標準庫的 thread::spawn,所以我們可以觀察 thread::spawn 的簽名在其參數中使用了何種 bound。

spawn 的 API 文件是這樣

pub fn spawn<F, T>(f: F) -> JoinHandle<T>
    where
        F: FnOnce() -> T + Send + 'static,
        T: Send + 'static

F 是這裡我們關心的參數;T 與回傳值有關所以我們並不關心。spawn 使用 FnOnce 作為 F 的 trait bound,這可能也是我們需要的,因為最終會將傳遞給 execute 的參數傳給 spawn。因為處理請求的線程只會執行閉包一次,這也進一步確認了 FnOnce 是我們需要的 trait,這裡符合 FnOnceOnce 的意思。

F 還有 trait bound Send 和生命週期綁定 'static,這對我們的情況也是有意義的:需要 Send來將閉包從一個線程轉移到另一個線程,而 'static 是因為不知道線程會執行多久。讓我們編寫一個使用帶有這些 bound 的泛型參數 FThreadPoolexecute 方法:

impl ThreadPool {
    // --snip--

    pub fn execute<F>(&self, f: F)
        where
            F: FnOnce() + Send + 'static
    {

    }
}
$ cargo check
    Checking hello v0.1.0 (/Users/charley/project/idea/rust/hello)
warning: unused variable: `size`
 --> src/lib.rs:4:16
  |
4 |     pub fn new(size: usize) -> ThreadPool {
  |                ^^^^ help: consider prefixing with an underscore: `_size`
  |
  = note: #[warn(unused_variables)] on by default

warning: unused variable: `f`
 --> src/lib.rs:8:30
  |
8 |     pub fn execute<F>(&self, f: F)
  |                              ^ help: consider prefixing with an underscore: `_f`

    Finished dev [unoptimized + debuginfo] target(s) in 1.57s

目前只能編譯,還無法運作

new 驗證 pool 中 thread 的數量

這裡仍然存在警告是因為其並沒有對 newexecute 的參數做任何操作。讓我們用期望的行為來實現這些函數。

new 開始。先前選擇使用無符號類型作為 size 參數的類型,因為線程數為負的線程池沒有意義。然而,線程數為零的線程池同樣沒有意義,不過零是一個完全有效的 u32 值。讓我們增加在返回 ThreadPool 實例之前檢查 size 是否大於零的代碼,並使用 assert! 宏在得到零時 panic,如 20-13 所示:

impl ThreadPool {
    /// 創建線程池。
    ///
    /// 線程池中線程的數量。
    ///
    /// # Panics
    ///
    /// `new` 函數在 size 為 0 時會 panic。
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        ThreadPool
    }

    // --snip--
}

這裡用文檔註釋為 ThreadPool 增加了一些文件

如果要做得更好,可以改為傳回 Result

pub fn new(size: usize) -> Result<ThreadPool, PoolCreationError> {

產生儲存 threads 的空間

現在有了一個有效的線程池線程數,就可以實際產生這些線程,並在回傳前將他們儲存在 ThreadPool 結構中。不過要如何 “儲存” 一個線程?讓我們再看看 thread::spawn 的簽名:

pub fn spawn<F, T>(f: F) -> JoinHandle<T>
    where
        F: FnOnce() -> T + Send + 'static,
        T: Send + 'static

spawn 返回 JoinHandle<T>,其中 T 是閉包返回的類型。嘗試使用 JoinHandle 來看看會發生什麼。在我們的情況中,傳遞給線程池的閉包會處理連線並不返回任何值,所以 T 將會是單元類型 ()

20-14 中的代碼可以編譯,不過實際上還並沒有產生任何線程。我們改變了 ThreadPool 的定義來存放一個 thread::JoinHandle<()> 的 vector 實例,使用 size 容量來初始化,並設置一個 for 循環了來執行產生線程的code,並回傳包含這些線程的 ThreadPool 實例:

lib.rs

use std::thread;

pub struct ThreadPool {
    threads: Vec<thread::JoinHandle<()>>,
}

impl ThreadPool {
   pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        // with_capacity 與 Vec::new 做了同樣的工作,不過它為 vector 預先分配空間。因為已經知道了 vector 中需要 size 個元素,預先進行分配比僅僅 Vec::new 要稍微有效率一些
        let mut threads = Vec::with_capacity(size);

        for _ in 0..size {
            // create some threads and store them in the vector
        }

        ThreadPool {
            threads
        }
    }

    pub fn execute<F>(&self, f: F)
        where
            F: FnOnce() + Send + 'static
    {

    }
}

Worker struct 負責從 ThreadPool 將 code 傳給 thread

20-14 的 for 循環中留下了一個關於產生線程的註釋。實際上如何產生線程呢?這是一個難題。標準庫提供的產生線程的方法,thread::spawn,它期望獲取一些一旦產生線程就應該執行的代碼。然而,我們希望開始線程並使其等待稍後傳遞的代碼。標準庫的線程實現並沒有包含這麼做的方法;我們必須自己實現。

我們將要實現的行為是產生線程並稍後發送代碼,這會在 ThreadPool 和線程間引入一個新類別來管理這種新行為。這個資料結構稱為 Worker:這是一個 pool 中的常見概念。想像一下在餐館廚房工作的員工:員工等待來自客戶的訂單,他們負責接受這些訂單並完成它們。

不同於在線程池中儲存一個 JoinHandle<()> 實例的 vector,我們會儲存 Worker 結構的實例。每一個 Worker 會儲存一個單獨的 JoinHandle<()> 實例。接著會在 Worker 上實現一個方法,它會獲取需要允許代碼的閉包並將其發送給已經啟動的線程執行。我們還會賦予每一個 worker id,這樣就可以在日誌和呼叫中區別線程池中的不同 worker。

首先,讓我們做出產生 ThreadPool 時所需的修改。在通過如下方式設置完 Worker 之後,我們會實現向線程發送閉包的代碼:

  1. 定義 Worker 結構存放 idJoinHandle<()>
  2. 修改 ThreadPool 存放一個 Worker 實例的 vector
  3. 定義 Worker::new 函數,它獲取一個 id 數字並返回一個帶有 id 和用空閉包分配的線程的 Worker 實例
  4. ThreadPool::new 中,使用 for 循環計數生成 id,使用這個 id 新建 Worker,並儲存進 vector 中

20-15 就是一個做出了這些修改的例子:

use std::thread;

pub struct ThreadPool {
    workers: Vec<Worker>,
}

impl ThreadPool {
   pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id));
        }

        ThreadPool {
            workers
        }
    }

    pub fn execute<F>(&self, f: F)
        where
            F: FnOnce() + Send + 'static
    {

    }
}

struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}

impl Worker {
    fn new(id: usize) -> Worker {
        let thread = thread::spawn(|| {});

        Worker {
            id,
            thread,
        }
    }
}

ThreadPool 中從 threads 改為 workers,因為它現在儲存 Worker 而不是 JoinHandle<()>。使用 for 循環中的計數作為 Worker::new 的參數,並將每一個新建的 Worker 儲存在叫做 workers 的 vector 中。

Worker 結構和其 new 函數是私有的,因為外部代碼(比如 src/bin/main.rs 中的 server)並不需要知道關於 ThreadPool 中使用 Worker 結構的實現細節。Worker::new 函數使用 id 參數並儲存了使用一個 closure 產生的 JoinHandle<()>

這段code 能夠編譯並用指定給 ThreadPool::new 的參數產生儲存了一系列的 Worker 實例,不過 仍然 沒有處理 execute 中得到的閉包。

透過 Channels 發送 request 給 threads

下一個需要解決的問題是傳給 thread::spawn 的閉包完全沒有做任何工作。目前,我們在 execute 方法中獲得期望執行的閉包,不過在創建 ThreadPool 的過程中產生每一個 Worker 時需要向 thread::spawn 傳遞一個閉包。

我們希望剛創建的 Worker 結構能夠從 ThreadPool 的隊列中獲取需要執行的代碼,並發送到線程中執行他們。

在 chap 16,我們學習了 通道 —— 一個溝通兩個線程的簡單手段 —— 對於這個例子來說則是絕佳的。這裡通道將充當任務隊列的作用,execute 將通過 ThreadPool 向其中線程正在尋找工作的 Worker 實例發送任務。如下是計畫:

  1. ThreadPool 會產生一個通道並當作發送端。
  2. 每個 Worker 將會充當通道的接收端。
  3. 一個 Job 結構來存放用於向通道中發送的閉包。
  4. execute 方法會在通道發送端發出期望執行的任務。
  5. 在線程中,Worker 會歷遍通道的接收端並執行任何接收到的任務。

從在 ThreadPool::new 中產生通道並讓 ThreadPool 實例充當發送端開始,如 20-16 所示。Job 是將在通道中發出的類別,目前它是一個沒有任何內容的結構體

// --snip--
use std::sync::mpsc;

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Job>,
}

struct Job;

impl ThreadPool {
    // --snip--
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id));
        }

        ThreadPool {
            workers,
            sender,
        }
    }
    // --snip--
}

ThreadPool::new 中,產生了一個通道,並接著讓線程池在接收端等待。這段 code 能夠編譯,不過仍有警告。


嘗試在線程池產生每個 worker 時,將通道的接收端傳遞給他們。我們希望在 worker 所分配的線程中使用通道的接收端,所以將在閉包中引用 receiver 參數。20-17 中展示的 code 還不能編譯:

impl ThreadPool {
    // --snip--
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, receiver));
        }

        ThreadPool {
            workers,
            sender,
        }
    }
    // --snip--
}

// --snip--

impl Worker {
    fn new(id: usize, receiver: mpsc::Receiver<Job>) -> Worker {
        let thread = thread::spawn(|| {
            receiver;
        });

        Worker {
            id,
            thread,
        }
    }
}

這是一些小而直觀的修改:將通道的接收端傳遞進了 Worker::new,並接著在閉包中使用它。

Rust 所提供的通道實現是多 生產者,單 消費者 的。這意味著不能簡單的 clone 通道的消費端來解決問題。從通道隊列中取出任務涉及到修改 receiver,所以這些線程需要一個能安全的共享和修改 receiver 的方式,否則可能導致 race condition

chap 16 討論的線程安全智能指針,為了在多個線程間共享所有權並允許線程修改其值,需要使用 Arc<Mutex<T>>Arc 使得多個 worker 擁有接收端,而 Mutex 則確保一次只有一個 worker 能從接收端得到任務。

use std::sync::Arc;
use std::sync::Mutex;
// --snip--

impl ThreadPool {
    // --snip--
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool {
            workers,
            sender,
        }
    }

    // --snip--
}

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        // --snip--
    }
}

ThreadPool::new 中,將通道的接收端放入一個 Arc 和一個 Mutex 中。對於每一個新 worker,clone Arc 來增加引用計數,如此這些 worker 就可以共享接收端的所有權了。

通過這些修改,code可以編譯了

實作 execute

實現 ThreadPool 上的 execute 方法。同時也要修改 Job 結構:它將不再是結構,Job 將是一個有著 execute 接收到的閉包類型的 trait 對象的類別別名。 chap 19 “類別別名用來創建類別同義詞” 部分提到過,類別別名允許將長的類別變短。

// --snip--

type Job = Box<FnOnce() + Send + 'static>;

impl ThreadPool {
    // --snip--

    pub fn execute<F>(&self, f: F)
        where
            F: FnOnce() + Send + 'static
    {
        let job = Box::new(f);

        self.sender.send(job).unwrap();
    }
}

// --snip--

為存放每一個閉包的 Box 產生一個 Job 類型別名,接著在通道中發出任務

在使用 execute 得到的閉包產生 Job instance 之後,將這些任務從通道的發送端發出。這裡呼叫 send 上的 unwrap,因為發送可能會失敗,例如這可能發生於停止了所有線程執行的情況,這意味著接收端停止接收新消息了。不過目前我們無法停止線程執行;只要線程池存在他們就會一直執行。使用 unwrap 是因為我們知道失敗不可能發生,即便編譯器不這麼認為。

在 worker 中,傳給 thread::spawn 的閉包仍然還只是 引用 了通道的接收端。相反我們需要閉包一直循環,向通道的接收端請求任務,並在得到任務時執行他們。如 20-20 對 Worker::new 做出修改:

// --snip--

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(move || {
            loop {
                let job = receiver.lock().unwrap().recv().unwrap();

                println!("Worker {} got a job; executing.", id);

                (*job)();
            }
        });

        Worker {
            id,
            thread,
        }
    }
}

首先在 receiver 上呼叫了 lock 來獲取互斥器,接著 unwrap 在出現任何錯誤時 panic。如果互斥器處於一種叫做 被污染poisoned)的狀態時獲取鎖可能會失敗,這可能發生於其他線程在持有鎖時 panic 了且沒有釋放鎖。在這種情況下,呼叫 unwrap 使其 panic 是正確的行為。可將 unwrap 改為包含有意義錯誤信息的 expect

如果鎖定了互斥器,接著調用 recv 從通道中接收 Job。最後的 unwrap 也繞過了一些錯誤,這可能發生於持有通道發送端的線程停止的情況,類似於如果接收端關閉時 send 方法如何回傳 Err 一樣。

呼叫 recv 會阻塞當前線程,所以如果還沒有任務,其會等待直到有可用的任務。Mutex<T> 確保一次只有一個 Worker 線程嘗試請求任務。

但目前還是無法編譯


為了呼叫儲存在 Box<T> (這正是 Job 別名的類型)中的 FnOnce 閉包,該閉包需要能將自己移動 Box<T>,因為當呼叫這個閉包時,它獲取 self 的所有權。通常來說,將值移動出 Box<T> 是不被允許的,因為 Rust 不知道 Box<T> 中的值將會有多大;chap 15 能夠正常使用 Box<T> 是因為我們將未知大小的值儲存進 Box<T> 從而得到已知大小的值。

chap17 曾見過,17-15 中有使用了 self: Box<Self> 語法的方法,它允許方法獲取儲存在 Box<T> 中的 Self 值的所有權。這正是我們希望做的,然而不幸的是 Rust 不允許我們這麼做:Rust 當閉包被調用時行為的那部分並沒有使用 self: Box<Self> 實現。所以這裡 Rust 也不知道它可以使用 self: Box<Self> 來獲取閉包的所有權並將閉包移動出 Box<T>

Rust 仍在努力改進提升編譯器的過程中,未來新版的 rust,應該可讓 20-20 中的 code 正常工作。


不過目前讓我們通過一個小技巧來繞過這個問題。可以明確告訴 Rust 在這裡我們可以使用 self: Box<Self> 來獲取 Box<T> 中值的所有權,而一旦獲取了閉包的所有權就可以呼叫它了。這涉及到定義一個新 trait,它帶有一個在簽名中使用 self: Box<Self> 的方法 call_box,為任何實現了 FnOnce() 的類型定義這個 trait,修改類別別名來使用這個新 trait,並修改 Worker 使用 call_box 方法。這些修改如 20-21 所示:

trait FnBox {
    fn call_box(self: Box<Self>);
}

impl<F: FnOnce()> FnBox for F {
    fn call_box(self: Box<F>) {
        (*self)()
    }
}

type Job = Box<FnBox + Send + 'static>;

// --snip--

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(move || {
            loop {
                let job = receiver.lock().unwrap().recv().unwrap();

                println!("Worker {} got a job; executing.", id);

                job.call_box();
            }
        });

        Worker {
            id,
            thread,
        }
    }
}

新增一個 trait FnBox 來繞過當前 Box<FnOnce()> 的限制

新增了一個叫做 FnBox 的 trait。這個 trait 有一個方法 call_box,它類似於其他 Fn* trait 中的 call 方法,除了它獲取 self: Box<Self> 以便獲取 self 的所有權並將值從 Box<T> 中移動出來。

接下來,為任何實現了 FnOnce() trait 的類型 F 實現 FnBox trait。這實際上就等於任何 FnOnce() closure 都可以使用 call_box 方法。call_box 的實現使用 (*self)() 將閉包移動出 Box<T> 並呼叫此閉包。

現在我們需要 Job 類別別名是任何實現了新 trait FnBoxBox。這允許我們在得到 Job 值時使用 Worker 中的 call_box。為任何 FnOnce() 閉包都實現了 FnBox trait 意味著無需對實際在通道中發出的值做任何修改。現在 Rust 就能夠理解我們的行為是正確的了。

Graceful shutdown and cleanup

20-21 中的代碼如期通過使用線程池異步的響應請求。這裡有一些警告說 workersidthread 字段沒有直接被使用,這提醒了我們並沒有清理所有的內容。當使用不那麼優雅的 ctrl-C 終止主線程時,所有其他線程也會立刻停止,即便它們正處於處理請求的過程中。

現在我們要為 ThreadPool 實現 Drop trait 對線程池中的每一個線程調用 join,這樣這些線程將會執行完他們的請求。接著會為 ThreadPool 實現一個告訴線程他們應該停止接收新請求並結束的方式。為了實踐這些代碼,修改 server 在優雅停機(graceful shutdown)之前只接受兩個請求。

ThreadPool 實現 Drop Trait

當線程池被丟棄時,應該 join 所有線程以確保他們完成其操作。20-23 展示了 Drop 實現的第一次嘗試;這些代碼還不能夠編譯:

impl Drop for ThreadPool {
    fn drop(&mut self) {
        for worker in &mut self.workers {
            println!("Shutting down worker {}", worker.id);

            worker.thread.join().unwrap();
        }
    }
}

編譯錯誤

error[E0507]: cannot move out of borrowed content
  --> src/lib.rs:61:13
   |
61 |             worker.thread.join().unwrap();
   |             ^^^^^^^^^^^^^ cannot move out of borrowed content

error: aborting due to previous error

這告訴我們並不能調用 join,因為只有每一個 worker 的可變借用,而 join 獲取其參數的所有權。為瞭解決這個問題,需要一個方法將 thread 移動出擁有其所有權的 Worker 實例以便 join可以消費這個線程。17-15 中我們曾見過這麼做的方法:如果 Worker 存放的是 Option<thread::JoinHandle<()>,就可以在 Option 上調用 take 方法將值從 Some 成員中移動出來而對 None 成員不做處理。換句話說,正在運行的 Workerthread 將是 Some 成員值,而當需要清理 worker 時,將 Some 替換為 None,這樣 worker 就沒有可以運行的線程了。

struct Worker {
    id: usize,
    thread: Option<thread::JoinHandle<()>>,
}

當新建 Worker 時需要將 thread 值封裝進 Some

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        // --snip--

        Worker {
            id,
            thread: Some(thread),
        }
    }
}

調用 Option 上的 takethread 移動出 worker

impl Drop for ThreadPool {
    fn drop(&mut self) {
        for worker in &mut self.workers {
            println!("Shutting down worker {}", worker.id);

            if let Some(thread) = worker.thread.take() {
                thread.join().unwrap();
            }
        }
    }
}

向 thread 發送訊息,使其停止接收任務

有了這些修改,code 就能編譯且沒有任何警告。不過也有壞消息,這些代碼還不能以我們期望的方式運行。問題的關鍵在於 Worker 中分配的線程所運行的閉包中的邏輯:呼叫 join 並不會關閉線程,因為他們一直 loop 來尋找任務。如果採用這個實現來嘗試丟棄 ThreadPool ,則主線程會永遠阻塞在等待第一個線程結束上。

為了修復這個問題,修改線程既監聽是否有 Job 運行也要監聽一個應該停止監聽並退出無限循環的信號。所以通道將發送這個枚舉的兩個成員之一而不是 Job 實例

enum Message {
    NewJob(Job),
    Terminate,
}

Message 要麼是存放了線程需要運行的 JobNewJob 成員,要麼是會導致線程退出循環並終止的 Terminate 成員。

同時需要修改通道來使用 Message 而不是 Job,收發 Message 值並在 Worker 收到 Message::Terminate 時退出循環

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Message>,
}

// --snip--

impl ThreadPool {
    // --snip--

    pub fn execute<F>(&self, f: F)
        where
            F: FnOnce() + Send + 'static
    {
        let job = Box::new(f);

        self.sender.send(Message::NewJob(job)).unwrap();
    }
}

// --snip--

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Message>>>) ->
        Worker {

        let thread = thread::spawn(move ||{
            loop {
                let message = receiver.lock().unwrap().recv().unwrap();

                match message {
                    Message::NewJob(job) => {
                        println!("Worker {} got a job; executing.", id);

                        job.call_box();
                    },
                    Message::Terminate => {
                        println!("Worker {} was told to terminate.", id);

                        break;
                    },
                }
            }
        });

        Worker {
            id,
            thread: Some(thread),
        }
    }
}

為了使用 Message enum 需要將兩個地方的 Job 修改為 MessageThreadPool 的定義和 Worker::new 的簽名。ThreadPoolexecute 方法需要發送封裝進 Message::NewJob 成員的任務。然後,在 Worker::new 中當從通道接收 Message 時,當獲取到 NewJob成員會處理任務而收到 Terminate 成員則會退出循環。

修改後再次能夠編譯並繼續按照期望的行為運行。不過還是會得到一個警告,因為並沒有產生任何 Terminate 成員的消息。如 20-25 所示修改 Drop 實現來修復此問題:

impl Drop for ThreadPool {
    fn drop(&mut self) {
        println!("Sending terminate message to all workers.");

        for _ in &mut self.workers {
            self.sender.send(Message::Terminate).unwrap();
        }

        println!("Shutting down all workers.");

        for worker in &mut self.workers {
            println!("Shutting down worker {}", worker.id);

            if let Some(thread) = worker.thread.take() {
                thread.join().unwrap();
            }
        }
    }
}

在對每個 worker 線程調用 join 之前向 worker 發送 Message::Terminate

現在遍歷了 worker 兩次,一次向每個 worker 發送一個 Terminate 消息,一個呼叫每個 worker 線程上的 join。如果嘗試在同一循環中發送消息並立即 join 線程,則無法保證當前迭代的 worker 是從通道收到終止消息的 worker。

為了更好的理解為什麼需要兩個分開的循環,想像一下只有兩個 worker 的場景。如果在一個單獨的循環中遍歷每個 worker,在第一次迭代中向通道發出終止消息並對第一個 worker 線程調用 join。我們會一直等待第一個 worker 結束,不過它永遠也不會結束因為第二個線程接收了終止消息。deadlock!

為了避免此情況,首先在一個循環中向通道發出所有的 Terminate 消息,接著在另一個循環中 join 所有的線程。每個 worker 一旦收到終止消息即會停止從通道接收消息,意味著可以確保如果發送同 worker 數相同的終止消息,在 join 之前每個線程都會收到一個終止消息。

為了實踐這些代碼,如 20-26 所示修改 main 在優雅停止 server 之前只接受兩個請求:

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    let pool = ThreadPool::new(4);

    for stream in listener.incoming().take(2) {
        let stream = stream.unwrap();

        pool.execute(|| {
            handle_connection(stream);
        });
    }

    println!("Shutting down.");
}

你不會希望真實世界的 web server 只處理兩次請求就停機了,這只是為了展示優雅停機和清理處於正常工作狀態。

take 方法定義於 Iterator trait,這裡限制循環最多頭 2 次。ThreadPool 會在 main 的結尾離開作用域,而且還會看到 drop 實現的運行。

使用 cargo run 啟動 server,並發起三個 request。第三個request 應該會失敗

這個特定的運行過程中一個有趣的地方在於:注意我們向通道中發出終止消息,而在任何線程收到消息之前,就嘗試 join worker 0 了。worker 0 還沒有收到終止消息,所以主線程阻塞直到 worker 0 結束。與此同時,每一個線程都收到了終止消息。一旦 worker 0 結束,主線程就等待其他 worker 結束,此時他們都已經收到終止消息並能夠停止了。

以下是完整的程式

main.rs

extern crate hello;
use hello::ThreadPool;

use std::io::prelude::*;
use std::net::TcpListener;
use std::net::TcpStream;
use std::fs::File;
use std::thread;
use std::time::Duration;

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    let pool = ThreadPool::new(4);

    for stream in listener.incoming().take(2) {
        let stream = stream.unwrap();

        pool.execute(|| {
            handle_connection(stream);
        });
    }

    println!("Shutting down.");
}

fn handle_connection(mut stream: TcpStream) {
    let mut buffer = [0; 512];
    stream.read(&mut buffer).unwrap();

    let get = b"GET / HTTP/1.1\r\n";
    let sleep = b"GET /sleep HTTP/1.1\r\n";

    let (status_line, filename) = if buffer.starts_with(get) {
        ("HTTP/1.1 200 OK\r\n\r\n", "hello.html")
    } else if buffer.starts_with(sleep) {
        thread::sleep(Duration::from_secs(5));
        ("HTTP/1.1 200 OK\r\n\r\n", "hello.html")
    } else {
        ("HTTP/1.1 404 NOT FOUND\r\n\r\n", "404.html")
    };

     let mut file = File::open(filename).unwrap();
     let mut contents = String::new();

     file.read_to_string(&mut contents).unwrap();

     let response = format!("{}{}", status_line, contents);

     stream.write(response.as_bytes()).unwrap();
     stream.flush().unwrap();
}

lib.rs

use std::thread;
use std::sync::mpsc;
use std::sync::Arc;
use std::sync::Mutex;

enum Message {
    NewJob(Job),
    Terminate,
}

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Message>,
}

trait FnBox {
    fn call_box(self: Box<Self>);
}

impl<F: FnOnce()> FnBox for F {
    fn call_box(self: Box<F>) {
        (*self)()
    }
}

type Job = Box<dyn FnBox + Send + 'static>;

impl ThreadPool {
    /// 創建線程池。
    ///
    /// 線程池中線程的數量。
    ///
    /// # Panics
    ///
    /// `new` 函數在 size 為 0 時會 panic。
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool {
            workers,
            sender,
        }
    }

    pub fn execute<F>(&self, f: F)
        where
            F: FnOnce() + Send + 'static
    {
        let job = Box::new(f);

        self.sender.send(Message::NewJob(job)).unwrap();
    }
}

impl Drop for ThreadPool {
    fn drop(&mut self) {
        println!("Sending terminate message to all workers.");

        for _ in &mut self.workers {
            self.sender.send(Message::Terminate).unwrap();
        }

        println!("Shutting down all workers.");

        for worker in &mut self.workers {
            println!("Shutting down worker {}", worker.id);

            if let Some(thread) = worker.thread.take() {
                thread.join().unwrap();
            }
        }
    }
}

struct Worker {
    id: usize,
    thread: Option<thread::JoinHandle<()>>,
}

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Message>>>) ->
        Worker {

        let thread = thread::spawn(move ||{
            loop {
                let message = receiver.lock().unwrap().recv().unwrap();

                match message {
                    Message::NewJob(job) => {
                        println!("Worker {} got a job; executing.", id);

                        job.call_box();
                    },
                    Message::Terminate => {
                        println!("Worker {} was told to terminate.", id);

                        break;
                    },
                }
            }
        });

        Worker {
            id,
            thread: Some(thread),
        }
    }
}

References

The Rust Programming Language

中文版

中文版 2

2020年6月29日

rust19_Advanced Features

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

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

Unsafe Rust

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

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

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

unsafe superpowers

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

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

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

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

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

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

Dereference a raw pointer

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

raw pointer 跟 reference & smart pointer 的差別:

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

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

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

let mut num = 5;

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

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

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

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

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

以下是在 unsafe code 中,dereference raw pointer

let mut num = 5;

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

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

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

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

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

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

呼叫 unsafe function or method

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

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

unsafe fn dangerous() {}

unsafe {
    dangerous();
}

產生 unsafe code 的安全抽象

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

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

let r = &mut v[..];

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

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

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

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

    assert!(mid <= len);

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

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

編譯錯誤

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

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

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

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

use std::slice;

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

    assert!(mid <= len);

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

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

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

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

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

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


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

use std::slice;

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

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

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


使用 extern 呼叫外部 API

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

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

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

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

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

從其他程式語言呼叫 rust

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

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

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

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

存取或修改 a mutable static variable

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

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

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

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

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

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

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

static mut COUNTER: u32 = 0;

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

fn main() {
    add_to_count(3);

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

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

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

實作 an unsafe trait

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

unsafe trait Foo {
    // methods go here
}

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

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

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

Advanced Traits

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

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

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

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

pub trait Iterator {
    type Item;

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

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

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

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

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

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

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

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

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

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

Default Generic Type Parameters and Operator Overloading

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

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

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

use std::ops::Add;

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

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

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

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

這是 Add trait 的定義

trait Add<RHS=Self> {
    type Output;

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

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

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

use std::ops::Add;

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

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

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

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

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

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

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

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

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

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

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

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

trait Wizard {
    fn fly(&self);
}

struct Human;

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

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

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


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

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

執行結果

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

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

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

struct Dog;

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

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

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

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

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

完全限定語法的定義:

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

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

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

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

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

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

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

use std::fmt;

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

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

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

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

impl OutlinePrint for Point {}

編譯錯誤

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

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

use std::fmt;

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

使用 Newtype Pattern 在 external type 實作 external traits

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

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

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

use std::fmt;

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

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

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

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

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

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

Advanced Types

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

不會 return 的 never type

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

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

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

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

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

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

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

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

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

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


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

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

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


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

print!("forever ");

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

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

動態大小的類別 與 Sized Trait

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

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

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

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

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

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

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

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

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

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

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

實際上被當作如下處理:

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

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

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

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

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

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

Advanced Functions and Closures

function pointers and returning closures

function pointers

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

src/main.rs

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

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

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

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

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

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

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

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

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

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

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

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

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


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

enum Status {
    Value(u32),
    Stop,
}

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

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

returning closures

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

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

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

編譯器給出的錯誤是:

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

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

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

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

Macros

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

  • Declarative macro

    使用 macro_rules!

  • 三種 procedural macros

    • 自訂 #[derive] macros

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

    • Attribute-like macros

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

    • Function-like macros

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

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

巨集和函數的差異

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

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

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

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

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

使用 macro_rules! 的 declarative macros 用於 general mataprogramming

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

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

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

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

src/lib.rs

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

從屬性產生 code 的 procedural macros

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

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

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

src/lib.rs

use proc_macro;

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

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

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

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

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

如何撰寫自訂 derive 巨集

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

src/main.rs

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

#[derive(HelloMacro)]
struct Pancakes;

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

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

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

第一步是產生一個 lib crate:

$ cargo new hello_macro --lib

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

src/lib.rs

pub trait HelloMacro {
    fn hello_macro();
}

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

use hello_macro::HelloMacro;

struct Pancakes;

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

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

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

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

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

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

$ cargo new hello_macro_derive --lib

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

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

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

hellomacroderive/Cargo.toml

[lib]
proc-macro = true

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

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

hellomacroderive/src/lib.rs

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

extern crate proc_macro;

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

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

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

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

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

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

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

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

DeriveInput {
    // --snip--

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

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

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

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

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

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

hellomacroderive/src/lib.rs

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

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

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

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

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

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

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

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

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

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

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

Attribute-like macros

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

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

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

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

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

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

Function-like macros

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

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

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

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

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

References

The Rust Programming Language

中文版

中文版 2

2020年6月22日

rust18_Pattern Matching

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

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

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

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

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

所有可能用到 Pattern 的地方

match Arms

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

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

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

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

Conditional if let Expression

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

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

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

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

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

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

while let Conditional Loop

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

let mut stack = Vec::new();

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

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

for Loop

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

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

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

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

let statement

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

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

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

function parameters

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

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

ex:

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

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

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

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

Refutability: 是否 pattern 會匹配失敗

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

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

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

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

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

let Some(x) = some_option_value;

會發生編譯錯誤

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

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

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

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

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

會發生編譯錯誤

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

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

Pattern Syntax

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

matching literals

let x = 1;

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

matching named variables

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

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

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

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

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

multiple patterns

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

let x = 1;

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

matching rangers of values with

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

let x = 5;

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

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

let x = 'c';

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

destructing to break apart values

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

  • destructing structs

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

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

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

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

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

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

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

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

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

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

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

    以匹配嵌套的 enum

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

    struct 和 tuple 嵌套在 tuple 中

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

Ignoring values in a pattern

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

  • ignoring an entire value with _

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

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

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

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

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

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

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

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

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

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

    要改寫為

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

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

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

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

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

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

Extra Conditionals with Match Guards

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

let num = Some(4);

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

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

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

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

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

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

@ Bindings

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

enum Message {
    Hello { id: i32 },
}

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

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

References

The Rust Programming Language

中文版

中文版 2