6 深入类型

6.1 类型转换

使用as转换

rust不支持不同类型之间的比较:

1
2
3
4
5
6
7
8
fn main() {
let a: i32 = 10;
let b: u16 = 100;

if a < b {
println!("Ten is less than one hundred.");
}
}

为了让它们可以比较,可以使用 as 关键字进行显式类型转换(casting):

1
2
3
4
5
6
7
8
fn main() {
let a: i32 = 10;
let b: u16 = 100;

if a < b as i32 {
println!("Ten is less than one hundred.");
}
}

在这个例子中,需要将b显式转换为i32而不是将a转换为u16,这是因为每个类型能表达的数据范围不同,如果把范围较大的类型转换成较小的类型,比如:

1
2
3
4
5
fn main() {
// 0000 0000 0000 0000 0000 0001 0000 0100
let a: i32 = 260;
println!("{}", a as i8) // 0000 0100
}

会打印出4,而不是300,因为 i8 类型能表达的的最大值为 2^7 - 1,超出这个范围的位都会被抹去(保留符号位)。使用以下代码可以查看 i8 的最大值:

1
2
3
4
fn main() {
let a = i8::MAX;
println!("{}",a); // 127
}

下面是一些类型转换的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// #![allow(overflowing_literals)]
fn main() {
// -1_i8补码: 0b 1111 1111
println!(" -1 as a u8 is : {}", -1_i8 as u8);

// 浮点型不能直接转char
// println!("{}", 65.4321_f32 as char)
// 只有u8可以转换为char
// println!("{}", 65.4321_f32 as u16 as char)
println!("65.4321 as a u8 and as a char is : {}", 65.4321_f32 as u8 as char);

// 整数数据长度变小时,截断高位,保留低位
// 1000_i32补码:0b 0000 0011 1110 1000
println!("1000 as a u8 is : {}", 1000_i32 as u8);

// 当转换到有符号类型时,可以看做:先截断高位(如果数据长度变小),将其看作原码,然后再转换为补码
// 128_i32补码:0b 0000 0000 0000 0000 0000 0000 1000 0000
println!(" 128 as a i16 is: {}", 128_i32 as i16);
println!(" 128 as a i8 is: {}", 128_i32 as i8);

println!(" 232 as a i8 is : {}", 232_i32 as i8);

// char可以直接转为i8
println!(" 'a' as a i8 is: {}", 'a' as i8);

// 内存地址转换为指针
let mut values: [i32; 2] = [1, 2];
let p1: *mut i32 = values.as_mut_ptr();
// 将p1内存地址转换为一个整数
let first_address = p1 as usize;
// 4 == std::mem::size_of::<i32>,i32类型占用4个字节,因此将内存地址 + 4
let second_address = first_address + 4;
// 访问该地址指向的下一个整数p2,将整数转为指针
let p2 = second_address as *mut i32;
unsafe {
*p2 += 1;
}
assert_eq!(values[1], 3);
}

TryInto转换

如果你想要在类型转换上拥有完全的控制而不依赖内置的转换,例如处理转换错误,那么可以使用TryInto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use std::convert::TryInto;

fn main() {
let a: u8 = 10;
let b: u16 = 1500;

let b_: u8 = b.try_into().unwrap();

if a < b_ {
println!("Ten is less than one hundred.");
}
}
/* 运行时panic
thread 'main' panicked at 'msg: TryFromIntError(())', src\main.rs:5:30
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
*/

上面代码中引入了 std::convert::TryInto 特征,但是却没有使用它,主要原因在于如果你要使用一个特征的方法,那么需要引入该特征到当前的作用域中,不过std::prelude中已经包括了该特征,因此删掉第一行也不会出错。

try_into 会尝试进行一次转换,并返回一个 Result。此时就可以对其进行相应的错误处理(见错误处理)。这里为了快速测试,因此使用了 unwrap 方法,该方法在发现错误时,会直接调用 panic! 导致程序的崩溃退出。

try_into 转换会捕获大类型向小类型转换时导致的溢出错误,用match捕获:

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let b: i16 = 1500;

let b_: u8 = match b.try_into() {
Ok(b1) => b1,
Err(e) => {
println!("{:?}", e.to_string());
0
}
};
}

输出结果为 "out of range integral type conversion attempted",在这里程序捕获了错误,编译器告诉我们类型范围超出的转换是不被允许的,因为我们试图把 1500_i16 转换为 u8 类型,后者明显不足以承载这么大的值。

通用类型转换

下面会介绍一些更加复杂的类型转换,比如将一个结构体转换为另外一个结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Foo {
x: u32,
y: u16,
}

struct Bar {
a: u32,
b: u16,
}

fn reinterpret(foo: Foo) -> Bar {
let Foo { x, y } = foo;
Bar { a: x, b: y }
}

这是一种简单粗暴的方法。rust还为我们提供了更通用的方式来完成这个目的.

(1)强转

在一些情况下,rust支持类型自动强转(Type coercions),这些变化通常只是削弱类型,主要集中在指针和生命周期方面。它们的存在主要是为了让rust在更多的情况下正常工作,而且基本上是无害的。这些具体的规则可以参考Type coercions

as是强转的超集,任何允许自动强转的转换都可以由类型强制转换操作符as来显式执行。as强转需要符合强转规则,任何不符合强转规则的转换都会在编译时报错。

显式强转通常围绕着原始指针和原始数字类型。尽管它们很危险,但这些转换在运行时是不会出错的。如果一个强转触发了一些微妙的边界条件,也不会有任何迹象表明发生了这种情况,强转仍会成功。也就是说,强转必须在类型的级别上有效(符合规则),否则会在编译时被静态地阻止。例如,7u8 as bool编译会出错。

因此, 强转并不是unsafe的,因为它们本身通常不会违反内存安全。例如,将一个整数转换为一个原始指针很容易导致可怕的事情,然而,创建指针的行为本身是安全的,因为实际使用一个原始指针已经被标记为unsafe

强转不是递归的,也就是说,即使e as U1 as U2是一个有效的表达式,e as U2也不一定是。

在匹配特征时,不会做强制转换(除了接收器),如果某个类型U有一个impl,而T可以强转到U,这并不构成T的实现。换句话说,一个类型 T 可以强制转换为 U,不代表 impl T 可以强制转换为 impl U。例如,下面的内容不会通过类型检查,尽管将t强转到&T是可以的,并且有针对&Timpl

1
2
3
4
5
6
7
8
9
10
trait Trait {}

fn foo<X: Trait>(t: X) {}

impl<'a> Trait for &'a i32 {}

fn main() {
let t: &mut i32 = &mut 0;
foo(t);
}

报错如下:

1
2
3
4
5
6
7
8
9
10
error[E0277]: the trait bound `&mut i32: Trait` is not satisfied
--> src\main.rs:9:9
|
9 | foo(t);
| --- ^ the trait `Trait` is not implemented for `&mut i32`
| |
| required by a bound introduced by this call
|
= help: the trait `Trait` is implemented for `&'a i32`
= note: `Trait` is implemented for `&i32`, but not for `&mut i32`

&i32 实现了特征 Trait&mut i32 可以转换为 &i32,但是 &mut i32 依然无法作为 Trait 来使用。

(2)点操作符

点操作符(.)会执行许多类型转换的神奇操作。方法查找的详细机制可以参考Method lookup。简单描述为:假设我们有一个方法foo,它有一个接收器(一个self&self&mut self参数)。如果我们调用value.foo(),编译器需要确定Self是什么类型,然后才能调用该函数的正确实现。在这个例子中,我们假设 value 拥有类型 T

我们使用完全限定语法(后面会介绍,见完全限定语法)来更清楚地说明我们到底是在哪个类型上调用一个函数。

  • 首先,编译器会检查是否可以直接调用T::foo(value)。这被称为按值方法调用(by value)。
  • 如果它不能调用这个函数(例如,如果这个函数的类型不对,或者一个特征没有为Self实现),那么编译器就会尝试添加一个自动引用。这意味着编译器会尝试<&T>::foo(value)<&mut T>::foo(value)。这被称为自动引用方法调用(autoref)。
  • 如果这些候选方法都不奏效,它就对T解引用并再次尝试。这使用了Deref特性——如果T: Deref<Target = U>,那么它就用U而不是T类型再试。如果它不能解除对T的引用,它也可以尝试 unsizingT。这只是意味着,如果T在编译时有一个已知的大小参数,那么在解析方法时它就会“忘记”它。例如,这个 unsizing 步骤可以通过“忘记”数组的大小将[i32; 2]转换成[i32]

下面是一个方法查找算法的例子:

1
2
let array: Rc<Box<[T; 3]>> = ...;
let first_entry = array[0];

编译器是如何实际计算array[0]的呢?首先,array[0]实际上只是Index特性的语法糖——编译器会将array[0]转换成array.index(0)。接下来,编译器检查array是否实现了Index,这样它就可以调用这个函数。

然后,编译器检查Rc<Box<[T; 3]>>是否实现了Index,但它没有,&Rc<Box<[T; 3]>>&mut Rc<Box<[T; 3]>>也没有。由于这些方法都不起作用,编译器将Rc<Box<[T; 3]>解引用到Box<[T; 3]>中,并再次尝试。Box<[T; 3]>&Box<[T; 3]>&mut Box<[T; 3]>没有实现Index,所以它再次解引用。[T; 3]和它的自动引用也没有实现Index。它不能再继续解引用[T; 3],所以编译器“忘记”了它的大小,得到了[T]。最后,[T]实现了Index,所以它现在可以调用实际的index函数。

考虑一个更复杂的例子:

1
2
3
fn do_stuff<T: Clone>(value: &T) {
let cloned = value.clone();
}

cloned是什么类型?首先,编译器检查是否可以按值调用。value的类型是&T,所以clone函数的签名是fn clone(&T) -> T。由于在这个方法中,约束了T实现了Clone,因此 cloned 的类型是 T

如果取消T: Clone的约束,会发生什么?

1
2
3
fn do_stuff<T>(value: &T) {
let cloned = value.clone();
}

它将不能按值调用,因为T没有实现Clone。所以编译器会尝试通过自动搜索来调用。在这种情况下,该函数的签名是fn clone(&&T) -> &T,因为Self = &T。编译器发现&T实现了Clone(所有的引用类型都可以被复制,因为其实就是复制一份地址),然后推断出cloned: &T,所以,cloned&T 类型。最终,我们复制出一份引用指针,这很合理,因为值类型 T 没有实现 Clone,只能去复制一个指针了。

下面是另一个例子,自动搜索行为被用来创造一些微妙的效果:

1
2
3
4
5
6
7
#[derive(Clone)]
struct Container<T>(Arc<T>);

fn clone_containers<T>(foo: &Container<i32>, bar: &Container<T>) {
let foo_cloned = foo.clone();
let bar_cloned = bar.clone();
}

foo_clonedbar_cloned是什么类型?

复杂类型派生 Clone 的规则是:一个复杂类型能否派生 Clone,需要它内部的所有子类型都能进行 Clone。因此 Container<T>(Arc<T>) 是否实现 Clone 的关键在于 T 类型是否实现了 Clone 特征。

根据这个规则,由于i32 实现了Clone,因此Container<i32>实现了 Clone,所以编译器按值调用clone,得到foo_cloned 的类型是 Container<i32>

然而,bar_cloned实际上有&Container<T>类型。这肯定是不合理的,由于我们给Container添加了#[derive(Clone)],所以它必须实现Clone。看看由derive宏最终生成的代码(大致):

1
2
3
4
5
impl<T> Clone for Container<T> where T: Clone {
fn clone(&self) -> Self {
Self(Arc::clone(&self.0))
}
}

可以看出,派生 Clone 能实现的根本是 T 实现了Clone特征where T: Clone, 因此 Container<T> 就没有实现 Clone 特征。编译器接着查看&Container<T>是否实现了Clone,最终发现它实现了(所有的引用类型都可以被复制)。因此,它推断出clone是由 autoref 调用的,所以bar_cloned的类型是&Container<T>

我们可以通过手动实现Clone而不需要T实现Clone来解决这个问题:

1
2
3
4
5
impl<T> Clone for Container<T> {
fn clone(&self) -> Self {
Self(Arc::clone(&self.0))
}
}

现在,在尝试值方法调用时即可通过,因此 bar_cloned 的类型是 Container<T>

(3)Transmutes

接下来会介绍一种非常可怕的方法,这可能是你在rust中所能做的最可怕的不安全的事情,而这基本不设防。

mem::transmute接收一个T类型的值并将其重新解释为U类型。唯一的限制是TU被验证为占用相同大小的字节数。这所导致的未定义行为是令人难以置信的:

  • 首先,创建一个具有无效状态的任何类型的实例都会导致无法真正预测的任意混乱。即使根本不会去使用该bool 类型,也不要把3转化为bool
  • Transmute有一个重载的返回类型。如果你不指定返回类型,它可能会为了满足类型推导而返回一个令人惊讶的类型。
  • 将一个&转为&mut是未定义行为,尽管某些用法可能是安全的,但是需要注意,rust优化器可以自由地假设一个共享引用在它的生命周期内是不变的,而这种转换会违反这个假设。因此:
    • 将一个&转为&mut总是未定义行为
    • 不,你不能这样做
    • 不,你并不特别
  • Transmute到一个没有明确提供生命周期的引用会产生一个无限制的生命周期(Unbounded Lifetimes)
  • 当在不同的复合类型之间转换时,你必须确保它们的布局是一样的!如果布局不同,错误的字段就会被填入错误的数据,那么你怎么知道布局是否相同呢?对于repr(C)类型和repr(transparent)类型,布局是精确定义的。但是对于普通的repr(Rust)来说,它不是。即使是同一个通用类型的不同实例也可以有截然不同的布局。Vec<i32>Vec<u32>可能有相同的字段顺序,也可能没有。数据布局保证了什么,或者没保证什么的细节可以参考 UCG 工作组(Unsafe Code Guidelines Reference),然而到本篇文章发布之前,这部分的内容仍然处在WIP(work-in-progress)阶段。

进一步的,mem::transmute_copymem::transmute更不安全。它把size_of<U>字节从T中复制出来,并把它们解释为Umem::transmute尚可保证两个数据的内存大小一致,现在连这个限制也没有了(因为复制出一个前缀可能是有效的,也可能无效),不过UT大还是未定义的行为。

当然,你也可以使用原始指针转换或union来获得这些函数的所有功能,并关闭编译器提示Lint或其它基本的合理性检查。原始指针转换和union并不能避免上述规则。

下面列举两个有用的 transmute 应用场景:

  • 将裸指针变成函数指针:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    fn foo() -> i32 {
    0
    }

    let pointer = foo as *const ();
    let function = unsafe {
    // 将裸指针转换为函数指针
    std::mem::transmute::<*const (), fn() -> i32>(pointer)
    };
    assert_eq!(function(), 0);
  • 延长生命周期,或者缩短一个静态生命周期寿命:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    struct R<'a>(&'a i32);

    // 将 'b 生命周期延长至 'static 生命周期
    unsafe fn extend_lifetime<'b>(r: R<'b>) -> R<'static> {
    std::mem::transmute::<R<'b>, R<'static>>(r)
    }

    // 将 'static 生命周期缩短至 'c 生命周期
    unsafe fn shorten_invariant_lifetime<'b, 'c>(r: &'b mut R<'static>) -> &'b mut R<'c> {
    std::mem::transmute::<&'b mut R<'static>, &'b mut R<'c>>(r)
    }

6.2 高级类型

rust的类型系统有一些我们曾经提到但没有讨论过的功能。

newtype

newtype可以用于一些其他我们还未讨论的功能,包括静态的确保某值不被混淆,和用来表示一个值的单位。

简单来说,就是使用元组结构体的方式将已有的类型包裹起来:struct Meters(u32),那么此处 Meters 就是一个 newtype

使用newtype的好处如下:

  • 自定义类型可以让我们给出更有意义和可读性的类型名,例如与其使用 u32 作为距离的单位类型,我们可以使用 Meters,它的可读性要好得多。
  • 对于某些场景,newtype也可以用于抽象掉一些类型的实现细节:例如,封装类型可以暴露出与直接使用其内部私有类型时所不同的公有 API。
  • newtype可以隐藏其内部的泛型类型。例如,可以提供一个封装了 HashMap<i32, String>People 类型,用来储存人名以及相应的 ID。使用 People 的代码只需与提供的公有 API 交互即可,比如向 People 集合增加名字字符串的方法,这样这些代码就无需知道在内部我们将一个 i32 ID 赋予了这个名字了。newtype 模式是一种实现封装部分所讨论的隐藏实现细节的更轻量级方法。

类型别名

rust提供了声明类型别名(type alias)的能力,使用 type 关键字来给予现有类型另一个名字。例如,可以像这样创建 i32 的别名 Kilometers

1
2
3
4
// 类型别名
type Kilometers = i32;
// newtype
struct Meters(u32)

这意味着 Kilometersi32 的同义词(synonym)。与``struct Meters(u32)不同,Kilometers 不是一个新的、单独的类型。Kilometers类型的值将被完全当作i32` 类型值来对待:

1
2
3
4
5
6
type Kilometers = i32;

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

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

因为 Kilometersi32 的别名,它们是同一类型,可以将 i32Kilometers 相加,也可以将 Kilometers 传递给获取 i32 参数的函数。但通过这种手段无法获得上一部分讨论的 newtype所提供的类型检查的好处。换句话说,如果在哪里混用 Kilometersi32 的值,编译器也不会给出错误。

类型别名的主要用途是减少重复。例如,可能会有这样很长的类型:

1
Box<dyn Fn() + Send + 'static>

在函数签名或类型注解中每次都书写这个类型将是枯燥且易于出错的。使用类型别名可以减少项目中重复代码的数量来使其更加易于控制。比如这段代码:

1
2
3
4
5
6
7
8
9
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--
}

可以改写为:

1
2
3
4
5
6
7
8
9
10
11
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--
}

为类型别名选择一个好名字也可以帮助你表达意图。可以看出,这与C/C++的typedef有异曲同工之处。

类型别名也经常与 Result<T, E> 结合使用来减少重复。考虑一下标准库中的 std::io 模块。I/O 操作通常会返回一个 Result<T, E>,因为这些操作可能会失败。标准库中的 std::io::Error 结构体代表了所有可能的 I/O 错误。std::io 中大部分函数会返回 Result<T, E>,其中 Estd::io::Error,比如 Write 特征中的这些函数:

1
2
3
4
5
6
7
8
9
10
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>。为此,std::io 有这个类型别名声明:

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

因为这位于 std::io 中,可用的完全限定的别名是 std::io::Result<T>;也就是说,Result<T, E>E 放入了 std::io::ErrorWrite 特征中的函数最终看起来像这样:

1
2
3
4
5
6
7
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 中提供了一致的接口。因为这是一个别名,它只是另一个 Result<T, E>,这意味着可以在其上使用 Result<T, E> 的任何方法,以及像 ? 这样的特殊语法。

永不返回类型

发散函数!一节中,曾经介绍过 !! 用来说明一个函数永不返回任何值。在类型理论术语中,它被称为 empty type,因为它没有值。我们更倾向于称之为 never type。这个名字描述了它的作用:在函数从不返回的时候充当返回值。

1
2
3
fn bar() -> ! {
// --snip--
}

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

不过一个不能创建值的类型有什么用呢?在模式匹配章节中,我们忽略了一个问题,那就是match 的分支必须返回相同的类型。如下代码不能工作:

1
2
3
4
let guess = match guess.trim().parse() {
Ok(_) => 5,
Err(_) => "hello",
};

将其中的"hello"改成continue就可以通过编译:

1
2
3
4
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};

这里的 guess 必须既是整型也是字符串,而rust要求 guess 只能是一个类型。那么 continue 返回了什么呢?为什么continue可以通过编译,允许一个分支返回 u32 而另一个分支却以 continue 结束呢?

正如你可能猜到的,continue 的值是 !。也就是说,当rust要计算 guess 的类型时,它查看这两个分支。前者是 u32 值,而后者是 ! 值。因为 ! 并没有一个值,rust决定 guess 的类型是 u32

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

never type 的另一个用途是 panic!,对于 Option<T> 上的 unwrap 函数,它产生一个值或 panic。这里是它的定义:

1
2
3
4
5
6
7
8
impl<T> Option<T> {
pub fn unwrap(self) -> T {
match self {
Some(val) => val,
None => panic!("called `Option::unwrap()` on a `None` value"),
}
}
}

这里还是相同的情况,rust知道 valT 类型,panic!! 类型,所以整个 match 表达式的结果是 T 类型。这能工作是因为 panic! 并不产生一个值,它会终止程序。对于 None 的情况,unwrap 并不返回一个值,所以这些代码是有效的。

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

1
2
3
4
5
print!("forever ");

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

这里,循环永远也不结束,所以此表达式的值是 !。但是如果在其中引入 break 就不是这样了,因为循环在执行到 break 后就会终止。

6.3 动态大小类型和Sized 特征

动态大小类型

之前学过的几乎所有类型,都是固定大小的类型,包括集合 VecStringHashMap 等,而动态大小类型刚好与之相反,编译器无法在编译期得知该类型值的大小,只有到了运行时才才知道大小的类型。我们称这种类型为动态大小类型DST(Dynamic Sized Type)或者unsized类型。

box允许创建递归类型中,我们曾经创建了一个递归的、在编译时无法知道具体的大小的DST。现在,让我们继续认识一些动态大小类型。

(1)str

首先要深入研究的是一个贯穿本书都在使用的动态大小类型:str。没错,不是 &str,而是 str 本身。str 是一个 DST;直到运行时我们都不知道字符串有多长。因为直到运行时都不能知道其大小,也就意味着不能创建 str 类型的变量,也不能获取 str 类型的参数。考虑一下这些代码,它们不能工作:

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

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

那么该怎么办呢?你已经知道了这种问题的答案:s1s2 的类型是 &str 而不是 str。切片数据结构仅仅储存了开始位置和切片的长度以及容量。所以虽然 &T 是一个储存了 T 所在的内存位置的单个值,&str 则是两个值:str 的地址和其长度。这样,&str 就有了一个在编译时可以知道的大小:它是 usize 长度的两倍(地址+长度占用的空间)。也就是说,我们总是知道 &str 的大小,而无论其引用的字符串是多长。这里是rust中动态大小类型的常规用法:它们有一些额外的元信息来储存动态信息的大小。这引出了动态大小类型的黄金规则:必须将动态大小类型的值置于某种指针之后。

可以将 str 与所有类型的指针结合:比如 Box<str>Rc<str>

(2)特征对象

事实上,我们在之前已经见过另一种与指针结合的DST了,那就是特征。每一个特征都是一个可以通过特征名称来引用的动态大小类型。在特征对象章节,我们提到了为了将特征用于特征对象,必须将它们放入指针之后,比如 &dyn TraitBox<dyn Trait>Rc<dyn Trait> 也可以)。

(3)数组切片

我们在前面说过需要使用切片的引用,对于数组来说,它也是DST。

1
2
3
fn main() {
let arr:[i32] = [1, 2, 3, 4, 5];
}

这里的[i32]就是数组切片,与str一样,这段代码无法通过编译,因为无法在编译期知道 [i32] 类型的大小,只有到了运行期才能动态获知,这对于强类型、强安全的rust语言来说是不可接受的。因此,我们要使用切片的引用来代替切片:

1
2
3
4
5
fn main() {
let arr = [1, 2, 3, 4, 5];

let s3: &[i32] = &arr[1..3];
}

这段代码就可以正常通过,原因在于这些切片引用的大小在编译器都是已知的。

Sized 特征

为了处理 DST,rust提供了 Sized 特征来决定一个类型的大小是否在编译时可知。这个特征自动为编译器添加在编译时就知道大小的类型实现。另外,rust隐式的为每一个泛型函数增加了 Sized 特征约束。也就是说,对于如下泛型函数定义:

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

实际上被当作如下处理:

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

这样,对于一个泛型参数 T,rust保证 T 是固定大小的类型。

几乎所有类型都实现了 Sized 特征,除了 str和特征。每一个特征都是一个可以通过名称来引用的动态大小类型。因此如果想把特征作为具体的类型来传递给函数,你必须将其转换成一个特征对象:诸如 &dyn Trait 或者 Box<dyn Trait> (还有 Rc<dyn Trait>)这些引用类型。

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

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

?Sized 特征约束意味着 T 可能是也可能不是 Sized。同时这个注解会覆盖泛型类型必须在编译时拥有固定大小的默认规则。这种意义的 ?Trait 语法只能用于 Sized ,而不能用于任何其它特征。还有一点要注意的是,函数参数类型从 T 变成了 &T,因为 T 可能是动态大小的,因此需要用一个固定大小的指针(引用)来包裹它。

6.4 枚举和整数

==todo==

7 高级特征

7.1 关联类型

关联类型(associated types)是一个将类型占位符与特征相关联的方式,这样特征的方法签名中就可以使用这些占位符类型。特征的实现者会针对特定的实现在这个占位符类型指定相应的具体类型。如此可以定义一个使用多种类型的特征,直到实现此特征时都无需知道这些类型具体是什么。

一个带有关联类型的特征的例子是标准库提供的 Iterator 特征,我们曾经在Iterator特征和next方法这一章见过它:

1
2
3
4
5
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// ...
}

Item 是一个占位符类型,同时 next 方法定义表明它返回 Option<Self::Item> 类型的值。这个特征的实现者会指定 Item 的具体类型,然而不管实现者指定何种类型,next 方法都会返回一个包含了此具体类型值的 Option

关联类型看起来像一个类似泛型的概念,因为它允许定义一个函数而不指定其可以处理的类型。让我们通过在一个 Counter 结构体上实现 Iterator 特征的例子来检视其中的区别。Self 用来指代当前调用者的具体类型,那么 Self::Item 就用来指代该类型实现中定义的 Item 类型。比如下面这个实现中指定了 Item 的类型为 u32

1
2
3
4
5
impl Iterator for Counter {
type Item = u32;

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

在上述代码中,我们为 Counter 类型实现了 Iterator 特征,对于 next 方法而言,Self 是调用者的具体类型: Counter,而 Self::ItemCounter 中定义的 Item 类型: u32

这个语法类似于泛型。那么为什么不用这种方式定义呢:

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

区别在于使用泛型时,则不得不在每一个实现中标注类型。这是因为我们也可以实现为 Iterator<String> for Counter,或任何其他类型,这样就可以有多个 CounterIterator 的实现。换句话说,当特征有泛型参数时,可以多次实现这个特征,每次需改变泛型参数的具体类型。接着当使用 Counternext 方法时,必须提供类型注解来表明希望使用 Iterator 的哪一个实现。

通过关联类型,则无需标注类型,因为不能多次实现这个特征。对于使用关联类型的定义,我们只能选择一次 Item 会是什么类型,因为只能有一个 impl Iterator for Counter。当调用 Counternext 时不必每次指定我们需要 u32 值的迭代器。当类型定义复杂时,这种写法可以极大地增加可读性。

关联类型也会成为特征契约的一部分:特征的实现必须提供一个类型来替代关联类型占位符。关联类型通常有一个描述类型用途的名字,并且在 API 文档中为关联类型编写文档是一个最佳实践。

7.2 默认泛型类型参数和运算符重载

当使用泛型类型参数时,可以为泛型指定一个默认的具体类型。为泛型类型指定默认类型的语法是在声明泛型类型时使用<PlaceholderType=ConcreteType>

这种情况的一个非常好的例子是使用运算符重载(Operator overloading),这是指在特定情况下自定义运算符(比如 +)行为的操作。

rust并不允许创建自定义运算符或重载任意运算符,不过 std::ops 中所列出的运算符和相应的特征可以通过实现运算符相关特征来重载。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 }
);
}

这里展示了如何在 Point 结构体上实现 Add 特征来重载 + 运算符,这样就可以将两个 Point 实例相加了。add 方法将两个 Point 实例的 x 值和 y 值分别相加来创建一个新的 PointAdd 特征有一个叫做 Output 的关联类型,它用来决定 add 方法的返回值类型。

这里默认泛型类型位于 Add 特征中。其定义如下:

1
2
3
4
5
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 特征时不指定 Rhs 的具体类型,Rhs 的类型将是默认的 Self 类型,也就是在其上实现 Add 的类型。

当为 Point 实现 Add 时,使用了默认的 Rhs,因为我们希望将两个 Point 实例相加。让我们看看一个实现 Add特征时希望自定义 Rhs 类型而不是使用默认类型的例子:

1
2
3
4
5
6
7
8
9
10
11
12
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))
}
}

这里有两个存放不同单元值的结构体,MillimetersMeters,它们使用了newtype。我们希望能够将毫米值与米值相加,并让 Add 的实现正确处理转换。可以为 Millimeters 实现 Add 并以 Meters 作为 Rhs。为了使 MillimetersMeters 能够相加,我们指定 impl Add<Meters> 来设定 Rhs 类型参数的值而不是使用默认的 Self

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

  • 扩展类型而不破坏现有代码。
  • 在大部分用户都不需要的特定情况进行自定义。

标准库的 Add特征就是一个第二个目的例子:大部分时候你会将两个相似的类型相加,不过它提供了自定义额外行为的能力。在 Add 特征定义中使用默认类型参数意味着大部分时候无需指定额外的参数。换句话说,一小部分实现的样板代码是不必要的,这样使用特征就更容易了。

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

7.3 完全限定语法

rust既不能避免一个特征与另一个特征拥有相同名称的方法,也不能阻止为同一类型同时实现这两个特征。甚至直接在类型上实现开始已经有的同名方法也是可能的。

不过,当调用这些同名方法时,需要告诉rust我们希望使用哪一个。考虑如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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 h = Human;
h.fly();
}

这里定义了PilotWizard 都拥有方法 fly。接着在一个本身已经实现了名为 fly 方法的类型 Human 上实现这两个特征。每一个 fly 方法都进行了不同的操作。当调用 Human 实例的 fly 时,编译器默认调用直接实现在类型上的方法,因此输出:*waving arms furiously*。 这证明了rust调用了直接实现在 Human 上的 fly 方法。

为了能够调用 Pilot 特征或 Wizard 特征的 fly 方法,我们需要使用更明显的语法以便能指定我们指的是哪个 fly 方法:

1
2
3
4
5
6
fn main() {
let person = Human;
Pilot::fly(&person); // This is your captain speaking.
Wizard::fly(&person); // Up!
person.fly(); // *waving arms furiously*
}

在方法名前指定特征名告诉rust我们希望调用哪个 fly 实现。也可以选择写成 Human::fly(&person)这等同于默认的person.fly(),不过如果无需消歧义的话这么写就有点长了。

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

然而,不是方法的关联函数没有 self 参数。当存在多个类型或者特征定义了相同函数名的非方法函数时,rust就不总是能计算出我们期望的是哪一个类型,比如这里创建了一个希望将所有小狗叫做Spot的动物收容所的特征。Animal特征有一个关联非方法函数 baby_name。结构体 Dog 实现了 Animal,同时又直接提供了关联非方法函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 上定义的关联函数 baby_name 的实现代码将所有的小狗起名为 SpotDog 类型还实现了 Animal 特征,它描述了所有动物的共有的特征。小狗被称为 puppy,这表现为 DogAnimal特征实现中与Animal特征相关联的函数 baby_name

main 调用了 Dog::baby_name 函数,它直接调用了定义于 Dog 之上的关联函数。这段代码会打印出:A baby dog is called a Spot。这并不是我们需要的。我们希望调用的是 DogAnimal 特征实现那部分的 baby_name 函数,这样能够打印出 A baby dog is called a puppy。不过,这次我们不能使用同样的套路了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
println!("A baby dog is called a {}", Animal::baby_name());
}
/*
error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
--> src\main.rs:20:43
|
2 | fn baby_name() -> String;
| ------------------------- `Animal::baby_name` defined here
...
20 | println!("A baby dog is called a {}", Animal::baby_name());
| ^^^^^^^^^^^^^^^^^ cannot call associated function of trait
|
help: use the fully-qualified path to the only available implementation
|
20 | println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
| +++++++ +
*/

因为 Animal::baby_name 没有 self 参数,同时这可能会有其它类型实现了 Animal 特征,rust无法计算出所需的是哪一个 Animal::baby_name 实现。在报错提示中,给出了解决方案。为了消歧义并告诉rust我们希望使用的是 DogAnimal 实现而不是其它类型的 Animal 实现,需要使用完全限定语法,这是调用函数时最为明确的方式。

1
2
3
fn main() {
println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}

我们在尖括号中向rust提供了类型注解,并通过在此函数调用中将 Dog 类型当作 Animal 对待,来指定希望调用的是 DogAnimal 特征实现中的 baby_name 函数。现在这段代码会打印出我们期望的数据:A baby dog is called a puppy

通常,完全限定语法定义为:

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

上面定义中,第一个参数是方法接收器 receiver (三种 self),只有方法才拥有,例如关联函数就没有 receiver

对于不是方法的关联函数,其没有 receiver,故只会有其它参数的列表(next_arg, ...)。可以选择在任何函数或方法调用处使用完全限定语法。然而,允许省略任何rust能够从程序中的其他信息中计算出的部分。

因此大多数时候,我们都无需使用完全限定语法。只有当存在多个同名实现而rust需要帮助以便知道我们希望调用哪个实现时,才需要使用这个较为冗长的语法。

7.4 父特征用于在另一个特征中使用某特征的功能

我们可以定义依赖于某个特征的特征,被依赖的特征被称为父特征,新定义的特征可以使用父特征中定义的功能。这部分的内容在深入理解Fn特征中曾经介绍过,故不再赘述。

7.5 在外部类型上实现外部特征

在介绍特征时提到过孤儿规则 Orphan Rule,它说明只要特征或类型对于当前crate是本地的话就可以在此类型上实现该 特征。一个绕开这个限制的方法是使用之前介绍过的newtype,它涉及到在一个元组结构体中创建一个新类型。这个元组结构体带有一个字段作为希望实现特征的类型的简单封装。接着这个封装类型对于crate是本地的,这样就可以在这个封装上实现特征。Newtype是一个源自 Haskell 编程语言的概念。使用这个模式没有运行时性能惩罚,这个封装类型在编译时就被省略了。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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(", "))
}
}

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

Display 的实现使用 self.0 来访问其内部的 Vec<T>,因为 Wrapper 是元组结构体而 Vec<T> 是结构体总位于索引0的项。接着就可以使用 WrapperDisplay 的功能了。

此方法的缺点是,任何数组上的方法,你都无法直接调用,需要先用 self.0 取出数组,然后再进行调用。因为 Wrapper 是一个新类型,它没有定义于其值之上的方法;必须直接在 Wrapper 上实现 Vec<T> 的所有方法,这样就可以代理到self.0 上 —— 这就允许我们完全像 Vec<T> 那样对待 Wrapper

如果希望新类型拥有其内部类型的每一个方法,为封装类型实现 Deref 特征(在之前实现 Deref 特征中介绍过)并返回其内部类型是一种解决方案,实现该特征后,可以自动做一层类似类型转换的操作,可以将 Wrapper 变成 Vec<String> 来使用。这样就会像直接使用数组那样去使用 Wrapper,而无需为每一个操作都添加上 self.0。如果不希望封装类型拥有所有内部类型的方法(比如为了限制封装类型的行为),则必须只自行实现(重载)所需的方法。

8 生命周期高级篇

rust通过生命周期来执行相关的规则。生命周期是指一个引用必须有效的代码区域,这些区域可能相当复杂,因为它们对应着程序中的执行路径。这些执行路径中甚至可能存在空洞(空洞是指一个引用的生命周期可能不是一个连续的代码区域,中间可能有跳跃),因为我们可能会先使一个引用失效,之后再重新初始化并使用它。包含引用(或假装包含)的类型也可以用生命周期来标记,这样rust就可以防止它们也被失效。

在之前的大多数例子中,生命周期将与作用域重合,这是因为我们的例子很简单。下面将介绍它们不重合的更复杂的情况。

8.1 引用和别名

我们知道,引用有两种类型:

  • 共享的引用(不可变引用):&
  • 可变引用:&mut

它们其实遵守以下规则:

  • 一个引用的生命周期不能超过它所引用对象的生命周期
  • 一个可变的引用不能有别名

在继续讨论之前,需要先介绍一下别名是什么。不幸的是,rust还没有真正定义其别名模型,在我们等待rust的设计者明确他们语言的语义时,先来讨论下在一般场景下别名到底是什么,以及它为什么重要。

注意,为了便于讨论,我们规定:

  • 我们将使用最广泛的别名定义。rust的定义可能会有更多限制,以考虑到可变性和有效性。
  • 我们将假设一个单线程的、无中断的执行,我们还将忽略像内存映射硬件这样的东西。rust假定这些事情不会发生,除非你明确告诉它会发生。

所以,我们现行的定义是:如果变量和指针指向内存的重叠区域,那么它们就是别名。

为什么我们需要关注别名呢?让我们看下这个例子:

1
2
3
4
5
6
7
8
9
fn compute(input: &u32, output: &mut u32) {
if *input > 10 {
*output = 1;
}
if *input > 5 {
*output *= 2;
}
// 记住一点: 如果 `input>10`,那么 `output` 永远为 `2`
}

我们希望能够把它优化成下面这样的函数:

1
2
3
4
5
6
7
8
9
10
11
12
fn compute(input: &u32, output: &mut u32) {
let cached_input = *input; // 将 `*input` 中的内容保存在寄存器中
if cached_input > 10 {
// 如果输入比 10 大, 优化之前的代码会将 output 设置为 1,然后乘以 2,
// 结果一定返回 `2` (因为 `>10` 包括了 `>5` 的情况),
// 因此这里可以进行优化,
// 不对 output 重复赋值,直接将其设置为 2
*output = 2;
} else if cached_input > 5 {
*output *= 2;
}
}

在rust中,这种优化应该是可行的。但对于几乎任何其它语言来说,它都不是这样的(除非是全局分析)。这是因为这个优化依赖于知道别名不会发生,而大多数语言在这方面是相当宽松的。具体来说,我们需要担心那些使“输入”和“输出”重叠的函数参数,如compute(&x, &mut x)

如果按照这样的输入,我们实际上执行的代码如下:

1
2
3
4
5
6
7
8
9
                    //  input ==  output == 0xabad1dea
// *input == *output == 20
if *input > 10 { // true (*input == 20)
*output = 1; // 同时覆盖了 input 引用的内容,因为它们实际上引用了同一块内存
}
if *input > 5 { // false (*input == 1)
*output *= 2;
}
// *input == *output == 1

我们的优化函数对于这个输入会产生*output == 2,所以在这种情况下,我们的优化就无法实现了。

不过,在rust中,我们知道这个输入是不可能的,因为&mut不允许被别名。所以我们可以安全地认为这种情况不会发生,并执行这个优化。在大多数其它语言中,这种输入是完全可能的,因此必须加以考虑。

这就是为什么别名分析很重要的原因:它可以让编译器进行有用的优化。比如:

  • 通过证明没有指针访问该值的内存来保持寄存器中的值
  • 通过证明某些内存在我们上次读取后没有被写入,来消除读取
  • 通过证明某些内存在下一次写入之前从未被读过,来消除写入
  • 通过证明读和写之间不相互依赖来对指令进行移动或重排序

这些优化也用于证明更大的优化的合理性,如循环矢量化、常数传播和死代码消除。

在前面的例子中,我们利用&mut u32不能被别名的事实来证明对*output的写入不可能影响*input。这让我们把*input缓存在一个寄存器中,省去了读的过程。

通过缓存这个读,我们知道在> 10分支中的写不能影响我们是否采取> 5分支,使我们在*input > 10时也能消除一个读-修改-写(加倍*output)。

关于别名分析,需要记住的关键一点是,写是优化的主要危险。也就是说,阻止我们将读移到程序的任何其它部分的唯一原因是我们有可能将其与写到同一位置重新排序。

例如,在下面这个修改后的函数中,我们不需要担心别名问题,因为我们已经将唯一一个写到*output的地方移到了函数的最后。这使得我们可以自由地重新排序在它之前发生的对*input的读取:

1
2
3
4
5
6
7
8
9
10
fn compute(input: &u32, output: &mut u32) {
let mut temp = *output;
if *input > 10 {
temp = 1;
}
if *input > 5 {
temp *= 2;
}
*output = temp;
}

我们仍然依靠别名分析来假设temp没有别名input,但是证明要简单得多:局部变量的值不能被在它被声明之前就存在的东西所别名。这是每一种语言都可以自由做出的假设,因此这个版本的函数可以在任何语言中按照我们想要的方式进行优化。

这就是为什么rust将使用的“别名”的定义可能涉及到一些有效性和可变性的概念:如果没有任何实际写入内存的情况发生,我们实际上并不关心别名是否发生。

当然,rust的完整别名模型还必须考虑到函数调用(可能会改变我们看不到的东西)、裸指针(它本身没有别名要求)和 UnsafeCell(它让&的引用被改变)等东西。

我们后面会通过一些例子去继续介绍这个规则,在开始之前,还需要先介绍如何手动标注分析生命周期。

8.2 手动标注分析生命周期

在一个函数体中,rust通常不需要你明确地命名所涉及的生命周期。这是因为一般来说,在本地环境中谈论生命周期是没有必要的;rust拥有所有的信息,并且可以尽可能地以最佳方式解决所有问题。rust还会引入许多匿名作用域和临时变量, 你不必显式写出它们,代码也可以跑通。

然而,一旦你跨越了函数的边界,你就需要开始考虑生命周期了。为了尝试理解生命周期,我们将假装我们被允许用生命周期来标记作用域。一个有趣的语法糖是,每个let语句都隐含地引入了一个作用域。在大多数情况下,这其实并不重要。然而,这对那些相互引用的变量来说确实很重要。作为一个简单的例子,让我们对这段简单的rust代码进行完全解糖:

1
2
3
4
5
fn main() {
let x = 0;
let y = &x;
let z = &y;
}

借用检查器总是试图最小化生命周期的范围,所以它很可能会脱糖为以下内容:

1
2
3
4
5
6
7
8
9
10
11
'a: {
let x: i32 = 0;
'b: {
// y 的生命周期为 'b
let y: &'b i32 = &'b x;
'c: {
// 'c 同上所示
let z: &'c &'b i32 = &'c y; // z是i32的引用的引用
}
}
}

'a: {&'b x 不是有效的语法,这里只是为了说明生命周期的概念。

实际上,传递一个引用到外部作用域将导致rust推断出一个更大的生命周期。

1
2
3
4
let x = 0;
let z;
let y = &x;
z = y;

标注生命周期为:

1
2
3
4
5
6
7
8
9
10
11
'a: {
let x: i32 = 0;
'b: {
let z: &'b i32;
'c: {
// y 的生命周期一定为 'b,因为对 x 的引用被传递到了 'b 这个作用域
let y: &'b i32 = &'b x;
z = y;
}
}
}

这里由于将x的引用传递到了zz=y),因此rust推断出y的生命周期为'b,也就是说y要至少比z活的更久,这样才能保证z不失效。

例1:悬垂引用的分析

让我们看一个悬垂引用的例子:

1
2
3
4
fn as_str(data: &u32) -> &str {
let s = format!("{}", data);
&s
}

我们通过解语法糖来分析这段代码:

1
2
3
4
5
6
fn as_str<'a>(data: &'a u32) -> &'a str {
'b: {
let s = format!("{}", data);
return &'a s;
}
}

as_str的这个签名接收了一个具有某个生命周期的u32的引用,并返回一个可以存活同样长的 str 的引用。我们已经大致能猜到为什么这个函数签名可能是个麻烦了,这意味着我们要返回的那个 str要在u32的引用所处的作用域上,或者甚至在更大的作用域上。这要求有点高。

然后我们继续计算字符串s,并返回它的一个引用。由于我们的函数的签名规定这个引用必须超过'a,这就是我们推断出的引用的生命周期。不幸的是,s被定义在作用域'b中,所以唯一合理的方法是'b包含'a,这显然是错误的,因为'a必须包含函数调用本身。因此,我们创建的引用&s,它的生命周期没有超过'a,编译器理所当然地直接报错。

为了更清楚地说明这一点,我们可以扩展这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn as_str<'a>(data: &'a u32) -> &'a str {
'b: {
let s = format!("{}", data);
return &'a s
}
}

fn main() {
'c: {
let x: u32 = 0;
'd: {
// 这里引入了一个匿名作用域,因为借用不需要在整个 x 的作用域内生效,
// 这个函数必须返回一个在函数调用之前就存在的某个字符串的引用,事实显然不是这样
println!("{}", as_str::<'d>(&'d x));
}
}
}

当然,这个函数的正确写法是这样的:

1
2
3
fn to_string(data: &u32) -> String {
format!("{}", data)
}

我们必须在函数里面产生一个拥有所有权的值才能返回。唯一可以返回一个&'a str的方法是,它在&'a u32的一个字段中,但显然不是这样的。

例2:别名一个可变引用

来看另一个例子:

1
2
3
4
let mut data = vec![1, 2, 3];
let x = &data[0];
data.push(4);
println!("{}", x);

脱糖后:

1
2
3
4
5
6
7
8
9
10
11
12
'a: {
let mut data: Vec<i32> = vec![1, 2, 3];
'b: {
// 'b 这个生命周期范围如我们所愿地小(刚好够 println!)
let x: &'b i32 = Index::index::<'b>(&'b data, 0);
'c: {
// 这里有一个临时作用域,我们不需要更长时间的 &mut 借用
Vec::push(&'c mut data, 4);
}
println!("{}", x);
}
}

在前面点操作符中我们提到过:编译器会将中括号索引data[0]转换成data.index(0)。这里的问题更微妙、更有趣。我们希望rust拒绝编译这个程序,理由如下:有一个存活的共享引用xdata的一个子集,当我们试图把data的可变引用传给push时。这将创建一个可变引用的别名,而这将违反引用的第二条规则(一个可变的引用不能有别名)。

然而,这根本不是rust认为这个程序有问题的原因。rust不理解x是对data的一个子集的引用。它根本就不理解Vec。它看到的是,x必须在'b范围内保持存活才能被打印;接下来,Index::index的签名要求我们对data的引用必须在'b范围内存活。当我们试图调用push时,它看到我们试图构造一个&'c mut data。rust知道'c包含在'b中,并拒绝了我们的程序,因为&'b data必然还存活着(一个可变引用和不可变引用不能同时存在)。

在这里我们看到,和我们真正想要保证的引用规则语义相比,生命周期系统要粗略得多。本来根据我们所讨论的引用的第二条规则就应该报错了,但是实际报错的规则却是“一个可变引用和不可变引用不能同时存在”。

在大多数情况下,这完全没问题,因为它使我们不用花整天的时间向编译器解释我们的程序。然而,这确实意味着有部分程序对于rust的真正的语义来说是完全正确的,但却被拒绝了,因为生命周期系统太傻了。

通过这两个例子,对引用的脱糖使得我们可以对生命周期有更深入的理解。

8.3 生命周期所覆盖的区域

一个引用(borrow)从它被创建到最后一次使用都是存活的。根据引用和别名介绍的第一条引用规则,被引用的值的生命周期只需要超过引用的生命周期就行。这看起来很简单,但有一些微妙之处。

下面的代码可以成功编译,因为在打印完x之后,它就不再需要了,所以它是悬空的还是别名的都无所谓(尽管变量x技术上一直存活到作用域的最后):

1
2
3
4
5
let mut data = vec![1, 2, 3];
let x = &data[0];
println!("{}", x);
// 这是可行的,因为不再使用 x,编译器也就缩短了 x 的生命周期
data.push(4);

然而,如果该值有一个析构器,析构器就会在作用域的末端运行。而运行析构器被认为是一种使用——显然是最后一次使用。所以,这将会编译报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
#[derive(Debug)]
// newtype
struct X<'a>(&'a i32);

impl Drop for X<'_> {
fn drop(&mut self) {}
}

let mut data = vec![1, 2, 3];
let x = X(&data[0]);
println!("{:?}", x);
data.push(4);
// 编译器会在这里自动插入 drop 函数,也就意味着我们会访问 x 中引用的变量,因此编译失败

让编译器相信x不再有效的一个方法是在data.push(4)之前使用drop(x),这样就会缩短x的生命周期。

此外,可能会有多种最后一次的引用使用,例如在一个条件的每个分支中:

1
2
3
4
5
6
7
8
9
10
11
let mut data = vec![1, 2, 3];
let x = &data[0];

if some_condition() {
println!("{}", x); // 这是该分支中最后一次使用 x 这个引用
data.push(4); // 因此在这里 push 操作是可行的
} else {
// 这里不存在对 x 的使用,对于这个分支来说,
// x 创建即销毁
data.push(5);
}

生命周期中可以有暂停,或者你可以把它看成是两个不同的借用,只是被绑在同一个局部变量上。这种情况经常发生在循环周围(在循环结束时写入一个变量的新值,并在下一次迭代的顶部最后一次使用它)。

1
2
3
4
5
6
7
8
let mut data = vec![1, 2, 3];
// x 是可变的(通过 mut 声明),因此我们可以修改 x 指向的内容
let mut x = &data[0];

println!("{}", x); // 最后一次使用这个引用
data.push(4);
x = &data[3]; // x 在这里借用了新的变量
println!("{}", x);

rust在旧版本上一直保持着借用的生命周期,直到作用域结束,所以这些例子在旧的编译器中可能无法编译。此外,还有一些边界条件,rust不能正确地缩短借用的有效部分,即使看起来应该这样做,也不能编译。这些问题将随着时间的推移,编译器的完善而得到解决。

8.4 生命周期的局限

我们先来看看一些本以为可以编译,但是却因为生命周期系统不够聪明导致编译失败的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#[derive(Debug)]
struct Foo;

impl Foo {
fn mutate_and_share(&mut self) -> &Self {
&*self
}
fn share(&self) {}
}

fn main() {
let mut foo = Foo;
let loan = foo.mutate_and_share();
foo.share();
println!("{:?}", loan);
}
/*
error[E0502]: cannot borrow `foo` as immutable because it is also borrowed as mutable
--> src\main.rs:14:5
|
13 | let loan = foo.mutate_and_share();
| ---------------------- mutable borrow occurs here
14 | foo.share();
| ^^^^^^^^^^^ immutable borrow occurs here
15 | println!("{:?}", loan);
| ---- mutable borrow later used here
*/

上面的代码中,foo.mutate_and_share() 虽然借用了 &mut self,但是它最终返回的是一个 &self,然后赋值给 loan,因此理论上来说它最终是进行了不可变借用,同时 foo.share 也进行了不可变借用,那么根据rust的借用规则:多个不可变借用可以同时存在,因此该代码应该编译通过。但实际上却无法正常编译。

从报错中可以看出,在foo.mutate_and_share方法结束时进行了可变借用,这里有些难以理解,因为可变借用仅在 mutate_and_share 方法内部有效,出了该方法后,就只有返回的不可变借用,因此,按理来说可变借用不应该在 main 的作用范围内存在。

让我们从生命周期的角度分析这个过程,将这段代码脱糖::

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct Foo;

impl Foo {
fn mutate_and_share<'a>(&'a mut self) -> &'a Self {
&'a *self
}
fn share<'a>(&'a self) {}
}

fn main() {
'b: {
let mut foo: Foo = Foo;
'c: {
let loan: &'c Foo = Foo::mutate_and_share::<'c>(&'c mut foo);
'd: {
Foo::share::<'d>(&'d foo);
}
println!("{:?}", loan);
}
}
}

以上是模拟了编译器的生命周期标注后的代码,可以注意到 &mut fooloan 的生命周期都是 'c

根据生命周期消除(Lifetime Elision)规则,其中第三条规则是:如果方法有多个输入生命周期参数并且其中一个参数是 &self&mut self,说明是个对象的方法,那么所有输出生命周期参数被赋予 self 的生命周期。因为该规则,导致了 mutate_and_share 方法中,参数 &mut self 和返回值 &self 的生命周期是相同的,因此,若返回值的生命周期在 main 函数有效,那 &mut self 的借用也在 main 函数有效。换句话说,生命周期系统被迫将&mut foo扩展为'c的生命周期。

这就解释了可变借用为什么会在 main 函数作用域内有效。然后当我们试图调用share时,它看到我们试图别名&'c mut foo,不过,真正令程序失败的原因是生命周期系统发现它违背了可变借用与不可变借用不能同时存在的规则。

上述代码实际上完全是正确的,但由于生命周期系统不够聪明,&mut self 借用的生命周期和 loan 的生命周期相同,将持续到 println 结束。而在此期间 foo.share() 又进行了一次不可变 &foo 借用,最终导致了编译错误。

再来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#![allow(unused)]
fn main() {
use std::collections::HashMap;
use std::hash::Hash;
fn get_default<'m, K, V>(map: &'m mut HashMap<K, V>, key: K) -> &'m mut V
where
K: Clone + Eq + Hash,
V: Default,
{
match map.get_mut(&key) {
Some(value) => value,
None => {
map.insert(key.clone(), V::default());
map.get_mut(&key).unwrap()
}
}
}
}
/*
error[E0499]: cannot borrow `*map` as mutable more than once at a time
--> src\main.rs:13:17
|
5 | fn get_default<'m, K, V>(map: &'m mut HashMap<K, V>, key: K) -> &'m mut V
| -- lifetime `'m` defined here
...
10 | match map.get_mut(&key) {
| - ----------------- first mutable borrow occurs here
| _________|
| |
11 | | Some(value) => value,
12 | | None => {
13 | | map.insert(key.clone(), V::default());
| | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ second mutable borrow occurs here
14 | | map.get_mut(&key).unwrap()
15 | | }
16 | | }
| |_________- returning this value requires that `*map` is borrowed for `'m`

error[E0499]: cannot borrow `*map` as mutable more than once at a time
--> src\main.rs:14:17
|
5 | fn get_default<'m, K, V>(map: &'m mut HashMap<K, V>, key: K) -> &'m mut V
| -- lifetime `'m` defined here
...
10 | match map.get_mut(&key) {
| - ----------------- first mutable borrow occurs here
| _________|
| |
11 | | Some(value) => value,
12 | | None => {
13 | | map.insert(key.clone(), V::default());
14 | | map.get_mut(&key).unwrap()
| | ^^^^^^^^^^^^^^^^^ second mutable borrow occurs here
15 | | }
16 | | }
| |_________- returning this value requires that `*map` is borrowed for `'m`
*/

这段代码不能通过编译的原因是编译器未能精确地判断出某个可变借用不再需要,反而谨慎的给该借用安排了一个很大的作用域。分析代码可知在 match map.get_mut(&key) 方法调用完成后,对 map 的可变借用就可以结束了。但从报错看来,编译器不太聪明,它认为该借用会持续到整个 match 语句块的结束(第 16 行处),这便造成了后续借用的失败。

对于这种生命周期系统不够聪明导致的编译错误,目前没有太好的办法,只能修改代码去满足它的需求,不过这些问题最终会随着编译器的完善而得到解决。

8.5 无界(不受约束)的生命周期

不安全的代码经常会凭空产生引用或生命周期,这种生命周期是以无约束的形式出现在世界中的。最常见的原因是对原始指针的解引用,这产生了一个具有无约束生命周期的引用。这样的生命周期会随着上下文的要求而变大。这实际上比简单地标记为'static更强大,因为例如&'static &'a T将无法通过类型检查,但无约束的生命周期将根据需要完美地塑造为&'a &'a T。然而,对于大多数意图和目的来说,这样的无约束生命周期可以被看作是'static

给定一个函数,任何不来自输入的输出生命周期都是无约束的,比如说:

1
2
3
4
5
fn f<'a, T>(x: *const T) -> &'a T {
unsafe {
&*x
}
}

上述代码中,参数 x 是一个裸指针,它并没有任何生命周期,然后通过 unsafe 操作后,它被进行了解引用,变成了一个rust的标准引用类型,该类型必须要有生命周期,也就是 'a。将产生一个具有无约束生命周期的&T

避免无约束生命周期的最简单方法是在函数边界使用生命周期消除(Lifetime Elision)规则。如果一个输出的生命周期被省略了,那么它必须被一个输入的生命周期所约束。当然,它也可能被错误的生命周期所约束,但这通常只会引起编译错误,而不是让内存安全被简单地违反。

在一个函数中,对生命周期的约束更容易出错。约束生命周期的最安全和最简单的方法是从一个具有约束的生命周期的函数中返回它。然而,如果这样做是不可接受的,可以将引用放在一个有特定生命周期的位置。不幸的是,我们不可能命名一个函数中涉及的所有生命周期。

8.6 生命周期约束

生命周期约束可以应用于类型或其它生命周期。

'a: 'b

约束 'a: 'b 通常被解读为 'a'b 存活的时间久。'a: 'b意味着 'a 持续的时间比 'b 长,所以只要 &'b () 有效,引用 &'a () 就有效。

1
2
3
4
struct DoubleRef<'a,'b:'a, T> {
r: &'a T,
s: &'b T
}

例如上述代码定义一个结构体,它拥有两个引用字段,类型都是泛型 T,每个引用都拥有自己的生命周期,由于我们使用了生命周期约束 'b: 'a,因此 'b 必须存活的比 'a 久,也就是结构体中的 s 字段引用的值必须要比 r 字段引用的值存活的时间长。

T: 'a

T: 'a 意味着 T 的所有生命周期参数都比 'a 存活的时间长。

1
2
3
struct Ref<'a, T: 'a> {
r: &'a T
}

因为结构体字段 r 引用了 T,因此 r 的生命周期 'a 必须要比 T 的生命周期更短(被引用者的生命周期必须要比引用长)。

在 rust 1.30 版本之前,该写法是必须的,但是从 1.31 版本开始,编译器可以自动推导 T: 'a 类型的约束,因此我们只需这样写即可:

1
2
3
struct Ref<'a, T> {
r: &'a T
}

来看一个使用了生命周期约束的综合例子:

1
2
3
4
5
6
7
8
9
10
struct ImportantExcerpt<'a> {
part: &'a str,
}

impl<'a: 'b, 'b> ImportantExcerpt<'a> {
fn announce_and_return_part(&'a self, announcement: &'b str) -> &'b str {
println!("Attention please: {}", announcement);
self.part
}
}

上面的例子中必须添加约束 'a: 'b 后,才能成功编译,因为 self.part 的生命周期与 self的生命周期一致,将 &'a 类型的生命周期强行转换为 &'b 类型会报错,只有在 'a >= 'b 的情况下,'a 才能转换成 'b

8.7 Higher-Rank Trait Bounds(HRTB)

对于这里的内容,官方给出的例子不是很好懂,中文资料也比较欠缺。这里参考了一些资料,对于正确性还需要进一步验证(==todo==待后面有了深入理解之后重新编辑此章节):

HRTB是高阶特征约束,举几个例子来说明。例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
use std::fmt::Debug;

trait DoSomething<T> {
fn do_sth(&self, value: T);
}

impl<'a, T: Debug> DoSomething<T> for &'a usize {
fn do_sth(&self, value: T) {
println!("{:?}", value);
}
}

fn foo<'a>(b: Box<dyn DoSomething<&'a usize>>) {
let s: usize = 10;
b.do_sth(&s)
}

fn main() {
let x = Box::new(&2usize);
foo(x);
}
/*
error[E0597]: `s` does not live long enough
--> src\main.rs:15:14
|
13 | fn foo<'a>(b: Box<dyn DoSomething<&'a usize>>) {
| -- lifetime `'a` defined here
14 | let s: usize = 10;
15 | b.do_sth(&s)
| ---------^^-
| | |
| | borrowed value does not live long enough
| argument requires that `s` is borrowed for `'a`
16 | }
| - `s` dropped here while still borrowed
*/

这段代码定义了一个DoSomething特征,其中有一个do_sth方法,该方法接收一个T类型的泛型参数value,然后我们为 usize 的引用类型实现 DoSomething 特征,特征约束要求 T 实现 Debug 特征,这是为了能够打印value

接下来定义了一个foo 函数,它接受一个 Box<dyn DoSomething<&'a usize>> 参数,这个参数是一个特征对象,表示它可以接受任何实现了DoSomething<&usize>特征的类型,并使用usize类型的整数常量的引用 &s 调用 do_sth 方法。整数常量 sfoo 函数内部定义,值为 10

main 函数中,创建了一个 Box,其中包含一个值为 2 的整数常量的引用。然后,使用这个 Box 作为 foo 函数的参数调用它。

当运行程序时,我们期望它会打印 10,这是传递给引用类型 usizeDoSomething 实现的 do_sth 方法的 s 的值。

但如你所见,它编译失败了,这与生命周期有关。我们通过直觉理解一下这里出现的问题:foo函数失败是因为函数签名中的参数b的类型引入了'a,它关联到了DoSomething<T>中的T, 进而关联到do_sth第二个参数value的生命周期上,所以在编译器进行分析时,do_sth的第二个参数的生命周期至少和do_sth一样为'a,但是代码实际传入了一个本地变量s,它的生命周期'b短于do_sth的生命周期, 即有'a : 'b的约束关系,因此编译失败。

我们修改foo的签名:

1
2
3
4
fn foo<'a>(b: Box<&'a usize>) {
let s: usize = 10;
b.do_sth(&s);
}

在这个签名中,foo接受一个 Box<&'a usize> 类型的参数。因为b的引入只是将'a关联到了do_sthself的生命周期,也就是do_sth的调用者b本身,对于value没有关联。所以do_sthvalue的生命周期的推断推迟到了do_sth(&s),此时直接与本地变量s的生命周期'b绑定了,因此编译成功。

还有另一种修改方式,即HRTB:

1
2
3
4
fn foo(b: Box<dyn for<'a> DoSomething<&'a usize>>) {
let s: usize = 10;
b.do_sth(&s)
}

因为我们想表达对于任何生命周期的T我们都希望do_sth最终能接受这样的参数。所以将b的类型改为Box<dyn for<'a> DoSomething<&'a usize>,编译器看到后就会将此处'a的推断延迟到do_sth被调用时,也就最终直接关联到&s的生命周期。

我们对比一下这三种foo的签名:

1
2
3
4
5
6
// 1.
fn foo<'a>(b: Box<dyn DoSomething<&'a usize>>) {
// 2.
fn foo<'a>(b: Box<&'a usize>) {
// 3.
fn foo(b: Box<dyn for<'a> DoSomething<&'a usize>>) {

可以发现,编译行为不同本质就在于1中的签名一上来就给定了DoSometing的泛型参数T'a的引用,也就迫使编译器给其实例化了第二个参数只接受'a引用的do_sth实现,从而拒绝一个本地变量s引用的传入,因为其生命周期明显短于这个外部约束的'a(当然这个'a的进一步实例化需要到foo的真正调用时推断出,但此处已有足够的信息拒绝这个函数实现。)

而对于2,没有给出T的任何约束,所以对于T的推断就推迟到do_sth的调用处,也就是本地变量s引用补全了信息,从而实例化了对于参数&'a usizedo_sth

对于3,这其实解释了HRTB引入的目的,它在生命周期上再进行更高阶的类型约束,提示编译器进行晚绑定(late bound),也就是检查了b调用do_sth函数的具体实现,再进行生命周期泛型参数的实例化。编译器发现b实现的do_sth函数没有返回引用,也就不需要进行生命周期的检查了,因此通过编译。

9 自引用与循环引用

rust的内存安全性保证使其难以意外地制造永远也不会被清理的内存(被称为内存泄漏(memory leak)),但并不是不可能。与在编译时拒绝数据竞争不同,rust并不保证完全地避免内存泄漏,这意味着内存泄漏在rust被认为是内存安全的。这一点可以通过 Rc<T>RefCell<T> 看出:创建引用循环的可能性是存在的。这会造成内存泄漏,因为每一项的引用计数永远也到不了0,其值也永远不会被丢弃。

9.1 制造引用循环

让我们看看引用循环是如何发生的以及如何避免它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
enum List {
Cons(i32, RefCell<Rc<List>>),
Nil,
}

impl List {
fn tail(&self) -> Option<&RefCell<Rc<List>>> {
match self {
Cons(_, item) => Some(item),
Nil => None,
}
}
}

fn main() {}

我们创建了一个枚举类型 List,根据组合使用Rc<T>RefCell<T>来拥有多个可变数据所有者小节内容,我们这里结合使用Rc<T>RefCell<T> 来获得修改列表中值的能力。这里还增加了一个 tail 方法来方便我们在有 Cons 成员的时候访问其第二项。

接下来,我们使用这个List

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fn main() {
let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));

println!("a的初始化rc计数 = {}", Rc::strong_count(&a));
println!("a指向的节点 = {:?}", a.tail());

// 创建`b`到`a`的引用
let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));

println!("在b创建后,a的rc计数 = {}", Rc::strong_count(&a));
println!("b的初始化rc计数 = {}", Rc::strong_count(&b));
println!("b指向的节点 = {:?}", b.tail());

// 利用RefCell的可变性,创建了`a`到`b`的引用
if let Some(link) = a.tail() {
*link.borrow_mut() = Rc::clone(&b);
}

println!("在更改a后,b的rc计数 = {}", Rc::strong_count(&b));
println!("在更改a后,a的rc计数 = {}", Rc::strong_count(&a));

// 下面一行println!将导致循环引用,最终造成栈溢出
// println!("a next item = {:?}", a.tail());
}

这里在变量 a 中创建了一个 Rc<List> 实例来存放初值为 5, NilList 值。接着在变量 b 中创建了存放包含值10 和指向列表 aList 的另一个 Rc<List> 实例。

最后,修改 a 使其指向 b 而不是 Nil,这就创建了一个循环。为此需要使用 tail 方法获取 aRefCell<Rc<List>> 的引用,并放入变量 link 中。接着使用 RefCell<Rc<List>>borrow_mut 方法将其值从存放 NilRc<List> 修改为 b 中的 Rc<List>

现在,保持最后的 println! 行注释并运行代码,会得到如下输出:

1
2
3
4
5
6
7
a的初始化rc计数 = 1
a指向的节点 = Some(RefCell { value: Nil })
在b创建后,a的rc计数 = 2
b的初始化rc计数 = 1
b指向的节点 = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
在更改a后,b的rc计数 = 2
在更改a后,a的rc计数 = 2

可以看到将列表 a 修改为指向 b 之后, ab 中的 Rc<List> 实例的引用计数都是 2。在 main 的结尾,rust丢弃 b,这会使 b的引用计数从 2 减为 1。然而,b 不能被回收,因为其引用计数是 1 而不是 0。接下来rust会丢弃 aa 的引用计数从 2 减为 1。这个实例也不能被回收,因为 b 依然引用它,所以其引用计数是 1。这些列表的内存将永远保持未被回收的状态。因此,发生了内存泄漏。

下面的图片解释了这种引用关系:

v2-2dbfc981f05019bf70bf81c93f956c35_1440w

如果取消最后 println! 的注释并运行程序,rust会尝试打印出 a 指向 b 指向 a 这样的无限循环直到栈溢出。

相比真实世界的程序,这个例子中创建引用循环的结果并不可怕。创建了引用循环之后程序立刻就结束了。如果在更为复杂的程序中并在循环里分配了很多内存并占有很长时间,这个程序会使用多于它所需要的内存,并有可能压垮系统并造成没有内存可供使用。

创建引用循环并不容易,但也不是不可能。如果你有包含 Rc<T>RefCell<T> 值或类似的嵌套结合了内部可变性和引用计数的类型,请务必小心确保你没有形成一个引用循环。你无法指望rust帮你捕获它们。创建引用循环是一个程序上的逻辑bug,你应该使用自动化测试、代码评审和其他软件开发最佳实践来使其最小化。

另一个解决方案是重新组织数据结构,使得一部分引用拥有所有权而另一部分没有。换句话说,循环将由一些拥有所有权的关系和一些无所有权的关系组成,只有所有权关系才能影响值是否可以被丢弃。在这个例子中,我们总是希望 Cons 成员拥有其列表,所以重新组织数据结构是不可能的。

9.2 将Rc<T>变为Weak<T>以避免循环引用

到目前为止,我们已经展示了调用 Rc::clone 会增加 Rc<T> 实例的 strong_count,和只在其 strong_count 为 0 时才会被清理的 Rc<T> 实例。你也可以通过调用 Rc::downgrade 并传递 Rc<T> 实例的引用来创建其值的弱引用(weak reference)。强引用代表如何共享 Rc<T> 实例的所有权。弱引用并不属于所有权关系,当 Rc<T> 实例被清理时其计数没有影响。它们不会造成引用循环,因为任何弱引用的循环会在其相关的强引用计数为 0 时被打断。

调用 Rc::downgrade 时会得到 Weak<T> 类型的智能指针。不同于将 Rc<T> 实例的 strong_count 加 1,调用 Rc::downgrade 会将 weak_count 加 1。Rc<T> 类型使用 weak_count 来记录其存在多少个 Weak<T> 引用,类似于 strong_count。其区别在于 weak_count 无需计数为 0 就能使 Rc<T> 实例被清理。

因为 Weak<T> 引用的值可能已经被丢弃了,为了使用 Weak<T> 所指向的值,我们必须确保其值仍然有效。为此可以调用 Weak<T> 实例的 upgrade 方法,这会返回 Option<Rc<T>>。如果 Rc<T> 值还未被丢弃,则结果是 Some;如果 Rc<T> 已被丢弃,则结果是 None。因为 upgrade 返回一个 Option<Rc<T>>,rust会确保处理 SomeNone 的情况,所以它不会返回非法指针。

创建树形数据结构:带有子节点的 Node

下面看一个由父节点和子节点构成的树形结构的例子。

在最开始,我们将会构建一个带有子节点的树。让我们创建一个用于存放其拥有所有权的 i32 值和其子节点引用的 Node

1
2
3
4
5
6
7
8
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Node {
value: i32,
children: RefCell<Vec<Rc<Node>>>,
}

我们希望能够 Node 拥有其子节点,同时也希望通过变量来共享所有权,以便可以直接访问树中的每一个 Node,为此 Vec<T> 的项的类型被定义为 Rc<Node>。我们还希望能修改其它节点的子节点,所以 childrenVec<Rc<Node>> 被放进了 RefCell<T>

接下来,使用此结构体定义来创建一个叫做 leaf 的带有值 3 且没有子节点的 Node 实例,和另一个带有值 5 并以 leaf 作为子节点的实例 branch

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let leaf = Rc::new(Node {
value: 3,
children: RefCell::new(vec![]),
});

let branch = Rc::new(Node {
value: 5,
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
}

这里克隆了 leaf 中的 Rc<Node> 并储存在了 branch 中,这意味着 leaf 中的 Node 现在有两个所有者:leafbranch。可以通过 branch.childrenbranch 中获得 leaf,不过无法从 leafbranchleaf 没有到 branch 的引用且并不知道它们相互关联。

增加从子到父的引用

现在我们希望 leaf 知道 branch 是其父节点,需要在 Node 结构体定义中增加一个 parent 字段。问题是 parent 的类型应该是什么。我们知道其不能包含 Rc<T>,因为这样 leaf.parent 将会指向 branchbranch.children 会包含 leaf 的指针,这会形成引用循环,会造成其 strong_count 永远也不会为 0。

现在换一种方式思考这个关系,父节点应该拥有其子节点:如果父节点被丢弃了,其子节点也应该被丢弃。然而子节点不应该拥有其父节点:如果丢弃子节点,其父节点应该依然存在。这正是弱引用的例子!

所以 parent 使用 Weak<T> 类型而不是 Rc<T>,具体来说是 RefCell<Weak<Node>>。现在 Node 结构体定义看起来像这样:

1
2
3
4
5
6
7
8
9
use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}

这样,一个节点就能够引用其父节点,但不拥有其父节点。我们更新 main 来使用新定义以便 leaf 节点可以通过 branch 引用其父节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});

println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());

let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});

*leaf.parent.borrow_mut() = Rc::downgrade(&branch);

println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}

由于leaf 开始时没有父节点,所以我们新建了一个空的 Weak 引用实例。此时,当尝试使用 upgrade 方法获取 leaf 的父节点引用时,会得到一个 None 值。如第一个 println! 输出所示:

1
leaf parent = None

当创建 branch 节点时,其也会新建一个 Weak<Node> 引用,因为 branch 并没有父节点。leaf 仍然作为 branch 的一个子节点。一旦在 branch 中有了 Node 实例,就可以修改 leaf 使其拥有指向父节点的 Weak<Node> 引用。这里使用了 leafparent 字段里的 RefCell<Weak<Node>>borrow_mut 方法,接着使用了 Rc::downgrade 函数来从 branch 中的 Rc<Node> 值创建了一个指向 branchWeak<Node> 引用。

当再次打印出 leaf 的父节点时,这一次将会得到存放了 branchSome 值:现在 leaf 可以访问其父节点了!当打印出 leaf 时,我们也避免了循环引用最终会导致栈溢出的循环:Weak<Node> 引用被打印为 (Weak)

1
2
3
leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) },
children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) },
children: RefCell { value: [] } }] } })

没有无限的输出表明这段代码并没有造成引用循环。这一点也可以从观察 Rc::strong_countRc::weak_count 调用的结果看出。

可视化 strong_countweak_count 的改变

让我们通过创建了一个新的内部作用域并将 branch 的创建放入其中,来观察 Rc<Node> 实例的 strong_countweak_count 值的变化。这会展示当 branch 创建和离开作用域被丢弃时会发生什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});

println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);

{
let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});

*leaf.parent.borrow_mut() = Rc::downgrade(&branch);

println!(
"branch strong = {}, weak = {}",
Rc::strong_count(&branch),
Rc::weak_count(&branch),
);

println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
}

println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
}

一旦创建了 leaf,其 Rc<Node> 的强引用计数为 1,弱引用计数为 0。在内部作用域{}中创建了 branch 并与 leaf 相关联,此时 branchRc<Node> 的强引用计数为 1,弱引用计数为 1(因为 leaf.parent 通过 Weak<Node> 指向 branch)。这里 leaf 的强引用计数为 2,因为现在 branchbranch.children 中储存了 leafRc<Node> 的拷贝,不过弱引用计数仍然为 0。

当内部作用域结束时,branch 离开作用域,Rc<Node> 的强引用计数减少为 0,所以其 Node 被丢弃。来自 leaf.parent 的弱引用计数 1 与 Node 是否被丢弃无关,所以并没有产生任何内存泄漏。

如果在内部作用域结束后尝试访问 leaf 的父节点,会再次得到 None。在程序的结尾,leafRc<Node> 的强引用计数为 1,弱引用计数为 0,因为现在 leaf 又是 Rc<Node> 唯一的引用了。

所有这些管理计数和值的逻辑都内建于 Rc<T>Weak<T> 以及它们的 Drop 特征实现中。通过在 Node 定义中指定从子节点到父节点的关系为一个Weak<T>引用,就能够拥有父节点和子节点之间的双向引用而不会造成引用循环和内存泄漏。

Weak总结

Weak 非常类似于 Rc,但是与 Rc 持有所有权不同,Weak 不持有所有权,它仅仅保存一份指向数据的弱引用:如果你想要访问数据,需要通过 Weak 指针的 upgrade 方法实现,该方法返回一个类型为 Option<Rc<T>> 的值。因为 Weak 引用不计入所有权,因此它无法阻止所引用的内存值被释放掉,而且 Weak 本身不对值的存在性做任何担保,引用的值还存在就返回 Some,不存在就返回 None

Weak 通过 use std::rc::Weak 来引入,它具有以下特点:

  • 可访问,但没有所有权,不增加引用计数,因此不会影响被引用值的释放回收
  • 可由 Rc<T> 调用 downgrade 方法转换成 Weak<T>
  • Weak<T> 可使用 upgrade 方法转换成 Option<Rc<T>>,如果资源已经被释放,则 Option 的值是 None
  • 常用于解决循环引用的问题

WeakRc 的对比如下:

WeakRc
不计数引用计数
不拥有所有权拥有值的所有权
不阻止值被释放 drop所有权计数归零,才能 drop
引用的值存在返回 Some,不存在返回 None引用的值必定存在
通过 upgrade 取到 Option<Rc<T>>,然后再取值通过 Deref 自动解引用,取值无需任何操作

9.3 结构体自引用

让我们尝试创建一个自引用的结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct SelfRef<'a> {
value: String,
// 该引用指向上面的value
pointer_to_value: &'a str,
}

fn main(){
let s = "aaa".to_string();
let v = SelfRef {
value: s,
pointer_to_value: &s
};
}
/*
error[E0382]: borrow of moved value: `s`
--> src\main.rs:12:27
|
9 | let s = "aaa".to_string();
| - move occurs because `s` has type `String`, which does not implement the `Copy` trait
10 | let v = SelfRef {
11 | value: s,
| - value moved here
12 | pointer_to_value: &s
| ^^ value borrowed here after move
*/

这里创建了一个结构体并使用它,由于我们试图同时使用值s和值的引用&s,最终所有权转移和借用一起发生了。下面介绍解决自引用的几种方法。

使用Option解决自引用

最简单的方式就是使用 Option 分两步来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#[derive(Debug)]
struct WhatAboutThis<'a> {
name: String,
nickname: Option<&'a str>,
}

fn main() {
let mut tricky = WhatAboutThis {
name: "Annabelle".to_string(),
nickname: None,
};
tricky.nickname = Some(&tricky.name[..4]);

println!("{:?}", tricky);
}

在某种程度上来说,Option 这个方法可以工作,但是这个方法的限制较多,例如从一个函数创建并返回它是不可能的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn creator<'a>() -> WhatAboutThis<'a> {
let mut tricky = WhatAboutThis {
name: "Annabelle".to_string(),
nickname: None,
};
tricky.nickname = Some(&tricky.name[..4]);

tricky
}
/*
error[E0515]: cannot return value referencing local data `tricky.name`
--> src/main.rs:24:5
|
22 | tricky.nickname = Some(&tricky.name[..4]);
| ----------- `tricky.name` is borrowed here
23 |
24 | tricky
| ^^^^^^ returns a value referencing data owned by the current function
*/

从函数签名就能看出来端倪,'a 生命周期是凭空产生的。如果是通过方法使用,你需要一个无用 &'a self 生命周期标识,一旦有了这个标识,代码将变得更加受限,你将很容易就获得借用错误,就连 NLL 规则都没用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#[derive(Debug)]
struct WhatAboutThis<'a> {
name: String,
nickname: Option<&'a str>,
}

impl<'a> WhatAboutThis<'a> {
fn tie_the_knot(&'a mut self) {
self.nickname = Some(&self.name[..4]);
}
}

fn main() {
let mut tricky = WhatAboutThis {
name: "Annabelle".to_string(),
nickname: None,
};
tricky.tie_the_knot();

println!("{:?}", tricky);
}
/*
error[E0502]: cannot borrow `tricky` as immutable because it is also borrowed as mutable
--> src\main.rs:21:22
|
18 | tricky.tie_the_knot();
| --------------------- mutable borrow occurs here
...
21 | println!("{:?}", tricky);
| ^^^^^^
| |
| immutable borrow occurs here
| mutable borrow later used here
*/

unsafe 实现

使用unsafe可以避免借用规则的限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#[derive(Debug)]
struct SelfRef {
value: String,
pointer_to_value: *const String,
}

impl SelfRef {
fn new(txt: &str) -> Self {
SelfRef {
value: String::from(txt),
pointer_to_value: std::ptr::null(),
}
}

fn init(&mut self) {
let self_ref: *const String = &self.value;
self.pointer_to_value = self_ref;
}

fn value(&self) -> &str {
&self.value
}

fn pointer_to_value(&self) -> &String {
assert!(!self.pointer_to_value.is_null(),
"Test::b called without Test::init being called first");
unsafe { &*(self.pointer_to_value) }
}
}

fn main() {
let mut t = SelfRef::new("hello");
t.init();
// 打印值和指针地址
println!("{}, {:p}", t.value(), t.pointer_to_value());
}

在这里,我们在 pointer_to_value 中直接存储裸指针,而不是rust的引用,因此不再受到rust借用规则和生命周期的限制,而且实现起来非常清晰、简洁。但是缺点就是,通过指针获取值时需要使用 unsafe 代码。

当然,上面的代码你还能通过裸指针来修改 String,但是需要将 *const 修改为 *mut

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#[derive(Debug)]
struct SelfRef {
value: String,
pointer_to_value: *mut String,
}

impl SelfRef {
fn new(txt: &str) -> Self {
SelfRef {
value: String::from(txt),
pointer_to_value: std::ptr::null_mut(),
}
}

fn init(&mut self) {
let self_ref: *mut String = &mut self.value;
self.pointer_to_value = self_ref;
}

fn value(&self) -> &str {
&self.value
}

fn pointer_to_value(&self) -> &String {
assert!(!self.pointer_to_value.is_null(), "Test::b called without Test::init being called first");
unsafe { &*(self.pointer_to_value) }
}
}

fn main() {
let mut t = SelfRef::new("hello");
t.init();
println!("{}, {:p}", t.value(), t.pointer_to_value());

t.value.push_str(", world");
unsafe {
(&mut *t.pointer_to_value).push_str("!");
}

println!("{}, {:p}", t.value(), t.pointer_to_value());
}

运行后输出:

1
2
hello, 0x16f3aec70
hello, world!, 0x16f3aec70

上面的 unsafe 虽然简单好用,但是它不太安全,是否还有其他选择?那就是 Pin

无法被移动的 Pin

Pin 可以固定住一个值,防止该值在内存中被移动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
use std::marker::PhantomPinned;
use std::pin::Pin;
use std::ptr::NonNull;

// 下面是一个自引用数据结构体,因为 slice 字段是一个指针,指向了 data 字段
// 我们无法使用普通引用来实现,因为违背了 Rust 的编译规则
// 因此,这里我们使用了一个裸指针,通过 NonNull 来确保它不会为 null
struct Unmovable {
data: String,
slice: NonNull<String>,
_pin: PhantomPinned,
}

impl Unmovable {
// 为了确保函数返回时数据的所有权不会被转移,我们将它放在堆上,唯一的访问方式就是通过指针
fn new(data: String) -> Pin<Box<Self>> {
let res = Unmovable {
data,
// 只有在数据到位时,才创建指针,否则数据会在开始之前就被转移所有权
slice: NonNull::dangling(),
_pin: PhantomPinned,
};
let mut boxed = Box::pin(res);

let slice = NonNull::from(&boxed.data);
// 这里其实安全的,因为修改一个字段不会转移整个结构体的所有权
unsafe {
let mut_ref: Pin<&mut Self> = Pin::as_mut(&mut boxed);
Pin::get_unchecked_mut(mut_ref).slice = slice;
}
boxed
}
}

fn main() {
let unmoved = Unmovable::new("hello".to_string());
// 只要结构体没有被转移,那指针就应该指向正确的位置,而且我们可以随意移动指针
let mut still_unmoved = unmoved;
assert_eq!(still_unmoved.slice, NonNull::from(&still_unmoved.data));

// 因为我们的类型没有实现 `Unpin` 特征,下面这段代码将无法编译
// let mut new_unmoved = Unmovable::new("world".to_string());
// std::mem::swap(&mut *still_unmoved, &mut *new_unmoved);
}

上面的代码也非常清晰,虽然使用了 unsafe,其实更多的是无奈之举,跟之前的 unsafe 实现完全不可同日而语。

其实 Pin 在这里并没有魔法,它也并不是实现自引用类型的主要原因,最关键的还是里面的裸指针的使用,而 Pin 起到的作用就是确保我们的值不会被移走,否则指针就会指向一个错误的地址。

10 宏入门

实际上在前面已经出现很多宏的身影了,比如println!vec!等等。宏(Macro)指的是rust中一系列的功能,使用 macro_rules! 的声明(Declarative)宏,和三种过程(Procedural)宏:

  • 自定义 #[derive] 宏在结构体和枚举上指定通过 derive 属性添加的代码
  • 类属性(Attribute-like)宏定义可用于任意项的自定义属性
  • 类函数(Function-like)宏看起来像函数不过作用于作为参数传递的 token

我们会依次讨论每一种宏,首先要解释一下为什么已经有了函数还需要宏。

10.1 宏和函数的区别

  • 元编程:

    从根本上来说,宏是一种为写其它代码而写代码的方式,即所谓的元编程(metaprogramming),比如println! 宏和 vec! 宏。所有的这些宏以展开的方式来生成比你所手写出的更多的代码。元编程对于减少大量编写和维护的代码是非常有用的,它也扮演了函数扮演的角色。但宏有一些函数所没有的附加能力。

  • 可变参数

    一个函数签名必须声明函数参数个数和类型。相比之下,宏能够接收不同数量的参数:用一个参数调用println!("hello")或用两个参数调用println!("hello {}", name)

  • 宏展开

    宏可以在编译器翻译代码前展开,例如,宏可以在一个给定类型上实现特征。而函数则不行,因为函数是在运行时被调用,同时特征需要在编译时实现。

  • 宏相对复杂

    宏定义要比函数定义更复杂,因为你正在编写生成rust代码的rust代码。由于这样的间接性,宏定义通常要比函数定义更难阅读、理解以及维护。

10.2 声明式宏macro_rules!用于通用元编程

rust最常用的宏形式是声明宏(declarative macros)。它们有时也被称为“macros by example”、“macro_rules!宏” 或者就是“macros”。其核心概念是,声明宏允许我们编写一些类似rustmatch 表达式的代码。match 表达式是控制结构,其接收一个表达式,与表达式的结果进行模式匹配,然后根据模式匹配执行相关代码。宏也将一个值和包含相关代码的模式进行比较;此种情况下,该值是传递给宏的rust源代码字面值,模式用于和前面提到的源代码字面值进行比较,每个模式的相关代码会替换传递给宏的代码。所有这一切都发生于编译时。

可以使用 macro_rules! 来定义宏。让我们通过查看 vec! 宏定义来探索如何使用 macro_rules! 结构。我们知道,vec! 宏来生成一个给定值的vector,例如,下面的宏用三个整数创建一个 vector:

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

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

下面展示了vec! 稍微简化的定义:

标准库中实际定义的 vec! 包括预分配适当量的内存的代码。这部分为代码优化,为了让示例简化,此处并没有包含在内。

1
2
3
4
5
6
7
8
9
10
11
12
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}

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

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

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

需要注意的是,宏定义中有效模式语法和match模式匹配语法是不同的,因为宏模式所匹配的是rust代码结构而不是值。对于全部的宏模式语法,请查阅这里

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

$() 之后的逗号说明一个可有可无的逗号分隔符可以出现在 $() 所匹配的代码之后。紧随逗号之后的 * 说明该模式匹配零个或更多个 * 之前的任何模式。

举个例子来说,当以 vec![1, 2, 3]; 调用宏时,$x 模式与三个表达式 123 进行了三次匹配。

现在让我们来看看与此分支模式相关联的代码块中的模式:

1
2
3
4
5
6
7
8
9
{
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};

匹配到模式中的$()的每一部分,都会在(=>右侧)$()* 里生成temp_vec.push($x),生成零次还是多次取决于模式匹配到多少次。$x 由每个与之相匹配的表达式所替换。当以 vec![1, 2, 3]; 调用该宏时,替换该宏调用所生成的代码会是下面这样:

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

至此,我们定义了一个宏,它可以接受任意类型和数量的参数,并且理解了其语法的含义。

由于绝大多数rust开发者都是宏的用户而不是编写者,如需了解更多macro_rules的内容,可以参考 The Little Book of Rust Macros一书。

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

第二种形式的宏被称为过程宏(procedural macros),因为它们更像函数(一种过程类型)。过程宏接收rust代码作为输入,在这些代码上进行操作,然后产生另一些代码作为输出,而非像声明式宏那样匹配对应模式然后以另一部分代码替换当前代码。有三种类型的过程宏(自定义派生derive,类属性和类函数),不过它们的工作方式都类似。注意,过程宏中的 derive 宏输出的代码并不会替换之前的代码,这一点与声明宏有很大的不同!

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

在下面的代码中展示了如何定义过程宏,其中 some_attribute 是一个使用特定宏变体的占位符:

1
2
3
4
5
use proc_macro;

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

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

让我们看看不同种类的程序宏。我们将从一个自定义的派生宏开始,然后解释使其他形式不同的小差异。

10.4 自定义derive

让我们创建一个 hello_macrocrate包,其包含名为 HelloMacro 的特征和关联函数(静态方法) hello_macro。不同于让用户为其每一个类型实现 HelloMacro 特征,我们将会提供一个过程式宏以便用户可以使用 #[derive(HelloMacro)] 注解它们的类型来得到 hello_macro 函数的默认实现。该默认实现会打印 Hello, Macro! My name is TypeName!,其中 TypeName 为定义了特征的类型名。

换言之,我们会创建一个crate,使程序员能够写出下面的代码并正常运行:

1
2
3
4
5
6
7
8
9
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

1
2
3
cargo new hello_macro --lib
cd hello_macro/
touch src/main.rs

此时,src 目录下包含两个文件 lib.rsmain.rs,前者是 lib 库,后者是二进制库。

接下来,会定义 HelloMacro 特征以及其关联函数:

文件名:hello_macro/src/lib.rs

1
2
3
pub trait HelloMacro {
fn hello_macro();
}

现在有了一个包含函数的特征。此时,crate的用户可以实现该特征以达到其期望的功能,像这样:

文件名:hello_macro/src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
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 的类型编写impl实现的代码块。我们希望为其节约这些工作。

另外,我们也无法为 hello_macro 函数提供一个能够打印实现了该特征的类型的名字的默认实现:rust没有反射的能力,因此其无法在运行时获取类型名。我们需要一个在编译时生成代码的宏。

因此,下一步是定义过程式宏。过程式宏目前必须在其自己的crate内。该限制最终可能被取消。构造crate和其中宏的惯例如下:对于一个 foo 的包来说,一个自定义的派生过程宏的包被称为 foo_derive 。我们按照惯例在 hello_macro 项目中新建名为 hello_macro_derive 的包。

1
cargo new hello_macro_derive --lib

现在,整个目录结构如下:

1
2
3
4
5
6
7
8
9
10
hello_macro
├── Cargo.toml
├── Cargo.lock
├── src
│ ├── main.rs
│ └── lib.rs
└── hello_macro_derive
├── Cargo.toml
└── src
└── lib.rs

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

我们需要声明 hello_macro_derivecrate是过程宏 crate。我们还需要 synquote 两个crate 中的功能,需要将它们加到依赖中。将下面的代码加入到 hello_macro_deriveCargo.toml文件中。

1
2
3
4
5
6
[lib]
proc-macro = true

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

这里我们在 [lib] 中设置proc-macro=true定义一个过程式宏。

接下来,添加如下代码:

文件名:hello_macro_derive/src/lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use proc_macro::TokenStream;
use quote::quote;
use syn;

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree
// that we can manipulate
// 基于 input 构建 AST 语法树
let ast = syn::parse(input).unwrap();

// Build the trait implementation
// 构建特征实现代码
impl_hello_macro(&ast)
}

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

现在,我们已经引入了三个新的crateproc_macrosynquote 。rust自带proc_macrocrate,因此无需将其加到 Cargo.toml 文件的依赖中。proc_macro 是编译器用来读取和操作我们rust代码的 API。

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

当用户在一个类型上指定 #[derive(HelloMacro)] 时,hello_macro_derive 函数将会被调用。因为我们已经使用 proc_macro_derive 及其指定名称HelloMacrohello_macro_derive 函数进行了注解,指定名称HelloMacro就是特征名,这是大多数过程宏遵循的习惯。

该函数首先将来自 TokenStreaminput 转换为一个我们可以解释和操作的数据结构。这正是 syn 派上用场的地方。syn 中的 parse 函数获取一个 TokenStream 并返回一个表示解析出rust代码的 DeriveInput 结构体。

derive过程宏只能用在结构体(struct)/枚举(enum)/联合(union)上,多数用在结构体上。举个例子,从字符串 struct Pancakes; 中解析出来的 DeriveInput 结构体的相关部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
DeriveInput {
// --snip--

ident: Ident {
ident: "Pancakes",
span: #0 bytes(95..103)
},
// Data是一个枚举,分别可以是DataStruct,DataEnum,DataUnion
data: Struct(
DataStruct {
struct_token: Struct,
fields: Unit,
semi_token: Some(
Semi
)
}
)
}

该结构体的字段展示了我们解析的rust代码是一个类单元结构体,其 ident(identifier,表示名字)为 Pancakes。该结构体里面有更多字段描述了所有类型的rust代码,查阅 synDeriveInput 的文档 以获取更多信息。

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

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

现在我们有了将注解的rust代码从 TokenStream 转换为 DeriveInput 实例的代码,让我们来创建在注解类型上实现 HelloMacro 特征的代码。

文件名:hello_macro_derive/src/lib.rs

1
2
3
4
5
6
7
8
9
10
11
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()
}

我们得到一个包含以 ast.ident 作为注解类型名字(标识符)的 Ident 结构体实例。仍以刚才的例子来说,其值是 "Pancakes"

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

这个宏也提供了一些非常酷的模板机制;我们可以写 #name ,然后 quote! 会以名为 name 的变量值来替换它。你甚至可以做一些类似常用宏那样的重复代码的工作。查阅 quote 的文档 来获取详尽的介绍。

我们期望我们的过程式宏能够为通过 #name 获取到的用户注解类型生成 HelloMacro 特征的实现。该特征的实现有一个函数 hello_macro ,其函数体包括了我们期望提供的功能:打印 Hello, Macro! My name is 和注解的类型名。

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

此时,使用cargo build 应该都能成功编译 hello_macrohello_macro_derive

接下来,我们需要在项目的 src/main.rs 中引用 hello_macro_derive 包的内容?方法有两种,第一种是将 hello_macro_derive 发布到 crates.ioGitHub 中。第二种就是使用相对路径引入的本地化方式。

我们采用第二种方法,在项目根目录下新建一个二进制项目:

1
cargo new pancakes

整个的文件结构现在变成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
hello_macro
├── Cargo.toml
├── Cargo.lock
├── src
│ ├── main.rs
│ └── lib.rs
├── hello_macro_derive
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
pancakes
├── Cargo.toml
├── Cargo.lock
└── src
└── lib.rs

然后需要将 hello_macrohello_macro_derive 作为依赖加到 pancakes 包的Cargo.toml文件中去。

文件名:pancakes/Cargo.toml

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

最后,我们添加代码到main.rs

文件名:pancakes/src/main.rs

1
2
3
4
5
6
7
8
9
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;

#[derive(HelloMacro)]
struct Pancakes;

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

现在执行 cargo run:其应该打印 Hello, Macro! My name is Pancakes!。其包含了该过程宏中 HelloMacro 特征的实现,而无需 pancakes 这个crate实现它;#[derive(HelloMacro)] 增加了该特征实现。

另外,学习过程更好的办法是通过展开宏来阅读和调试自己写的宏,这里需要用到一个cargo-expand的工具,可以通过下面的命令安装:

1
cargo install cargo-expand

然后,使用命令cargo expand即可展开宏。不过,这个命令依赖于不稳定的编译器标志,因此它需要安装nightly工具链。

我们可以使用如下命令查看已安装的工具链:

1
rustup toolchain list

默认情况下安装的是stable稳定版本,我们需要安装nightly版本并设置为默认:

1
rustup default nightly

查看rustc版本,验证是否切换成功

1
rustc --version

当然,也可以使用同样的方法切换回稳定版:

1
rustup default stable

我们展开宏看看代码是什么样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
struct Pancakes;
impl HelloMacro for Pancakes {
fn hello_macro() {
{
::std::io::_print(
format_args!("Hello, Macro! My name is {0}!\n", "Pancakes"),
);
};
}
}
fn main() {
Pancakes::hello_macro();
}

这里从展开的代码能看出derive宏的特性,struct Pancakes;被保留下来了,也就是说最后impl_hello_macro()返回的token被加到结构体后面,输入的token没有被修改。

10.5 类属性宏

类属性过程宏跟 derive 宏类似,但是前者允许我们定义自己的属性。除此之外,derive 只能用于结构体、枚举和union,而类属性宏可以用于其它类型项,例如函数。

假设我们在开发一个 web 框架,当用户通过 HTTP GET 请求访问 / 根路径时,使用 index 函数为其提供服务:

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

如上所示,代码功能非常清晰、简洁,这里的 #[route] 属性就是一个过程宏,其宏定义的函数签名看起来像这样:

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

derive 宏不同,类属性宏的定义函数有两个参数:

  • 第一个参数时用于说明属性包含的内容:Get, "/" 部分
  • 第二个是属性所标注的类型项,在这里是 fn index() {...},注意,函数体也被包含其中

除此之外,类属性宏跟 derive 宏的工作方式并无区别:创建一个包,类型是 proc-macro,接着实现一个函数用于生成想要的代码。

10.6 类函数宏

类函数宏可以让我们定义像函数那样调用的宏,从这个角度来看,它跟声明式宏 macro_rules 较为类似,它们比函数更灵活;例如,可以接受未知数量的参数。

区别在于,macro_rules 的定义形式与 match 匹配非常相像,而类函数宏的定义形式则类似于之前讲过的两种过程宏,即自定义派生宏的签名:获取括号中的 token,并返回希望生成的代码。

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

而使用形式则类似于函数调用:

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

这个宏会解析其中的 SQL 语句并检查其是否是句法正确的,这是比 macro_rules! 可以做到的更为复杂的处理。这个复杂的过程是 macro_rules 难以对付的,而过程宏相比起来就会灵活的多。

10.7 补充学习资料

  1. dtolnay/proc-macro-workshop,学习如何编写过程宏
  2. The Little Book of Rust Macros,学习如何编写声明宏 macro_rules!
  3. synquote ,用于编写过程宏的包,它们的文档有很多值得学习的东西
  4. Structuring, testing and debugging procedural macro crates,从测试、debug、结构化的角度来编写过程宏
  5. blog.turbo.fish,里面的过程宏系列文章值得一读
  6. Rust 宏小册中文版,非常详细的解释了宏各种知识

11 注释

在rust中,注释分为两类:

  • 非文档型注释(Non-documentation comments),用于说明某一块代码的功能。
  • 文档型注释(documentation comments),支持 Markdown,对项目描述、公共 API 等用户关心的功能进行介绍,同时还能提供示例代码,目标读者往往是想要了解你这个crate项目的人。

11.1 非文档注释

所有程序员都力求使其代码易于理解,不过有时还需要提供额外的解释。在这种情况下,程序员在源码中留下注释(comments),编译器会忽略它们,不过阅读代码的人可能觉得有用。

rust代码中的注释一般遵循 C/C++ 风格的行(//)和块(/* ... */)注释形式,也支持嵌套的块注释。非文档型注释编译时被解释为某种形式的空白符。

这是一个简单的单行注释:

1
// hello, world

对于超过一行的注释,需要在每一行前都加上 //,像这样:

1
2
3
// So we’re doing something complicated here, long enough that we need
// multiple lines of comments to do it! Whew! Hopefully, this comment will
// explain what’s going on.

注释也可以放在包含代码的行的末尾:

1
2
3
fn main() {
let lucky_number = 7; // I’m feeling lucky today
}

不过你更经常看到的是以这种格式使用它们,也就是位于它所解释的代码行的上面一行:

1
2
3
4
fn main() {
// I’m feeling lucky today
let lucky_number = 7;
}

多行注释的示例如下:

1
2
3
4
5
/* 
So we’re doing something complicated here, long enough that we need
multiple lines of comments to do it! Whew! Hopefully, this comment will
explain what’s going on.
*

11.2 文档注释

准确的crate文档有助于其他用户理解如何以及何时使用他们,所以花一些时间编写文档是值得的。

rust有特定的用于文档的注释类型,通常被称为文档型注释(documentation comments),他们通常是以三个斜线(///)开始的行文档型注释,以及块文档型注释(/** ... */)。顾名思义,文档型注释会生成 HTML 文档。这些 HTML 展示公有 API 文档注释的内容,意在让对库感兴趣的程序员理解如何使用这个crate,而不是它是如何被实现 的。

行文档注释

文档注释就位于需要文档的项的之前,举例来说:

文件名:src/lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
/// Adds one to the number given.
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
}

这里,我们提供了一个 add_one 函数工作的描述,接着开始了一个标题为 Examples 的部分,和展示如何使用 add_one 函数的代码。

以上代码有几点需要注意:

  • 文档注释需要位于 lib 类型的包中,例如 src/lib.rs
  • 文档注释可以使用 markdown语法。例如 # Examples 的标题,以及代码块高亮
  • 被注释的对象需要使用 pub 对外可见,记住:文档注释是给用户看的,内部实现细节不应该被暴露出去

可以通过命令:

1
cargo doc

来生成这个文档注释的 HTML 文档。这个命令运行由rust分发的工具 rustdoc 并将生成的 HTML 文档放入target/doc目录。

块文档注释

当注释内容多时,使用块注释可以减少 /// 的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
/** Adds two to the number given.

# Examples
```
let arg = 5;
let answer = my_crate::add_two(arg);

assert_eq!(7, answer);
```
*/
pub fn add_two(x: i32) -> i32 {
x + 2
}

在编写文档时,可以使用# Examples 这样的Markdown标题在 HTML 中创建了一个以 “Examples” 为标题的部分。一些 crate作者经常在文档注释中使用的部分有:

  • Panics:这个函数可能会 panic! 的场景。并不希望程序崩溃的函数调用者应该确保他们不会在这些情况下调用此函数。
  • Errors:如果这个函数返回 Result,此部分描述可能会出现何种错误以及什么情况会造成这些错误,这有助于调用者编写代码来采用不同的方式处理不同的错误。
  • Safety:如果这个函数使用 unsafe 代码,这一部分应该会涉及到期望函数调用者支持的确保 unsafe 块中代码正常工作的不变条件(invariants)。

当然大部分文档注释不需要所有这些部分,也可能有额外的部分,这只是一个参考。

我们可以使用命令:

1
cargo doc --open

可以在生成文档后,自动在浏览器中打开网页,查看刚才编写的文档注释效果。

注释包含项的结构

文档注释风格 //! 为包含注释的项,而不是位于注释之后的项增加文档。这通常用于crate根文件(通常是 src/lib.rs)或模块的根文件为crate或模块整体提供文档。

文件名:src/lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//! # My Crate
//!
//! `my_crate` is a collection of utilities to make performing certain
//! calculations more convenient.

/** Adds two to the number given.

# Examples
```
let arg = 5;
let answer = my_crate::add_two(arg);

assert_eq!(7, answer);
```
*/
pub fn add_two(x: i32) -> i32 {
x + 2
}

注意 //! 的最后一行之后没有任何代码。因为他们以 //! 开头而不是 ///,这是属于包含此注释的项而不是注释之后项的文档。在这个情况下时src/lib.rs文件,也就是crate根文件。这些注释描述了整个crate

11.3 文档注释中的代码跳转

跳转到标准库

rust在文档注释中还提供了一个非常强大的功能,那就是可以实现对外部项的链接:

1
2
3
4
/// `add_one` 返回一个[`Option`]类型
pub fn add_one(x: i32) -> Option<i32> {
Some(x + 1)
}

此处的 [Option] 就是一个链接,指向了标准库中的 Option 枚举类型,有两种方式可以进行跳转:

  • 在 IDE 中(如vscode),使用 Command + 鼠标左键(macOS),CTRL + 鼠标左键(Windows)
  • 在文档中直接点击链接

再比如,还可以使用路径的方式跳转:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use std::sync::mpsc::Receiver;

/// [`Receiver<T>`] [`std::future`].
///
/// [`std::future::Future`] [`Self::recv()`].
pub struct AsyncReceiver<T> {
sender: Receiver<T>,
}

impl<T> AsyncReceiver<T> {
pub async fn recv() -> T {
unimplemented!()
}
}

使用完整路径跳转到指定项

除了跳转到标准库,你还可以通过指定具体的路径跳转到自己代码或者其它库的指定项,例如在 lib.rs 中添加以下代码:

1
2
3
4
5
6
7
8
9
pub mod a {
/// `add_one` 返回一个[`Option`]类型
/// 跳转到[`crate::MySpecialFormatter`]
pub fn add_one(x: i32) -> Option<i32> {
Some(x + 1)
}
}

pub struct MySpecialFormatter;

使用 crate::MySpecialFormatter 这种路径就可以实现跳转到 lib.rs 中定义的结构体上。

同名项的跳转

如果遇到同名项,可以使用标示类型的方式进行跳转:

1
2
3
4
5
6
7
8
9
10
11
12
13
/// 跳转到结构体  [`Foo`](struct@Foo)
pub struct Bar;

/// 跳转到同名函数 [`Foo`](fn@Foo)
pub struct Foo {}

/// 跳转到同名宏 [`foo!`]
pub fn Foo() {}

#[macro_export]
macro_rules! foo {
() => {}
}

跳转到链接

文档注释支持Markdown,因此你可以使用链接跳转到任意地址:

1
2
3
4
5
6
/// Adds one to the number given.
///
/// see [github](https://github.com/rust-lang/rust)
pub fn add_one(x: i32) -> i32 {
x + 1
}

当然,支持Markdown就意味着它也支持HTML,你甚至可以添加js代码

1
2
3
4
5
6
7
/// Adds one to the number given.
///
/// see <a href="https://github.com/rust-lang/rust" style="color:red">github</a>
/// <script>alert(123)</script>
pub fn add_one(x: i32) -> i32 {
x + 1
}

11.4 文档搜索别名

rust文档支持搜索功能,我们可以为自己的类型定义几个别名,以实现更好的搜索展现,当别名命中时,搜索结果会被放在第一位:

1
2
3
4
5
6
#[doc(alias = "x")]
#[doc(alias = "big")]
pub struct BigX;

#[doc(alias("y", "big"))]
pub struct BigY;