Rust 高階特徵

二次元攻城狮發表於2024-03-07

本文有刪減,原文連結高階特徵

目錄
  • 不安全 Rust
    • 不安全的超能力
    • 解引用裸指標
    • 呼叫不安全函式或方法
      • 建立不安全程式碼的安全抽象
      • 使用 extern 函式呼叫外部程式碼
    • 訪問或修改可變靜態變數
    • 實現不安全 trait
    • 訪問聯合體中的欄位
    • 何時使用不安全程式碼
  • 高階 trait
    • 關聯型別在 trait 定義中指定佔位符型別
    • 預設泛型型別引數和運算子過載
    • 完全限定語法與消歧義:呼叫相同名稱的方法
    • 父 trait 用於在另一個 trait 中使用某 trait 的功能
    • newtype 模式用以在外部型別上實現外部 trait
  • 高階型別
    • 為了型別安全和抽象而使用 newtype 模式
    • 型別別名用來建立型別同義詞
    • 從不返回的 never type
    • 動態大小型別和 Sized trait
  • 高階函式與閉包
    • 函式指標
    • 返回閉包
    • 宏和函式的區別
    • 使用 macro_rules! 的宣告宏用於通用超程式設計
    • 用於從屬性生成程式碼的過程宏
    • 如何編寫自定義 derive 宏
    • 類屬性宏
    • 類函式宏

不安全 Rust

Rust 還隱藏有第二種語言,它不會強制執行這類記憶體安全保證:這被稱為 不安全 Rust(unsafe Rust)。

不安全 Rust 之所以存在,是因為靜態分析本質上是保守的。可以使用不安全程式碼告訴編譯器,“相信我,我知道我在幹什麼。”

另一個 Rust 存在不安全一面的原因是:底層計算機硬體固有的不安全性。如果 Rust 不允許進行不安全操作,那麼有些任務則根本完成不了。

不安全的超能力

可以透過 unsafe 關鍵字來切換到不安全 Rust,接著可以開啟一個新的存放不安全程式碼的塊。這裡有五類可以在不安全 Rust 中進行而不能用於安全 Rust 的操作,它們稱之為 “不安全的超能力(unsafe superpowers)” :

  • 解引用裸指標
  • 呼叫不安全的函式或方法
  • 訪問或修改可變靜態變數
  • 實現不安全 trait
  • 訪問 union 的欄位

unsafe 並不會關閉借用檢查器或禁用任何其他 Rust 安全檢查:如果在不安全程式碼中使用引用,它仍會被檢查。unsafe 關鍵字只是提供了那五個不會被編譯器檢查記憶體安全的功能。

unsafe 不意味著塊中的程式碼就一定是危險的或者必然導致記憶體安全問題:其意圖在於作為程式設計師會確保 unsafe 塊中的程式碼以有效的方式訪問記憶體。

解引用裸指標

不安全 Rust 有兩個被稱為 裸指標(raw pointers)的類似於引用的新型別。和引用一樣,裸指標是不可變或可變的,分別寫作 *const T 和 *mut T。這裡的星號不是解引用運算子;它是型別名稱的一部分。

裸指標與引用和智慧指標的區別在於:

  • 允許忽略借用規則,可以同時擁有不可變和可變的指標,或多個指向相同位置的可變指標
  • 不保證指向有效的記憶體
  • 允許為空
  • 不能實現任何自動清理功能

透過去掉 Rust 強加的保證,可以放棄安全保證以換取效能或使用另一個語言或硬體介面的能力,此時 Rust 的保證並不適用。

透過引用建立裸指標:

let mut num = 5;

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

注:可以在安全程式碼中 建立 裸指標,只是不能在不安全塊之外 解引用 裸指標。

建立指向任意記憶體地址的裸指標:

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

在 unsafe 塊中解引用裸指標:

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);
}

為何還要使用裸指標呢?一個主要的應用場景便是呼叫 C 程式碼介面,另一個場景是構建借用檢查器無法理解的安全抽象

呼叫不安全函式或方法

不安全函式和方法與常規函式方法十分類似,除了其開頭有一個額外的 unsafe。

一個沒有做任何操作的不安全函式 dangerous 的例子:

unsafe fn dangerous() {}

unsafe {
    dangerous();
}

必須在一個單獨的 unsafe 塊中呼叫 dangerous 函式,否則會得到一個錯誤。不安全函式體也是有效的 unsafe 塊,所以在不安全函式中進行另一個不安全操作時無需新增額外的 unsafe 塊。

建立不安全程式碼的安全抽象

函式包含不安全程式碼並不意味著整個函式都需要標記為不安全的,如標準庫中的函式 split_at_mut:它獲取一個 slice 並從給定的索引引數開始將其分為兩個 slice,用法如下:

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 來實現 split_at_mut:

//這段程式碼無法透過編譯!
fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = values.len();

    assert!(mid <= len);

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

Rust 的借用檢查器不能理解我們要借用這個 slice 的兩個不同部分:它只知道我們借用了同一個 slice 兩次。

在 split_at_mut 函式的實現中使用不安全程式碼:

use std::slice;

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

    assert!(mid <= len);

    unsafe {
        (
            slice::from_raw_parts_mut(ptr, mid),
            slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}

注意:無需將 split_at_mut 函式的結果標記為 unsafe,並可以在安全 Rust 中呼叫此函式。

透過任意記憶體地址建立 slice,slice::from_raw_parts_mut 在使用 slice 時很有可能會崩潰:

use std::slice;

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

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

使用 extern 函式呼叫外部程式碼

Rust 程式碼可能需要與其他語言編寫的程式碼互動。為此 Rust 有一個關鍵字,extern,有助於建立和使用 外部函式介面(Foreign Function Interface,FFI)。外部函式介面是一個程式語言用以定義函式的方式,其允許不同(外部)程式語言呼叫這些函式

宣告並呼叫另一個語言中定義的 extern 函式:

extern "C" {
    //希望能夠呼叫的另一個語言中的外部函式的簽名和名稱
    fn abs(input: i32) -> i32;
}

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

"C" 部分定義了外部函式所使用的 應用二進位制介面(application binary interface,ABI) —— ABI 定義瞭如何在組合語言層面呼叫此函式

也可以使用 extern 來建立一個允許其他語言呼叫 Rust 函式的介面,在 fn 關鍵字之前增加 extern 關鍵字併為相關函式指定所用到的 ABI,還需增加 #[no_mangle] 註解來告訴 Rust 編譯器不要 mangle 此函式的名稱。

一旦其編譯為動態庫並從 C 語言中連結,call_from_c 函式就能夠在 C 程式碼中訪問:

//extern 的使用無需 unsafe
#[no_mangle]
pub extern "C" fn call_from_c() {
    println!("Just called a Rust function from C!");
}

訪問或修改可變靜態變數

全域性變數在 Rust 中被稱為 靜態(static)變數,一個擁有字串 slice 值的靜態變數的宣告和應用:

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

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

通常靜態變數的名稱採用 SCREAMING_SNAKE_CASE 寫法,靜態變數只能儲存擁有 'static 生命週期的引用,這意味著 Rust 編譯器可以自己計算出其生命週期而無需顯式標註,訪問不可變靜態變數是安全的

常量與不可變靜態變數的區別:

  • 靜態變數中的值有一個固定的記憶體地址,使用這個值總是會訪問相同的地址,常量則允許在任何被用到的時候複製其資料。
  • 靜態變數可以是可變的,訪問和修改可變靜態變數都是 不安全 的。

讀取或修改一個可變靜態變數是不安全的:

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 的程式碼都必須位於 unsafe 塊中。

實現不安全 trait

當 trait 中至少有一個方法中包含編譯器無法驗證的不變式(invariant)時 trait 是不安全的:

//在 trait 之前增加 unsafe 關鍵字將 trait 宣告為 unsafe
unsafe trait Foo {
    // methods go here
}

// trait 的實現也必須標記為 unsafe 
unsafe impl Foo for i32 {
    // method implementations go here
}

fn main() {}

訪問聯合體中的欄位

union 和 struct 類似,但是在一個例項中同時只能使用一個宣告的欄位。聯合體主要用於和 C 程式碼中的聯合體互動。訪問聯合體的欄位是不安全的,因為 Rust 無法保證當前儲存在聯合體例項中資料的型別。

何時使用不安全程式碼

當有理由使用 unsafe 程式碼時,是可以這麼做的,透過使用顯式的 unsafe 標註可以更容易地在錯誤發生時追蹤問題的源頭。

高階 trait

關聯型別在 trait 定義中指定佔位符型別

關聯型別(associated types)是一個將型別佔位符與 trait 相關聯的方式,這樣 trait 的方法簽名中就可以使用這些佔位符型別,trait 的實現者會針對特定的實現在這個佔位符型別指定相應的具體型別。

Iterator trait 的定義中帶有關聯型別 Item,它用來替代遍歷的值的型別:

pub trait Iterator {
    //佔位符型別,trait 的實現者會指定 Item 的具體型別
    type Item;

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

關聯型別看起來像一個類似泛型的概念,因為它允許定義一個函式而不指定其可以處理的型別。

在一個 Counter 結構體上實現 Iterator trait ,指定了 Item 的型別為 u32:

impl Iterator for Counter {
    type Item = u32;

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

一個使用泛型的 Iterator trait 假想定義:

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

如果使用泛型就可以有多個 Counter 的 Iterator 的實現,當使用 Counter 的 next 方法時,必須提供型別註解來表明希望使用 Iterator 的哪一個實現。

透過關聯型別,則無需標註型別,因為不能多次實現這個 trait。當呼叫 Counter 的 next 時不必每次指定 u32 值的迭代器。

關聯型別也會成為 trait 契約的一部分:trait 的實現必須提供一個型別來替代關聯型別佔位符

預設泛型型別引數和運算子過載

當使用泛型型別引數時,可以為泛型指定一個預設的具體型別,為泛型型別指定預設型別的語法是在宣告泛型型別時使用 <PlaceholderType=ConcreteType>

Rust 並不允許建立自定義運算子或過載任意運算子,不過 std::ops 中所列出的運算子和相應的 trait 可以透過實現運算子相關 trait 來過載

實現 Add trait 過載 Point 例項的 + 運算子:

use std::ops::Add;

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

impl Add for Point {
    type Output = Point;

    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:

trait Add<Rhs=Self> {
    type Output;

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

尖括號中的 Rhs=Self 語法叫做 預設型別引數(default type parameters),Rhs 是一個泛型型別引數(“right hand side” 的縮寫),它用於定義 add 方法中的 rhs 引數。

在 Millimeters 上實現 Add,以便能夠將 Millimeters 與 Meters 相加:

use std::ops::Add;

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))
    }
}

預設引數型別主要用於如下兩個方面:

  • 擴充套件型別而不破壞現有程式碼。
  • 在大部分使用者都不需要的特定情況進行自定義。

第一個目的是相似的,但過程是反過來的:如果需要為現有 trait 增加型別引數,為其提供一個預設型別將允許在不破壞現有實現程式碼的基礎上擴充套件 trait 的功能

完全限定語法與消歧義:呼叫相同名稱的方法

Rust 既不能避免一個 trait 與另一個 trait 擁有相同名稱的方法,也不能阻止為同一型別同時實現這兩個 trait。

兩個 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*");
    }
}

當呼叫 Human 例項的 fly 時,編譯器預設呼叫直接實現在型別上的方法:

fn main() {
    let person = Human;
    person.fly();
}
//會列印出 *waving arms furiously*

指定希望呼叫哪一個 trait 的 fly 方法:

fn main() {
    let person = Human;
    Pilot::fly(&person);
    Wizard::fly(&person);
    person.fly();
}

執行這段程式碼會列印出:

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

不是方法的關聯函式沒有 self 引數,當存在多個型別或者 trait 定義了相同函式名的非方法函式時,Rust 無法計算出期望的型別,除非使用 完全限定語法(fully qualified syntax)。

一個帶有關聯函式的 trait 和一個帶有同名關聯函式並實現了此 trait 的型別:

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() {
    println!("A baby dog is called a {}", Dog::baby_name());
}
//會列印出:A baby dog is called a Spot

嘗試呼叫 Animal trait 的 baby_name 函式,不過 Rust 並不知道該使用哪一個實現:

//會得到一個編譯錯誤
fn main() {
    println!("A baby dog is called a {}", Animal::baby_name());
}

因為 Animal::baby_name 沒有 self 引數,同時這可能會有其它型別實現了 Animal trait,Rust 無法計算出所需的是哪一個 Animal::baby_name 實現。

使用完全限定語法來指定希望呼叫的是 Dog 上 Animal trait 實現中的 baby_name 函式:

fn main() {
    println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}
//會列印出:A baby dog is called a puppy

在尖括號中向 Rust 提供了型別註解,並透過在此函式呼叫中將 Dog 型別當作 Animal 對待,來指定希望呼叫的是 Dog 上 Animal trait 實現中的 baby_name 函式。

通常,完全限定語法定義為:

<Type as Trait>::function(receiver_if_method, next_arg, ...);
  • 對於不是方法的關聯函式,其沒有一個 receiver,故只會有其他引數的列表。
  • 可以選擇在任何函式或方法呼叫處使用完全限定語法。
  • 允許省略任何 Rust 能夠從程式中的其他資訊中計算出的部分。

父 trait 用於在另一個 trait 中使用某 trait 的功能

對於一個實現了第一個 trait 的型別,希望要求這個型別也實現了第二個 trait。如此就可使 trait 定義使用第二個 trait 的關聯項,這個所需的 trait 是我們實現的 trait 的 父(超)trait(supertrait)。

建立一個帶有 outline_print 方法的 trait OutlinePrint,它會將給定的值格式化為帶有星號框。給定一個實現了標準庫 Display trait 的並返回 (x, y) 的 Point,當呼叫以 1 作為 x 和 3 作為 y 的 Point 例項的 outline_print 會顯示如下:

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

實現 OutlinePrint trait,它要求來自 Display 的功能:

use std::fmt;

//指定了 OutlinePrint 需要 Display trait
//否則會報錯:在當前作用域中沒有找到用於 &Self 型別的方法 to_string
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));
    }
}

嘗試在一個沒有實現 Display 的型別上實現 OutlinePrint :

//這段程式碼無法透過編譯!
struct Point {
    x: i32,
    y: i32,
}

impl OutlinePrint for Point {}

一旦在 Point 上實現 Display 並滿足 OutlinePrint 要求的限制,則能成功編譯:

use std::fmt;

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

newtype 模式用以在外部型別上實現外部 trait

孤兒規則(orphan rule):只要 trait 或型別對於當前 crate 是本地的話就可以在此型別上實現該 trait,一個繞開這個限制的方法是使用 newtype 模式(newtype pattern)。

如果想要在 Vec 上實現 Display,而孤兒規則阻止我們直接這麼做,因為 Display trait 和 Vec 都定義於我們的 crate 之外。可以建立一個包含 Vec 例項的 Wrapper 結構體,接著可以如列表 19-23 那樣在 Wrapper 上實現 Display 並使用 Vec 的值:

建立 Wrapper 型別封裝 Vec<String> 以便能夠實現 Display:

use std::fmt;

//Wrapper 是元組結構體而 Vec<T> 是結構體總位於索引 0 的項
struct Wrapper(Vec<String>);

//使用 self.0 來訪問其內部的 Vec<T>
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);
}

此方法的缺點是必須直接在 Wrapper 上實現 Vec<T> 的所有方法,這樣才可以代理到self.0 上。如果不希望封裝型別擁有所有內部型別的方法,只需要自行實現所需的方法。

高階型別

為了型別安全和抽象而使用 newtype 模式

newtype 模式也可以用於一些其他還未討論的功能:

  • 靜態的確保某值不被混淆,和用來表示一個值的單位,如 Millimeters 和 Meters 結構體。
  • 用於抽象掉一些型別的實現細節,如暴露出與直接使用其內部私有型別時所不同的公有 API
  • 隱藏其內部的泛型型別,如封裝了 HashMap<i32, String> 的 People 型別。

型別別名用來建立型別同義詞

Rust 提供了宣告 型別別名(type alias)的能力,使用 type 關鍵字來給予現有型別另一個名字:

//建立 i32 的別名 Kilometers
type Kilometers = i32;
//Kilometers 不是一個新的、單獨的型別,值將被完全當作 i32 型別值來對待
let x: i32 = 5;
let y: Kilometers = 5;

println!("x + y = {}", x + y);

型別別名的主要用途是減少重複,例如可能會有這樣很長的型別:

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 來減少重複:

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--
}

std::io 中大部分函式會返回 Result<T, E>,其中 E 是 std::io::Error,比如 Write trait 中的這些函式:

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

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

std::io 有這個型別別名宣告:

type Result<T> = std::result::Result<T, 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: fmt::Arguments) -> Result<()>;
}

型別別名在兩個方面有幫助:易於編寫 並 在整個 std::io 中提供了一致的介面

從不返回的 never type

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

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

讀作 “函式 bar 從不返回”,而從不返回的函式被稱為 發散函式(diverging functions)。

match 語句和一個以 continue 結束的分支:

// match 的分支必須返回相同的型別
let guess: u32 = match guess.trim().parse() {
    Ok(num) => num,      //u32 值
    Err(_) => continue,  //! 值
};
//! 並沒有一個值,Rust 決定 guess 的型別是 u32

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

Option<T> 上的 unwrap 函式產生一個值或 panic:

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

最後一個有著 ! 型別的表示式是 loop:

print!("forever ");

//迴圈永遠也不結束,所以此表示式的值是 !
loop {
    print!("and ever ");
}

動態大小型別和 Sized trait

動態大小型別(dynamically sized types)有時被稱為 “DST”“unsized types”,這些型別允許我們處理只有在執行時才知道大小的型別。

str 是一個 DST,直到執行時都不知道字串有多長,以下程式碼不能工作:

//正確的程式碼中:s1 和 s2 的**型別是 &str 而不是 str**
let s1: str = "Hello there!";
let s2: str = "How's it going?";

&str 是 兩個 值:str 的地址和其長度,由此可知關於動態大小型別:

  • 動態大小型別的常規用法:它們有一些額外的元資訊來儲存動態資訊的大小。
  • 動態大小型別的黃金規則:必須將動態大小型別的值置於某種指標之後。

為了處理 DST,Rust 提供了 Sized 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 意味著 “T 可能是也可能不是 Sized” 同時這個註解會覆蓋泛型型別必須在編譯時擁有固定大小的預設規則。
  • ?Trait 語法只能用於 Sized ,而不能用於任何其他 trait。
  • 將 t 引數的型別從 T 變為了 &T:因為其型別可能不是 Sized 的,所以需要將其置於某種指標之後。

高階函式與閉包

函式指標

函式滿足型別 fn(小寫的 f),不要與閉包 trait 的 Fn 相混淆。fn 被稱為 函式指標(function pointer),透過函式指標允許使用函式作為另一個函式的引數。

指定引數為函式指標的語法類似於閉包,使用 fn 型別接受函式指標作為引數:

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

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

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

函式指標實現了所有三個閉包 trait(Fn、FnMut 和 FnOnce),所以總是可以在呼叫期望閉包的函式時傳遞函式指標作為引數。傾向於編寫使用泛型和閉包 trait 的函式,這樣它就能接受函式或閉包作為引數。

使用 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();

可以指定建構函式作為接受閉包的方法的引數:

enum Status {
    Value(u32),
    Stop,
}

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

返回閉包

閉包表現為 trait,這意味著不能直接返回閉包。

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

fn returns_closure() -> dyn Fn(i32) -> i32 {
    |x| x + 1
}
//錯誤:Rust 並不知道需要多少空間來儲存閉包

解決辦法:可以使用 trait 物件:

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

(Macro)指的是 Rust 中一系列的功能:使用 macro_rules! 的 宣告(Declarative)宏,和三種 過程(Procedural)宏

  • 自定義 #[derive] 宏在結構體和列舉上指定透過 derive 屬性新增的程式碼
  • 類屬性(Attribute-like)宏定義可用於任意項的自定義屬性
  • 類函式宏看起來像函式不過作用於作為引數傳遞的 token

宏和函式的區別

宏和函式的區別可以總結如下:

  • 宏是一種超程式設計的方式,用於生成程式碼,而函式是執行程式碼的實體。
  • 宏可以接收不同數量的引數,而函式的引數個數和型別需要在函式簽名中宣告。
  • 宏可以在編譯器翻譯程式碼前展開,例如在給定型別上實現 trait,而函式是在執行時被呼叫,無法在編譯時實現 trait。
  • 宏定義比函式定義更復雜,因為宏定義是編寫生成 Rust 程式碼的 Rust 程式碼,而函式定義只是普通的程式碼。
  • 在呼叫宏之前必須先定義它或將其引入作用域,而函式可以在任何地方定義和呼叫。

使用 macro_rules! 的宣告宏用於通用超程式設計

Rust 最常用的宏形式是 宣告宏(declarative macros),有時也被稱為 “macros by example”、“macro_rules! 宏” 或者就是 “macros”,核心概念是宣告宏允許編寫一些類似 Rust match 表示式的程式碼。

使用 vec! 宏來生成一個給定值的 vector:

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

一個 vec! 宏定義的簡化版本:

//註解表明只要匯入了定義這個宏的 crate,該宏就應該是可用的
#[macro_export]
//使用 macro_rules! 和宏名稱開始宏定義,且所定義的宏並不帶感嘆號
macro_rules! vec {
    //分支模式 ( $( $x:expr ),* ) ,後跟 => 以及和模式相關的程式碼塊
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}

注意:標準庫中實際定義的 vec! 包括預分配適當量的記憶體的程式碼。這部分為程式碼最佳化,為了讓示例簡化,此處並沒有包含在內。

對於全部的宏模式語法,請查閱 Rust 參考

當以 vec![1, 2, 3]; 呼叫該宏時,替換該宏呼叫所生成的程式碼會是下面這樣:

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

用於從屬性生成程式碼的過程宏

第二種形式的宏被稱為 過程宏(procedural macros),它們更像函式(一種過程型別)。過程宏接收 Rust 程式碼作為輸入,在這些程式碼上進行操作,然後產生另一些程式碼作為輸出,而非像宣告式宏那樣匹配對應模式然後以另一部分程式碼替換當前程式碼。

有三種型別的過程宏,工作方式都類似:

  • 自定義派生(derive)
  • 類屬性
  • 類函式

建立過程宏時,其定義必須駐留在它們自己的具有特殊 crate 型別的 crate 中。這是出於複雜的技術原因,將來可能消除這些限制。

一個定義過程宏的例子:

use proc_macro;

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

宏所處理的原始碼組成了輸入 TokenStream,宏生成的程式碼是輸出 TokenStream。

如何編寫自定義 derive 宏

建立一個 hello_macro crate,其包含名為 HelloMacro 的 trait 和關聯函式 hello_macro。提供一個過程式宏以便使用者可以使用 #[derive(HelloMacro)] 註解它們的型別來得到 hello_macro 函式的預設實現。

crate 使用者所寫的能夠使用過程式宏的程式碼:

//這段程式碼無法透過編譯!
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;

#[derive(HelloMacro)]
struct Pancakes;

fn main() {
    Pancakes::hello_macro();
}
//會列印 Hello, Macro! My name is Pancakes!

第一步是像下面這樣新建一個庫 crate:

cargo new hello_macro --lib

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

pub trait HelloMacro {
    fn hello_macro();
}

此時,crate 使用者可以實現該 trait 以達到其期望的功能:

use hello_macro::HelloMacro;

struct Pancakes;

//需要為每一個想使用 hello_macro 的型別編寫實現的程式碼塊
impl HelloMacro for Pancakes {
    fn hello_macro() {
        //無法為 hello_macro 函式提供一個能夠列印實現了該 trait 的型別的名字的預設實現:Rust 沒有反射的能力
        println!("Hello, Macro! My name is Pancakes!");
    }
}

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

下一步是定義過程式宏,過程式宏必須在其自己的 crate 內,該限制最終可能被取消。構造 crate 和其中宏的慣例如下:對於一個 foo 的包來說,一個自定義的派生過程宏的包被稱為 foo_derive 。

在 hello_macro 專案中新建名為 hello_macro_derive 的包:

cargo new hello_macro_derive --lib

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

宣告 hello_macro_derive crate 是過程宏 (proc-macro) crate,還需要 syn 和 quote crate 中的功能。將下面的程式碼加入到 hello_macro_derive 的 Cargo.toml 檔案中:

[lib]
proc-macro = true

[dependencies]
syn = "1.0"
quote = "1.0"

為定義一個過程式宏,以下程式碼放在 hello_macro_derive crate 的 src/lib.rs 檔案裡面:

//這段程式碼無法透過編譯!未新增 impl_hello_macro 函式的定義
use proc_macro::TokenStream; //Rust 自帶,無需新增 Cargo.toml
use quote::quote;
use syn;

//大多數過程式宏處理 Rust 程式碼時所需的程式碼
#[proc_macro_derive(HelloMacro)]
//負責解析 TokenStream
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // Construct a representation of Rust code as a syntax tree
    // that we can manipulate
    let ast = syn::parse(input).unwrap();
    //在錯誤時 panic 對過程宏來說是必須的,這裡用 unwrap 來簡化了例子
    //在生產程式碼中則應該透過 panic! 或 expect 來提供更加明確的錯誤資訊

    // Build the trait implementation
    //負責轉換語法樹
    impl_hello_macro(&ast)
}

引入了三個新的 crate:proc_macro 、 syn 和 quote:

  • proc_macro crate 是編譯器用來讀取和操作我們 Rust 程式碼的 API。
  • syn crate 將字串中的 Rust 程式碼解析成為一個可以操作的資料結構。
  • quote 則將 syn 解析的資料結構轉換回 Rust 程式碼。

當使用者在一個型別上指定** #[derive(HelloMacro)]** 時,hello_macro_derive 函式將會被呼叫,這是大多數過程宏遵循的習慣。

syn 中的 parse 函式獲取一個 TokenStream 並返回一個表示解析出 Rust 程式碼的 DeriveInput 結構體。從字串 struct Pancakes; 中解析出來的 DeriveInput 結構體的相關部分:

DeriveInput {
    // --snip--
    
    //ident(identifier,表示名字)為 Pancakes
    ident: Ident {
        ident: "Pancakes",
        span: #0 bytes(95..103)
    },
    data: Struct(
        DataStruct {
            struct_token: Struct,
            fields: Unit,
            semi_token: Some(
                Semi
            )
        }
    )
}

定義 impl_hello_macro 函式,其用於構建所要包含在內的 Rust 新程式碼:

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));
                //stringify! 為 Rust 內建宏。其接收一個 Rust 表示式,如 1 + 2 
                //然後在編譯時將表示式轉換為一個字串常量,如 "1 + 2" 
            }
        }
    };
    gen.into()
}

quote! 宏執行的直接結果並不是編譯器所期望的所以需要轉換為 TokenStream,呼叫 into 方法會消費這個中間表示(intermediate representation,IR)並返回所需的 TokenStream 型別值,詳情查閱 quote crate 的文件。

在 projects 目錄下用 cargo new pancakes 命令新建一個二進位制專案。新增依賴到 pancakes 包的 Cargo.toml 檔案中,可以像下面這樣將其指定為 path 依賴

hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }

把最開始的程式碼放在 src/main.rs ,然後執行 cargo run:其應該列印 Hello, Macro! My name is Pancakes!。

類屬性宏

類屬性宏與自定義派生宏相似,不同的是:

  • derive 屬性生成程式碼,類屬性宏能建立新的屬性
  • derive 只能用於結構體和列舉;屬性還可以用於其它的項,比如函式。

建立一個名為 route 的屬性用於註解 web 應用程式框架(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)宏的定義看起來像函式呼叫的宏,比函式更靈活,可以接受未知數量的引數。

一個類函式宏例子是可以像這樣被呼叫的 sql! 宏:

//解析其中的 SQL 語句並檢查其是否是句法正確的
let sql = sql!(SELECT * FROM posts WHERE id=1);

sql! 宏應該被定義為如此:

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

這類似於自定義派生宏的簽名:獲取括號中的 token,並返回希望生成的程式碼。

相關文章