Loading

Rust 高级特征

本文有删减,原文链接高级特征

不安全 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,并返回希望生成的代码。

posted @ 2024-03-07 10:51  二次元攻城狮  阅读(188)  评论(0编辑  收藏  举报