19、Rust程序设计语言——高级特性

目录

  • [1. 不安全Rust](#1. 不安全Rust)
    • [1.1 执行不安全的超能力](#1.1 执行不安全的超能力)
    • [1.2 解引用裸指针](#1.2 解引用裸指针)
    • [1.3 调用不安全函数或方法](#1.3 调用不安全函数或方法)
      • [1.3.1 创建不安全代码的安全抽象](#1.3.1 创建不安全代码的安全抽象)
      • [1.3.2 使用extern函数调用外部代码](#1.3.2 使用extern函数调用外部代码)
      • [1.3.3 从其他语言调用Rust函数](#1.3.3 从其他语言调用Rust函数)
    • [1.4 访问或修改可变静态变量](#1.4 访问或修改可变静态变量)
    • [1.5 实现不安全trait](#1.5 实现不安全trait)
    • [1.6 访问联合体中的字段](#1.6 访问联合体中的字段)
    • [1.7 使用Miri检查不安全代码](#1.7 使用Miri检查不安全代码)
    • [1.8 正确使用不安全代码](#1.8 正确使用不安全代码)
  • [2. 高级trait](#2. 高级trait)
    • [2.1 关联类型](#2.1 关联类型)
    • [2.2 使用默认泛型类型参数和运算符重载](#2.2 使用默认泛型类型参数和运算符重载)
    • [2.3 在同名方法之间消歧义](#2.3 在同名方法之间消歧义)
    • [2.4 使用超trait](#2.4 使用超trait)
    • [2.5 使用newtype模式在外部类型上实现外部trait](#2.5 使用newtype模式在外部类型上实现外部trait)
  • [3. 高级类型](#3. 高级类型)
    • [3.1 使用newtype模式实现类型安全与抽象](#3.1 使用newtype模式实现类型安全与抽象)
    • [3.2 类型同义词与类型别名](#3.2 类型同义词与类型别名)
    • [3.3 从不返回的never type](#3.3 从不返回的never type)
    • [3.4 动态大小类型和Sized trait](#3.4 动态大小类型和Sized trait)
  • [4. 高级函数与闭包](#4. 高级函数与闭包)
    • [4.1 函数指针](#4.1 函数指针)
    • [4.2 返回闭包](#4.2 返回闭包)
  • [5. 宏](#5. 宏)
    • [5.1 宏和函数的区别](#5.1 宏和函数的区别)
    • [5.2 用macro_rules!编写用于通用元编程的声明宏](#5.2 用macro_rules!编写用于通用元编程的声明宏)
    • [5.3 用于从属性生成代码的过程宏](#5.3 用于从属性生成代码的过程宏)
    • [5.4 自定义derive宏](#5.4 自定义derive宏)
    • [5.5 类属性宏](#5.5 类属性宏)
    • [5.6 类函数宏](#5.6 类函数宏)
  • 参考

1. 不安全Rust

  目前为止的代码都有Rust在编译时会强制执行的内存安全保证。然而,Rust还隐藏有第二种语言,它不会强制执行这类内存安全保证:这被称为不安全Rust unsafe Rust。它与常规Rust代码无异,但是会提供额外的超能力。

  不安全Rust之所以存在,是因为静态分析本质上是保守的。当编译器尝试确定一段代码是否支持某个保证时,拒绝一些合法的程序比接受无效的程序要好一些。这必然意味着有时代码可能是合法的,但是Rust编译器没有足够的信息来确定,它将拒绝该代码。在这种情况下,可以使用不安全代码告诉编译器,"相信我,我知道自己在干什么。"不过千万注意,使用不安全Rust风险自担:如果不安全代码出错了,比如解引用空指针,可能会导致不安全的内存使用。

  另一个Rust存在不安全一面的原因是底层计算机硬件固有的不安全性。如果Rust不允许进行不安全操作,那么有些任务则根本完成不了。Rust需要能够进行像直接与操作系统交互甚至于编写你自己的操作系统这样的底层系统编程。底层系统编程也是Rust语言的目标之一。

1.1 执行不安全的超能力

  要切换到unsafe Rust,可以使用unsafe关键字,然后开启一个包含不安全代码的新块。这里有五类可以在不安全Rust中进行而不能用于安全Rust的操作,它们称之为不安全的超能力unsafe superpowers。这些超能力包括:

  1. 解引用裸指针。

  2. 调用不安全的函数或方法。

  3. 访问或修改可变静态变量。

  4. 实现不安全trait。

  5. 访问union的字段。

  unsafe并不会关闭借用检查器或禁用任何其他Rust安全检查:如果在不安全代码中使用引用,它仍会被检查。unsafe关键字只是提供了那五个不会被编译器检查内存安全的功能。你仍然能在不安全块中获得某种程度的安全。

  unsafe不意味着块中的代码就一定是危险的或者必然导致内存安全问题:其意图在于作为程序员,你将会确保unsafe块中的代码以有效的方式访问内存。

  人难免出错,错误总会发生,不过通过要求这五类不安全操作必须位于标记为unsafe的块中,就能够知道任何与内存安全相关的错误必定位于unsafe块中。保持unsafe块尽可能小;如此当之后调查内存bug时就会感谢你自己了。

  为了尽可能隔离不安全代码,最好将不安全代码封装进一个安全的抽象并提供安全API。标准库的一部分被实现为在被评审过的不安全代码之上的安全抽象。这个技术防止了unsafe泄漏到所有你或者用户希望使用unsafe代码实现的功能的地方,因为使用其安全抽象是安全的。

1.2 解引用裸指针

  不安全Rust有两个被称为裸指针raw pointers的类似于引用的新类型。和引用一样,裸指针是不可变或可变的,分别写作*const T和*mut T。这里的星号不是解引用运算符;它是类型名称的一部分。在裸指针的上下文,不可变意味着指针解引用之后不能直接赋值。

  裸指针与引用和智能指针的区别在于:

  1. 允许忽略借用规则,可以同时拥有不可变和可变的指针,或多个指向相同位置的可变指针。

  2. 不保证指向有效的内存。

  3. 允许为空。

  4. 不能实现任何自动清理功能。

  通过去掉Rust强加的保证,你可以放弃安全保证以换取性能或使用另一个语言或硬件接口的能力,此时Rust的保证并不适用。

rust 复制代码
fn main() {
	let mut num = 5;
	let r1 = &raw const num;
	let r2 = &raw mut num;
}

  这段代码并没有引入unsafe关键字。可以在安全代码中创建裸指针;只是不能在不安全块之外解引用裸指针。

  我们通过使用裸指针借用操作符raw borrow operators创建裸指针:&raw const num会创建一个*const i32的不可变裸指针。因为我们是直接从一个局部变量创建它们的,因此可以确定这些特定的裸指针是有效的,但是不能对任何裸指针都做出如此假设。

  为了演示这一点,接下来我们将创建一个有效性无法确定的裸指针,使用as进行类型转换而不是使用裸指针借用操作符。

rust 复制代码
fn main() {
	let address = 0x01235usize;
	let r = address as *const i32;
}

  上面的代码创建了一个指向任意内存地址的裸指针。尝试使用任意内存是未定义行为:此地址可能有数据也可能没有,编译器可能会优化掉这个内存访问,或者程序可能因为段错误segmentation fault而终止。通常在有裸指针借用操作符可用的情况下,没有充分理由编写这样的代码,但这确实是可行的。

rust 复制代码
fn main() {
	let mut num = 5;
	let r1 = &raw const num;
	let r2 = &raw mut num;
	unsafe {
		println!("r1 is: {}", *r1);
		println!("r2 is: {}", *r2);
	}
}

  在裸指针上使用解引用运算符*,该操作必须包含在unsafe块中。创建一个指针不会造成任何危害,只有当访问其指向的值时才有可能遇到无效的值。

  裸指针允许同时创建同一地址的可变指针和不可变指针,若通过可变指针修改数据,则可能造成潜在数据竞争。

  既然存在这么多危险,为何还要使用裸指针?一个主要的应用场景是调用C代码接口。另一个场景是构建借用检查器无法理解的安全抽象。

1.3 调用不安全函数或方法

  不安全函数和方法与常规函数方法十分类似,除了其开头有一个额外的unsafe。在此上下文中,关键字unsafe表示该函数具有调用时需要满足的要求,而Rust不会保证满足这些要求。通过在unsafe块中调用不安全函数,表明我们已经阅读过此函数的文档并对其是否满足函数自身的契约负责。

rust 复制代码
unsafe fn dangerous() {}

fn main() {
	unsafe {
		dangerous();
	}
}

  必须在一个单独的unsafe块中调用dangerous函数。如果尝试不使用unsafe调用dangerous,会报错:

  通过unsafe块,我们向Rust断言我们已经阅读过函数的文档,理解如何正确使用它,并核实我们履行了该函数的契约。

  在不安全函数的函数体内部执行不安全操作时,同样需要unsafe块,就像在普通函数中一样,如果忘记了,编译器会发出警告。这有助于将unsafe块保持得尽可能小,因为不安全操作未必需要覆盖整个函数体。

1.3.1 创建不安全代码的安全抽象

  仅仅因为函数包含不安全代码并不意味着整个函数都需要标记为不安全的。事实上,将不安全代码封装进安全函数是一种常见的抽象方式。以标准库中的函数split_at_mut为例。

rust 复制代码
fn main() {
	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]);
}

  这个安全函数定义于可变slice上,它获取一个slice并从给定的索引参数开始将其分割为两个slice。这个函数无法只通过安全Rust实现。一个尝试如下,但是它不能编译。这个尝试将split_at_mut实现为函数,且只处理i32值。

rust 复制代码
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..])
}

  此函数首先获取slice的长度,然后通过检查参数是否小于或等于这个长度来断言参数所给定的所以位于slice当中。该断言意味着如果传入的索引比要分割的slice的索引更大,此函数在尝试使用这个索引前panic。

  之后我们在一个元组中返回两个可变的slice:一个从原始slice的开头直到mid索引,另一个从mid直到原slice的结尾。

  尝试编译会得到如下的错误:

  Rust借用检查器无法理解我们要借用这个slice的两个不同的部分:它只知道我们借用了同一个slice两次。本质上借用slice的不同部分是可以的,因为这两段slice不会重叠,不过Rust还没智能到能够理解这些。当我们知道某些事是可以的而Rust不知道的时候,就是触及不安全代码的时候。

  下面使用unsafe和裸指针和一些不安全的函数调用来实现split_at_mut函数:

rust 复制代码
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),
		)
	}
}

  slice是一个指向一些数据的指针,并带有该slice的长度。可以使用len方法获取slice的长度,使用as_mut_ptr方法访问slice的裸指针。在这个例子中,因为有一个i32值的可变slice,as_mut_ptr返回一个*mut i32类型的裸指针,并将其存储在ptr变量中。

  我们保持索引mid在slice中的断言。接着是不安全代码:slice::from_raw_parts_mut函数获取一个裸指针和一个长度来创建一个slice。这里使用此函数从ptr中创建了一个有mid个项的slice。之后在ptr上调用add方法并使用mid作为参数来获取一个从mid开始的裸指针,使用这个裸指针并以mid之后项的数量为长度创建另一个slice。

&emsp: slice::from_raw_parts_mut函数是不安全的,因为它获取一个裸指针,并必须确保这个指针是有效的。裸指针上的add方法也不安全,因为其必须确信此地址偏移量也是有效的指针。因此必须将slice::from_raw_parts_mut和add放入unsafe块中以便能调用它们。通过观察代码,和增加mid必然小于等于len的断言,我们可以说unsafe块中所有的裸指针将是有效的slice中数据的指针。这是一个可以接受的unsafe的恰当用法。

  注意无需将split_at_mut函数标记为unsafe,并可以在安全Rust中调用此函数。我们创建了一个不安全代码的安全抽象,其代码以一种安全的方式使用了unsafe代码,因为其只从这个函数访问的数据中创建了有效的指针。

rust 复制代码
use std::slice;

fn main() {
	let address = 0x01234usize;
	let r = address as *mut i32;
	let values: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };
}

  这段代码获取任意内存地址并创建了一个长度为10000的slice。我们并不能拥有这个任意地址的内存,也不能保证这段代码创建的slice包含有效的i32值。试图使用臆测为有效的values会导致未定义的行为。

1.3.2 使用extern函数调用外部代码

  有时你的Rust代码可能需要与其他语言编写的代码交互。为此,Rust有一个关键字,extern,有助于创建和使用外部函数接口Foreign Function Interface, FFI。外部函数接口是一个编程语言用以定义函数的方式,其允许不同外部编程语言调用这些函数。

  下面展示了如何集成C标准库中的abs函数。extern块中声明的函数在Rust代码中通常是不安全的,因此extern块本身也必须标注unsafe。之所以如此,是因为其他语言不会强制执行Rust的规则,Rust也无法检查这些约束,因此程序员有责任确保调用的安全性。

rust 复制代码
unsafe extern "C" {
	fn abs(input: i32) -> i32;
}

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

  在unsafe extern "C"块中,我们列出了希望能够调用的另一个语言中的外部函数的签名和名称。"C"部分定义了外部函数所使用的应用二进制接口application binary interface, ABI------ABI定义了如何在汇编语言层面调用此函数。"C"ABI是最常见的,并遵循C编程语言的ABI 。

  unsafe extern中声明的任何项都隐式地是unsafe的。然而一些FFI函数可以安全地调用。例如,C标准库中的abs函数没有任何内存安全方面的考量并且我们知道它可以使用任何i32调用。在类似这样的例子中,我们可以使用safe关键字来表明这个特定的函数即便是在unsafe extern块中也是可以安全调用的。一旦我们做出这个修改,调用它不再需要unsafe块。

rust 复制代码
unsafe extern "C" {
	safe fn abs(input: i32) -> i32;
}

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

  将一个函数标记为safe并不会固有地使其变得安全!这像是对Rust的承诺表明它是安全的。确保履行这个承诺仍然是你的责任!

1.3.3 从其他语言调用Rust函数

  也可以使用extern来创建一个允许其它语言调用Rust函数的接口。不同于创建整个extern块,就在fn关键字之前增加extern关键字并为相关函数指定所用到的ABI。还需增加#no_mangle注解来告诉Rust编译器不要mangle此函数的名称。Mangling指编译器将我们命名的函数名更改为包含更多供其他编译过程使用的信息的名称,不过可读性较差。每一个编程语言的编译器都会以稍微不同的方式mangle函数名,所以为了使Rust函数能在其他语言中指定,必须禁用Rust编译器的name mangling。这是不安全的因为在没有内置mangling的时候在库之间可能有命名冲突,所以确保所选的名称可以不用mangling地安全导出是我们的责任。

  在下面的例子中,一旦其编译为动态库并从C语言中链接,call_from_c函数就能够在C代码中访问:

rust 复制代码
#[unsafe(no_mangle)]
pub extern "C" fn call_from_c() {
	println!("Just called a Rust function from C!");
}

这种extern用法只在属性中需要unsafe,而不需要在extern块本身使用unsafe。

1.4 访问或修改可变静态变量

  Rust支持全局变量global variables,不过这对于Rust的所有权规则来说是有问题的。如果有两个线程访问相同的可变全局变量,则可能造成数据竞争。

  全局变量在Rust中被称为静态变量。

rust 复制代码
static HELLO_WORLD: &str = "Hello, world!";

fn main() {
	println!("value is {HELLO_WORLD}");
}

  静态变量类似于常量。通常静态变量的名称采用SCREAMING_SNAKE_CASE写法。静态变量只能储存拥有'static生命周期的引用,这意味着Rust编译器可以自己计算出其生命周期而无需显式标注。访问不可变静态变量是安全的。

 &esmp;常量与不可变静态变量的一个微妙的区别是静态变量中的值有一个固定的内存地址。使用这个值总是会访问相同的地址。另一方面,常量则允许在任何被用到的时候复制其数据。另一个区别是在于静态变量可以是可变的。访问和修改可变静态变量都是不安全的。

rust 复制代码
static mut COUNTER: u32 = 0;

// SAFETY: 同时在多个线程调用这个方法是未定义的行为,所以你*必须*保证同一时间只
// 有一个线程在调用它。
unsafe fn add_to_count(inc: u32) {
	unsafe {
		COUNTER += inc;
	}
}

fn main() {
	unsafe {
		// SAFETY: 它只在`main`这一个线程中被调用。
		add_to_count(3);
		println!("COUNTER: {}", *(&raw const COUNTER));
	}
}

  就像常规变量一样,我们使用mut关键字来指定可变性。任何读写COUNTER的代码都必须位于unsafe块中。这段代码可以编译并如期打印出COUNTER: 3,因为这是单线程的。拥有多个线程访问COUNTER则可能导致数据竞争,所以这是未定义行为。因此,我们需要把整个函数标记为unsafe,并在文档注释中说明其安全性限制,以便调用者明确那些操作是安全的、那些操作是不安全的。

  每当我们编写一个不安全函数,惯常做法是编写一个以SAFETY开头的注释并解释调用者需要做什么才可以安全地调用该方法。同理,当我们进行不安全操作时,惯常做法是编写一个以SAFETY开头并解释安全性规则是如何维护的。

  另外,编译器不会允许你创建一个可变静态变量的引用。你只能通过用裸指针解引用操作符创建的裸指针访问它。这包括引用的创建是不可见的情况。可变静态变量只能通过裸指针创建的要求有助于确保它们的安全要求更为明确。

  拥有可以以全局访问的可变数据,难以保证不存在数据竞争,这就是为何Rust认为可变静态变量是不安全的。在任何可能的情况下,请优先使用并发和线程安全智能指针,这样编译器就能检测不同线程间的数据访问是否是安全。

1.5 实现不安全trait

  可以使用unsafe来实现一个不安全trait。当trait中至少有一个方法中包含编译器无法验证的不变式invariant时该trait就是不安全的。可以在trait之前增加unsafe关键字将trait声明为unsafe,同时trait的实现也必须标记为unsafe。

rust 复制代码
unsafe trait Foo {
	// 方法
}

unsafe impl Foo for i32 {
	// 方法实现
}

  通过unsafe impl,我们承诺将保证编译器所不能验证的不变式。

  如果我们的类型完全由实现了Send与Sync的其他类型组成,编译器会自动为其实现这些trait。如果我们定义的类型包含某些未实现Send或Sync的类型,例如裸指针,但又想将该类型标记为Send或Sync,就必须使用unsafe。Rust不能验证我们的类型保证可以安全地跨线程发送或在多线程间访问,所以需要我们自己检查,并通过unsafe表明这一点。

1.6 访问联合体中的字段

  最后一个只能在unsafe块中执行的操作是访问union中的字段。union和struct类似,但是在一个实例中同时只能使用一个已声明的字段。联合体主要用于和C代码中的联合体进行交互。访问联合体的字段是不安全的,因为Rust无法保证当前存储在联合体实例中的数据类型。

1.7 使用Miri检查不安全代码

  当编写不安全代码时,你可能会想要检查编写的代码是否真的安全正确。最好的方式之一是使用Miri,一个用来检测未定义行为的Rust官方工具。鉴于借用检查器是一个在编译时工作的静态工具,Miri是一个在运行时工作的动态工具。它通过运行程序,或者测试集来检查代码,并检测你是否违反了它理解的Rust应该如何工作的规则。

  使用Miri要求使用nightly版本的Rust。可以通过输入rustup +nightly component add miri来安装nightly版本的Rust和Miri。这并不会改变你项目正在使用的Rust版本;它只是为你的系统增加了这个工具,所以你可以在需要的时候使用它。你可以通过输入cargo +nightly miri run或者cargo +nightly miri test在项目中使用Miri。

  Miri并不能捕获编写不安全代码时可能出现的所有错误。Miri是一个动态分析工具,因此它只能捕获代码实际运行时出现的问题。这意味需要将其与良好的测试技术相结合以增强你对所编写的不安全代码的信心。Miri也不能覆盖代码所有的不可靠的地方。

1.8 正确使用不安全代码

  使用unsafe来进行这五个操作之一是没有问题的,甚至是不需要深思熟虑的,不过使得unsafe代码正确也实属不易,因为编译器不能帮助保证内存安全。当有理由使用unsafe代码时,是可以这么做的,通过使用显式的unsafe标注可以更容易地在错误发生时追踪问题的源头。每当编写不安全代码时,都可以借助Miri来更加自信地验证所写代码是否遵循Rust的规则。

2. 高级trait

2.1 关联类型

  关联类型associated types将一个类型占位符与trait相关联,使得该trait的方法定义可以在签名中使用这些占位符类型。该trait的每个实现者会为每个具体实现指定要使用的具体类型来替代占位符类型。这样,我们就能在定义trait时使用占位符类型,而无需预先知道这些类型的具体内容,直到实现该trait时再进行指定。

  一个带有关联类型的trait的例子是标准库提供的Iterator trait。它有一个叫做Item的关联类型来替代遍历的值的类型。iterator trait的定义如下:

rust 复制代码
pub trait Iterator {
	type Item;
	
	fn next(&mut self) -> Option<Self::Item>;
}

  Item是一个占位符类型,同时next方法的定义表明它返回Option<Self::Item>类型的值。Iterator trait的实现者会指定Item的具体类型,于是next方法会返回一个包含该具体类型值的option。

  关联类型可能看起来与泛型类似,后者允许我们在定义函数时不必指定它可以处理的类型。为了体现两者的区别,我们来看一个名为Counter的类型上的Iterator trait实现,其中指定Item的类型为u32:

rust 复制代码
impl Iterator for Counter {
	type Item = u32;

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

  这种语法看起来与泛型类似。那么为什么不用泛型来定义Iterator trait类型:

rust 复制代码
pub trait Iterator<T> {
	fn next(&mut self) -> Option<T>;
}

  区别在于使用泛型时,则不得不在每一个实现中标注类型;这是因为我们可以实现为Iterator<String> for Counter,或者任何其他类型,这样就可以有多个针对Counter的Iterator的实现。换句话说,当trait有泛型参数,可以多次实现这个trait,每次都使用不同的具体泛型参数类型。当我们在Counter上调用next方法,就必须通过类型注解来指明要使用哪一个Iterator的实现。

  使用关联类型后,则无需标注类型,因为不能对同一个类型多次实现该trait。关联类型也会成为trait契约的一部分:trait的实现必须提供一个类型来替代关联类型占位符。关联类型通常以它的用途来命名,在API文档中对关联类型中进行说明也是一种良好实践。

2.2 使用默认泛型类型参数和运算符重载

  当使用泛型类型参数时,可以为泛型指定一个默认的具体类型。如果默认类型就足够的话,这消除了为具体类型实现trait的需要。为泛型类型指定默认类型的语法是在声明泛型类型时使用<PlaceholderType=ConcreteType>。

  这种技术的一个很好的示例是运算符重载operator overloading,即在特定情况下自定义运算符的行为。

  Rust并不允许创建自定义运算符或重载任意运算符,但可以通过实现std::ops中列出的运算符相关trait来重载它。

rust 复制代码
use std::ops::Add;

#[derive(Clone, Copy, Debug, 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方法将两个Point实例的x值和y值分别相加来创建一个新的Point。Add trait有一个叫做Output的关联类型,它用来定义add方法的返回值类型。

  这里默认泛型类型位于Add trait中:

rust 复制代码
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实例相加。让我们看看一个实现Add trait时希望自定义Rhs类型而不是使用默认类型的例子。

  这里有两个存放不同单元值的结构体,Millimeters和Meters。这种将现有类型简单封装进另一个结构体的方式被称为newtype模式 newtype pattern。我们希望能够将毫米值和米值相加,并让Add实现正确处理单位转换。可以为Millimeters实现Add并以Meters作为Rhs:

rust 复制代码
use std::ops::Add;

struct Millimeters(u32);
struct Meters(u32);

impl Add<Meters> for Millimeters {
	type Output = Millimeters;

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

  默认参数类型主要用于如下两个方面:

  1. 扩展类型而不破坏现有代码。

  2. 在大部分用户都不需要的特定情况进行自定义。

  标准库的Add trait就是第二个目的的一个例子:大部分时候你会将两个相似的类型相加,但Add trait也提供了自定义额外行为的能力。在Add trait定义中使用默认类型参数意味着大部分时候无需指定额外的参数。

  第一个目的与第二个相似但方向相反:如果需要为现有trait增加类型参数,为其提供一个默认类型将允许我们在不破坏现有实现代码的基础上扩展trait的功能。

2.3 在同名方法之间消歧义

  Rust既不能避免一个trait与另一个trait拥有相同名称的方法,也不能阻止为同一个类型同时实现这两个trait。同时还可以直接在类型上实现一个与trait方法同名的方法。

  当调用这些同名方法时,需要告诉Rust我们想要使用哪一个。

rust 复制代码
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;
	person.fly(); // Human.fly
}

  为了能够调用Pilot trait或Wizard trait的fly方法,需要使用更明确的语法来指定具体要调用的fly方法。

rust 复制代码
fn main() {
	let person = Human;
	Pilot::fly(&person);
	Wizard::fly(&person);
	person.fly();
}

  在方法名前指定trait的名称可让Rust明确我们想调用哪个fly实现。也可以选择写成Human::fly(&person),不过无需消歧义的话,这么写有点冗长了。

  因为fly方法获取一个self参数,如果有两个类型都实现了同一trait,Rust可以根据self的类型计算出应该使用哪一个trait实现。

  然而,关联函数中非方法的函数不带有self参数。当存在多个类型或者trait定义了相同函数名的非方法函数时,Rust就不总是能计算出我们期望的是哪一个类型,除非使用完全限定语法fully qualified syntax。

rust 复制代码
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());
}

  这里直接调用了定义于Dog之上的关联函数。但是我们希望调用Dog的Animal trait实现中与Animal trait相关联的函数baby_name。但是在这里直接使用指定trait名称的技巧在这里不起作用。

  因为Animal::baby_name没有Self参数,而且可能有其他类型实现了Animal trait,Rust无法确定我们想调用哪一个Animal::baby_name的实现。

  为了消歧义并告诉Rust我们希望使用的是Dog的Animal实现而不是其他类型的Animal实现,需要使用完全限定语法:

rust 复制代码
fn main() {
	println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}

  我们在尖括号中向Rust提供了类型注解,这表明我们希望在此次函数调用将Dog类型视为Animal,从而调用在Dog上实现的Animal trait中的baby_name方法。

  通常完全限定语法定义如下:

rust 复制代码
<Type as Trait>::function(receiver_if_method, next_arg, ...);

  对于不是方法的关联函数,并没有一个receiver:故而只会有其他参数的列表。可以选择在任何函数或方法调用处使用完全限定语法。然而,允许省略任何Rust能够从程序中的其他信息中计算出的部分。只有当存在多个同名实现而Rust需要帮助以便知道我们希望调用哪个实现时,才需要使用这个较为冗长的语法。

2.4 使用超trait

  有时我们可能会需要编写一个依赖另一个trait的trait定义:对于一个实现了第一个trait的类型,你希望要求这个类型也实现了第二个trait。如此就可使trait定义使用第二个trait的关联项。这个所需的trait是我们实现的trait的超trait supertrait。

  例如我们希望创建一个带有outline_print方法的trait OutlinePrint,它会将给定的值格式化为带有星号框。给定一个实现了标准库Display trait的并返回(x, y)的Point,当我们对一个x为1,y为3的Point实例调用outline_print时,它应该打印出如下内容:

txt 复制代码
**********
*        *
* (1, 3) *
*        *
**********

  在outline_print的实现中,我们希望使用Display trait的功能。因此,需要说明OutlinePrint trait仅适用于那些实现了Display并提供OutlinePrint所需功能的类型。可以在trait定义中指定OutlinePrint: Display来做到这一点。这种技术类似于为trait增加trait约束。

rust 复制代码
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,因而可以使用自动为任何实现了Display的类型提供的to_string方法。如果我们在没有在trait名称后添加冒号并指定Display trait的情况下尝试使用to_string,就会出现错误,提示在当前作用域中未为类型&self找到名为to_string的方法。

rust 复制代码
struct Point {
	x: i32,
	y: i32,
}

impl OutlinePrint for Point {}

  错误说Display没有在Point上实现,我们在Point上实现Display并满足OutlinePrint要求的限制:

rust 复制代码
use std::fmt;

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

  现在可以在Point实例上调用outline_print方法了。

2.5 使用newtype模式在外部类型上实现外部trait

  孤儿规则orphan rule,它规定只有当trait或类型至少有一方或两者都对于crate是本地时,才能在该类型上实现该trait。一个绕开这个限制的方法是使用newtype模式,它涉及在一个元组结构体中创建一个新类型。这个元组结构体带有一个字段作为希望实现该trait的类型的简单封装。由于这个封装类型对于crate是本地的,这样就可以在这个封装上实现trait。Newtype是一个源自Haskell编程语言的概念。使用这个模式没有运行时性能惩罚,这个封装类型在编译时就被省略了。

  例如,如果想要在Vec<T>上实现Display,而孤儿规则组织我们直接这么做,因为Display trait和Vec<T>都定义于我们的crate之外。可以创建一个包含Vec<T>实例的Wrapper结构体,在这个结构体上实现Display:

rust 复制代码
use std::fmt;

struct Wrapper(Vec<String>);

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

  这种做法的缺点在于因为Wrapper是一个新类型,它并不具备其所封装值的方法。必须直接在Wrapper上实现Vec<T>的所有方法,这样就可以代理到self.0上,这就允许我们完全像Vec<T>那样对待Wrapper。如果希望新类型拥有其内部类型的每一个方法,未封装类型实现Deref trait并返回其内部类型是一种解决方案。如果不希望封装类型拥有所有内部类型的方法------比如为了限制封装类型的行为------则只需自行实现所需的方法即可。

3. 高级类型

3.1 使用newtype模式实现类型安全与抽象

  newtype模式还可用于一些其他任务,包括静态地确保值不会混淆以及标注值的单位。比如Millimeters和Meters。

  newtype模式也可以用于抽象掉某个类型的部分实现细节:新的类型可以暴露与其私有内部类型不同的共有API。

  newtype模式还可以隐藏内部实现。

3.2 类型同义词与类型别名

  Rust提供了声明类型别名type alias的能力,使用type关键字为现有类型赋予另一个名字。

rust 复制代码
type Kilometers = i32;

  这意味着Kilometers是i32的同义词synonym;Kilometers并不是一个新的、单独的类型。Kilometers类型的值将被完全当作i32类型值来对待:

rust 复制代码
type Kilometers = i32;

fn main() {
	let x: i32 = 5;
	let y: Kilometers = 5;
	println!("x + y = {}", x + y);
}

  类型别名无法获得newtype模式提供的类型检查的好处。

  类型别名的主要用途是减少重复。

rust 复制代码
Box<dyn Fn() + Send + 'static>

  在函数签名和类型注解中到处书写这个冗长的类型既乏味又容易出错。

rust 复制代码
fn takes_long_type(f: Box<dyn Fn() + Send + 'static>) {
	// 省略
}

fn returns_long_type(f: Box<dyn Fn() + Send + 'static>) {
	// 省略
}

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

  下面为这个冗长的类型引入名为Thunk的别名,并可以使用更简洁的Thunk来替换所有使用该类型的地方。

rust 复制代码
type Thunk = Box<dyn Fn() + Send + 'static>;

fn takes_long_type(f: Thunk) {
	// 省略
}
fn returns_long_type(f: Thunk) {
	// 省略
}

fn main() {
	let f: Thunk = Box::new(|| println!("hi"));
}

  为类型别名选择一个好名字可以帮助你表达意图。

  类型别名也经常与Result<T, E>结合来减少重复。

rust 复制代码
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>;
}

  这里重复了很多次Result<..., Error>。可以声明类型别名:

rust 复制代码
type Result<T> = std::result::Result<T, std::io::Error>;

  使用了类型别名之后:

rust 复制代码
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<()>;
}

3.3 从不返回的never type

  Rust一个叫做!的特殊类型。在类型理论术语中被称为empty type,因为它没有值。我们更倾向于称之为never type。这个名字描述了它的作用:在函数从不返回的时候充当返回值。

rust 复制代码
fn bar -> ! {
	// 省略
}

  这段代码可以读作函数bar从不返回,而从不返回的函数被称为发散函数diverging functions。不能创建!类型的值,所以bar也不可能返回值。

  之前猜数字游戏的代码中:

rust 复制代码
fn main() {
	// 省略
	let guess: u32 = match guess.trim().parse() {
		Ok(num) => num,
		Err(_) => continue,
	};
	// 省略
}

  match的各个分支必须返回相同的类型。所以下面的代码是行不通的:

rust 复制代码
fn main() {
	// 省略
	let guess = match guess.trim().parse() {
		Ok(_) => 5,
		Err(_) => "hello",
	};
	// 省略
}

  这里guess必须既是整型也是字符串,而Rust要求Guess只能是一个类型。那么continue返回了什么?

  continue的值是!。当Rust要计算guess类型时,它会查看两个分支。前者是u32值,后者是!值。因为!类型永远不会有值,Rust决定guess的类型是u32。

  描述这种行为的正式方式是,类型为!的表达式可以被强制转换为任意其他类型。之所以允许match分支以continue结束是因为continue并不真正返回值;相反它把控制权交回上层循环,所以在Err的情况,事实上并未对guess进行赋值。

  never type在panic!宏中也很有用。Option<T>上的unwrap函数:

rust 复制代码
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并不返回一个值,所以代码是有效的。

  最后一个有着!类型的表达式是loop:

rust 复制代码
fn main() {
	print!("forever ");
	loop {
		print!("and ever ");
	}
}

  循环永远不会结束,所以此表达式的值是!。只要有break,loop就获得了具体的返回值类型,不再是发散类型!了。

3.4 动态大小类型和Sized trait

  Rust需要知道有关类型的某些细节,例如为特定类型的值需要分配多少空间。这便是起初留下的一个类型系统中令人迷惑的角落:动态大小类型dynamically sized types的概念。这有时被称为"DST"或"unsized types",它们让我们能够编写使用那些只有在运行时才能知道大小的值的代码。

  深入研究本书中一直在使用的动态大小类型str的细节。单独的str就是一个DST。直到运行时我们都不知道字符串有多长。我们无法在编译时知道字符串的长度,这意味这我们无法创建str类型的变量,也不能获取str类型的参数。下面的代码不能工作:

rust 复制代码
fn main() {
	let s1: str = "Hello there!";
	let s2: str = "How's it going?";
}

  Rust需要知道应该为特定类型的值分配多少内存,同时所有同一类型的值必须使用相同数量的内存。如果允许编写这样的代码,也就意味着这两个str需要占用完全相同大小的空间。不过它们有着不同的长度:s1需要12字节存储,s2需要15字节。这也就是为什么不可能创建一个存放动态大小类型的变量的原因。

  s1和s2的类型是&str而不是str。slice数据结构仅仅存储了开始位置和slice长度。虽然&T是一个储存了T所在的内存位置的单个值,&str则是两个值:str的地址和长度。这样,&str就有了一个在编译时可以知道的大小:它是usize长度的两倍。一般来说,这就是Rust使用动态大小类型的方式:它们有一些额外的元信息来储存动态信息的大小。这引出了动态大小类型的黄金法则:必须将动态大小类型的值置于某种指针之后。

  可以将str与各种指针类型组合使用。之前trait也与指针结合起来使用。每个trait本身也是一个动态大小类型,我们可以通过trait的名字来引用它。为了把trait用作trait object,必须把它放在某种指针之后,比如&dyn Trait或者Box<dyn Trait>。

  为了处理DST,Rust提供了Sized trait来决定一个类型的大小是否在编译时可知。该trait会自动为所有在编译时大小已知的类型实现。此外,Rust隐式地为每一个泛型函数增加了Sized约束。对于如下泛型函数定义:

rust 复制代码
fn generic<T>(t: T) {
	// 省略
}

  实际上,被当作如下内容来处理:

rust 复制代码
fn generic<T: Sized>(t: T) {
	// 省略
}

  默认情况下,泛型函数只能用于在编译时大小已知的类型。然而,你可以使用如下特殊语法来放宽这一限制:

rust 复制代码
fn generic<T: ?Sized>(t: &T) {
	// 省略
}

  ?Sized这个trait约束表明T可以是Sized,也可以不是Sized,同时这个注解会覆盖泛型类型必须在编译时拥有固定大小的默认规则。具有该含义的?Trait的语法仅适用于Sized。

  此外,t参数的类型从T变为&T,因为其类型可能不是Sized的,需要将其置于某种指针之后。

4. 高级函数与闭包

4.1 函数指针

  可以将普通函数传递给函数!这个技术在我们希望传递已经定义函数而不是重新定义闭包作为参数时很有用。函数会被强制转换为fn类型。fn被称为函数指针。通过函数指针允许我们使用函数作为其它函数的参数。

  指定参数为函数指针的语法类似于闭包:

rust 复制代码
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}");
}

  do_twice中的f被指定为一个接受一个i32参数并返回i32的fn。fn是一个类型,直接指定fn作为参数。

  函数指针实现了所有三个闭包trait------Fn、FnMut、FnOnce,所以总是可以在调用期望闭包的函数时传递函数指针作为参数。倾向于编写使用泛型和闭包trait的函数,这样它就能接受函数或闭包作为参数。

  尽管如此,一个只期望接受fn而不接受闭包的情况的例子是与不存在闭包的外部代码交互时:C语言的函数可以接受函数作为参数,但C语言没有闭包。

  作为一个既可以使用内联闭包又可以使用命名函数的例子,标准库Iterator trait提供的map方法。使用map可以将vector中每个元素进行转换:

rust 复制代码
fn main() {
	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的参数来替代闭包:

rust 复制代码
fn main() {
	let list_of_numbers = vec![1, 2, 3];
	let list_of_strings = Vec<String> = 
		list_of_numbers.iter().map(ToString::to_string).collect();
}

  这里必须使用完全限定语法,因为存在多个叫做to_string的函数。这里使用了定义于ToString trait的to_string函数,标准库为所有实现了Display的类型实现了这个trait。

  枚举中定义的每一个枚举成员也是构造函数。可以使用这些构造函数作为实现了闭包trait的函数指针,这意味着只可以指定构造函数作为接受闭包的方法的参数:

rust 复制代码
enum Status {
	Value(u32),
	Stop,
}

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

  这里通过Status::Value的初始化函数,对map所作用的范围内每个u32值创建Status::Value实例。

4.2 返回闭包

  闭包表现为trait,这意味着不能直接返回闭包。对于大部分需要返回trait的场景中,可以使用实现了期望返回的trait的具体类型来替代函数的返回值。但是着不能作用于闭包,因为它们没有一个可返回的具体类型。例如当闭包从作用域捕获任何值时,就不允许使用函数指针fn作为返回类型。

  可以使用impl Trait语法。可以使用Fn、FnOnce和FnMut返回任何函数类型。

rust 复制代码
fn returns_closure() -> impl Fn(i32) -> i32 {
	|x| x + 1
}

  每个闭包也都有自己独特的类型。如果你需要处理多个签名相同但实现不同的函数,就需要为它们使用trait对象。

rust 复制代码
fn main() {
	let handlers = vec![returns_closure(), returns_initialized_closure(123)];
	for handler in handlers {
		let output = handler(5);
		println!("{output}");
	}
}

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

fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
	move |x| x + init
}

  这里有两个函数,returns_closure和returns_initialized_closure,它们都返回impl Fn(i32) -> i32。注意它们返回的闭包是不同的,即使它们实现了相同的类型。

  错误信息告诉我们每当返回一个impl Trait,Rust会创建一个独特的不透明类型opaque type,这是一个无法看清Rust为我们构建了什么细节的类型。所以即使这些函数都返回了实现了相同trait的闭包,Rust为我们生成的不透明类型也是不同的。这类似于Rust如何为不同的异步代码块生成不同的具体类型,即使它们有着相同的输出类型。使用trait object解决这个问题:

rust 复制代码
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
	Box::new(|x| x + 1)
}

fn returns_initialized_closure(init: i32) -> Box<dyn Fn(i32) -> i32> {
	Box::new(move |x| x + init)
}

5. 宏

  宏macro值的是Rust中一系列的功能:使用macro_rules!的声明宏declarative macro,和三种过程宏procedural macro:

  1. 自定义#derive宏,用于在结构体和枚举上通过添加derive属性生成代码。

  2. 类属性宏,定义可用于任何项的自定义属性。

  3. 类函数宏,看起来像函数,但操作的是作为其参数传递的token。

5.1 宏和函数的区别

  宏是一种为写其他代码而写代码的方式,即所谓的元编程metaprogramming。所有的这些宏以展开的方式来生成比你所手写出的更多的代码。

  元编程对于减少大量编写和维护的代码是非常有用的,它也扮演了函数所扮演的角色。但宏有一些函数所没有的附加能力。

  一个函数签名必须声明函数参数的数量和类型。相比之下,宏能够接收可变数量的参数:用一个参数调用println!("hello")或用两个参数调用println!("hello {}", name)。而且,宏可以在编译器解析代码前展开,例如,宏可以在一个给定类型上实现trait。而函数则不行,因为函数是在运行时被调用,而trait需要在编译时实现。

  实现宏的缺点是与函数的定义相比宏的定义更复杂,因为你正在编写生成Rust代码的Rust代码。由于这样的间接性,宏定义通常要比函数定义更难阅读、理解和维护。

  宏与函数的最后一个重要的区别是:在一个文件里调用宏之前必须定义它,或将其引入作用域,而函数则可以在任何地方定义和调用。宏的定义在使用代码前面,函数可以没有这个顺序。函数要导入的时候宏也要导入。

5.2 用macro_rules!编写用于通用元编程的声明宏

  Rust最常用的宏形式是声明宏declarative macros。它们有时也被称为macros by example、macro_rules!宏或者macros。其核心概念是,声明宏允许我们编写一些类似Rust match表达式的代码。宏也将一个值和包含相关代码的模式进行比较:此种情况下,该值是传递给宏的Rust源代码字面值;模式用于和前面提到的源代码字面值进行比较,一旦匹配成功,每个模式的相关代码会替换传递给宏的代码。所有这一切都发生于编译时。

  可以使用macro_rules!定义宏。以vec!宏为例:

rust 复制代码
fn main() {
	let v: Vec<u32> = vec![1, 2, 3];
}

  可以使用vec!宏来构造两个整数的vector或者五个字符串slice的vector。但却无法使用函数做相同的事情,因为我们无法预先知道参数值的数量和类型。

  下面展示vec!宏的一个简化过的定义:

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

  标准库中实际定义的vec!包括预分配正确数量内存的代码。这部分为代码优化,没有包含在内。

  #macro_export注解表明只要导入了定义这个宏的crate,该宏就应该是可用的。如果没有该注解,这个宏不能被引入作用域。

  接着使用macro_rules!和宏名称开始宏定义,且所定义的宏并不带感叹号。名字后跟大括号表示宏定义体。

  vec!宏的结构和match表达式的结构类似。此处有一个分支模式( ( x:expr ),* ),后跟=>以及和模式相关的代码块。如果模式匹配,该相关代码块将被展开。鉴于这个宏只有一个模式,那就只有一个有效匹配方式,其他任何模式方向都会导致错误。更复杂的宏会有不止一个分支。

  宏定义中有效模式语法与之前的模式语法是不同的,因为宏匹配的是Rust代码结构不是值。宏模式语法在后面会讲。

  首先,一对括号包含了整个模式。我们使用美元符号在宏系统中声明一个变量来包含匹配该模式的Rust代码。美元符号明确表明这是一个宏变量而不是普通Rust变量。之后是一对括号,其捕获了符合括号内模式的值用以在替代代码中使用。()内则是x:expr,其匹配Rust的任意表达式,并将该表达式命名为$x。

  在()之后的逗号表示在每个与()内代码匹配的实例之间必须出现一个字面量逗号分隔符。紧随逗号之后的*说明该模式匹配零个或更多个*之前的任何模式。

  当以vec!1, 2, 3;调用宏时,$x模式与三个表达式1、2和3对应进行了三次匹配。

  与此分支模式相关联的代码块中的模式:在()\*部分,temp_vec.push(x)会针对模式中每次匹配到()的部分,生成零次或多次,取决于模式匹配到多少次。x由每个与之相匹配的表达式所替换。当以vec!1, 2, 3;调用该宏时,替换该宏调用所生成的代码是:

rust 复制代码
{
	let mut temp_vec = Vec::new();
	temp_vec.push(1);
	tmep_vec.push(2);
	temp_vec.push(3);
	temp_vec
}

5.3 用于从属性生成代码的过程宏

  第二种形式的宏被称为过程宏procedural macros,因为它们更像函数。过程宏接受Rust代码作为输入,在这些代码上进行操作,然后产生另一些代码作为输出,而非像声明式宏那样匹配对应模式然后以另一部分代码替换当前代码。有三种类型的过程宏:自定义派生宏derive,类属性和类函数,它们的工作原理都类似。

  创建过程宏时,其定义必须驻留在它们自己的具有特殊crate类型的crate中。这么做出于一些复杂的技术原因,将来我们希望能够消除这些限制。

rust 复制代码
use proc_macro::TokenStream;

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

  定义过程宏的函数接收一个TokenStream作为输入并生成TokenStream作为输出。TokenStream是定义于proc_macro crate里代表一系列token的类型,Rust默认携带了proc_macro crate。这就是宏的核心:宏所处理的源代码组成了输入TokenStream,宏生成的代码是输出TokenStream。函数上还有一个属性;这个属性指明了我们创建的过程宏的类型。在同一个crate中可以有多种过程宏。

5.4 自定义derive宏

  创建一个hello_macro crate,其中包含名为HelloMacro的trait和关联函数hello_macro。不同于让用户为其每一个类型实现HelloMacro trait,我们将会提供一个过程宏以便用户可以使用#derive(HelloMacro)注解它们的类型来得到hello_macro函数的默认实现。该默认实现会打印Hello, Macro! My name is TypeName!,其中TypeName为定义了trait的类型名。换言之,我们会创建一个crate,使程序员能够写成下面的代码。

rust 复制代码
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;

#[derive(HelloMacro)]
struct Pancakes;

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

  要实现上面的效果,先定义HelloMacro trait以及其关联函数:

rust 复制代码
pub trait HelloMacro {
	fn hello_macro();
}

  现在有了一个trait及其相关函数。可以通过实现该trait达到期望的功能:

rust 复制代码
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没有反射能力,因此其无法在运行时获取类型名。我们需要一个在编译时生成代码的宏。

  下一步是定义过程宏。在编写本部分时,过程式宏必须在其自己的crate内。该限制最终可能取消。crate及其宏crate的结构惯例如下:对于一个名为foo的crate,其自定义derive过程宏crate通常命名为foo_derive。

  由于两个crate紧密相关,因此在hello_macro包的目录下创建过程式宏的crate。如果改变在hello_macro中定义的trait,同时也必须改变在hello_macro_derive中过程宏的实现。这两个包需要分别发布,编程人员如果使用这些包,则需要同时添加两个依赖并将其引入作用域。我们也可以只用hello_macro包而将hello_macro_derive作为一个依赖,并重导出过程式宏的代码。但我们现在组织项目的方式使编程人员在无需derive功能时也能够单独使用hello_macro。

  我们需要声明hello_macro_derive crate为过程宏(proc-macro) crate。同时在它的Cargo.toml中声明依赖syn和quote。

toml 复制代码
[lib]
proc-macro = true

[dependencies]
syn = "3.0.3"
quote = "1.0.47"

  下面是定义过程宏的代码,放在hello-macro-derive/src/lib.rs,在没有添加impl_hello_macro函数的定义之前是无法编译:

rust 复制代码
use proc_macro::TokenStream;
use quote::quote;

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
	// 将Rust代码构建成我们可以操作的语法树
	let ast = syn::parse(input).unwrap();
	// 生成trait的实现
	impl_hello_macro(&ast)
}

  注意我们将代码分成了hello_macro_derive和impl_hello_macro两个函数,前者负责解析TokenStream,后者负责转换语法树:这使得编写过程宏更加方便。几乎你看到或者创建的每一个过程宏的外部函数中的代码都跟这里是一样的。你放入内部函数中的代码根据你的过程宏的设计目的会有所不同。

  在hello-macro-derive中引入了三个crate:proc-macro和syn和quote。Rust自带proc-macro crate,因此无需添加到Cargo.toml文件的依赖中。proc-macro crate是编译器提供的API,让我们能够在自己的代码中读取并操作Rust代码。

  syn crate将字符串中的Rust代码解析成为一个可以操作的数据结构。quote crate则将syn解析的数据结构转换回Rust代码。这些crate让解析任何我们所要处理的Rust代码变得简单:为Rust编写完整的解析器并不是一件简单的工作。

  当用户在一个类型上指定#derive(HelloMacro)时,hello_macro_derive函数将会被调用。我们已使用proc_macro_derive注解该函数并指定名称HelloMacro,该名称与我们的trait名称相匹配;这是大多数过程宏遵循的惯例。

  该函数首先将来自TokenStream的input转换为一个我们可以解释和操作的数据结构。这是syn派上用场的地方。syn中的parse函数获取一个TokenStream并返回一个表示解析出Rust代码的DeriveInput结构体。下面是从字符串struct Pancakes;中解析出来的DeriveInput结构体的相关部分:

rust 复制代码
DeriveInput {
	// 省略
	ident: Ident {
		ident: "Pancakes",
		span: #0 bytes(95..103),
	},
	data: Struct(
		DataStruct {
			struct_token: Struct,
			fields: Unit,
			semi_token: Some(
				Semi
			)
		}
	)
}

  该结构体字段表明:我们解析出的Rust代码是一个类单元结构体,它的标识符ident是Pancakes。这个结构体里还有更多字段,用来描述各种Rust代码。

  很快我们将定义impl_hello_macro函数,其用于构建所要包含在内的Rust新代码。但在此之前,注意其输出也是TokenStream。所返回的TokenStream会被加到我们的crate用户所写的代码中,因此,当用户编译他们的crate时,他们会通过修改后的TokenStream获取到我们所提供的额外功能。

  当调用syn::parse函数失败时,我们用unwrap来使hello_macro_derive函数panic。在错误时panic对过程宏来说是必须的,因为proc_macro_derive函数必须返回TokenStream而不是Result,以此来符合过程宏的API。这里选择用unwrap来简化这个例子;在生产代码中,则应该通过panic!或expect来提供关于发生何种错误的更加明确的错误信息。

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

  我们会得到一个Ident结构体实例,其中包含了注解类型的名字,这个值来自ast.ident。它被打印出来时,就是结构体的名称。

  quote!宏能让我们编写希望返回的Rust代码。quote!宏执行的直接结果并不是编译器所期望的所以需要转换为TokenStream。为此需要调用into方法,它会消费这个中间表示并返回所需的TokenStream类型值。

  这个宏还提供了一套很方便的模板机制;我们可以写#name,然后quote!会把它替换成名为name的变量的值。

  我们期望我们的过程式宏能够为通过#name获取的用户注解类型生成HelloMacro trait的实现。该trait的实现有一个函数hello_macro,其函数体包括了我们期望提供的功能。

  此处使用stringify!为Rust内置宏。其接收一个Rust表达式,如1 + 2,然后再编译时将表达式转换为一个字符串常量,如"1 + 2"。这与计算表达式并接着将结果转换为String的format!或println!不同。有一种可能的情况是,所输入的#name可能是一个需要打印的表达式,因此我们用stringify!。stringify!也能通过在编译时将#name转换为字符串字面值来节省一次内存分配。

  这是可以正确编译hello-macro和hello-macro-derive。对于一个新的二进制crate还是库crate。如果你没有将hello-macro和hello-macro-derive发布到crates.io上,可以在本地通过路径导入依赖:

toml 复制代码
[dependencies]
hello-macro = { path = "../hello-macro" }
hello-macro-derive = { path = "../hello-macro/hello-macro-derive" }

  路径要依据使用的项目到这两个文件夹的路径来给出。

5.5 类属性宏

  类属性Attribute-Like宏与自定义derive宏相似,不同之处在于它们不是为derive属性生成代码,而是允许你创建新的属性。它们也更为灵活;derive只能用于枚举和结构体;属性还可以用于其它的项,比如函数。作为一个使用属性宏的例子,可以创建一个名为route的属性用于注解web应用程序框架的函数:

rust 复制代码
#[route(GET, "/")]
fn index() {

  #route属性将由框架本身定义为一个过程宏。其宏定义的函数签名看起来像这样:

rust 复制代码
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {

  这里有两个TokenStream类型的参数;一个用于属性内容本身,也就是GET, "/"部分。第二个是属性所标记的项:在本例中,是fn index {}和剩下的函数体。

  类属性宏与自定义派生宏工作方式一致:创建proc-macro crate类型的crate并实现生成所希望代码的函数!

5.6 类函数宏

  类函数Function-Like宏的定义看起来像函数调用的宏。类似于macro_rules!,它们比函数更灵活;可以接收未知数量的参数。然而macro_rules!宏只能使用类匹配的语法定义。类函数宏获取TokenStream参数。一个类函数宏例子是可以像这样被调用的sql!宏:

rust 复制代码
let sql = sql!(SELECT * FROM posts WHERE id=1);

  这个宏会解析其中的SQL语句并检查是否是句法正确的,这是比macro_rules!可以做到更为复杂的处理。sql!宏会被类似于像这样定义:

rust 复制代码
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {

  这类似于自定义derive宏的签名:获取括号中的token,并返回希望生成的代码。

参考

1、高级特性

相关推荐
SomeB1oody3 小时前
【RustyML入门】2.13. 孤立森林
开发语言·后端·机器学习·rust·教程
DLYSB_4 小时前
Kafka 消费倾斜死锁与 Partition 掉队:我用 Rust 写了个“数据管道物理哨兵”,比 Grafana 报警快了 18 秒
rust·kafka·grafana·报警灯
SomeB1oody4 小时前
【RustyML入门】3.8. 正则化与归一化层
开发语言·后端·机器学习·rust·教程
SomeB1oody6 小时前
【RustyML入门】3.7. 循环层
开发语言·后端·机器学习·rust·教程
k4m7v2pz1 天前
Rust 长跑守护进程日志治理:切分、时区与结构化
开发语言·后端·rust·日志系统·日志轮转·ndjson
k4m7v2pz1 天前
Rust 高并发 WebSocket 连接管理:从线程地狱到 tokio 异步架构
websocket·架构·rust·并发编程·tokio
k4m7v2pz1 天前
Rust 规则热重载实战:用 serde_json + notify 告别重启
开发语言·rust·json·规则引擎·notify·热重载
rustfs1 天前
GitLab 如何与 RustFS 集成?
分布式·rust·gitlab
龙智DevSecOps解决方案1 天前
Perforce QAC + Klocwork 视频教程:Rust 与 C/C++ 混合项目静态分析演示
rust·静态分析·perforce·klocwork·qac·代码合规