這邊是特定狀況下會遇到的 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_mut
和 offset
放入 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 “使用 Sync
和 Send
trait 的可擴展並發” 部分中的 Sync
和 Send
標記 trait,編譯器會自動為完全由 Send
和 Sync
類別組成的類別自動實現他們。如果實作了一個包含一些不是 Send
或 Sync
的類別,比如 raw pointer,並希望將此類別標記為 Send
或 Sync
,則必須使用 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
,或任何其他類別,這樣就可以有多個 Counter
的 Iterator
的實作。
換句話說,當 trait 有泛型參數時,可以多次實現這個 trait,每次需改變泛型參數的具體類別。接著當使用 Counter
的 next
方法時,必須提供類別註解來表明希望使用 Iterator
的哪一個實現。
通過關聯類別,則無需標註類別因為不能多次實現這個 trait。如果是使用關聯類別的定義,我們只能選擇一次 Item
會是什麼類別,因為只能有一個 impl Iterator for Counter
。當呼叫 Counter
的 next
時不必每次指定我們需要 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 Pilot
和 Wizard
都擁有方法 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_name
, Dog
實作了 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 的 父 trait(supertrait)。
例如我們希望產生一個帶有 outline_print
方法的 trait OutlinePrint
,它會列印出帶有星號框的值。也就是說,如果 Point
實現了 Display
並返回 (x, y)
,呼叫以 1
作為 x
和 3
作為 y
的 Point
實例的 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 阻止我們直接這麼做,因為 Display
trait 和 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 的項。接著就可以使用 Wrapper
中 Display
的功能了。
此方法的缺點是,因為 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 還可以用於一些其他我們還未討論的功能,包括靜態的確保某值不被混淆,和用來表示一個值的單元。實際上剛剛已經有一個這樣的例子:Millimeters
和 Meters
structs 都在 newtype 中封裝了 u32
值。如果撰寫了一個有 Millimeters
類別參數的函數,不小心使用 Meters
或普通的 u32
值來呼叫該函數的程序是不能編譯的。
另一個 newtype pattern 的應用在於抽象掉一些類別的實現細節:例如,封裝類別可以暴露出與直接使用其內部私有類別時所不同的公有 API,以便限制其功能。
newtype 也可以隱藏其內部的泛型類別。例如,可以提供一個封裝了 HashMap<i32, String>
的 People
類別,用來儲存人名以及相應的 ID。使用 People
的 code 只需呼叫提供的公有 API 即可,比如向 People
集合增加名字的方法,這樣這些 code 就無需知道在內部我們將一個 i32
ID 賦予了這個名字了。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>
,其中 E
是 std::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::Error
。Write
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 知道 val
是 T
類別,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
需要佔用完全相同大小的空間,不過它們有著不同的長度。這也就是為什麼不可能創建一個存放動態大小類別的變數的原因。
那麼該怎麼辦呢?你已經知道了這種問題的答案:s1
和 s2
的類別是 &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,必須將他們放在指針之後,比如 &Trait
或 Box<Trait>
(Rc<Trait>
也可以)。trait 之所以是動態大小類別的是因為只有這樣才能使用它。
為了處理 dynamically sized types DST,Rust 有一個特定的 trait 用來決定一個類別的大小是否在編譯時可知:這就是 Sized
trait。這個 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: 12
。do_twice
中的 f
被指定為一個接受一個 i32
參數並返回 i32
的 fn
。接著就可以在 do_twice
函數體中呼叫 f
。在 main
中,可以將函數名 add_one
作為第一個參數傳遞給 do_twice
。
不同於閉包,fn
是一個類別而不是一個 trait,所以直接指定 fn
作為參數而不是宣告一個帶有 Fn
作為 trait bound 的泛型參數。
函數指針實現了三種 closure trait(Fn
、FnMut
和 FnOnce
),所以總是可以在呼叫期望閉包的函數時傳遞函數指針作為參數。傾向於編寫使用泛型和閉包 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 與 enumsAttribute-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
模式與三個表達式 1
、2
和 3
進行了三次匹配。
現在讓我們來看看這個出現在與此單邊模式相關的代碼塊中的模式:在 $()*
部分中所生成的 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。同時也需要 syn
和 quote
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_macro
、 syn
和 quote
。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 名,這是大多數過程巨集遵循的習慣。
該函數首先將來自 TokenStream
的 input
轉換為一個我們可以解釋和操作的資料結構。這正是 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 代碼,查閱 syn
中 DeriveInput
的文檔 以獲取更多信息。
此時,尚未定義 impl_hello_macro
函數,其用於構建所要包含在內的 Rust 新代碼。但在此之前,注意其輸出也是 TokenStream
。所返回的 TokenStream
會被加到我們的 crate 用戶所寫的代碼中,因此,當用戶編譯他們的 crate 時,他們會獲取到我們所提供的額外功能。
你可能也注意到了,當呼叫 parse_derive_input
或 parse
失敗時。在錯誤時 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_macro
和 hello_macro_derive
。我們將這些 crate 連接到 19-38 的代碼中來看看過程巨集的行為!在 projects 目錄下用 cargo new pancakes
命令產生一個二進制項目。需要將 hello_macro
和 hello_macro_derive
作為依賴加到 pancakes
包的 Cargo.toml 文件中去。如果你正將 hello_macro
和 hello_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