通过例子学 Rust

中文翻译注(Chinese translation of the Rust By Example):

Rust 是一门注重安全(safety)、速度(speed)和并发(concurrency)的现代系统编程语言。Rust 通过内存安全来实现以上目标,但不使用垃圾回收机制(garbage collection, GC)。

《通过例子学 Rust》(Rust By Example, RBE)内容由一系列可运行的实例组成,通过这些例子阐明了各种 Rust 的概念和基本库。想获取这些例子外的更多内容,不要忘了安装 Rust 到本地并查阅官方标准库文档。另外为了满足您的好奇心,您还可以查阅本网站的源代码

现在让我们开始学习吧!

  • Hello World - 从经典的 “Hello World” 程序开始学习。

  • 原生类型 - 学习有符号整型,无符号整型和其他原生类型。

  • 自定义类型 - 结构体 struct 和 枚举 enum

  • 变量绑定 - 变量绑定,作用域,变量遮蔽。

  • 类型系统 - 学习改变和定义类型。

  • 类型转换

  • 表达式

  • 流程控制 - if/elsefor,以及其他流程控制有关内容。

  • 函数 - 学习方法、闭包和高阶函数。

  • 模块 - 使用模块来组织代码。

  • Crate - crate 是 Rust 中的编译单元。学习创建一个库。

  • Cargo - 学习官方的 Rust 包管理工具的一些基本功能。

  • 属性 - 属性是应用于某些模块、crate 或项的元数据(metadata)。

  • 泛型 - 学习编写能够适用于多种类型参数的函数或数据类型。

  • 作用域规则 - 作用域在所有权(ownership)、借用(borrowing)和生命周期(lifetime)中起着重要作用。

  • 特性 trait - trait 是对未知类型(Self)定义的方法集。

  • 错误处理 - 学习 Rust 语言处理失败的方式。

  • 标准库类型 - 学习 std 标准库提供的一些自定义类型。

  • 标准库更多介绍 - 更多关于文件处理、线程的自定义类型。

  • 测试 - Rust 语言的各种测试手段。

  • 不安全操作

  • 兼容性

  • 补充 - 文档和基准测试

Hello World

这是传统的 Hello World 程序的源码。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

println! 是一个(macros),可以将文本输出到控制台(console)。

使用 Rust 的编译器 rustc 可以从源程序生成可执行文件:

$ rustc hello.rs

使用 rustc 编译后将得到可执行文件 hello

$ ./hello Hello World!

动手试一试

单击上面的 "Run" 按钮并观察输出结果。然后增加一行代码,再一次使用宏 println!,得到下面结果:

Hello World! I'm a Rustacean!

注释

注释对任何程序都不可缺少,同样 Rust 支持几种不同的注释方式。

  • 普通注释,其内容将被编译器忽略掉:
    • // 单行注释,注释内容直到行尾。
    • /* 块注释,注释内容一直到结束分隔符。 */
  • 文档注释,其内容将被解析成 HTML 帮助文档
    • /// 为接下来的项生成帮助文档。
    • //! 为注释所属于的项(译注:如 crate、模块或函数)生成帮助文档。
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

文档注释

格式化输出

打印操作由 std::fmt 里面所定义的一系列来处理,包括:

  • format!:将格式化文本写到字符串
  • print!:与 format! 类似,但将文本输出到控制台(io::stdout)。
  • println!: 与 print! 类似,但输出结果追加一个换行符。
  • eprint!:与 print! 类似,但将文本输出到标准错误(io::stderr)。
  • eprintln!:与 eprint! 类似,但输出结果追加一个换行符。

这些宏都以相同的做法解析文本。有个额外优点是格式化的正确性会在编译时检查。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

std::fmt 包含多种 trait(特质)来控制文字显示,其中重要的两种 trait 的基本形式如下:

  • fmt::Debug:使用 {:?} 标记。格式化文本以供调试使用。
  • fmt::Display:使用 {} 标记。以更优雅和友好的风格来格式化文本。

上例使用了 fmt::Display,因为标准库提供了那些类型的实现。若要打印自定义类型的文本,需要更多的步骤。

动手试一试

  • 改正上面代码中的两个错误(见 “改正”),使它可以没有错误地运行。
  • 再用一个 println! 宏,通过控制显示的小数位数来打印:Pi is roughly 3.142(Pi 约等于 3.142)。为了达到练习目的,使用 let pi = 3.141592 作为 Pi 的近似值(提示:设置小数位的显示格式可以参考文档 std::fmt)。

参见:

std::fmt, macros, structtrait

调试(Debug)

所有的类型,若想用 std::fmt 的格式化打印,都要求实现至少一个可打印的 traits。 自动的实现只为一些类型提供,比如 std 库中的类型。所有其他类型 都必须手动实现。

fmt::Debug 这个 trait 使这项工作变得相当简单。所有类型都能推导(derive,即自 动创建)fmt::Debug 的实现。但是 fmt::Display 需要手动实现。

#![allow(unused)] fn main() { // 这个结构体不能使用 `fmt::Display` 或 `fmt::Debug` 来进行打印。 struct UnPrintable(i32); // `derive` 属性会自动创建所需的实现,使这个 `struct` 能使用 `fmt::Debug` 打印。 #[derive(Debug)] struct DebugPrintable(i32); }

所有 std 库类型都天生可以使用 {:?} 来打印:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

所以 fmt::Debug 确实使这些内容可以打印,但是牺牲了一些美感。Rust 也通过 {:#?} 提供了 “美化打印” 的功能:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

你可以通过手动实现 fmt::Display 来控制显示效果。

参见:

attributes, derive, std::fmtstruct

显示(Display)

fmt::Debug 通常看起来不太简洁,因此自定义输出的外观经常是更可取的。这需要通过 手动实现 fmt::Display 来做到。fmt::Display 采用 {} 标记。实现方式看 起来像这样:

#![allow(unused)] fn main() { // (使用 `use`)导入 `fmt` 模块使 `fmt::Display` 可用 use std::fmt; // 定义一个结构体,咱们会为它实现 `fmt::Display`。以下是个简单的元组结构体 // `Structure`,包含一个 `i32` 元素。 struct Structure(i32); // 为了使用 `{}` 标记,必须手动为类型实现 `fmt::Display` trait。 impl fmt::Display for Structure { // 这个 trait 要求 `fmt` 使用与下面的函数完全一致的函数签名 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // 仅将 self 的第一个元素写入到给定的输出流 `f`。返回 `fmt:Result`,此 // 结果表明操作成功或失败。注意 `write!` 的用法和 `println!` 很相似。 write!(f, "{}", self.0) } } }

fmt::Display 的效果可能比 fmt::Debug 简洁,但对于 std 库来说,这就有一个问 题。模棱两可的类型该如何显示呢?举个例子,假设标准库对所有的 Vec<T> 都实现了同 一种输出样式,那么它应该是哪种样式?下面两种中的一种吗?

  • Vec<path>/:/etc:/home/username:/bin(使用 : 分割)
  • Vec<number>1,2,3(使用 , 分割)

我们没有这样做,因为没有一种合适的样式适用于所有类型,标准库也并不擅自规定一种样 式。对于 Vec<T> 或其他任意泛型容器(generic container),fmt::Display 都没有 实现。因此在这些泛型的情况下要用 fmt::Debug

这并不是一个问题,因为对于任何泛型的容器类型, fmt::Display 都能够实 现。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

fmt::Display 被实现了,而 fmt::Binary 没有,因此 fmt::Binary 不能使用。 std::fmt 有很多这样的 trait,它们都要求有各自的实现。这些内容将在 后面的 std::fmt 章节中详细介绍。

动手试一试

检验上面例子的输出,然后在示例程序中,仿照 Point2D 结构体增加一个复数结构体。 使用一样的方式打印,输出结果要求是这个样子:

Display: 3.3 + 7.2i Debug: Complex { real: 3.3, imag: 7.2 }

参见:

derive, std::fmt, macros, struct, trait, 和 use

测试实例:List

对一个结构体实现 fmt::Display,其中的元素需要一个接一个地处理到,这可能会很麻 烦。问题在于每个 write! 都要生成一个 fmt::Result。正确的实现需要 处理所有的 Result。Rust 专门为解决这个问题提供了 ? 操作符。

write! 上使用 ? 会像是这样:

// 对 `write!` 进行尝试(try),观察是否出错。若发生错误,返回相应的错误。 // 否则(没有出错)继续执行后面的语句。 write!(f, "{}", value)?;

另外,你也可以使用 try! 宏,它和 ? 是一样的。这种写法比较罗嗦,故不再推荐, 但在老一些的 Rust 代码中仍会看到。使用 try! 看起来像这样:

try!(write!(f, "{}", value));

有了 ?,对一个 Vec 实现 fmt::Display 就很简单了:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

动手试一试:

更改程序使 vector 里面每个元素的下标也能够打印出来。新的结果如下:

[0: 1, 1: 2, 2: 3]

参见:

for, ref, Result, struct, ?, 和 vec!

格式化

我们已经看到,格式化的方式是通过格式字符串来指定的:

  • format!("{}", foo) -> "3735928559"
  • format!("0x{:X}", foo) -> "0xDEADBEEF"
  • format!("0o{:o}", foo) -> "0o33653337357"

根据使用的参数类型Xo 还是未指定,同样的变量(foo)能够格式化 成不同的形式。

这个格式化的功能是通过 trait 实现的,每种参数类型都对应一种 trait。最常见的格式 化 trait 就是 Display,它可以处理参数类型为未指定的情况,比如 {}

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

fmt::fmt 文档中可以查看格式化 traits 一览表和它们的参 数类型。

动手试一试

为上面的 Color 结构体实现 fmt::Display,应得到如下的输出结果:

RGB (128, 255, 90) 0x80FF5A RGB (0, 3, 254) 0x0003FE RGB (0, 0, 0) 0x000000

如果感到疑惑,可看下面两条提示:

参见:

std::fmt

原生类型

Rust 提供了多种原生类型(primitives),包括:

标量类型(scalar type)

  • 有符号整数(signed integers):i8i16i32i64i128isize(指针宽度)
  • 无符号整数(unsigned integers): u8u16u32u64u128usize(指针宽度)
  • 浮点数(floating point): f32f64
  • char(字符):单个 Unicode 字符,如 'a''α''∞'(每个都是 4 字节)
  • bool(布尔型):只能是 truefalse
  • 单元类型(unit type):()。其唯一可能的值就是 () 这个空元组

尽管单元类型的值是个元组,它却并不被认为是复合类型,因为并不包含多个值。

复合类型(compound type)

  • 数组(array):如 [1, 2, 3]
  • 元组(tuple):如 (1, true)

变量都能够显式地给出类型说明(type annotation)。数字还可以通过后缀(suffix)或默认方式来声明类型。整型默认为 i32 类型,浮点型默认为 f64类型。注意 Rust 还可以根据上下文来推断(infer)类型(译注:比如一个未声明类型整数和 i64 的整数相加,则该整数会自动推断为 i64 类型。仅当根据环境无法推断时,才按默认方式取整型数值为 i32,浮点数值为 f64)。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

stdmut类型推断变量遮蔽

字面量和运算符

整数 1、浮点数 1.2、字符 'a'、字符串 "abc"、布尔值 true 和单元类型 () 可以用数字、文字或符号之类的 “字面量”(literal)来表示。

另外,通过加前缀 0x0o0b,数字可以用十六进制、八进制或二进制记法表示。

为了改善可读性,可以在数值字面量中插入下划线,比如:1_000 等同于 10000.000_001 等同于 0.000001

我们需要把字面量的类型告诉编译器。如前面学过的,我们使用 u32 后缀来表明字面量是一个 32 位无符号整数,i32 后缀表明字面量是一个 32 位有符号整数。

Rust 提供了一系列的运算符(operator),它们的优先级和类 C 语言类似。(译注:类 C 语言包括 C/C++、Java、PHP 等语言)

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

元组

元组是一个可以包含各种类型值的组合。元组使用括号 () 来构造(construct),而每个元组自身又是一个类型标记为 (T1, T2, ...) 的值,其中 T1T2 是每个元素的类型。函数可以使用元组来返回多个值,因为元组可以拥有任意多个值。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

动手试一试

  1. 复习:在上面的例子中给 Matrix 结构体 加上 fmt::Display trait,这样当你从 Debug 格式化 {:?} 切换到 Display 格式化 {} 时,会得到如下的输出:

    ( 1.1 1.2 ) ( 2.1 2.2 )

    可以回顾之前学过的显示(display)的例子。

  2. reverse 函数作为样板,写一个 transpose 函数,它可以接受一个 Matrix 作为参数,并返回一个右上 - 左下对角线上的两元素交换后的 Matrix。举个例子:

    println!("Matrix:\n{}", matrix); println!("Transpose:\n{}", transpose(matrix));

    输出结果:

    Matrix: ( 1.1 1.2 ) ( 2.1 2.2 ) Transpose: ( 1.1 2.1 ) ( 1.2 2.2 )

数组和切片

数组(array)是一组拥有相同类型 T 的对象的集合,在内存中是连续存储的。数组使用中括号 [] 来创建,且它们的大小在编译时会被确定。数组的类型标记为 [T; length](译注:T 为元素类型,length 表示数组大小)。

切片(slice)类型和数组类似,但其大小在编译时是不确定的。相反,切片是一个双字对象(two-word object),第一个字是一个指向数据的指针,第二个字是切片的长度。这个 “字” 的宽度和 usize 相同,由处理器架构决定,比如在 x86-64 平台上就是 64 位。slice 可以用来借用数组的一部分。slice 的类型标记为 &[T]

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

自定义类型

Rust 自定义数据类型主要是通过下面这两个关键字来创建:

  • struct: 定义一个结构体(structure)
  • enum: 定义一个枚举类型(enumeration)

而常量(constant)可以通过 conststatic 关键字来创建。

结构体

结构体(structure,缩写成 struct)有 3 种类型,使用 struct 关键字来创建:

  • 元组结构体(tuple struct),事实上就是具名元组而已。
  • 经典的 C 语言风格结构体(C struct)。
  • 单元结构体(unit struct),不带字段,在泛型中很有用。
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

动手试一试:

  1. 增加一个计算 Rectangle (长方形)面积的函数 rect_area(尝试使用嵌套的解构方式)。
  2. 增加一个函数 square,接受的参数是一个 Point 和一个 f32,并返回一个 Rectangle(长方形),其左上角位于该点上,长和宽都对应于 f32

参见:

属性解构

枚举

enum 关键字允许创建一个从数个不同取值中选其一的枚举类型(enumeration)。任何一个在 struct 中合法的取值在 enum 中也合法。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

类型别名

若使用类型别名,则可以通过其别名引用每个枚举变量。当枚举的名称太长或者太一般化,且你想要对其重命名,那么这对你会有所帮助。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

最常见的情况就是在 impl 块中使用 Self 别名。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

该功能已在 Rust 中稳定下来, 可以阅读 stabilization report 来了解更多有关枚举和类型别名的知识。

参见:

match, fn, 和 String, “类型别名枚举变量” 的 RFC

使用 use

使用 use 声明的话,就可以不写出名称的完整路径了:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

matchuse

C 风格用法

enum 也可以像 C 语言风格的枚举类型那样使用。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参考:

类型转换

测试实例:链表

enum 的一个常见用法就是创建链表(linked-list):

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

Box方法

常量

Rust 有两种常量,可以在任意作用域声明,包括全局作用域。它们都需要显式的类型声明:

  • const:不可改变的值(通常使用这种)。
  • static:具有 'static 生命周期的,可以是可变的变量(译注:须使用 static mut 关键字)。

有个特例就是 "string" 字面量。它可以不经改动就被赋给一个 static 变量,因为它 的类型标记:&'static str 就包含了所要求的生命周期 'static。其他的引用类型都 必须特地声明,使之拥有'static 生命周期。这两种引用类型的差异似乎也无关紧要,因 为无论如何,static 变量都得显式地声明。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

const/static RFC, 'static 生命周期

变量绑定

Rust 通过静态类型确保类型安全。变量绑定可以在声明时说明类型,不过在多数情况下, 编译器能够从上下文推导出变量的类型,从而大大减少了类型说明的工作。

使用 let 绑定操作可以将值(比如字面量)绑定(bind)到变量。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

可变变量

变量绑定默认是不可变的(immutable),但加上 mut 修饰语后变量就可以改变。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

编译器会给出关于变量可变性的详细诊断信息。

作用域和遮蔽

变量绑定有一个作用域(scope),它被限定只在一个代码块(block)中生存(live)。 代码块是一个被 {} 包围的语句集合。另外也允许变量遮蔽(variable shadowing)。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

变量先声明

可以先声明(declare)变量绑定,后面才将它们初始化(initialize)。但是这种做法很 少用,因为这样可能导致使用未初始化的变量。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

编译器禁止使用未经初始化的变量,因为这会产生未定义行为(undefined behavior)。

冻结

当数据被相同的名称不变地绑定时,它还会冻结(freeze)。在不可变绑定超出作用域之前,无法修改已冻结的数据:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

类型系统

Rust 提供了多种机制,用于改变或定义原生类型和用户定义类型。接下来会讲到:

类型转换

Rust 不提供原生类型之间的隐式类型转换(coercion),但可以使用 as 关键字进行显 式类型转换(casting)。

整型之间的转换大体遵循 C 语言的惯例,除了 C 会产生未定义行为的情形。在 Rust 中所 有整型转换都是定义良好的。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

字面量

对数值字面量,只要把类型作为后缀加上去,就完成了类型说明。比如指定字面量 42 的 类型是 i32,只需要写 42i32

无后缀的数值字面量,其类型取决于怎样使用它们。如果没有限制,编译器会对整数使用 i32,对浮点数使用 f64

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

上面的代码使用了一些还没有讨论过的概念。心急的读者可以看看下面的简短解释:

  • fun(&foo)传引用(pass by reference)的方式把变量传给函数,而非 传值(pass by value,写法是 fun(foo))。更多细节请看借用
  • std::mem::size_of_val 是一个函数,这里使用其完整路径(full path)调用。代 码可以分成一些叫做模块(module)的逻辑单元。在本例中,size_of_val 函数是 在 mem 模块中定义的,而 mem 模块又是在 std crate 中定义的。更多细节 请看模块crate.

类型推断

Rust 的类型推断引擎是很聪明的,它不只是在初始化时看看右值(r-value)的 类型而已,它还会考察变量之后会怎样使用,借此推断类型。这是一个类型推导的进阶例子:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

没有必要写类型说明,编译器和程序员皆大欢喜!

别名

可以用 type 语句给已有的类型取个新的名字。类型的名字必须遵循驼峰命名法(像是 CamelCase 这样),否则编译器将给出警告。原生类型是例外,比如: usizef32,等等。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

别名的主要用途是避免写出冗长的模板化代码(boilerplate code)。如 IoResult<T>Result<T, IoError> 类型的别名。

参见:

属性

类型转换

Rust 使用 trait 解决类型之间的转换问题。最一般的转换会用到 Frominto 两个 trait。不过,即便常见的情况也可能会用到特别的 trait,尤其是 从 String 转换到别的类型,以及把别的类型转换到 String 时。

FromInto

FromInto 两个 trait 是内部相关联的,实际上这是它们实现的一部分。如果我们能够从类型 B 得到类型 A,那么很容易相信我们也能把类型 B 转换为类型 A。

From

From trait 允许一种类型定义 “怎么根据另一种类型生成自己”,因此它提供了一种类型转换的简单机制。在标准库中有无数 From 的实现,规定原生类型及其他常见类型的转换功能。

比如,可以很容易地把 str 转换成 String

#![allow(unused)] fn main() { let my_str = "hello"; let my_string = String::from(my_str); }

也可以为我们自己的类型定义转换机制:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Into

Into trait 就是把 From trait 倒过来而已。也就是说,如果你为你的类型实现了 From,那么同时你也就免费获得了 Into

使用 Into trait 通常要求指明要转换到的类型,因为编译器大多数时候不能推断它。不过考虑到我们免费获得了 Into,这点代价不值一提。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

TryFrom and TryInto

类似于 FromIntoTryFromTryInto 是 类型转换的通用 trait。不同于 From/Into 的是,TryFromTryInto trait 用于易出错的转换,也正因如此,其返回值是 Result 型。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

ToStringFromStr

ToString

要把任何类型转换成 String,只需要实现那个类型的 ToString trait。然而不要直接这么做,您应该实现fmt::Display trait,它会自动提供 ToString,并且还可以用来打印类型,就像 print! 一节中讨论的那样。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

解析字符串

我们经常需要把字符串转成数字。完成这项工作的标准手段是用 parse 函数。我们得 提供要转换到的类型,这可以通过不使用类型推断,或者用 “涡轮鱼” 语法(turbo fish,<>)实现。

只要对目标类型实现了 FromStr trait,就可以用 parse 把字符串转换成目标类型。 标准库中已经给无数种类型实现了 FromStr。如果要转换到用户定义类型,只要手动实现 FromStr 就行。

fn main() { let parsed: i32 = "5".parse().unwrap(); let turbo_parsed = "10".parse::<i32>().unwrap(); let sum = parsed + turbo_parsed; println!{"Sum: {:?}", sum}; }

表达式

Rust 程序(大部分)由一系列语句构成:

fn main() { // 语句 // 语句 // 语句 }

Rust 有多种语句。最普遍的语句类型有两种:一种是声明绑定变量,另一种是表达式带上英文分号(;):

fn main() { // 变量绑定 let x = 5; // 表达式; x; x + 1; 15; }

代码块也是表达式,所以它们可以用作赋值中的值。代码块中的最后一个表达式将赋给适当的表达式,例如局部变量。但是,如果代码块的最后一个表达式结尾处有分号,则返回值为 ()(译注:代码块中的最后一个语句是代码块中实际执行的最后一个语句,而不一定是代码块中最后一行的语句)。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

流程控制

任何编程语言都包含的一个必要部分就是改变控制流程:if/elsefor 等。让我们 谈谈 Rust 语言中的这部分内容。

if/else

if-else 分支判断和其他语言类似。不同的是,Rust 语言中的布尔判断条件不必使用小括号包裹,且每个条件后面都跟着一个代码块。if-else 条件选择是一个表达式,并且所有分支都必须返回相同的类型。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

loop 循环

Rust 提供了 loop 关键字来表示一个无限循环。

可以使用 break 语句在任何时候退出一个循环,还可以使用 continue 跳过循环体的剩余部分并开始下一轮循环。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

嵌套循环和标签

在处理嵌套循环的时候可以 breakcontinue 外层循环。在这类情形中,循环必须用一些 'label(标签)来注明,并且标签必须传递给 break/continue 语句。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

从 loop 循环中返回

loop 有个用途是尝试一个操作直到成功为止。若操作返回一个值,则可能需要将其传递给代码的其余部分:将该值放在 break 之后,它就会被 loop 表达式返回。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

while 循环

while 关键字可以用作当型循环(当条件满足时循环)。

让我们用 while 循环写一下臭名昭著的 FizzBuzz(译者补充:LeetCode 上的 FizzBuzz 问题描述) 程序。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

for 循环

for 与区间

for in 结构可以遍历一个 Iterator(迭代器)。创建迭代器的一个最简单的方法是使用区间标记 a..b。这会生成从 a(包含此值) 到 b(不含此值)的,步长为 1 的一系列值。

让我们使用 for 代替 while 来写 FizzBuzz 程序。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

或者,可以使用a..=b表示两端都包含在内的范围。上面的代码可以写成:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

for 与迭代器

for in 结构能以几种方式与 Iterator 互动。在 迭代器 trait 一节将会谈到,如果没有特别指定,for 循环会对给出的集合应用 into_iter 函数,把它转换成一个迭代器。这并不是把集合变成迭代器的唯一方法,其他的方法有 iteriter_mut 函数。

这三个函数会以不同的方式返回集合中的数据。

  • iter - 在每次迭代中借用集合中的一个元素。这样集合本身不会被改变,循环之后仍可以使用。
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

译注:Ferris 是 Rust 的非官方吉祥物

  • into_iter - 会消耗集合。在每次迭代中,集合中的数据本身会被提供。一旦集合被消耗了,之后就无法再使用了,因为它已经在循环中被 “移除”(move)了。
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
  • iter_mut - 可变地(mutably)借用集合中的每个元素,从而允许集合被就地修改。
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

在上面这些代码中,注意 match 的分支中所写的类型不同,这是不同迭代方式的关键区别。因为类型不同,能够执行的操作当然也不同。

参见:

Iterator

match 匹配

Rust 通过 match 关键字来提供模式匹配,和 C 语言的 switch 用法类似。第一个匹配分支会被比对,并且所有可能的值都必须被覆盖。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

解构

match 代码块能以多种方式解构物件。

元组

元组可以在 match 中解构,如下所示:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

元组

枚举

和前面相似,解构 enum 的方式如下:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

#[allow(...)], 色彩模型enum

指针和引用

对指针来说,解构(destructure)和解引用(dereference)要区分开,因为这两者的概念是不同的,和 C 那样的语言用法不一样。

  • 解引用使用 *
  • 解构使用 &ref、和 ref mut
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

ref 模式

结构体

类似地,解构 struct 如下所示:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

结构体, ref 模式

卫语句

可以加上 match 卫语句(guard) 来过滤分支。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

元组

绑定

match 中,若间接地访问一个变量,则不经过重新绑定就无法在分支中再使用它。match 提供了 @ 符号来绑定变量到名称:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

你也可以使用绑定来“解构” enum 变体,例如 Option:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

函数枚举Option

if let

在一些场合下,用 match 匹配枚举类型并不优雅。比如:

#![allow(unused)] fn main() { // 将 `optional` 定为 `Option<i32>` 类型 let optional = Some(7); match optional { Some(i) => { println!("This is a really long string and `{:?}`", i); // ^ 行首需要 2 层缩进。这里从 optional 中解构出 `i`。 // 译注:正确的缩进是好的,但并不是 “不缩进就不能运行” 这个意思。 }, _ => {}, // ^ 必须有,因为 `match` 需要覆盖全部情况。不觉得这行很多余吗? }; }

if let 在这样的场合要简洁得多,并且允许指明数种失败情形下的选项:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

同样,可以用 if let 匹配任何枚举值:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另一个好处是:if let 允许匹配枚举非参数化的变量,即枚举未注明 #[derive(PartialEq)],我们也没有为其实现 PartialEq。在这种情况下,通常 if Foo::Bar==a 会出错,因为此类枚举的实例不具有可比性。但是,if let 是可行的。

你想挑战一下吗?使用 if let修复以下示例:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

枚举Option,和相关的 RFC

while let

if let 类似,while let 也可以把别扭的 match 改写得好看一些。考虑下面这段使 i 不断增加的代码:

#![allow(unused)] fn main() { // 将 `optional` 设为 `Option<i32>` 类型 let mut optional = Some(0); // 重复运行这个测试。 loop { match optional { // 如果 `optional` 解构成功,就执行下面语句块。 Some(i) => { if i > 9 { println!("Greater than 9, quit!"); optional = None; } else { println!("`i` is `{:?}`. Try again.", i); optional = Some(i + 1); } // ^ 需要三层缩进! }, // 当解构失败时退出循环: _ => { break; } // ^ 为什么必须写这样的语句呢?肯定有更优雅的处理方式! } } }

使用 while let 可以使这段代码变得更加优雅:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

枚举Option,和相关的 RFC

函数

函数(function)使用 fn 关键字来声明。函数的参数需要标注类型,就和变量一样,如果函数返回一个值,返回类型必须在箭头 -> 之后指定。

函数最后的表达式将作为返回值。也可以在函数内使用 return 语句来提前返一个值,甚至可以在循环或 if 内部使用。

让我们使用函数来重写 FizzBuzz 程序吧!

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

方法

方法(method)是依附于对象的函数。这些方法通过关键字 self 来访问对象中的数据和其他。方法在 impl 代码块中定义。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

闭包

Rust 中的闭包(closure),也叫做 lambda 表达式或者 lambda,是一类能够捕获周围作用域中变量的函数。例如,一个可以捕获 x 变量的闭包如下:

|val| val + x

它们的语法和能力使它们在临时(on the fly)使用时相当方便。调用一个闭包和调用一个函数完全相同,不过调用闭包时,输入和返回类型两者都可以自动推导,而输入变量名必须指明。

其他的特点包括:

  • 声明时使用 || 替代 () 将输入参数括起来。
  • 函数体定界符({})对于单个表达式是可选的,其他情况必须加上。
  • 有能力捕获外部环境的变量。
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

捕获

闭包本质上很灵活,能做功能要求的事情,使闭包在没有类型标注的情况下运行。这使得捕获(capture)能够灵活地适应用例,既可移动(move),又可借用(borrow)。闭包可以通过以下方式捕获变量:

  • 通过引用:&T
  • 通过可变引用:&mut T
  • 通过值:T

闭包优先通过引用来捕获变量,并且仅在需要时使用其他方式。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

在竖线 | 之前使用 move 会强制闭包取得被捕获变量的所有权:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

Boxstd::mem::drop

作为输入参数

虽然 Rust 无需类型说明就能在大多数时候完成变量捕获,但在编写函数时,这种模糊写法是不允许的。当以闭包作为输入参数时,必须指出闭包的完整类型,它是通过使用以下 trait 中的一种来指定的。其受限制程度按以下顺序递减:

  • Fn:表示捕获方式为通过引用(&T)的闭包
  • FnMut:表示捕获方式为通过可变引用(&mut T)的闭包
  • FnOnce:表示捕获方式为通过值(T)的闭包

译注:顺序之所以是这样,是因为 &T 只是获取了不可变的引用,&mut T 则可以改变变量,T 则是拿到了变量的所有权而非借用。

对闭包所要捕获的每个变量,编译器都将以限制最少的方式来捕获。

译注:这句可能说得不对,事实上是在满足使用需求的前提下尽量以限制最多的方式捕获。

例如用一个类型说明为 FnOnce 的闭包作为参数。这说明闭包可能采取 &T&mut TT 中的一种捕获方式,但编译器最终是根据所捕获变量在闭包里的使用情况决定捕获方式。

这是因为如果能以移动的方式捕获变量,则闭包也有能力使用其他方式借用变量。注意反过来就不再成立:如果参数的类型说明是 Fn,那么不允许该闭包通过 &mut TT 捕获变量。

在下面的例子中,试着分别用一用 FnFnMutFnOnce,看看会发生什么:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

std::mem::drop, Fn, FnMut, 和 FnOnce

类型匿名

闭包从周围的作用域中捕获变量是简单明了的。这样会有某些后果吗?确实有。观察一下使用闭包作为函数参数,这要求闭包是泛型的,闭包定义的方式决定了这是必要的。

#![allow(unused)] fn main() { // `F` 必须是泛型的。 fn apply<F>(f: F) where F: FnOnce() { f(); } }

当闭包被定义,编译器会隐式地创建一个匿名类型的结构体,用以储存闭包捕获的变量,同时为这个未知类型的结构体实现函数功能,通过 FnFnMutFnOnce 三种 trait 中的一种。

若使用闭包作为函数参数,由于这个结构体的类型未知,任何的用法都要求是泛型的。然而,使用未限定类型的参数 <T> 过于不明确,并且是不允许的。事实上,指明为该结构体实现的是 FnFnMut、或 FnOnce 中的哪种 trait,对于约束该结构体的类型而言就已经足够了。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

详尽分析, Fn, FnMut, 和 FnOnce

输入函数

既然闭包可以作为参数,你很可能想知道函数是否也可以呢。确实可以!如果你声明一个接受闭包作为参数的函数,那么任何满足该闭包的 trait 约束的函数都可以作为其参数。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

多说一句,FnFnMutFnOnce 这些 trait 明确了闭包如何从周围的作用域中捕获变量。

参见:

Fn, FnMut, 和 FnOnce

作为输出参数

闭包作为输入参数是可能的,所以返回闭包作为输出参数(output parameter)也应该是可能的。然而返回闭包类型会有问题,因为目前 Rust 只支持返回具体(非泛型)的类型。按照定义,匿名的闭包的类型是未知的,所以只有使用impl Trait才能返回一个闭包。

返回闭包的有效特征是:

  • Fn
  • FnMut
  • FnOnce

除此之外,还必须使用 move 关键字,它表明所有的捕获都是通过值进行的。这是必须的,因为在函数退出时,任何通过引用的捕获都被丢弃,在闭包中留下无效的引用。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

Fn, FnMut, 泛型impl Trait.

std 中的例子

本小节列出几个标准库中使用闭包的例子。

Iterator::any

Iterator::any 是一个函数,若传给它一个迭代器(iterator),当其中任一元素满足谓词(predicate)时它将返回 true,否则返回 false(译注:谓词是闭包规定的, true/false 是闭包作用在元素上的返回值)。它的签名如下:

pub trait Iterator { // 被迭代的类型。 type Item; // `any` 接受 `&mut self` 参数(译注:回想一下,这是 `self: &mut Self` 的简写) // 表明函数的调用者可以被借用和修改,但不会被消耗。 fn any<F>(&mut self, f: F) -> bool where // `FnMut` 表示被捕获的变量最多只能被修改,而不能被消耗。 // `Self::Item` 指明了被捕获变量的类型(译注:是迭代器的元素本身的类型) F: FnMut(Self::Item) -> bool {} // 译注:原文说 `Self::Item` 表明变量是通过值传递给闭包的,这是说错了。 // `FnMut` 就表示闭包只能通过引用捕获变量。把类型为 `T` 的变量作为闭包 // 的参数不代表闭包会拿走它的值,也可能是拿走它的引用。 }
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

std::iter::Iterator::any

Iterator::find

Iterator::find 是一个函数,在传给它一个迭代器时,将用 Option 类型返回第一个满足谓词的元素。它的签名如下:

pub trait Iterator { // 被迭代的类型。 type Item; // `find` 接受 `&mut self` 参数,表明函数的调用者可以被借用和修改, // 但不会被消耗。 fn find<P>(&mut self, predicate: P) -> Option<Self::Item> where // `FnMut` 表示被捕获的变量最多只能被修改,而不能被消耗。 // `&Self::Item` 指明了被捕获变量的类型(译注:是对迭代器元素的引用类型) P: FnMut(&Self::Item) -> bool {} }
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

std::iter::Iterator::find

高阶函数

Rust 提供了高阶函数(Higher Order Function, HOF),指那些输入一个或多个函数,并且/或者产生一个更有用的函数的函数。HOF 和惰性迭代器(lazy iterator)给 Rust 带来了函数式(functional)编程的风格。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Option迭代器 都实现了不少高阶函数。

发散函数

发散函数(diverging function)绝不会返回。 它们使用 ! 标记,这是一个空类型。

#![allow(unused)] fn main() { fn foo() -> ! { panic!("This call never returns."); } }

和所有其他类型相反,这个类型无法实例化,因为此类型可能具有的所有可能值的集合为空。 注意,它与 () 类型不同,后者只有一个可能的值。

如下面例子,虽然返回值中没有信息,但此函数会照常返回。

fn some_fn() { () } fn main() { let a: () = some_fn(); println!("This function returns and you can see this line.") }

下面这个函数相反,这个函数永远不会将控制内容返回给调用者。

#![feature(never_type)] fn main() { let x: ! = panic!("This call never returns."); println!("You will never see this line!"); }

虽然这看起来像是一个抽象的概念,但实际上这非常有用且方便。这种类型的主要优点是它可以被转换为任何其他类型,从而可以在需要精确类型的地方使用,例如在 match 匹配分支。 这允许我们编写如下代码:

fn main() { fn sum_odd_numbers(up_to: u32) -> u32 { let mut acc = 0; for i in 0..up_to { // 注意这个 match 表达式的返回值必须为 u32, // 因为 “addition” 变量是这个类型。 let addition: u32 = match i%2 == 1 { // “i” 变量的类型为 u32,这毫无问题。 true => i, // 另一方面,“continue” 表达式不返回 u32,但它仍然没有问题, // 因为它永远不会返回,因此不会违反匹配表达式的类型要求。 false => continue, }; acc += addition; } acc } println!("Sum of odd numbers up to 9 (excluding): {}", sum_odd_numbers(9)); }

这也是永远循环(如 loop {})的函数(如网络服务器)或终止进程的函数(如 exit())的返回类型。

模块

Rust 提供了一套强大的模块(module)系统,可以将代码按层次分成多个逻辑 单元(模块),并管理这些模块之间的可见性(公有(public)或私有(private))。

模块是项(item)的集合,项可以是:函数,结构体,trait,impl 块,甚至其它模块。

可见性

默认情况下,模块中的项拥有私有的可见性(private visibility),不过可以加上 pub 修饰语来重载这一行为。模块中只有公有的(public)项可以从模块外的作用域 访问。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

结构体的可见性

结构体的字段也是一个可见性的层次。字段默认拥有私有的可见性,也可以加上 pub 修 饰语来重载该行为。只有从结构体被定义的模块之外访问其字段时,这个可见性才会 起作用,其意义是隐藏信息(即封装,encapsulation)。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

泛型方法

use 声明

use 声明可以将一个完整的路径绑定到一个新的名字,从而更容易访问。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

superself

可以在路径中使用 super (父级)和 self(自身)关键字,从而在访问项时消除 歧义,以及防止不必要的路径硬编码。

fn function() { println!("called `function()`"); } mod cool { pub fn function() { println!("called `cool::function()`"); } } mod my { fn function() { println!("called `my::function()`"); } mod cool { pub fn function() { println!("called `my::cool::function()`"); } } pub fn indirect_call() { // 让我们从这个作用域中访问所有名为 `function` 的函数! print!("called `my::indirect_call()`, that\n> "); // `self` 关键字表示当前的模块作用域——在这个例子是 `my`。 // 调用 `self::function()` 和直接调用 `function()` 都得到相同的结果, // 因为他们表示相同的函数。 self::function(); function(); // 我们也可以使用 `self` 来访问 `my` 内部的另一个模块: self::cool::function(); // `super` 关键字表示父作用域(在 `my` 模块外面)。 super::function(); // 这将在 *crate* 作用域内绑定 `cool::function` 。 // 在这个例子中,crate 作用域是最外面的作用域。 { use crate::cool::function as root_function; root_function(); } } } fn main() { my::indirect_call(); }

文件分层

模块可以分配到文件/目录的层次结构中。让我们将《可见性》一节中 的例子的代码拆分到多个文件中:

$ tree . . |-- my | |-- inaccessible.rs | |-- mod.rs | `-- nested.rs `-- split.rs

split.rs 的内容:

// 此声明将会查找名为 `my.rs` 或 `my/mod.rs` 的文件,并将该文件的内容放到 // 此作用域中一个名为 `my` 的模块里面。 mod my; fn function() { println!("called `function()`"); } fn main() { my::function(); function(); my::indirect_access(); my::nested::function(); }

my/mod.rs 的内容:

// 类似地,`mod inaccessible` 和 `mod nested` 将找到 `nested.rs` 和 // `inaccessible.rs` 文件,并在它们放到各自的模块中。 mod inaccessible; pub mod nested; pub fn function() { println!("called `my::function()`"); } fn private_function() { println!("called `my::private_function()`"); } pub fn indirect_access() { print!("called `my::indirect_access()`, that\n> "); private_function(); }

my/nested.rs 的内容:

pub fn function() { println!("called `my::nested::function()`"); } #[allow(dead_code)] fn private_function() { println!("called `my::nested::private_function()`"); }

my/inaccessible.rs 的内容:

#[allow(dead_code)] pub fn public_function() { println!("called `my::inaccessible::public_function()`"); }

我们看到代码仍然正常运行,就和前面的一样:

$ rustc split.rs && ./split called `my::function()` called `function()` called `my::indirect_access()`, that > called `my::private_function()` called `my::nested::function()`

crate

crate(中文有 “包,包装箱” 之意)是 Rust 的编译单元。当调用 rustc some_file.rs 时,some_file.rs 被当作 crate 文件。如果 some_file.rs 里面含有 mod 声明,那么模块文件的内容将在编译之前被插入 crate 文件的相应声明处。换句话说,模 块不会单独被编译,只有 crate 才会被编译。

crate 可以编译成二进制可执行文件(binary)或库文件(library)。默认情况 下,rustc 将从 crate 产生二进制可执行文件。这种行为可以通过 rustc 的选项 --crate-type 重载。

让我们创建一个库,然后看看如何把它链接到另一个 crate。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
$ rustc --crate-type=lib rary.rs $ ls lib* library.rlib

默认情况下,库会使用 crate 文件的名字,前面加上 “lib” 前缀,但这个默认名称可以 使用 crate_name 属性 覆盖。

使用库

要将一个 crate 链接到上节新建的库,可以使用 rustc--extern 选项。然后将所有的物件导入到与库名相同的模块下。此模块的操作通常与任何其他模块相同。

// extern crate rary; // 在 Rust 2015 版或更早版本需要这个导入语句 fn main() { rary::public_function(); // 报错! `private_function` 是私有的 //rary::private_function(); rary::indirect_access(); }
# library.rlib 是已编译好的库的路径,这里假设它在同一目录下: $ rustc executable.rs --extern rary=library.rlib --edition=2018 && ./executable called rary's `public_function()` called rary's `indirect_access()`, that > called rary's `private_function()`

cargo

cargo 是官方的 Rust 包管理工具。 它有很多非常有用的功能来提高代码质量和开发人员的开发效率! 这些功能包括:

  • 依赖管理和与 crates.io(官方 Rust 包注册服务)集成
  • 方便的单元测试
  • 方便的基准测试

本章将介绍一些快速入门的基础知识,不过你可以在 cargo 官方手册中找到详细内容。

依赖

大多数程序都会依赖于某些库。如果你曾经手动管理过库依赖,那么你就知道这会带来的极大的痛苦。幸运的是,Rust 的生态链标配 cargo 工具!cargo 可以管理项目的依赖关系。

下面创建一个新的 Rust 项目:

# 二进制可执行文件 cargo new foo # 或者库 cargo new --lib foo

对于本章的其余部分,我们选定创建的都是二进制可执行文件而不是库,但所有的概念都是相同的。

完成上述命令后,将看到如下内容:

foo ├── Cargo.toml └── src └── main.rs

main.rs 是新项目的入口源文件——这里没什么新东西。 Cargo.toml 是本项目(foo)的 cargo 的配置文件。 浏览 Cargo.toml 文件,将看到类似以下的的内容:

[package] name = "foo" version = "0.1.0" authors = ["mark"] [dependencies]

package 下面的 name 字段表明项目的名称。 如果您发布 crate(后面将做更多介绍),那么 crates.io 将使用此字段标明的名称。 这也是编译时输出的二进制可执行文件的名称。

version 字段是使用语义版本控制(Semantic Versioning)的 crate 版本号。

authors 字段表明发布 crate 时的作者列表。

dependencies 这部分可以让你为项目添加依赖。

举个例子,假设我们希望程序有一个很棒的命令行界面(command-line interface,CLI))。 你可以在 crates.io(官方的 Rust 包注册服务)上找到很多很棒的 Rust 包。其中一个受欢迎的包是 clap(译注:一个命令行参数的解析器)。在撰写本文时,[clap] 最新发布的版本为 2.27.1。要在程序中添加依赖,我们可以很简单地在 Cargo.toml 文件中的 dependencies 项后面将以下内容添加进来 :clap = "2.27.1"。当然,在 main.rs 文件中写上 extern crate clap,就和平常一样。 就是这样!你就可以在程序中开始使用 clap 了。

cargo 还支持其他类型的依赖。 下面是一个简单的示例:

[package] name = "foo" version = "0.1.0" authors = ["mark"] [dependencies] clap = "2.27.1" # 来自 crates.io rand = { git = "https://github.com/rust-lang-nursery/rand" } # 来自网上的仓库 bar = { path = "../bar" } # 来自本地文件系统的路径

cargo 不仅仅是一个包依赖管理器。Cargo.toml 的所有可用配置选项都列在 格式规范中。

要构建我们的项目,我们可以在项目目录中的任何位置(包括子目录!)执行 cargo build。我们也可以执行 cargo run 来构建和运行。请注意,这些命令将处理所有依赖,在需要时下载 crate,并构建所有内容,包括 crate。(请注意,它只重新构建尚未构建的内容,这和 make 类似)。

瞧!这里的所有都和 cargo 有关!

约定规范

在上一小节中,我们看到了以下目录层次结构:

foo ├── Cargo.toml └── src └── main.rs

假设我们要在同一个项目中有两个二进制可执行文件。 那要怎样做呢?

很显然,cargo 支持这一点。正如我们之前看到的,默认二进制名称是 main,但可以通过将文件放在 bin/ 目录中来添加其他二进制可执行文件:

foo ├── Cargo.toml └── src ├── main.rs └── bin └── my_other_bin.rs

为了使得 cargo 编译或运行这个二进制可执行文件而不是默认或其他二进制可执行文件,我们只需给 cargo 增加一个参数 --bin my_other_bin,其中 my_other_bin 是我们想要使用的二进制可执行文件的名称。

除了可添加其他二进制可执行文件外,cargo 还支持更多功能,如基准测试,测试和示例。

在下一节中,我们将更仔细地学习测试的内容。

测试

我们知道测试是任何软件不可缺少的一部分!Rust 对单元和集成测试提供一流的支持(参见《Rust 程序设计语言》中的关于测试的章节)。

通过上面链接的关于测试章节,我们看到了如何编写单元测试和集成测试。在代码目录组织上,我们可以将单元测试放在需要测试的模块中,并将集成测试放在源码中 tests/ 目录中:

foo ├── Cargo.toml ├── src │ └── main.rs └── tests ├── my_test.rs └── my_other_test.rs

tests 目录下的每个文件都是一个单独的集成测试。

cargo 很自然地提供了一种便捷的方法来运行所有测试!

cargo test

你将会看到像这样的输出:

$ cargo test Compiling blah v0.1.0 (file:///nobackup/blah) Finished dev [unoptimized + debuginfo] target(s) in 0.89 secs Running target/debug/deps/blah-d3b32b97275ec472 running 3 tests test test_bar ... ok test test_baz ... ok test test_foo_bar ... ok test test_foo ... ok test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

你还可以运行如下测试,其中名称匹配一个模式:

cargo test test_foo
$ cargo test test_foo Compiling blah v0.1.0 (file:///nobackup/blah) Finished dev [unoptimized + debuginfo] target(s) in 0.35 secs Running target/debug/deps/blah-d3b32b97275ec472 running 2 tests test test_foo ... ok test test_foo_bar ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out

需要注意的一点是:cargo 可能同时进行多项测试,因此请确保它们不会相互竞争。例如,如果它们都输出到文件,则应该将它们写入不同的文件。

构建脚本

有时使用 cargo 正常构建还是不够的。也许你的 crate 在 cargo 成功编译之前需要一些先决条件,比如代码生成或者需要编译的一些本地代码。为了解决这个问题,我们构建了 cargo 可以运行的脚本。

要向包中添加构建脚本,可以在 Cargo.toml 中指定它,如下所示:

[package] ... build = "build.rs"

跟默认情况不同,这里 cargo 将在项目目录中优先查找 build.rs 文件。(本句采用意译,英文原文为:Otherwise Cargo will look for a build.rs file in the project directory by default.)

怎么使用构建脚本

构建脚本只是另一个 Rust 文件,此文件将在编译包中的任何其他内容之前,优先进行编译和调用。 因此,此文件可实现满足 crate 的先决条件。

cargo 通过此处指定的可以使用的环境变量为脚本提供输入。(英文原文:Cargo provides the script with inputs via environment variables specified here that can be used.)

此脚本通过 stdout (标准输出)提供输出。打印的所有行都写入到 target/debug/build/<pkg>/output。另外,以 cargo: 为前缀的行将由 cargo 直接解析,因此可用于定义包编译的参数。

有关进一步的说明和示例,请阅读 cargo 规定说明文档

属性

属性是应用于某些模块、crate 或项的元数据(metadata)。这元数据可以用来:

当属性作用于整个 crate 时,它们的语法为 #![crate_attribute],当它们用于模块 或项时,语法为 #[item_attribute](注意少了感叹号 !)。

属性可以接受参数,有不同的语法形式:

  • #[attribute = "value"]
  • #[attribute(key = "value")]
  • #[attribute(value)]

属性可以多个值,它们可以分开到多行中:

#[attribute(value, value2)] #[attribute(value, value2, value3, value4, value5)]

死代码 dead_code

编译器提供了 dead_code(死代码,无效代码)lint,这会对未使用的函数 产生警告。可以用一个属性来禁用这个 lint。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

注意在实际程序中,需要将死代码清除掉。由于本书的例子是交互性的,因而其中需要 允许一些死代码的出现。

crate

crate_type 属性可以告知编译器 crate 是一个二进制的可执行文件还是一个 库(甚至是哪种类型的库),crate_name 属性可以设定 crate 的名称。

不过,一定要注意在使用 cargo 时,这两种类型时都没有作用。由于大多数 Rust 工程都使用 cargo,这意味着 crate_typecrate_name 的作用事实上很有限。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

当用到 crate_type 属性时,就不再需要给 rustc 命令加上 --crate-type 标记。

$ rustc lib.rs $ ls lib* library.rlib

cfg

条件编译可能通过两种不同的操作符实现:

  • cfg 属性:在属性位置中使用 #[cfg(...)]
  • cfg! 宏:在布尔表达式中使用 cfg!(...)

两种形式使用的参数语法都相同。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

引用, cfg!, 和 .

自定义条件

有部分条件如 target_os 是由 rustc 隐式地提供的,但是自定义条件必须使用 --cfg 标记来传给 rustc

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

试试不使用自定义的 cfg 标记会发生什么:

$ rustc custom.rs && ./custom No such file or directory (os error 2)

使用自定义的 cfg 标记:

$ rustc --cfg some_condition custom.rs && ./custom condition met!

泛型

泛型(generic)是关于泛化类型和函数功能,以扩大其适用范围的话题。泛型极大地 减少了代码的重复,但它自身的语法很要求细心。也就是说,采用泛型意味着仔细地指定 泛型类型具体化时,什么样的具体类型是合法的。泛型最简单和常用的用法是用于类型参数。

译注:定义泛型类型或泛型函数之类的东西时,我们会用 <A> 或者 <T> 这类标记 作为类型的代号,就像函数的形参一样。在使用时,为把 <A><T> 具体化,我们 会把类型说明像实参一样使用,像是 <i32> 这样。这两种把(泛型的或具体的)类型 当作参数的用法就是类型参数

泛型的类型参数是使用尖括号和大驼峰命名的名称:<Aaa, Bbb, ...> 来指定的。泛型类型参数一般用 <T> 来表示。在 Rust 中,“泛型的” 除了表示 类型,还表示可以接受一个或多个泛型类型参数 <T> 的任何内容。任何用泛型类型参数 表示的类型都是泛型,其他的类型都是具体(非泛型)类型。

例如定义一个名为 foo泛型函数,它可接受类型为 T 的任何参数 arg

fn foo<T>(arg: T) { ... }

因为我们使用了泛型类型参数 <T>,所以这里的 (arg: T) 中的 T 就是泛型 类型。即使 T 在之前被定义为 struct,这里的 T 仍然代表泛型。

下面例子展示了泛型语法的使用:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

struct

函数

同样的规则也适用于函数:在使用类型 T 前给出 <T>,那么 T 就变成了泛型。

调用泛型函数有时需要显式地指明类型参量。这可能是因为调用了返回类型是泛型的 函数,或者编译器没有足够的信息来推断类型参数。

调用函数时,使用显式指定的类型参数会像是这样:fun::<A, B, ...>()

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

函数structs

实现

和函数类似,impl 块要想实现泛型,也需要很仔细。

#![allow(unused)] fn main() { struct S; // 具体类型 `S` struct GenericVal<T>(T,); // 泛型类型 `GenericVal` // GenericVal 的 `impl`,此处我们显式地指定了类型参数: impl GenericVal<f32> {} // 指定 `f32` 类型 impl GenericVal<S> {} // 指定为上面定义的 `S` // `<T>` 必须在类型之前写出来,以使类型 `T` 代表泛型。 impl <T> GenericVal<T> {} }
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

返回引用的函数, impl, 和 struct

trait

当然 trait 也可以是泛型的。我们在这里定义了一个 trait,它把 Drop trait 作为泛型方法实现了,可以 drop(丢弃)调用者本身和一个输入参数。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

Drop, struct, 和 trait

约束

在使用泛型时,类型参数常常必须使用 trait 作为约束(bound)来明确规定 类型应实现哪些功能。例如下面的例子用到了 Display trait 来打印,所以它用 Display 来约束 T,也就是说 T 必须实现 Display

// 定义一个函数 `printer`,接受一个类型为泛型 `T` 的参数, // 其中 `T` 必须实现 `Display` trait。 fn printer<T: Display>(t: T) { println!("{}", t); }

约束把泛型类型限制为符合约束的类型。请看:

struct S<T: Display>(T); // 报错!`Vec<T>` 未实现 `Display`。此次泛型具体化失败。 let s = S(vec![1]);

约束的另一个作用是泛型的实例可以访问作为约束的 trait 的方法。例如:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

多说一句,某些情况下也可使用 where 分句来形成约束,这拥有更好的表现力。

参见:

std::fmt, struct, 和 trait

测试实例:空约束

约束的工作机制会产生这样的效果:即使一个 trait 不包含任何功能,你仍然可以用它 作为约束。标准库中的 EqOrd 就是这样的 trait

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

std::cmp::Eq, std::cmp::Ord, 和 trait

多重约束

多重约束(multiple bounds)可以用 + 连接。和平常一样,类型之间使用 , 隔开。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

std::fmttrait

where 分句

约束也可以使用 where 分句来表达,它放在 { 的前面,而不需写在类型第一次出现 之前。另外 where 从句可以用于任意类型的限定,而不局限于类型参数本身。

where 在下面一些情况下有很用:

  • 当分别指定泛型的类型和约束会更清晰时:
impl <A: TraitB + TraitC, D: TraitE + TraitF> MyTrait<A, D> for YourType {} // 使用 `where` 从句来表达约束 impl <A, D> MyTrait<A, D> for YourType where A: TraitB + TraitC, D: TraitE + TraitF {}
  • 当使用 where 从句比正常语法更有表现力时。本例中的 impl 如果不用 where 从句,就无法直接表达。
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

相关的 RFCstructtrait

new type 惯用法

newtype 惯用法(译注:即为不同种类的数据分别定义新的类型)能保证在编译时,提供 给程序的都是正确的类型。

比如说,实现一个 “年龄认证” 函数,它要求输入必须Years 类型。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

取消最后一行的注释,就可以发现提供给 old_enough 的必须是 Years 类型。

See also:

structs

关联项

“关联项”(associated item)指与多种类型的有关的一组规则。它是 trait 泛型的扩展,允许在 trait 内部定义新的项。

一个这样的项就叫做一个关联类型。当 trait 对于实现了它的容器类型是泛型的,关联 项就提供了简单的使用方法。

译注:“关联项”这个说法实际上只在 RFC 里出现了,官方的《The Rust Programming Language》第一版和第二版都只有“关联类型”的说法。如果觉得这里的说法很别扭的话 不要理会就是了。TRPL 对关联类型的定义是:“一种将类型占位符与 trait 联系起来的 做法,这样 trait 中的方法签名中就可以使用这些占位符类型。trait 的实现会指定在 该实现中那些占位符对应什么具体类型。”等看完这一节再回头看这个定义就很明白了。

参见:

RFC

存在问题

trait 如果对实现了它的容器类型是泛型的,则须遵守类型规范要求——trait 的 使用者必须指出 trait 的全部泛型类型。

在下面例子中,Contains trait 允许使用泛型类型 AB。然后我们为 Container 类型实现了这个 trait,将 AB 指定为 i32,这样就可以对 它们使用 difference() 函数。

因为 Contains 是泛型的,我们必须在 fn difference() 中显式地指出所有的泛型 类型。但实际上,我们想要表达,AB 究竟是什么类型是由输入 C 决定的。在 下一节会看到,关联类型恰好提供了这样的功能。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

struct, 和 trait

关联类型

通过把容器内部的类型放到 trait 中作为输出类型,使用 “关联类型” 增加了代码 的可读性。这样的 trait 的定义语法如下:

#![allow(unused)] fn main() { // `A` 和 `B` 在 trait 里面通过 `type` 关键字来定义。 // (注意:此处的 `type` 不同于为类型取别名时的 `type`)。 trait Contains { type A; type B; // 这种语法能够泛型地表示这些新类型。 fn contains(&self, _: &Self::A, _: &Self::B) -> bool; } }

注意使用了 Contains trait 的函数就不需要写出 AB 了:

// 不使用关联类型 fn difference<A, B, C>(container: &C) -> i32 where C: Contains<A, B> { ... } // 使用关联类型 fn difference<C: Contains>(container: &C) -> i32 { ... }

让我们使用关联类型来重写上一小节的例子:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

虚类型参数

虚类型(phantom type)参数是一种在运行时不出现,而在(且仅在)编译时进行静态检查 的类型参数。

可以用额外的泛型类型参数指定数据类型,这类型可以充当标记,也可以供编译时类型检查 使用。这些额外的参数没有存储值,也没有运行时行为。

在下面例子中,我们使用 std::marker::PhantomData 作为虚类型参数的类型,创建 包含不同数据类型的元组。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

Derive, 结构体, 和 元组结构体

测试实例:单位说明

通过实现一个带虚类型参数的 Add trait 可以实现单位检查。这种 Add trait 的 代码如下:

// 这个 `trait` 会要求 `Self + RHS = Output`。`<RHS = Self>` 表示 RHS 的默认值 // 为 Self 类型,也就是如果没有在实现中另行指定,RHS 就取 Self 类型。 pub trait Add<RHS = Self> { type Output; fn add(self, rhs: RHS) -> Self::Output; } // `Output` 必须是 `T<U>` 类型,所以是 `T<U> + T<U> = T<U>`。 impl<U> Add for T<U> { type Output = T<U>; ... }

完整实现:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

Borrowing (&), Bounds (X: Y), enum, impl & self, Overloading, ref, Traits (X for Y), 和 TupleStructs.

作用域规则

作用域在所有权(ownership)、借用(borrow)和生命周期(lifetime)中起着重要作用。也就是说,作用域告诉编译器什么时候借用是合法的、什么时候资源可以释放、以及变量何时被创建或销毁。

RAII

Rust 的变量不只是在栈中保存数据:它们也占有资源,比如 Box<T> 占有堆(heap)中的内存。Rust 强制实行 RAII(Resource Acquisition Is Initiallization,资源获取即初始化),所以任何对象在离开作用域时,它的析构函数(destructor)就被调用,然后它占有的资源就被释放。

这种行为避免了资源泄漏(resource leak),所以你再也不用手动释放内存或者担心内存泄漏(memory leak)!下面是个快速入门示例:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

当然我们可以使用 valgrind 对内存错误进行仔细检查:

$ rustc raii.rs && valgrind ./raii ==26873== Memcheck, a memory error detector ==26873== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al. ==26873== Using Valgrind-3.9.0 and LibVEX; rerun with -h for copyright info ==26873== Command: ./raii ==26873== ==26873== ==26873== HEAP SUMMARY: ==26873== in use at exit: 0 bytes in 0 blocks ==26873== total heap usage: 1,013 allocs, 1,013 frees, 8,696 bytes allocated ==26873== ==26873== All heap blocks were freed -- no leaks are possible ==26873== ==26873== For counts of detected and suppressed errors, rerun with: -v ==26873== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 2 from 2)

完全没有泄漏!

析构函数

Rust 中的析构函数概念是通过 Drop trait 提供的。当资源离开作用域,就调用析构函数。你无需为每种类型都实现 Drop trait,只要为那些需要自己的析构函数逻辑的类型实现就可以了。

运行下列例子,看看 Drop trait 是怎样工作的。当 main 函数中的变量离开作用域,自定义的析构函数就会被调用:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

Box

所有权和移动

因为变量要负责释放它们拥有的资源,所以资源只能拥有一个所有者。这也防止了资源的重复释放。注意并非所有变量都拥有资源(例如引用)。

在进行赋值(let x = y)或通过值来传递函数参数(foo(x))的时候,资源的所有权(ownership)会发生转移。按照 Rust 的说法,这被称为资源的移动(move)。

在移动资源之后,原来的所有者不能再被使用,这可避免悬挂指针(dangling pointer)的产生。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

可变性

当所有权转移时,数据的可变性可能发生改变。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

部分移动

在单个变量的解构内,可以同时使用 by-moveby-reference 模式绑定。这样做将导致变量的部分移动(partial move),这意味着变量的某些部分将被移动,而其他部分将保留。在这种情况下,后面不能整体使用父级变量,但是仍然可以使用只引用(而不移动)的部分。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

解构

借用

多数情况下,我们更希望能访问数据,同时不取得其所有权。为实现这点,Rust 使用了借用(borrowing)机制。对象可以通过引用(&T)来传递,从而取代通过值(T)来传递。

编译器(通过借用检查)静态地保证了引用总是指向有效的对象。也就是说,当存在引用指向一个对象时,该对象不能被销毁。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

可变性

可变数据可以使用 &mut T 进行可变借用。这叫做可变引用(mutable reference),它使借用者可以读/写数据。相反,&T 通过不可变引用(immutable reference)来借用数据,借用者可以读数据而不能更改数据:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

static

别名使用

数据可以多次不可变借用,但是在不可变借用的同时,原始数据不能使用可变借用。或者说,同一时间内只允许一次可变借用。仅当最后一次使用可变引用之后,原始数据才可以再次借用。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

ref 模式

在通过 let 绑定来进行模式匹配或解构时,ref 关键字可用来创建结构体/元组的字段的引用。下面的例子展示了几个实例,可看到 ref 的作用:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

生命周期

生命周期(lifetime)是这样一种概念,编译器(中的借用检查器)用它来保证所有的借用都是有效的。确切地说,一个变量的生命周期在它创建的时候开始,在它销毁的时候结束。虽然生命周期和作用域经常被一起提到,但它们并不相同。

例如考虑这种情况,我们通过 & 来借用一个变量。该借用拥有一个生命周期,此生命周期由它声明的位置决定。于是,只要该借用在出借者(lender)被销毁前结束,借用就是有效的。然而,借用的作用域则是由使用引用的位置决定的。

在下面的例子和本章节剩下的内容里,我们将看到生命周期和作用域的联系与区别。

译注:如果代码中的生命周期示意图乱掉了,请把它复制到任何编辑器中,用等宽字体查看。为避免中文的显示问题,下面一些注释没有翻译。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

注意到这里没有用到名称或类型来标注生命周期,这限制了生命周期的用法,在后面我们将会看到生命周期更强大的功能。

显式标注

借用检查器使用显式的生命周期标记来明确引用的有效时间应该持续多久。在生命周期没有省略1的情况下,Rust 需要显式标注来确定引用的生命周期应该是什么样的。可以用撇号显式地标出生命周期,语法如下:

foo<'a> // `foo` 带有一个生命周期参数 `'a`

闭包类似,使用生命周期需要泛型。另外这个生命周期的语法也表明了 foo 的生命周期不能超出 'a 的周期。若要给类型显式地标注生命周期,其语法会像是 &'a T 这样,其中 'a 的作用刚刚已经介绍了。

foo<'a, 'b> // `foo` 带有生命周期参数 `'a` 和 `'b`

在上面这种情形中,foo 的生命周期不能超出 'a'b 中任一个的周期。

看下面的例子,了解显式生命周期标注的运用:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
1

省略 隐式地标注了生命周期,所以情况不同。

参见:

泛型闭包

函数

排除省略(elision)的情况,带上生命周期的函数签名有一些限制:

  • 任何引用都必须拥有标注好的生命周期。
  • 任何被返回的引用都必须有和某个输入量相同的生命周期或是静态类型(static)。

另外要注意,如果没有输入的函数返回引用,有时会导致返回的引用指向无效数据,这种情况下禁止它返回这样的引用。下面例子展示了一些合法的带有生命周期的函数:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

函数

方法

方法的标注和函数类似:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

译注:方法一般是不需要标明生命周期的,因为 self 的生命周期会赋给所有的输出生命周期参数,详见 TRPL

参见:

方法

结构体

在结构体中标注生命周期也和函数的类似:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

结构体

trait

trait 方法中生命期的标注基本上与函数类似。注意,impl 也可能有生命周期的标注。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

trait

约束

就如泛型类型能够被约束一样,生命周期(它们本身就是泛型)也可以使用约束。: 字符的意义在这里稍微有些不同,不过 + 是相同的。注意下面的说明:

  1. T: 'a:在 T 中的所有引用都必须比生命周期 'a 活得更长。
  2. T: Trait + 'aT 类型必须实现 Trait trait,并且在 T 中的所有引用都必须比 'a 活得更长。

下面例子展示了上述语法的实际应用:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

泛型, 泛型中的约束, 以及 泛型中的多重约束

强制转换

一个较长的生命周期可以强制转成一个较短的生命周期,使它在一个通常情况下不能工作的作用域内也能正常工作。强制转换可由编译器隐式地推导并执行,也可以通过声明不同的生命周期的形式实现。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

static

'static 生命周期是可能的生命周期中最长的,它会在整个程序运行的时期中存在。'static 生命周期也可被强制转换成一个更短的生命周期。有两种方式使变量拥有 'static 生命周期,它们都把数据保存在可执行文件的只读内存区:

  • 使用 static 声明来产生常量(constant)。
  • 产生一个拥有 &'static str 类型的 string 字面量。

看下面的例子,了解列举到的各个方法:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

'static 常量

省略

有些生命周期的模式太常用了,所以借用检查器将会隐式地添加它们以减少程序输入量和增强可读性。这种隐式添加生命周期的过程称为省略(elision)。在 Rust 使用省略仅仅是因为这些模式太普遍了。

下面代码展示了一些省略的例子。对于省略的详细描述,可以参考官方文档的生命周期省略

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

省略

特质 trait

trait 是对未知类型 Self 定义的方法集。该类型也可以访问同一个 trait 中定义的 其他方法。

对任何数据类型都可以实现 trait。在下面例子中,我们定义了包含一系列方法 的 Animal。然后针对 Sheep 数据类型实现 Animal trait,因而 Sheep 的实例可以使用 Animal 中的所有方法。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

派生

通过 #[derive] 属性,编译器能够提供某些 trait 的基本实现。如果 需要更复杂的行为,这些 trait 也可以手动实现。

下面是可以自动派生的 trait:

  • 比较 trait: Eq, PartialEq, Ord, PartialOrd
  • Clone, 用来从 &T 创建副本 T
  • Copy,使类型具有 “复制语义”(copy semantics)而非 “移动语义”(move semantics)。
  • Hash,从 &T 计算哈希值(hash)。
  • Default, 创建数据类型的一个空实例。
  • Debug,使用 {:?} formatter 来格式化一个值。
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见

derive

使用 dyn 返回 trait

Rust 编译器需要知道每个函数的返回类型需要多少空间。这意味着所有函数都必须返回一个具体类型。与其他语言不同,如果你有个像 Animal 那样的的 trait,则不能编写返回 Animal 的函数,因为其不同的实现将需要不同的内存量。

但是,有一个简单的解决方法。相比于直接返回一个 trait 对象,我们的函数返回一个包含一些 AnimalBoxbox 只是对堆中某些内存的引用。因为引用的大小是静态已知的,并且编译器可以保证引用指向已分配的堆 Animal,所以我们可以从函数中返回 trait!

每当在堆上分配内存时,Rust 都会尝试尽可能明确。因此,如果你的函数以这种方式返回指向堆的 trait 指针,则需要使用 dyn 关键字编写返回类型,例如 Box<dyn Animal>

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

运算符重载

在 Rust 中,很多运算符可以通过 trait 来重载。也就是说,这些运算符可以根据它们的 输入参数来完成不同的任务。这之所以可行,是因为运算符就是方法调用的语法糖。例 如,a + b 中的 + 运算符会调用 add 方法(也就是 a.add(b))。这个 add 方 法是 Add trait 的一部分。因此,+ 运算符可以被任何 Add trait 的实现者使用。

会重载运算符的 trait(比如 Add 这种)可以在这里查看。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

Add, 语法索引

Drop

Drop trait 只有一个方法:drop,当对象离开作用域时会自动调用该 方法。Drop trait 的主要作用是释放实现者的实例拥有的资源。

BoxVecStringFile,以及 Process 是一些实现了 Drop trait 来释放 资源的类型。Drop trait 也可以为任何自定义数据类型手动实现。

下面示例给 drop 函数增加了打印到控制台的功能,用于宣布它在什么时候被调用。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Iterator

Iterator trait 用来对集合(collection)类型(比如数组)实现迭代器。

这个 trait 只需定义一个返回 next(下一个)元素的方法,这可手动在 impl 代码块 中定义,或者自动定义(比如在数组或区间中)。

为方便起见,for 结构会使用 .into_iterator() 方法将一些集合类型 转换为迭代器。

下面例子展示了如何使用 Iterator trait 的方法,更多可用的方法可以看这里

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

impl Trait

如果函数返回实现了 MyTrait 的类型,可以将其返回类型编写为 -> impl MyTrait。这可以大大简化你的类型签名!

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

更重要的是,某些 Rust 类型无法写出。例如,每个闭包都有自己未命名的具体类型。在使用 impl Trait 语法之前,必须在堆上进行分配才能返回闭包。但是现在你可以像下面这样静态地完成所有操作:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

您还可以使用 impl Trait 返回使用 mapfilter 闭包的迭代器!这使得使用 mapfilter 更容易。因为闭包类型没有名称,所以如果函数返回带闭包的迭代器,则无法写出显式的返回类型。但是有了 impl Trait,你就可以轻松地做到这一点:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Clone

当处理资源时,默认的行为是在赋值或函数调用的同时将它们转移。但是我们有时候也需要 把资源复制一份。

Clone trait 正好帮助我们完成这任务。通常,我们可以使用由 Clone trait 定义的 .clone() 方法。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

父 trait

Rust 没有“继承”,但是您可以将一个 trait 定义为另一个 trait 的超集(即父 trait)。例如:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

《Rust 程序设计语言》的“父级 trait”章节

消除重叠 trait

一个类型可以实现许多不同的 trait。如果两个 trait 都需要相同的名称怎么办?例如,许多 trait 可能拥有名为 get() 的方法。他们甚至可能有不同的返回类型!

有个好消息:由于每个 trait 实现都有自己的 impl 块,因此很清楚您要实现哪个 trait 的 get 方法。

何时需要调用这些方法呢?为了消除它们之间的歧义,我们必须使用完全限定语法(Fully Qualified Syntax)。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

《Rust 程序设计语言》中关于“完全限定语法”的章节

使用 macro_rules! 来创建宏

Rust 提供了一个强大的宏系统,可进行元编程(metaprogramming)。你已经在前面的 章节中看到,宏看起来和函数很像,只不过名称末尾有一个感叹号 ! 。宏并不产 生函数调用,而是展开成源码,并和程序的其余部分一起被编译。Rust 又有一点和 C 以及其他语言都不同,那就是 Rust 的宏会展开为抽象语法树(AST,abstract syntax tree),而不是像字符串预处理那样直接替换成代码,这样就不会产生无法预料的优先权 错误。

宏是通过 macro_rules! 宏来创建的。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

为什么宏是有用的?

  1. 不写重复代码(DRY,Don't repeat yourself.)。很多时候你需要在一些地方针对不同 的类型实现类似的功能,这时常常可以使用宏来避免重复代码(稍后详述)。
  2. 领域专用语言(DSL,domain-specific language)。宏允许你为特定的目的创造特定的 语法(稍后详述)。
  3. 可变接口(variadic interface)。有时你需要能够接受不定数目参数的接口,比如 println!,根据格式化字符串的不同,它需要接受任意多的参数(稍后详述)。

语法

在下面的小节中,我们将展示如何在 Rust 中定义宏。基本的概念有三个:

指示符

宏的参数使用一个美元符号 $ 作为前缀,并使用一个指示符(designator)来 注明类型:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

这里列出全部指示符:

  • block
  • expr 用于表达式
  • ident 用于变量名或函数名
  • item
  • pat (模式 pattern)
  • path
  • stmt (语句 statement)
  • tt (标记树 token tree)
  • ty (类型 type)

重载

宏可以重载,从而接受不同的参数组合。在这方面,macro_rules! 的作用类似于 匹配(match)代码块:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

重复

宏在参数列表中可以使用 + 来表示一个参数可能出现一次或多次,使用 * 来表示该 参数可能出现零次或多次。

在下面例子中,把模式这样: $(...),+ 包围起来,就可以匹配一个或多个用逗号隔开 的表达式。另外注意到,宏定义的最后一个分支可以不用分号作为结束。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

DRY (不写重复代码)

通过提取函数或测试集的公共部分,宏可以让你写出 DRY 的代码(DRY 是 Don't Repeat Yourself 的缩写,意思为 “不要写重复代码”)。这里给出一个例子,对 Vec<T> 实现 并测试了关于 +=*=-= 等运算符。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
$ rustc --test dry.rs && ./dry running 3 tests test test::mul_assign ... ok test test::add_assign ... ok test test::sub_assign ... ok test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured

DSL(领域专用语言)

DSL 是 Rust 的宏中集成的微型 “语言”。这种语言是完全合法的,因为宏系统会把它转换 成普通的 Rust 语法树,它只不过看起来像是另一种语言而已。这就允许你为一些特定功能 创造一套简洁直观的语法(当然是有限制的)。

比如说我想要定义一套小的计算器 API,可以传给它表达式,它会把结果打印到控制台上。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

输出:

1 + 2 = 3 (1 + 2) * (3 / 4) = 0

这个例子非常简单,但是已经有很多利用宏开发的复杂接口了,比如 lazy_staticclap

可变参数接口

可变参数接口可以接受任意数目的参数。比如说 println 就可以,其参数的数目是由 格式化字符串指定的。

我们可以把之前的 calculater! 宏改写成可变参数接口:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

输出:

1 + 2 = 3 3 + 4 = 7 (2 * 3) + 1 = 7

错误处理

错误处理(error handling)是处理可能发生的失败情况的过程。例如读取一个文件时 失败了,如果继续使用这个无效的输入,那显然是有问题的。注意到并且显式地 处理这种错误可以避免程序的其他部分产生潜在的问题。

在 Rust 中有多种处理错误的方式,在接下来的小节中会一一介绍。它们多少有些 区别,使用场景也不尽相同。总的来说:

  • 显式的 panic 主要用于测试,以及处理不可恢复的错误。在原型开发中这很有用,比如 用来测试还没有实现的函数,不过这时使用 unimplemented 更能表达意图。另外在 测试中,panic 是一种显式地失败(fail)的好方法。
  • Option 类型是为了值是可选的、或者缺少值并不是错误的情况准备的。比如说寻找 父目录时,/C: 这样的目录就没有父目录,这应当并不是一个错误。当处理 Option 时,unwrap 可用于原型开发,也可以用于能够确定 Option 中一定有值 的情形。然而 expect 更有用,因为它允许你指定一条错误信息,以免万一还是出现 了错误。
  • 当错误有可能发生,且应当由调用者处理时,使用 Result。你也可以 unwrap 然后 使用 expect,但是除了在测试或者原型开发中,请不要这样做。

有关错误处理的更多内容,可参考官方文档的错误处理的章节。

panic

我们将要看到的最简单的错误处理机制就是 panic。它会打印一个错误消息,开始 回退(unwind)任务,且通常会退出程序。这里我们显式地在错误条件下调用 panic

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Optionunwrap

上个例子展示了如何主动地引入程序失败(program failure)。当公主收到蛇这件不合适 的礼物时,我们就让程序 panic。但是,如果公主期待收到礼物,却没收到呢?这同样 是一件糟糕的事情,所以我们要想办法来解决这个问题!

我们可以检查空字符串(""),就像处理蛇那样。但既然我们在用 Rust,不如 让编译器辨别没有礼物的情况。

在标准库(std)中有个叫做 Option<T>(option 中文意思是 “选项”)的枚举 类型,用于有 “不存在” 的可能性的情况。它表现为以下两个 “option”(选项)中 的一个:

  • Some(T):找到一个属于 T 类型的元素
  • None:找不到相应元素

这些选项可以通过 match 显式地处理,或使用 unwrap 隐式地处理。隐式处理要么 返回 Some 内部的元素,要么就 panic

请注意,手动使用 expect 方法自定义 panic 信息是可能的,但相比显式 处理,unwrap 的输出仍显得不太有意义。在下面例子中,显式处理将举出更受控制的 结果,同时如果需要的话,仍然可以使程序 panic

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

使用 ? 解开 Option

你可以使用 match 语句来解开 Option,但使用 ? 运算符通常会更容易。如果 xOption,那么若 xSome ,对x?表达式求值将返回底层值,否则无论函数是否正在执行都将终止且返回 None

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

你可以将多个 ? 链接在一起,以使代码更具可读性。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

组合算子:map

match 是处理 Option 的一个可用的方法,但你会发现大量使用它会很繁琐,特别是当 操作只对一种输入是有效的时。这时,可以使用组合算子(combinator),以 模块化的风格来管理控制流。

Option 有一个内置方法 map(),这个组合算子可用于 Some -> SomeNone -> None 这样的简单映射。多个不同的 map() 调用可以串起来,这样更加灵活。

在下面例子中,process() 轻松取代了前面的所有函数,且更加紧凑。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

闭包, Option, 和 Option::map()

组合算子:and_then

map() 以链式调用的方式来简化 match 语句。然而,如果以返回类型是 Option<T> 的函数作为 map() 的参数,会导致出现嵌套形式 Option<Option<T>>。这样多层串联 调用就会变得混乱。所以有必要引入 and_then(),在某些语言中它叫做 flatmap。

and_then() 使用被 Option 包裹的值来调用其输入函数并返回结果。 如果 OptionNone,那么它返回 None

在下面例子中,cookable_v2() 会产生一个 Option<Food>。如果在这里使用 map() 而不是 and_then() 将会得到 Option<Option<Food>>,这对 eat() 来说是一个 无效类型。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

闭包Option::map(), 和 Option::and_then()

结果 Result

ResultOption 类型的更丰富的版本,描述的是可能 的错误而不是可能的不存在

也就是说,Result<T,E> 可以有两个结果的其中一个:

  • Ok<T>:找到 T 元素
  • Err<E>:找到 E 元素,E 即表示错误的类型。

按照约定,预期结果是 “Ok”,而意外结果是 “Err”。

Result 有很多类似 Option 的方法。例如 unwrap(),它要么举出元素 T,要么就 panic。 对于事件的处理,ResultOption 有很多相同的组合算子。

在使用 Rust 时,你可能会遇到返回 Result 类型的方法,例如 parse() 方法。它并不是总能把字符串解析成指定的类型,所以 parse() 返回一个 Result 表示可能的失败。

我们来看看当 parse() 字符串成功和失败时会发生什么:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

在失败的情况下,parse() 产生一个错误,留给 unwrap() 来解包并产生 panic。另 外,panic 会退出我们的程序,并提供一个让人很不爽的错误消息。

为了改善错误消息的质量,我们应该更具体地了解返回类型并考虑显式地处理错误。

Resultmap

上一节的 multiply 函数的 panic 设计不是健壮的(robust)。一般地,我们希望把 错误返回给调用者,这样它可以决定回应错误的正确方式。

首先,我们需要了解需要处理的错误类型是什么。为了确定 Err 的类型,我们可以 用 parse() 来试验。Rust 已经为 i32 类型使用 FromStr trait 实现了 parse()。结果表明,这里的 Err 类型被指定为 ParseIntError

译注:原文没有具体讲如何确定 Err 的类型。由于目前用于获取类型的函数仍然是不 稳定的,我们可以用间接的方法。使用下面的代码:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

由于不可能把 Result 类型赋给单元类型变量 i,编译器会提示我们:

note: expected type `()` found type `std::result::Result<i32, std::num::ParseIntError>`

这样就知道了 parse<i32> 函数的返回类型详情。

在下面的例子中,使用简单的 match 语句导致了更加繁琐的代码。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

幸运的是,Optionmapand_then、以及很多其他组合算子也为 Result 实现 了。官方文档的 Result 一节包含完整的方法列表。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Result 取别名

当我们要重用某个 Result 类型时,该怎么办呢?回忆一下,Rust 允许我们 创建别名。若某个 Result 有可能被重用,我们可以方便地给它取一个别名。

在模块的层面上创建别名特别有帮助。同一模块中的错误常常会有相同的 Err 类 型,所以单个别名就能简便地定义所有相关的 Result。这太有用了,以至于标准库 也提供了一个别名: io::Result

下面给出一个简短的示例来展示语法:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

io::Result

提前返回

在上一个例子中,我们显式地使用组合算子处理了错误。另一种处理错误的方式是使用 match 语句和提前返回(early return)的结合。

这也就是说,如果发生错误,我们可以停止函数的执行然后返回错误。对有些人来说,这样 的代码更好写,更易读。这次我们使用提前返回改写之前的例子:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

到此为止,我们已经学会了如何使用组合算子和提前返回显式地处理错误。我们一般是 想要避免 panic 的,但显式地处理所有错误确实显得过于繁琐。

在下一部分,我们将看到,当只是需要 unwrap 并且不产生 panic 时,可以使用 ? 来达到同样的效果。

引入 ?

有时我们只是想 unwrap 且避免产生 panic。到现在为止,对 unwrap 的错误处理都在强迫 我们一层层地嵌套,然而我们只是想把里面的变量拿出来。? 正是为这种情况准备的。

当找到一个 Err 时,可以采取两种行动:

  1. panic!,不过我们已经决定要尽可能避免 panic 了。
  2. 返回它,因为 Err 就意味着它已经不能被处理了。

? 几乎1 就等于一个会返回 Err 而不是 panicunwrap。我们来看看 怎样简化之前使用组合算子的例子:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

try!

? 出现以前,相同的功能是使用 try! 宏完成的。现在我们推荐使用 ? 运算符,但是 在老代码中仍然会看到 try!。如果使用 try! 的话,上一个例子中的 multiply 函数 看起来会像是这样:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
1

更多细节请看? 的更多用法

处理多种错误类型

前面出现的例子都是很方便的情况;都是 Result 和其他 Result 交互,还有 Option 和其他 Option 交互。

有时 Option 需要和 Result 进行交互,或是 Result<T, Error1> 需要和 Result<T, Error2> 进行交互。在这类情况下,我们想要以一种方式来管理不同的错误 类型,使得它们可组合且易于交互。

在下面代码中,unwrap 的两个实例生成了不同的错误类型。Vec::first 返回一个 Option,而 parse::<i32> 返回一个 Result<i32, ParseIntError>

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

在下面几节中,我们会看到处理这类问题的几种策略。

Option 中取出 Result

处理混合错误类型的最基本的手段就是让它们互相包含。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

有时候我们不想再处理错误(比如使用 ? 的时候),但如果 OptionNone 则继续处理错误。一些组合算子可以让我们轻松地交换 ResultOption

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

定义一个错误类型

有时候把所有不同的错误都视为一种错误类型会简化代码。我们将用一个自定义错误类型来 演示这一点。

Rust 允许我们定义自己的错误类型。一般来说,一个 “好的” 错误类型应当:

  • 用同一个类型代表了多种错误
  • 向用户提供了清楚的错误信息
  • 能够容易地与其他类型比较
    • 好的例子:Err(EmptyVec)
    • 坏的例子:Err("Please use a vector with at least one element".to_owned())
  • 能够容纳错误的具体信息
    • 好的例子:Err(BadChar(c, position))
    • 坏的例子:Err("+ cannot be used here".to_owned())
  • 能够与其他错误很好地整合
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

把错误 “装箱”

如果又想写简单的代码,又想保存原始错误信息,一个方法是把它们装箱Box)。这 样做的坏处就是,被包装的错误类型只能在运行时了解,而不能被静态地 判别

对任何实现了 Error trait 的类型,标准库的 Box 通过 From 为它们提供了 到 Box<Error> 的转换。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

动态分发 and Error trait

? 的其他用法

注意在上一个例子中,我们调用 parse 后总是立即将错误从标准库的错误 map(映射)到装箱错误。

.and_then(|s| s.parse::<i32>() .map_err(|e| e.into())

因为这个操作很简单常见,如果有省略写法就好了。遗憾的是 and_then 不够灵活,所以实现不了这样的写法。不过,我们可以使用 ? 来代替它。

? 之前被解释为要么 unwrap,要么 return Err(err),这只是在大多数情况下是正确的。? 实际上是指 unwrapreturn Err(From::from(err))。由于 From::from 是不同类型之间的转换工具,也就是说,如果在错误可转换成返回类型地方使用 ?,它将自动转换成返回类型。

我们在这里使用 ? 重写之前的例子。重写后,只要为我们的错误类型实现 From::from,就可以不再使用 map_err

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

这段代码已经相当清晰了。与原来的 panic 相比,除了返回类型是 Result 之外,它就像是把所有的 unwrap 调用都换成 ? 一样。因此必须在顶层解构它们。

参见:

From::from?

包裹错误

把错误装箱这种做法也可以改成把它包裹到你自己的错误类型中。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

这种做法会在错误处理中增加一些模板化的代码,而且也不是所有的应用都需要这样做。一些 库可以帮你处理模板化代码的问题。

See also:

From::from and 枚举类型

遍历 Result

Iter::map 操作可能失败,比如:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

我们来看一些处理这种问题的策略:

使用 filter_map() 忽略失败的项

filter_map 会调用一个函数,过滤掉为 None 的所有结果。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

使用 collect() 使整个操作失败

Result 实现了 FromIter,因此结果的向量(Vec<Result<T, E>>)可以被转换成 结果包裹着向量(Result<Vec<T>, E>)。一旦找到一个 Result::Err ,遍历就被终止。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

同样的技巧可以对 Option 使用。

使用 Partition() 收集所有合法的值与错误

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

当你看着这些结果时,你会发现所有东西还在 Result 中保存着。要取出它们,需要一些 模板化的代码。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

标准库类型

标准库提供了很多自定义类型,在原生类型基础上进行了大量扩充。这是部分自定义 类型:

  • 可增长的 String(字符串),如: "hello world"
  • 可增长的向量(vector): [1, 2, 3]
  • 选项类型(optional types): Option<i32>
  • 错误处理类型(error handling types): Result<i32, i32>
  • 堆分配的指针(heap allocated pointers): Box<i32>

参见:

原生类型标准库

箱子、栈和堆

在 Rust 中,所有值默认都是栈分配的。通过创建 Box<T>,可以把值装箱(boxed)来 使它在堆上分配。箱子(box,即 Box<T> 类型的实例)是一个智能指针,指向堆分配 的 T 类型的值。当箱子离开作用域时,它的析构函数会被调用,内部的对象会被 销毁,堆上分配的内存也会被释放。

被装箱的值可以使用 * 运算符进行解引用;这会移除掉一层装箱。

use std::mem; #[allow(dead_code)] #[derive(Debug, Clone, Copy)] struct Point { x: f64, y: f64, } #[allow(dead_code)] struct Rectangle { p1: Point, p2: Point, } fn origin() -> Point { Point { x: 0.0, y: 0.0 } } fn boxed_origin() -> Box<Point> { // 在堆上分配这个点(point),并返回一个指向它的指针 Box::new(Point { x: 0.0, y: 0.0 }) } fn main() { // (所有的类型标注都不是必需的) // 栈分配的变量 let point: Point = origin(); let rectangle: Rectangle = Rectangle { p1: origin(), p2: Point { x: 3.0, y: 4.0 } }; // 堆分配的 rectangle(矩形) let boxed_rectangle: Box<Rectangle> = Box::new(Rectangle { p1: origin(), p2: origin() }); // 函数的输出可以装箱 let boxed_point: Box<Point> = Box::new(origin()); // 两层装箱 let box_in_a_box: Box<Box<Point>> = Box::new(boxed_origin()); println!("Point occupies {} bytes in the stack", mem::size_of_val(&point)); println!("Rectangle occupies {} bytes in the stack", mem::size_of_val(&rectangle)); // box 的宽度就是指针宽度 println!("Boxed point occupies {} bytes in the stack", mem::size_of_val(&boxed_point)); println!("Boxed rectangle occupies {} bytes in the stack", mem::size_of_val(&boxed_rectangle)); println!("Boxed box occupies {} bytes in the stack", mem::size_of_val(&box_in_a_box)); // 将包含在 `boxed_point` 中的数据复制到 `unboxed_point` let unboxed_point: Point = *boxed_point; println!("Unboxed point occupies {} bytes in the stack", mem::size_of_val(&unboxed_point)); }

动态数组 vector

vector 是大小可变的数组。和 slice(切片)类似,它们的大小在编译时是未知的,但 它们可以随时扩大或缩小。一个 vector 使用 3 个词来表示:一个指向数据的指针,vector 的长度,还有它的容量。此容量指明了要为这个 vector 保留多少内存。vector 的长度 只要小于该容量,就可以随意增长;当需要超过这个阈值时,会给 vector 重新分配一段 更大的容量。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

更多 Vec 方法可以在 std::vec 模块中找到。

字符串

Rust 中有两种字符串类型:String&str

String 被存储为由字节组成的 vector(Vec<u8>),但保证了它一定是一个有效的 UTF-8 序列。String 是堆分配的,可增长的,且不是零结尾的(null terminated)。

&str 是一个总是指向有效 UTF-8 序列的切片(&[u8]),并可用来查看 String 的内容,就如同 &[T]Vec<T> 的全部或部分引用。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

更多 str/String 方法可以在 std::strstd::string 模块中 找到。

字面量与转义字符

书写含有特殊字符的字符串字面量有很多种方法。它们都会产生类似的 &str,所以最好 选择最方便的写法。类似地,字节串(byte string)字面量也有多种写法,它们都会产生 &[u8; N] 类型。

通常特殊字符是使用反斜杠字符 \ 来转义的,这样你就可以在字符串中写入各种各样的 字符,甚至是不可打印的字符以及你不知道如何输入的字符。如果你需要反斜杠字符,再用 另一个反斜杠来转义它就可以,像这样:\\

字面量中出现的字符串或字符定界符必须转义:"\""'\''

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

有时会有太多需要转义的字符,或者是直接原样写出会更便利。这时可以使用原始字符 串(raw string)。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

想要非 UTF-8 字符串(记住,&strString 都必须是合法的 UTF-8 序列),或者 需要一个字节数组,其中大部分是文本?请使用字节串(byte string)!

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

若需要在编码间转换,请使用 encoding crate。

Rust 参考中的 Tokens 一章详细地列出了书写字符串字面量和转义字符的方法。

选项 Option

有时候想要捕捉到程序某部分的失败信息,而不是调用 panic!;这可使用 Option 枚举类型来实现。

Option<T> 有两个变量:

  • None,表明失败或缺少值
  • Some(value),元组结构体,封装了一个 T 类型的值 value
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

结果 Result

我们已经看到 Option 枚举类型可以用作可能失败的函数的返回值,其中返回 None 可以表明失败。但是有时要强调为什么一个操作会失败。为做到这点,我们提供 了 Result 枚举类型。

Result<T, E> 类型拥有两个取值:

  • Ok(value) 表示操作成功,并包装操作返回的 valuevalue 拥有 T 类型)。
  • Err(why),表示操作失败,并包装 why,它(但愿)能够解释失败的原因(why 拥有 E 类型)。
mod checked { // 我们想要捕获的数学 “错误” #[derive(Debug)] pub enum MathError { DivisionByZero, NegativeLogarithm, NegativeSquareRoot, } pub type MathResult = Result<f64, MathError>; pub fn div(x: f64, y: f64) -> MathResult { if y == 0.0 { // 此操作将会失败,那么(与其让程序崩溃)不如把失败的原因包装在 // `Err` 中并返回 Err(MathError::DivisionByZero) } else { // 此操作是有效的,返回包装在 `Ok` 中的结果 Ok(x / y) } } pub fn sqrt(x: f64) -> MathResult { if x < 0.0 { Err(MathError::NegativeSquareRoot) } else { Ok(x.sqrt()) } } pub fn ln(x: f64) -> MathResult { if x < 0.0 { Err(MathError::NegativeLogarithm) } else { Ok(x.ln()) } } } // `op(x, y)` === `sqrt(ln(x / y))` fn op(x: f64, y: f64) -> f64 { // 这是一个三层的 match 金字塔! match checked::div(x, y) { Err(why) => panic!("{:?}", why), Ok(ratio) => match checked::ln(ratio) { Err(why) => panic!("{:?}", why), Ok(ln) => match checked::sqrt(ln) { Err(why) => panic!("{:?}", why), Ok(sqrt) => sqrt, }, }, } } fn main() { // 这会失败吗? println!("{}", op(1.0, 10.0)); }

? 运算符

把 result 用 match 连接起来会显得很难看;幸运的是,? 运算符可以把这种逻辑变得 干净漂亮。? 运算符用在返回值为 Result 的表达式后面,它等同于这样一个匹配 表达式:其中 Err(err) 分支展开成提前返回的 return Err(err),而 Ok(ok) 分支展开成 ok 表达式。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

记得查阅文档,里面有很多匹配/组合 Result 的方法。

panic!

panic! 宏可用于产生一个 panic (恐慌),并开始回退(unwind)它的栈。在回退栈 的同时,运行时将会释放该线程所拥有的所有资源,这是通过调用线程中所有对象的 析构函数完成的。

因为我们正在处理的程序只有一个线程,panic! 将会引发程序报告 panic 消息并退出。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

可以看到,panic! 不会泄露内存:

$ rustc panic.rs && valgrind ./panic ==4401== Memcheck, a memory error detector ==4401== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al. ==4401== Using Valgrind-3.10.0.SVN and LibVEX; rerun with -h for copyright info ==4401== Command: ./panic ==4401== thread '<main>' panicked at 'division by zero', panic.rs:5 ==4401== ==4401== HEAP SUMMARY: ==4401== in use at exit: 0 bytes in 0 blocks ==4401== total heap usage: 18 allocs, 18 frees, 1,648 bytes allocated ==4401== ==4401== All heap blocks were freed -- no leaks are possible ==4401== ==4401== For counts of detected and suppressed errors, rerun with: -v ==4401== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

散列表 HashMap

vector 通过整型下标来存储值,而 HashMap(散列表)通过键(key)来存储 值。HashMap 的键可以是布尔型、整型、字符串,或任意实现了 EqHash trait 的其他类型。在下一节将进一步介绍。

和 vector 类似,HashMap 也是可增长的,但 HashMap 在占据了多余空间时还可以缩小 自己。可以使用 HashMap::with_capacity(unit) 创建具有一定初始容量的 HashMap,也 可以使用 HashMap::new() 来获得一个带有默认初始容量的 HashMap(这是推荐方式)。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

想要了解更多关于散列(hash)与散列表(hash map)(有时也称作 hash table)的 工作原理,可以查看 Wikipedia 的散列表词条。

更改或自定义关键字类型

任何实现了 EqHash trait 的类型都可以充当 HashMap 的键。这包括:

  • bool (当然这个用处不大,因为只有两个可能的键)
  • intunit,以及其他整数类型
  • String&str(友情提示:如果使用 String 作为键来创建 HashMap,则可以 将 &str 作为散列表的 .get() 方法的参数,以获取值)

注意到 f32f64 没有实现 Hash,这很大程度上是由于若使用浮点数作为 散列表的键,浮点精度误差会很容易导致错误。

对于所有的集合类(collection class),如果它们包含的类型都分别实现了 EqHash,那么这些集合类也就实现了 EqHash。例如,若 T 实现了 Hash,则 Vec<T> 也实现了 Hash

对自定义类型可以轻松地实现 EqHash,只需加上一行代码:#[derive(PartialEq, Eq, Hash)]

编译器将会完成余下的工作。如果你想控制更多的细节,你可以手动 实现 Eq 和/或 Hash。本指南不包含实现 Hash 的细节内容。

为了试验 HashMap 中的 struct,让我们试着做一个非常简易的用户登录系统:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

散列集 HashSet

请把 HashSet 当成这样一个 HashMap:我们只关心其中的键而非值(HashSet<T> 实际上只是对 HashMap<T, ()> 的封装)。

你可能会问:“这有什么意义呢?我完全可以将键存储到一个 Vec 中呀。”

HashSet 的独特之处在于,它保证了不会出现重复的元素。这是任何 set 集合类型(set collection)遵循的规定。HashSet 只是它的一个实现。(参见:BTreeSet

如果插入的值已经存在于 HashSet 中(也就是,新值等于已存在的值,并且拥有相同 的散列值),那么新值将会替换旧的值。

如果你不想要一样东西出现多于一次,或者你要判断一样东西是不是已经存在,这种做法 就很有用了。

不过集合(set)可以做更多的事。

集合(set)拥有 4 种基本操作(下面的调用全部都返回一个迭代器):

  • union(并集):获得两个集合中的所有元素(不含重复值)。

  • difference(差集):获取属于第一个集合而不属于第二集合的所有元素。

  • intersection(交集):获取同时属于两个集合的所有元素。

  • symmetric_difference(对称差):获取所有只属于其中一个集合,而不同时属于 两个集合的所有元素。

在下面的例子中尝试使用这些操作。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

(例子改编自文档。)

引用计数 Rc

当需要多个所有权时,可以使用 Rc(引用计数,Reference Counting 缩写)。Rc 跟踪引用的数量,这相当于包裹在 Rc 值的所有者的数量.

每当克隆一个 Rc 时,Rc 的引用计数就会增加 1,而每当克隆得到的 Rc 退出作用域时,引用计数就会减少 1。当 Rc 的引用计数变为 0 时,这意味着已经没有所有者,Rc 和值两者都将被删除。

克隆 Rc 从不执行深拷贝。克隆只创建另一个指向包裹值的指针,并增加计数。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见:

std::rcstd::sync::arc

共享引用计数 Arc

当线程之间所有权需要共享时,可以使用Arc(共享引用计数,Atomic Reference Counted 缩写)可以使用。这个结构通过 Clone 实现可以为内存堆中的值的位置创建一个引用指针,同时增加引用计数器。由于它在线程之间共享所有权,因此当指向某个值的最后一个引用指针退出作用域时,该变量将被删除。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

标准库更多介绍

标准库也提供了很多其他类型来支持某些功能,例如:

  • 线程(Threads)
  • 信道(Channels)
  • 文件输入输出(File I/O)

这些内容在原生类型之外进行了有效扩充。

参见:

原生类型标准库类型

线程

Rust 通过 spawn 函数提供了创建本地操作系统(native OS)线程的机制,该函数的参数是一个通过值捕获变量的闭包(moving closure)。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

这些线程由操作系统调度(schedule)。

测试实例:map-reduce

Rust 使数据的并行化处理非常简单,在 Rust 中你无需面对并行处理的很多传统难题。

标准库提供了开箱即用的线程类型,把它和 Rust 的所有权概念与别名规则结合 起来,可以自动地避免数据竞争(data race)。

当某状态对某线程是可见的,别名规则(即一个可变引用 XOR 一些只读引用。译注:XOR 是异或的意思,即「二者仅居其一」)就自动地避免了别的线程对它的操作。(当需要同步 处理时,请使用 MutexChannel 这样的同步类型。)

在本例中,我们将会计算一堆数字中每一位的和。我们将把它们分成几块,放入不同的 线程。每个线程会把自己那一块数字的每一位加起来,之后我们再把每个线程提供的结果 再加起来。

注意到,虽然我们在线程之间传递了引用,但 Rust 理解我们是在传递只读的引用,因此 不会发生数据竞争等不安全的事情。另外,因为我们把数据块 move 到了线程中,Rust 会保证数据存活至线程退出,因此不会产生悬挂指针。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

作业

根据用户输入的数据来决定线程的数量是不明智的。如果用户输入的数据中有一大堆空格 怎么办?我们真的想要创建 2000 个线程吗?

请修改程序,使得数据总是被分成有限数目的段,这个数目是由程序开头的静态常量决定的。

参见:

通道

Rust 为线程之间的通信提供了异步的通道(channel)。通道允许两个端点之间信息的 单向流动:Sender(发送端) 和 Receiver(接收端)。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

路径

Path 结构体代表了底层文件系统的文件路径。Path 分为两种:posix::Path,针对 类 UNIX 系统;以及 windows::Path,针对 Windows。prelude 会选择并输出符合平台类型 的 Path 种类。

译注:prelude 是 Rust 自动地在每个程序中导入的一些通用的东西,这样我们就不必每写 一个程序就手动导入一番。

Path 可从 OsStr 类型创建,并且它提供数种方法,用于获取路径指向的文件/目录 的信息。

注意 Path 在内部并不是用 UTF-8 字符串表示的,而是存储为若干字节(Vec<u8>)的 vector。因此,将 Path 转化成 &str 并非零开销的(free),且可能失败(因此它 返回一个 Option)。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

记得看看其他的 Path 方法(posix::Pathwindows::Path 的),还有 Metadata 结构体类型。

参见

OsStrMetadata

文件输入输出(I/O)

File 结构体表示一个被打开的文件(它包裹了一个文件描述符),并赋予了对所表示的 文件的读写能力。

由于在进行文件 I/O(输入/输出)操作时可能出现各种错误,因此 File 的所有方法都 返回 io::Result<T> 类型,它是 Result<T, io::Error> 的别名。

这使得所有 I/O 操作的失败都变成显式的。借助这点,程序员可以看到所有的失败 路径,并被鼓励主动地处理这些情形。

打开文件 open

open 静态方法能够以只读模式(read-only mode)打开一个文件。

File 拥有资源,即文件描述符(file descriptor),它会在自身被 drop 时关闭文件。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

下面是所希望的成功的输出:

$ echo "Hello World!" > hello.txt $ rustc open.rs && ./open hello.txt contains: Hello World!

(我们鼓励您在不同的失败条件下测试前面的例子:hello.txt 不存在,或 hello.txt 不可读,等等。)

创建文件 create

create 静态方法以只写模式(write-only mode)打开一个文件。若文件已经存在,则 旧内容将被销毁。否则,将创建一个新文件。

static LOREM_IPSUM: &'static str = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. "; use std::error::Error; use std::io::prelude::*; use std::fs::File; use std::path::Path; fn main() { let path = Path::new("out/lorem_ipsum.txt"); let display = path.display(); // 以只写模式打开文件,返回 `io::Result<File>` let mut file = match File::create(&path) { Err(why) => panic!("couldn't create {}: {}", display, why.description()), Ok(file) => file, }; // 将 `LOREM_IPSUM` 字符串写进 `file`,返回 `io::Result<()>` match file.write_all(LOREM_IPSUM.as_bytes()) { Err(why) => { panic!("couldn't write to {}: {}", display, why.description()) }, Ok(_) => println!("successfully wrote to {}", display), } }

下面是预期成功的输出:

$ mkdir out $ rustc create.rs && ./create successfully wrote to out/lorem_ipsum.txt $ cat out/lorem_ipsum.txt Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

(和前面例子一样,我们鼓励你在失败条件下测试这个例子。)

还有一个更通用的 open_mode 方法,这能够以其他方式来来打开 文件,如:read+write(读+写),追加(append),等等。

读取行

方法 lines() 在文件的行上返回一个迭代器。

File::open 需要一个泛型 AsRef<Path>。这正是 read_lines() 期望的输入。

use std::fs::File; use std::io::{self, BufRead}; use std::path::Path; fn main() { // 在生成输出之前,文件主机必须存在于当前路径中 if let Ok(lines) = read_lines("./hosts") { // 使用迭代器,返回一个(可选)字符串 for line in lines { if let Ok(ip) = line { println!("{}", ip); } } } } // 输出包裹在 Result 中以允许匹配错误, // 将迭代器返回给文件行的读取器(Reader)。 fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>> where P: AsRef<Path>, { let file = File::open(filename)?; Ok(io::BufReader::new(file).lines()) }

运行此程序将一行行将内容打印出来。

$ echo -e "127.0.0.1\n192.168.0.1\n" > hosts $ rustc read_lines.rs && ./read_lines 127.0.0.1 192.168.0.1

这个过程比在内存中创建 String 更有效,特别是处理更大的文件。

子进程

process::Output 结构体表示已结束的子进程(child process)的输出,而 process::Command 结构体是一个进程创建者(process builder)。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

(再试试上面的例子,给 rustc 命令传入一个错误的 flag)

管道

std::Child 结构体代表了一个正在运行的子进程,它暴露了 stdin(标准 输入),stdout(标准输出) 和 stderr(标准错误) 句柄,从而可以通过管道与 所代表的进程交互。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

等待

如果你想等待一个 process::Child 完成,就必须调用 Child::wait,这会返回 一个 process::ExitStatus

use std::process::Command; fn main() { let mut child = Command::new("sleep").arg("5").spawn().unwrap(); let _result = child.wait().unwrap(); println!("reached end of main"); }
$ rustc wait.rs && ./wait reached end of main # `wait` keeps running for 5 seconds # `sleep 5` command ends, and then our `wait` program finishes

文件系统操作

std::io::fs 模块包含几个处理文件系统的函数。

use std::fs; use std::fs::{File, OpenOptions}; use std::io; use std::io::prelude::*; use std::os::unix; use std::path::Path; // `% cat path` 的简单实现 fn cat(path: &Path) -> io::Result<String> { let mut f = File::open(path)?; let mut s = String::new(); match f.read_to_string(&mut s) { Ok(_) => Ok(s), Err(e) => Err(e), } } // `% echo s > path` 的简单实现 fn echo(s: &str, path: &Path) -> io::Result<()> { let mut f = File::create(path)?; f.write_all(s.as_bytes()) } // `% touch path` 的简单实现(忽略已存在的文件) fn touch(path: &Path) -> io::Result<()> { match OpenOptions::new().create(true).write(true).open(path) { Ok(_) => Ok(()), Err(e) => Err(e), } } fn main() { println!("`mkdir a`"); // 创建一个目录,返回 `io::Result<()>` match fs::create_dir("a") { Err(why) => println!("! {:?}", why.kind()), Ok(_) => {}, } println!("`echo hello > a/b.txt`"); // 前面的匹配可以用 `unwrap_or_else` 方法简化 echo("hello", &Path::new("a/b.txt")).unwrap_or_else(|why| { println!("! {:?}", why.kind()); }); println!("`mkdir -p a/c/d`"); // 递归地创建一个目录,返回 `io::Result<()>` fs::create_dir_all("a/c/d").unwrap_or_else(|why| { println!("! {:?}", why.kind()); }); println!("`touch a/c/e.txt`"); touch(&Path::new("a/c/e.txt")).unwrap_or_else(|why| { println!("! {:?}", why.kind()); }); println!("`ln -s ../b.txt a/c/b.txt`"); // 创建一个符号链接,返回 `io::Resutl<()>` if cfg!(target_family = "unix") { unix::fs::symlink("../b.txt", "a/c/b.txt").unwrap_or_else(|why| { println!("! {:?}", why.kind()); }); } println!("`cat a/c/b.txt`"); match cat(&Path::new("a/c/b.txt")) { Err(why) => println!("! {:?}", why.kind()), Ok(s) => println!("> {}", s), } println!("`ls a`"); // 读取目录的内容,返回 `io::Result<Vec<Path>>` match fs::read_dir("a") { Err(why) => println!("! {:?}", why.kind()), Ok(paths) => for path in paths { println!("> {:?}", path.unwrap().path()); }, } println!("`rm a/c/e.txt`"); // 删除一个文件,返回 `io::Result<()>` fs::remove_file("a/c/e.txt").unwrap_or_else(|why| { println!("! {:?}", why.kind()); }); println!("`rmdir a/c/d`"); // 移除一个空目录,返回 `io::Result<()>` fs::remove_dir("a/c/d").unwrap_or_else(|why| { println!("! {:?}", why.kind()); }); }

下面是所期望的成功的输出:

$ rustc fs.rs && ./fs `mkdir a` `echo hello > a/b.txt` `mkdir -p a/c/d` `touch a/c/e.txt` `ln -s ../b.txt a/c/b.txt` `cat a/c/b.txt` > hello `ls a` > "a/b.txt" > "a/c" `rm a/c/e.txt` `rmdir a/c/d`

a 目录的最终状态为:

$ tree a a |-- b.txt `-- c `-- b.txt -> ../b.txt 1 directory, 2 files

另一种定义 cat 函数的方式是使用 ? 标记:

fn cat(path: &Path) -> io::Result<String> { let mut f = File::open(path)?; let mut s = String::new(); f.read_to_string(&mut s)?; Ok(s) }

参见:

cfg!

程序参数

标准库

命令行参数可使用 std::env::args 进行接收,这将返回一个迭代器,该迭代器会对 每个参数举出一个字符串。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
$ ./args 1 2 3 My path is ./args. I got 3 arguments: ["1", "2", "3"].

crate

另外,也有很多 crate 提供了编写命令行应用的额外功能。Rust Cookbook 展示了使用 最流行的命令行参数 crate,即 clap 的最佳实践。

参数解析

可以用模式匹配来解析简单的参数:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
$ ./match_args Rust This is not the answer. $ ./match_args 42 This is the answer! $ ./match_args do something error: second argument not an integer usage: match_args <string> Check whether given string is the answer. match_args {increase|decrease} <integer> Increase or decrease given integer by one. $ ./match_args do 42 error: invalid command usage: match_args <string> Check whether given string is the answer. match_args {increase|decrease} <integer> Increase or decrease given integer by one. $ ./match_args increase 42 43

外部语言函数接口

Rust 提供了到 C 语言库的外部语言函数接口(Foreign Function Interface,FFI)。外 部语言函数必须在一个 extern 代码块中声明,且该代码块要带有一个包含库名称 的 #[link] 属性。

use std::fmt; // 这个 extern 代码块链接到 libm 库 #[link(name = "m")] extern { // 这个外部函数用于计算单精度复数的平方根 fn csqrtf(z: Complex) -> Complex; // 这个用来计算单精度复数的复变余弦 fn ccosf(z: Complex) -> Complex; } // 由于调用其他语言的函数被认为是不安全的,我们通常会给它们写一层安全的封装 fn cos(z: Complex) -> Complex { unsafe { ccosf(z) } } fn main() { // z = -1 + 0i let z = Complex { re: -1., im: 0. }; // 调用外部语言函数是不安全操作 let z_sqrt = unsafe { csqrtf(z) }; println!("the square root of {:?} is {:?}", z, z_sqrt); // 调用不安全操作的安全的 API 封装 println!("cos({:?}) = {:?}", z, cos(z)); } // 单精度复数的最简实现 #[repr(C)] #[derive(Clone, Copy)] struct Complex { re: f32, im: f32, } impl fmt::Debug for Complex { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if self.im < 0. { write!(f, "{}-{}i", self.re, -self.im) } else { write!(f, "{}+{}i", self.re, self.im) } } }

测试

Rust 是一门非常重视正确性的语言,这门语言本身就提供了对编写软件测试的支持。

测试有三种风格:

Rust 也支持在测试中指定额外的依赖:

参见

单元测试

测试(test)是这样一种 Rust 函数:它保证其他部分的代码按照所希望的行为正常 运行。测试函数的函数体通常会进行一些配置,运行我们想要测试的代码,然后 断言(assert)结果是不是我们所期望的。

大多数单元测试都会被放到一个叫 tests 的、带有 #[cfg(test)] 属性 的模块中,测试函数要加上 #[test] 属性。

当测试函数中有什么东西 panic 了,测试就失败。有一些这方面的 辅助

  • assert!(expression) - 如果表达式的值是 false 则 panic。
  • assert_eq!(left, right)assert_ne!(left, right) - 检验左右两边是否 相等/不等。
pub fn add(a: i32, b: i32) -> i32 { a + b } // 这个加法函数写得很差,本例中我们会使它失败。 #[allow(dead_code)] fn bad_add(a: i32, b: i32) -> i32 { a - b } #[cfg(test)] mod tests { // 注意这个惯用法:在 tests 模块中,从外部作用域导入所有名字。 use super::*; #[test] fn test_add() { assert_eq!(add(1, 2), 3); } #[test] fn test_bad_add() { // 这个断言会导致测试失败。注意私有的函数也可以被测试! assert_eq!(bad_add(1, 2), 3); } }

可以使用 cargo test 来运行测试。

$ cargo test running 2 tests test tests::test_bad_add ... FAILED test tests::test_add ... ok failures: ---- tests::test_bad_add stdout ---- thread 'tests::test_bad_add' panicked at 'assertion failed: `(left == right)` left: `-1`, right: `3`', src/lib.rs:21:8 note: Run with `RUST_BACKTRACE=1` for a backtrace. failures: tests::test_bad_add test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

测试 panic

一些函数应当在特定条件下 panic。为测试这种行为,请使用 #[should_panic] 属性。这 个属性接受可选参数 expected = 以指定 panic 时的消息。如果你的函数能以多种方式 panic,这个属性就保证了你在测试的确实是所指定的 panic。

pub fn divide_non_zero_result(a: u32, b: u32) -> u32 { if b == 0 { panic!("Divide-by-zero error"); } else if a < b { panic!("Divide result is zero"); } a / b } #[cfg(test)] mod tests { use super::*; #[test] fn test_divide() { assert_eq!(divide_non_zero_result(10, 2), 5); } #[test] #[should_panic] fn test_any_panic() { divide_non_zero_result(1, 0); } #[test] #[should_panic(expected = "Divide result is zero")] fn test_specific_panic() { divide_non_zero_result(1, 10); } }

运行这些测试会输出:

$ cargo test running 3 tests test tests::test_any_panic ... ok test tests::test_divide ... ok test tests::test_specific_panic ... ok test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out Doc-tests tmp-test-should-panic running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

运行特定的测试

要运行特定的测试,只要把测试名称传给 cargo test 命令就可以了。

$ cargo test test_any_panic running 1 test test tests::test_any_panic ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out Doc-tests tmp-test-should-panic running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

要运行多个测试,可以仅指定测试名称中的一部分,用它来匹配所有要运行的测试。

$ cargo test panic running 2 tests test tests::test_any_panic ... ok test tests::test_specific_panic ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out Doc-tests tmp-test-should-panic running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

忽略测试

可以把属性 #[ignore] 赋予测试以排除某些测试,或者使用 cargo test -- --ignored 命令来运行它们。

#![allow(unused)] fn main() { pub fn add(a: i32, b: i32) -> i32 { a + b } #[cfg(test)] mod tests { use super::*; #[test] fn test_add() { assert_eq!(add(2, 2), 4); } #[test] fn test_add_hundred() { assert_eq!(add(100, 2), 102); assert_eq!(add(2, 100), 102); } #[test] #[ignore] fn ignored_test() { assert_eq!(add(0, 0), 0); } } }
$ cargo test running 1 test test tests::ignored_test ... ignored test result: ok. 0 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out Doc-tests tmp-ignore running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out $ cargo test -- --ignored running 1 test test tests::ignored_test ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out Doc-tests tmp-ignore running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

文档测试

为 Rust 工程编写文档的主要方式是在源代码中写注释。文档注释使用 markdown 语法 书写,支持代码块。Rust 很注重正确性,这些注释中的代码块也会被编译并且用作测试。

/// 第一行是对函数的简短描述。 /// /// 接下来数行是详细文档。代码块用三个反引号开启,Rust 会隐式地在其中添加 /// `fn main()` 和 `extern crate <cratename>`。比如测试 `doccomments` crate: /// /// ``` /// let result = doccomments::add(2, 3); /// assert_eq!(result, 5); /// ``` pub fn add(a: i32, b: i32) -> i32 { a + b } /// 文档注释通常可能带有 "Examples"、"Panics" 和 "Failures" 这些部分。 /// /// 下面的函数将两数相除。 /// /// # Examples /// /// ``` /// let result = doccomments::div(10, 2); /// assert_eq!(result, 5); /// ``` /// /// # Panics /// /// 如果第二个参数是 0,函数将会 panic。 /// /// ```rust,should_panic /// // panics on division by zero /// doccomments::div(10, 0); /// ``` pub fn div(a: i32, b: i32) -> i32 { if b == 0 { panic!("Divide-by-zero error"); } a / b }

这些测试仍然可以通过 cargo test 执行:

$ cargo test running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out Doc-tests doccomments running 3 tests test src/lib.rs - add (line 7) ... ok test src/lib.rs - div (line 21) ... ok test src/lib.rs - div (line 31) ... ok test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

文档测试的目的

文档测试的主要目的是作为使用函数功能的例子,这是最重要的指导 原则之一。文档测试应当可以作为完整的代码段被直接 使用(很多用户会复制文档中的代码来用,所以例子要写得完善)。但使用 ? 符号会 导致编译失败,因为 main 函数会返回 unit 类型。幸运的是,我们可以在文档中 隐藏几行源代码:你可以写 fn try_main() -> Result<(), ErrorType> 这样的 函数,把它隐藏起来,然后在隐藏的 main 中展开它。听起来很复杂?请看例子:

/// 在文档测试中使用隐藏的 `try_main`。 /// /// ``` /// # // 被隐藏的行以 `#` 开始,但它们仍然会被编译! /// # fn try_main() -> Result<(), String> { // 隐藏行包围了文档中显示的函数体 /// let res = try::try_div(10, 2)?; /// # Ok(()) // 从 try_main 返回 /// # } /// # fn main() { // 开始主函数,其中将展开 `try_main` 函数 /// # try_main().unwrap(); // 调用并展开 try_main,这样出错时测试会 panic /// # } pub fn try_div(a: i32, b: i32) -> Result<i32, String> { if b == 0 { Err(String::from("Divide-by-zero")) } else { Ok(a / b) } }

参见

集成测试

单元测试一次仅能单独测试一个模块,这种测试是小规模的,并且能测试私有 代码;集成测试是 crate 外部的测试,并且仅使用 crate 的公共接口,就像其他使用 该 crate 的程序那样。集成测试的目的是检验你的库的各部分是否能够正确地协同工作。

cargo 在与 src 同级别的 tests 目录寻找集成测试。

文件 src/lib.rs

// 在一个叫做 'adder' 的 crate 中定义此函数。 pub fn add(a: i32, b: i32) -> i32 { a + b }

包含测试的文件:tests/integration_test.rs

#[test] fn test_add() { assert_eq!(adder::add(3, 2), 5); }

使用 cargo test 命令:

$ cargo test running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out Running target/debug/deps/integration_test-bcd60824f5fbfe19 running 1 test test test_add ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

tests 目录中的每一个 Rust 源文件都被编译成一个单独的 crate。在集成测试中要想 共享代码,一种方式是创建具有公用函数的模块,然后在测试中导入并使用它。

文件 tests/common.rs:

pub fn setup() { // 一些配置代码,比如创建文件/目录,开启服务器等等。 }

包含测试的文件:tests/integration_test.rs

// 导入共用模块。 mod common; #[test] fn test_add() { // 使用共用模块。 common::setup(); assert_eq!(adder::add(3, 2), 5); }

带有共用代码的模块遵循和普通的模块一样的规则,所以完全可以把公共模块 写在 tests/common/mod.rs 文件中。

开发依赖

有时仅在测试中才需要一些依赖(比如基准测试相关的)。这种依赖要写在 Cargo.toml[dev-dependencies] 部分。这些依赖不会传播给其他依赖于这个包的包。

比如说使用 pretty_assertions,这是扩展了标准的 assert! 宏的一个 crate。

文件 Cargo.toml:

# 这里省略了标准的 crate 数据 [dev-dependencies] pretty_assertions = "1"

文件 src/lib.rs:

pub fn add(a: i32, b: i32) -> i32 { a + b } #[cfg(test)] mod tests { use super::*; use pretty_assertions::assert_eq; // 仅用于测试, 不能在非测试代码中使用 #[test] fn test_add() { assert_eq!(add(2, 3), 5); } }

参见

Cargo 文档中关于指定依赖的部分。

不安全操作

在本章一开始,我们借用官方文档的一句话,“在整个代码库(code base,指 构建一个软件系统所使用的全部代码)中,要尽可能减少不安全代码的量”。记住这句 话,接着我们进入学习!在 Rust 中,不安全代码块用于避开编译器的保护策略;具体 地说,不安全代码块主要用于四件事情:

  • 解引用裸指针
  • 通过 FFI 调用函数(这已经在之前的章节介绍过了)
  • 调用不安全的函数
  • 内联汇编(inline assembly)

原始指针

原始指针(raw pointer,裸指针)* 和引用 &T 有类似的功能,但引用总是安全 的,因为借用检查器保证了它指向一个有效的数据。解引用一个裸指针只能通过不安全 代码块执行。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

调用不安全函数

一些函数可以声明为不安全的(unsafe),这意味着在使用它时保证正确性不再是编译器 的责任,而是程序员的。一个例子就是 std::slice::from_raw_parts,向它传入指向 第一个元素的指针和长度参数,它会创建一个切片。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

slice::from_raw_parts 假设传入的指针指向有效的内存,且被指向的内存具有正确的 数据类型,我们必须满足这一假设,否则程序的行为是未定义的(undefined),于是 我们就不能预测会发生些什么了。

兼容性

Rust 语言正在快速发展,因此尽管努力确保尽可能向前兼容,但仍可能出现某些兼容性问题。

原始标志符

与许多编程语言一样,Rust 拥有“关键字”的概念。 这些标识符对语言有特定意义,所以不能在变量名、函数名和其他位置使用它们。 原始标识符允许你使用通常不允许的关键字。 当 Rust 引入新关键字时,使用旧版 Rust 的库拥有与新版本中引入的关键字同名的变量或函数,这一点就特别有用。

举个例子,使用 2015 版 Rust 编译的 crate foo,它导出一个名为 try 的函数。 此关键字(try)在 2018 版本的新功能中保留下来,因此如果没有原始标识符,我们将无法命名该功能。

extern crate foo; fn main() { foo::try(); }

将得到如下错误:

error: expected identifier, found keyword `try` --> src/main.rs:4:4 | 4 | foo::try(); | ^^^ expected identifier, found keyword

使用原始标志符重写上述代码:

extern crate foo; fn main() { foo::r#try(); }

补充

在软件工程中,有些主题和写程序并没有直接的关联,但它们为你提供了工具和基础设施 支持,使得软件对每个人都变得更易用。这些主题包括:

  • 文档:通过附带的 rustdoc 生成库文档给用户。
  • 测试:为库创建测试套件,确保库准确地实现了你想要的功能。
  • 基准测试(benchmark):对功能进行基准测试,保证其运行速度足够快。

文档

cargo doc 构建文档到 target/doc

cargo test 运行所有测试(包括文档测试),用 cargo test --doc 仅运行文档测试。

这些命令会恰当地按需调用 rustdoc(以及 rustc)。

文档注释

文档注释对于需要文档的大型项目来说非常重要。当运行 rustdoc,文档注释就会 编译成文档。它们使用 /// 标记,并支持 Markdown

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

要运行测试,首先将代码构建为库,然后告诉 rustdoc 在哪里找到库,这样它就可以 使每个文档中的程序链接到库:

$ rustc doc.rs --crate-type lib $ rustdoc --test --extern doc="libdoc.rlib" doc.rs

文档属性

下面是一些使用 rustdoc 时最常使用的 #[doc] 属性的例子。

inline

用于内联文档,而不是链接到单独的页面。

#[doc(inline)] pub use bar::Bar; /// bar 的文档 mod bar { /// Bar 的文档 pub struct Bar; }

no_inline

用于防止链接到单独的页面或其他位置。

// 来自 libcore/prelude 的例子 #[doc(no_inline)] pub use crate::mem::drop;

hidden

使用此属性来告诉 rustdoc 不要包含此项到文档中:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

对文档来说, rustdoc 被社区广泛采用。标准库文档也是用它生成的。

参见:

Playpen

Rust Playpen 是一个在线运行 Rust 代码的网络接口。现在该项目通常称为 Rust Playground

mdbook 使用

mdbook 中,你可以让示例代码运行和编辑。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

这使读者既可以运行你的代码示例,也可以对其进行修改和调整。此处的关键是将单词添加 editable 到代码块中,并用逗号分隔。

```rust,editable //...将你的代码写在这里 ```

此外,如果想要 mdbook 在构建和测试时跳过该代码,则可以添加 ignore

```rust,editable,ignore //...将你的代码写在这里 ```

在文档中使用

可能你已经在某些 Rust 官方文档中注意到了一个名为 “Run” 的按钮,该按钮在 Rust Playground 的新选项卡中打开了代码示例。如果使用名为的 html_playground_url 的 #[doc] 属性,则启用此功能。

参见: