Rust by Example

Rust 是一门现代系统编程语言,专注于安全性、速度和并发性。通过内存安全而不使用垃圾回收来实现这些目标。

《通过例子学 Rust》(Rust By Example, RBE)是一系列可运行的示例,它们展示了各种 Rust 概念和标准库。为了更好地利用这些示例,请不要忘记本地安装 Rust并查看官方文档。此外,好奇的话,你也可以查看这个网站的源代码

现在让我们开始吧!

  • Hello World - 从一个经典的 Hello World 程序开始。

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

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

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

  • 类型 - 学习如何改变和定义类型。

  • 转换 - 在不同类型的数据之间进行转换,如字符串、整数和浮点数。

  • 表达式 - 学习表达式及其使用方法。

  • 控制流 - if/elsefor 等。

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

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

  • Crates - Crate 是 Rust 中的编译单元。学习如何创建库。

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

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

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

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

  • 特质 - 特质(trait)是为未知类型 Self 定义的一组方法。

  • - 宏是一种编写代码以生成其他代码的方式,也被称为元编程。

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

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

  • 标准库中的其他内容 - 更多关于文件处理、线程的自定义类型。

  • 测试 - Rust 中的各种测试方法。

  • 不安全操作 - 学习如何编写和使用不安全代码块。

  • 兼容性 - 应对 Rust 语言的演进及可能出现的兼容性问题。

  • 补充 - 文档,基准测试。

Hello World

这是经典的 Hello World 程序的源代码。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

println! 是一个用于在控制台打印文本的

可以使用 Rust 编译器 rustc 来生成可执行文件。

$ rustc hello.rs

rustc 将生成一个名为 hello 的可执行文件。

$ ./hello Hello World!

练习

点击上方的"运行"按钮查看预期输出。接下来,添加一行新代码,再次使用 println! 宏,输出显示如下内容:

Hello World! I'm a Rustacean!

注释

每个程序都需要注释,Rust 支持几种不同类型的注释:

  • 普通注释:编译器会忽略这些注释
    • // 行注释,从双斜杠开始到行尾。
    • /* 块注释,从开始符号到结束符号。 */
  • 文档注释:这些注释会被解析成 HTML 格式的库文档
    • /// 为接下来的项生成库文档。
    • //! 为当前项(如 crate、模块或函数)生成库文档。
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

库文档

格式化打印

打印功能由 std::fmt 中定义的一系列处理,其中包括:

  • format!:将格式化文本写入 String
  • print!:与 format! 类似,但文本会打印到控制台(io::stdout)
  • println!:与 print! 类似,但会在末尾添加换行符
  • eprint!:与 print! 类似,但文本会打印到标准错误输出(io::stderr)
  • eprintln!:与 eprint! 类似,但会在末尾添加换行符

所有这些宏都以相同的方式解析文本。此外,Rust 会在编译时检查格式化的正确性。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

std::fmt 包含许多控制文本显示的 traits。下面列出了两个重要的基本形式:

  • fmt::Debug: 使用 {:?} 标记。用于调试目的的文本格式化。
  • fmt::Display: 使用 {} 标记。以更优雅、用户友好的方式格式化文本。

这里我们使用 fmt::Display,因为标准库为这些类型提供了实现。要打印自定义类型的文本,需要额外的步骤。

实现 fmt::Display 特性会自动实现 ToString 特性,这允许我们将该类型转换String

43 行#[allow(dead_code)]是一个 属性(attribute),它只适用于它之后的模块。

练习

  • 修复上述代码中的问题(参见 FIXME 注释),使其能够正常运行。
  • 尝试取消注释那行尝试格式化 Structure 结构体的代码(参见 TODO 注释)
  • 添加一个 println! 宏调用,打印:Pi 约等于 3.142,通过控制显示的小数位数来实现。 在本练习中,使用 let pi = 3.141592 作为 pi 的近似值。(提示:你可能需要查阅 std::fmt 文档来了解如何设置显示的小数位数)

另请参阅:

std::fmtmacrosstructtraitsdead_code

调试 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 来控制显示方式。

另请参阅:

attributesderivestd::fmtstruct

显示 Display

fmt::Debug 的输出往往不够简洁清晰,因此自定义输出外观通常更有优势。 这可以通过手动实现 fmt::Display 来完成, 它使用 {} 打印标记。实现方式如下:

#![allow(unused)] fn main() { // 通过 `use` 导入 `fmt` 模块使其可用。 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 { // 将第一个元素严格写入提供的输出流 `f`。 // 返回 `fmt::Result`,表示操作是否成功。 // 注意 `write!` 的语法与 `println!` 非常相似。 write!(f, "{}", self.0) } } }

fmt::Display 可能比 fmt::Debug 更简洁,但这给 std 库带来了一个问题:如何显示歧义类型?例如,如果 std 库为所有 Vec<T> 实现统一的样式,应该采用哪种样式?是以下两种之一吗?

  • Vec<path>/:/etc:/home/username:/bin(以 : 分隔)
  • Vec<number>1,2,3(以 , 分隔)

答案是否定的。因为不存在适用于所有类型的理想样式,std 库也不应擅自规定一种。因此,fmt::Display 并未为 Vec<T> 或其他泛型容器实现。在这些泛型情况下,必须使用 fmt::Debug

不过,这不是问题。对于任何新的非泛型容器类型,都可以实现 fmt::Display

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

因此,虽然实现了 fmt::Display,但未实现 fmt::Binary,所以无法使用。std::fmt 包含许多这样的traits,每个都需要单独实现。更多详情请参阅 std::fmt

练习

查看上述示例的输出后,参考 Point2D 结构体,向示例中添加一个 Complex 结构体。以相同方式打印时,输出应为:

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

另请参阅:

derivestd::fmtmacrosstructtraituse

测试实例:列表

为一个需要顺序处理元素的结构体实现 fmt::Display 是棘手的。问题在于每个 write! 都会生成一个 fmt::Result。正确处理这种情况需要处理所有的结果。Rust 提供了 ? 运算符专门用于此目的。

write! 上使用 ? 的示例如下:

// 尝试执行 `write!`,检查是否出错。如果出错,返回错误。 // 否则继续执行。 write!(f, "{}", value)?;

有了 ? 运算符,为 Vec 实现 fmt::Display 就变得简单明了:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

练习

尝试修改程序,使向量中每个元素的索引也被打印出来。新的输出应该如下所示:

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

另请参阅:

forrefResultstruct?vec!

格式化

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

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

同一个变量(foo)可以根据使用的参数类型而有不同的格式化方式:Xo未指定

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

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

你可以在 std::fmt 文档中查看格式化 trait 的完整列表及其参数类型。

练习

为上面的 Color 结构体实现 fmt::Display trait,使输出显示如下:

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

如果你遇到困难,这里有三个提示:

另请参阅:

std::fmt

原生类型

Rust 提供了多种原生类型。以下是一些示例:

标量类型

  • 有符号整数:i8i16i32i64i128isize(指针大小)
  • 无符号整数:u8u16u32u64u128usize(指针大小)
  • 浮点数:f32f64
  • char Unicode 标量值,如 'a''α''∞'(每个都是 4 字节)
  • bool 值为 truefalse
  • 单元类型 (),其唯一可能的值是空元组:()

尽管单元类型的值是一个元组,但它不被视为复合类型,因为它不包含多个值。

复合类型

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

变量总是可以进行类型标注。数字还可以通过后缀默认方式来标注。整数默认为 i32 类型,浮点数默认为 f64 类型。请注意,Rust 也可以从上下文中推断类型。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

stdmutinferenceshadowing

字面量和运算符

整数 1、浮点数 1.2、字符 'a'、字符串 "abc"、布尔值 true 和单元类型 () 可以用字面值表示。

整数也可以使用十六进制、八进制或二进制表示法,分别使用这些前缀:0x0o0b

可以在数字字面值中插入下划线以提高可读性,例如 1_0001000 相同,0.000_0010.000001 相同。

Rust 还支持科学计数法 E-notation,例如 1e67.6e-4。相关类型是 f64

我们需要告诉编译器我们使用的字面值的类型。现在,我们将使用 u32 后缀来表示该字面值是一个无符号 32 位整数,使用 i32 后缀来表示它是一个有符号 32 位整数。

Rust 中可用的运算符及其优先级与其他 类 C 语言 类似。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

元组

元组是一个可以包含各种类型的值的集合。元组使用圆括号 () 来构造,而且每个元组本身是一个类型标记为 (T1, T2, ...) 的值,其中 T1T2 是其成员的类型。函数可以使用元组来返回多个值,因为元组可以存储任意数量的值。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

练习

  1. 复习:为上面示例中的 Matrix 结构体添加 fmt::Display trait, 这样当你从打印调试格式 {:?} 切换到显示格式 {} 时, 你会看到以下输出:

    ( 1.1 1.2 ) ( 2.1 2.2 )

    你可能需要回顾一下 打印显示 的示例。

  2. 参考 reverse 函数的模板,添加一个 transpose 函数。 该函数接受一个矩阵作为参数,并返回一个交换了两个元素的矩阵。例如:

    println!("矩阵:\n{}", matrix); println!("转置:\n{}", transpose(matrix));

    输出结果为:

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

数组和切片

数组是一种存储在连续内存中的相同类型 T 的对象集合。 数组使用方括号 [] 创建,其长度在编译时已知, 是其类型签名 [T; length] 的一部分。

切片类似于数组,但其长度在编译时未知。切片是一个双字对象: 第一个字是指向数据的指针,第二个字是切片的长度。字的大小与 usize 相同, 由处理器架构决定,例如在 x86-64 上是 64 位。切片可用于借用数组的一部分, 其类型签名为 &[T]

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

自定义类型

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

  • struct:定义结构体
  • enum:定义枚举

常量也可以通过 conststatic 关键字来创建。

结构体

使用 struct 关键字可以创建三种类型的结构体("structs"):

  • 元组结构体:本质上是具名元组。
  • 经典的 C 语言风格结构体
  • 单元结构体:没有字段,在泛型中很有用。
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

练习

  1. 添加一个 rect_area 函数来计算 Rectangle 的面积(尝试使用嵌套解构)。
  2. 添加一个 square 函数,它接受一个 Point 和一个 f32 作为参数,返回一个 Rectangle,其左上角在该点上,宽度和高度都等于 f32 参数。

另请参阅

属性原始标识符解构

枚举

enum 关键字允许创建一个可能是几种不同变体之一的类型。任何作为 struct 有效的变体在 enum 中也是有效的。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

类型别名

使用类型别名可以通过别名引用每个枚举变体。当枚举名称过长或过于泛化,而你想重命名它时,这会很有用。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

你最常见到这种用法的地方是在使用 Self 别名的 impl 块中。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

要了解更多关于枚举和类型别名的信息,你可以阅读这个特性被稳定到 Rust 时的稳定化报告

另请参阅:

matchfnString"类型别名枚举变体" RFC

use

use 声明可以用来避免手动作用域限定:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

matchuse

C 风格用法

enum 也可以像 C 语言那样使用。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

类型转换

测试实例:链表

使用 enum 是实现链表的常见方法:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

Box方法

常量

Rust 有两种常量类型,可以在任何作用域(包括全局作用域)中声明。两者都需要显式类型标注:

  • const:不可变值(常见用法)。
  • static:具有 'static 生命周期的可能可变变量。静态生命周期会被自动推断,无需明确指定。访问或修改可变静态变量是 unsafe 的。
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

const/static RFC'static 生命周期

变量绑定

Rust 通过静态类型提供类型安全。变量绑定在声明时可以添加类型注解。然而,在大多数情况下,编译器能够从上下文推断出变量类型,大大减少了类型注解的负担。

可以使用 let 关键字将值(如字面量)绑定到变量。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

可变性

变量绑定默认是不可变的,但可以使用 mut 修饰符来改变这一行为。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

编译器会对可变性错误给出详细的诊断信息。

作用域和遮蔽

变量绑定有作用域,它们被限制在一个代码块中生存。代码块是由花括号 {} 包围的一系列语句。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

此外,Rust 允许变量遮蔽

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

先声明

It is possible to declare variable bindings first and initialize them later, but all variable bindings must be initialized before they are used: the compiler forbids use of uninitialized variable bindings, as it would lead to undefined behavior.

It is not common to declare a variable binding and initialize it later in the function. It is more difficult for a reader to find the initialization when initialization is separated from declaration. It is common to declare and initialize a variable binding near where the variable will be used.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

冻结

当数据以相同名称被不可变地绑定时,它也会冻结。被冻结的数据在不可变绑定离开作用域之前不能被修改:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

类型

Rust 提供了几种机制来更改或定义原生类型和用户定义类型。以下部分将介绍:

类型转换

Rust 不支持原始类型之间的隐式类型转换(强制转换)。但可以使用 as 关键字进行显式类型转换(转型)。

整数类型之间的转换规则通常遵循 C 语言惯例,但 C 中存在未定义行为的情况除外。在 Rust 中,所有整数类型之间的转换行为都有明确定义。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

字面量

数字字面值可以通过添加类型后缀进行类型标注。例如,要指定字面值 42 的类型为 i32,可以写成 42i32

无后缀数字字面值的类型取决于其使用方式。如果没有约束,编译器将对整数使用 i32,对浮点数使用 f64

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

前面的代码中使用了一些尚未解释的概念。为了满足迫不及待的读者,这里简要说明如下:

  • std::mem::size_of_val 是一个函数,这里使用了它的"完整路径"来调用。代码可以被划分为称为"模块"的逻辑单元。在这个例子中,size_of_val 函数定义在 mem 模块中,而 mem 模块则定义在 std crate 中。更多详情请参阅模块crate

类型推断

类型推断引擎相当智能。它不仅在初始化时分析值表达式的类型,还会根据变量后续的使用方式来推断其类型。下面是一个类型推断的高级示例:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

无需为变量添加类型注解,编译器和程序员都很满意!

别名

type 语句用于为现有类型创建新名称。类型名必须使用 UpperCamelCase(大驼峰)命名,否则编译器会发出警告。此规则的例外是原始类型,如 usizef32 等。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

别名的主要用途是减少重复代码。例如,io::Result<T> 类型是 Result<T, io::Error> 类型的别名。

另请参阅:

属性

转换

原始类型可以通过类型转换相互转换。

Rust 通过使用特质来处理自定义类型(如 structenum)之间的转换。通用转换使用 FromInto 特质。然而,对于更常见的情况,特别是与 String 相互转换时,还有一些更具体的特质。

FromInto

FromInto 特质本质上是相互关联的,这实际上是其实现的一部分。如果你能将类型 A 从类型 B 转换,那么我们也应该能够将类型 B 转换为类型 A。

From

From 特质允许一个类型定义如何从另一个类型创建自身,从而提供了一种非常简单的机制来在多种类型之间进行转换。标准库中有许多这个特质的实现,用于原始类型和常见类型的转换。

例如,我们可以轻松地将 str 转换为 String

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

我们可以为自己的类型定义类似的转换。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Into

Into 特质简单来说就是 From 特质的反向操作。它定义了如何将一个类型转换为另一个类型。

调用 into() 通常需要我们指定结果类型,因为编译器大多数时候无法确定这一点。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

From and Into are interchangeable

FromInto 被设计为互补的。我们不需要为两个特质都提供实现。如果你为你的类型实现了 From 特质,Into 会在必要时调用它。但请注意,反过来并不成立:为你的类型实现 Into 不会自动为它提供 From 的实现。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

TryFromTryInto

FromInto 类似,TryFromTryInto 是用于类型转换的泛型特质。与 From/Into 不同,TryFrom/TryInto 特质用于可能失败的转换,因此返回 Result

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

String 类型转换

转换为字符串

To convert any type to a String is as simple as implementing the ToString trait for the type. Rather than doing so directly, you should implement the fmt::Display trait which automatically provides ToString and also allows printing the type as discussed in the section on print!.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

解析字符串

将字符串转换为其他类型很有用,其中最常见的操作之一是将字符串转换为数字。惯用的方法是使用 parse 函数,可以通过类型推断或使用"涡轮鱼"语法指定要解析的类型。以下示例展示了这两种方法。

只要为目标类型实现了 FromStr 特质,就可以将字符串转换为指定的类型。标准库中为许多类型实现了这个特质。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

要在自定义类型上获得这个功能,只需为该类型实现 FromStr 特质。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

表达式

Rust 程序(主要)由一系列语句组成:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Rust 中有几种语句。最常见的两种是声明变量绑定,以及在表达式后使用分号 ;

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

代码块也是表达式,因此可以在赋值中作为值使用。代码块中的最后一个表达式会被赋值给左值表达式(如局部变量)。但是,如果代码块的最后一个表达式以分号结尾,返回值将是 ()

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

控制流

控制流是任何编程语言的重要组成部分,如 if/elsefor 等。让我们来讨论 Rust 中的这些内容。

if/else

if-else 分支结构与其他语言类似。不同之处在于,布尔条件不需要用括号括起来,每个条件后面都跟着一个代码块。if-else 条件是表达式,所有分支必须返回相同的类型。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

loop

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

break 语句可以随时退出循环,而 continue 语句可以跳过当前迭代的剩余部分并开始下一次迭代。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

嵌套和标签

在处理嵌套循环时,可以 breakcontinue 外层循环。这种情况下,循环必须用 'label 标记,并且必须将标签传递给 break/continue 语句。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

在 loop 中返回值

loop 的一个用途是重试操作直到成功。如果操作返回一个值,你可能需要将它传递给代码的其余部分:将它放在 break 之后,它将被 loop 表达式返回。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

while

while 关键字用于在条件为真时运行循环。

让我们用 while 循环来编写著名的 FizzBuzz 程序。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

for 循环

for 和 range

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

让我们用 for 而不是 while 来编写 FizzBuzz。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另外,可以使用 a..=b 表示两端都包含的范围。上面的代码可以改写为:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

for 与迭代器

for in 结构能以多种方式与 Iterator 交互。正如在 Iterator 特质一节中讨论的那样,默认情况下 for 循环会对集合应用 into_iter 函数。然而,这并不是将集合转换为迭代器的唯一方法。

into_iteriteriter_mut 都以不同的方式处理集合到迭代器的转换,通过提供对数据的不同视图。

  • iter - 在每次迭代中借用集合的每个元素。因此,集合保持不变,并且在循环之后可以重复使用。
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
  • into_iter - 这会消耗集合,使得在每次迭代中提供确切的数据。一旦集合被消耗,它就不再可用于重复使用,因为它已经在循环中被"移动"了。
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
  • iter_mut - 这会可变地借用集合的每个元素,允许在原地修改集合。
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

在上面的代码片段中,注意 match 分支的类型,这是迭代类型的关键区别。类型的差异意味着可以执行不同的操作。

另请参阅:

迭代器

match

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

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

解构

match 块可以以多种方式解构项。

元组

元组可以在 match 中按如下方式解构:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

元组

数组/切片

与元组类似,数组和切片也可以用这种方式解构:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

关于 @ 符号的更多信息,请参阅数组和切片绑定

枚举

enum 的解构方式类似:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

#[allow(...)]颜色模型enum

指针/引用

对于指针,需要区分解构和解引用,因为它们是不同的概念,其用法与 C/C++ 等语言不同。

  • 解引用使用 *
  • 解构使用 &refref mut
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

ref 模式

结构体

同样,struct 可以按如下方式解构:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

结构体

守卫

match 分支可以使用守卫进行额外的筛选。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

注意,编译器在检查 match 表达式是否涵盖了所有模式时,不会考虑守卫条件。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

元组 枚举

绑定

间接访问变量时,无法在分支中使用该变量而不重新绑定。match 提供了 @ 符号,用于将值绑定到名称:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

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

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

函数枚举Option

if let

在某些情况下,使用 match 匹配枚举可能会显得繁琐。例如:

#![allow(unused)] fn main() { // 创建 `Option<i32>` 类型的 `optional` let optional = Some(7); match optional { Some(i) => println!("这是一个很长的字符串,其中包含 `{:?}`", i), _ => {}, // ^ 这是必需的,因为 `match` 要求穷举所有情况。 // 是不是觉得有些浪费空间? }; }

对于这种情况,if let 更加简洁,而且还允许指定各种失败时的处理选项:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

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

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

if let 的另一个优点是它允许我们匹配非参数化的枚举变体。即使在枚举没有实现或派生 PartialEq 的情况下也是如此。在这种情况下,if Foo::Bar == a 将无法编译,因为枚举的实例无法进行相等比较,但 if let 仍然可以正常工作。

想要挑战一下吗?请修改以下示例,使用 if let

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

枚举OptionRFC

let-else

🛈 自 Rust 1.65 版本起稳定

🛈 你可以通过这种方式编译来指定特定版本:rustc --edition=2021 main.rs

let-else 语法允许可能失败的模式匹配像普通 let 一样绑定变量到当前作用域,或在匹配失败时执行中断操作(如 breakreturnpanic!)。

use std::str::FromStr; fn get_count_item(s: &str) -> (u64, &str) { let mut it = s.split(' '); let (Some(count_str), Some(item)) = (it.next(), it.next()) else { panic!("无法分割计数项对:'{s}'"); }; let Ok(count) = u64::from_str(count_str) else { panic!("无法解析整数:'{count_str}'"); }; (count, item) } fn main() { assert_eq!(get_count_item("3 chairs"), (3, "chairs")); }

名称绑定的作用域是使其区别于 matchif let-else 表达式的主要特点。在此之前,你可能需要通过一些冗余的重复和外部 let 来近似实现这些模式:

#![allow(unused)] fn main() { use std::str::FromStr; fn get_count_item(s: &str) -> (u64, &str) { let mut it = s.split(' '); let (count_str, item) = match (it.next(), it.next()) { (Some(count_str), Some(item)) => (count_str, item), _ => panic!("无法分割计数项对:'{s}'"), }; let count = if let Ok(count) = u64::from_str(count_str) { count } else { panic!("无法解析整数:'{count_str}'"); }; (count, item) } assert_eq!(get_count_item("3 chairs"), (3, "chairs")); }

另请参阅:

Optionmatchif letlet-else RFC

while let

if let 类似,while let 可以简化繁琐的 match 序列。让我们来看一个递增 i 的例子:

#![allow(unused)] fn main() { // 创建 `Option<i32>` 类型的 `optional` let mut optional = Some(0); // 重复执行此测试。 loop { match optional { // 如果 `optional` 解构成功,则执行代码块。 Some(i) => { if i > 9 { println!("大于 9,退出!"); optional = None; } else { println!("`i` 是 `{:?}`。再试一次。", i); optional = Some(i + 1); } // ^ 需要 3 层缩进! }, // 当解构失败时退出循环: _ => { break; } // ^ 为什么需要这样?一定有更好的方法! } } }

使用 while let 可以让这个序列更加简洁:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

enumOptionRFC

函数

函数使用 fn 关键字声明。函数参数需要标注类型,就像变量一样。如果函数返回值,则必须在箭头 -> 后指定返回类型。

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

让我们用函数重写 FizzBuzz 吧!

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

关联函数和方法

某些函数与特定类型相关联。这些函数有两种形式:关联函数和方法。关联函数是在类型上定义的函数,而方法是在类型的特定实例上调用的关联函数。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

闭包

闭包是可以捕获周围环境的函数。例如,下面是一个捕获变量 x 的闭包:

|val| val + x

闭包的语法和功能使其非常适合即时使用。调用闭包与调用函数完全相同。不过,闭包的输入和返回类型可以被推断,而输入变量名必须指定。

闭包的其他特点包括:

  • 使用 || 而不是 () 来包围输入变量。
  • 单行表达式可省略函数体定界符({}),其他情况则必须使用
  • 能够捕获外部环境的变量
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

捕获

闭包本质上很灵活,无需注解就能根据功能需求自动适应。这使得捕获可以灵活地适应不同场景,有时移动,有时借用。闭包可以通过以下方式捕获变量:

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

闭包优先通过引用捕获变量,仅在必要时才使用更底部的的捕获方式。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

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

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

Boxstd::mem::drop

作为输入参数

Rust 通常能自动选择如何捕获变量,无需类型标注。但在编写函数时,这种模糊性是不允许的。当将闭包作为输入参数时,必须使用特定的 trait 来注解闭包的完整类型。这些 trait 由闭包对捕获值的处理方式决定。按限制程度从高到低排列如下:

  • Fn:闭包通过引用使用捕获的值(&T
  • FnMut:闭包通过可变引用使用捕获的值(&mut T
  • FnOnce:闭包通过值使用捕获的值(T

编译器会以尽可能最少限制的方式逐个捕获变量。

例如,考虑一个注解为 FnOnce 的参数。这表示闭包可能通过 &T&mut TT 进行捕获,但编译器最终会根据捕获变量在闭包中的使用方式来决定。

这是因为如果可以移动,那么任何类型的借用也应该是可能的。注意反过来并不成立。如果参数被注解为 Fn,那么通过 &mut TT 捕获变量是不允许的。但 &T 是允许的。

在下面的例子中,尝试交换 FnFnMutFnOnce 的用法,看看会发生什么:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

std::mem::dropFnFnMut泛型whereFnOnce

类型匿名

闭包能简洁地从外部作用域捕获变量。这会有什么影响吗?当然会有。注意观察如何将闭包作为函数参数使用时需要泛型,这是由于闭包的定义方式所决定的:

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

当定义一个闭包时,编译器会隐式创建一个新的匿名结构来存储内部捕获的变量,同时通过 FnFnMutFnOnce 这些 trait 之一为这个未知类型实现功能。这个类型被赋给变量并存储,直到被调用。

由于这个新类型是未知类型,在函数中使用时就需要泛型。然而,一个无界的类型参数 <T> 仍然会是模糊的,不被允许。因此,通过 FnFnMutFnOnce 这些 trait 之一(它实现的)来约束就足以指定其类型。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

深入分析FnFnMutFnOnce

输入函数

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

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另外需要注意的是,FnFnMutFnOnce 这些 trait 决定了闭包如何从外部作用域捕获变量。

另请参阅:

FnFnMutFnOnce

作为输出参数

既然闭包可以作为输入参数,那么将闭包作为输出参数返回也应该是可行的。然而,匿名闭包类型本质上是未知的,因此我们必须使用 impl Trait 来返回它们。

可用于返回闭包的有效 trait 包括:

  • Fn
  • FnMut
  • FnOnce

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

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

FnFnMut泛型impl Trait

std 中的例子

本节包含一些使用 std 库中闭包的示例。

Iterator::any

Iterator::any 是一个函数,它接受一个迭代器作为参数。如果任何元素满足给定的条件,则返回 true,否则返回 false。其签名如下:

pub trait Iterator { // 被迭代的类型 type Item; // `any` 接受 `&mut self`,意味着调用者可能被借用 // 和修改,但不会被消耗 fn any<F>(&mut self, f: F) -> bool where // `FnMut` 表示任何捕获的变量最多只能被修改,不能被消耗 // `Self::Item` 表示它通过值将参数传递给闭包 F: FnMut(Self::Item) -> bool; }
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

std::iter::Iterator::any

通过迭代器搜索

Iterator::find 是一个函数,它遍历迭代器并搜索满足特定条件的第一个值。如果没有值满足条件,则返回 None。其签名如下:

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

Iterator::find 返回元素的引用。如果需获取元素的索引,则使用 Iterator::position

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

std::iter::Iterator::find

std::iter::Iterator::find_map

std::iter::Iterator::position

std::iter::Iterator::rposition

高阶函数

Rust 提供了高阶函数(Higher Order Functions,HOF)。这些函数接受一个或多个函数作为参数,并/或产生一个更有用的函数。HOF 和惰性迭代器赋予了 Rust 函数式编程的特性。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

OptionIterator 实现了相当多的高阶函数。

发散函数

发散函数永不返回。它们使用 ! 标记,这是一个空类型。

#![allow(unused)] fn main() { fn foo() -> ! { panic!("此调用永不返回。"); } }

与所有其他类型相反,这个类型不能被实例化,因为这个类型可能拥有的所有可能值的集合是空的。注意,它与 () 类型不同,后者恰好有一个可能的值。

例如,这个函数像往常一样返回,尽管返回值中没有任何信息。

fn some_fn() { () } fn main() { let _a: () = some_fn(); println!("这个函数返回了,你可以看到这一行。"); }

与之相对的是这个函数,它永远不会将控制权返回给调用者。

#![feature(never_type)] fn main() { let x: ! = panic!("此调用永不返回。"); println!("你永远不会看到这一行!"); }

虽然这看起来像是一个抽象概念,但它实际上非常有用且经常派上用场。这种类型的主要优势是它可以被转换为任何其他类型,这使得它在需要精确类型的情况下非常灵活,比如在 match 分支中。这种灵活性允许我们编写如下代码:

fn main() { fn sum_odd_numbers(up_to: u32) -> u32 { let mut acc = 0; for i in 0..up_to { // 注意这个 match 表达式的返回类型必须是 u32, // 因为 "addition" 变量的类型是 u32。 let addition: u32 = match i%2 == 1 { // "i" 变量的类型是 u32,这完全没问题。 true => i, // 另一方面,"continue" 表达式不返回 u32, // 但这仍然可以,因为它永远不会返回, // 因此不违反 match 表达式的类型要求。 false => continue, }; acc += addition; } acc } println!("9 以下(不包括 9)的奇数之和:{}", sum_odd_numbers(9)); }

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

模块

Rust 提供了一个强大的模块系统,可以用来将代码分层地拆分成逻辑单元(模块),并管理它们之间的可见性(公有/私有)。

一个模块是一系列项的集合:函数、结构体、trait、impl 块,甚至其他模块。

可见性

默认情况下,模块中的项具有私有可见性,但可以使用 pub 修饰符来覆盖这一默认行为。只有模块中的公有项可以从模块作用域外部访问。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

结构体可见性

结构体的字段具有额外的可见性级别。字段默认为私有,可以使用 pub 修饰符来覆盖。这种可见性只在从结构体定义模块外部访问时才有意义,其目的是实现信息隐藏(封装)。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

泛型方法

use 声明

use 声明可以将完整路径绑定到新名称,以便更轻松地访问。它通常这样使用:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

你可以使用 as 关键字将导入绑定到不同的名称:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

superself

superself 关键字可以在路径中使用,以消除访问项目时的歧义,并避免不必要的路径硬编码。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

文件分层

模块可以映射到文件/目录层次结构。让我们将可见性示例拆分成多个文件:

$ tree . . ├── my │   ├── inaccessible.rs │   └── nested.rs ├── my.rs └── split.rs

split.rs 文件中:

// 这个声明会查找名为 `my.rs` 的文件,并将其内容 // 插入到当前作用域下名为 `my` 的模块中 mod my; fn function() { println!("调用了 `function()`"); } fn main() { my::function(); function(); my::indirect_access(); my::nested::function(); }

my.rs 文件中:

// 同样,`mod inaccessible` 和 `mod nested` 会分别定位到 // `nested.rs` 和 `inaccessible.rs` 文件, // 并将它们的内容插入到这里对应的模块中 mod inaccessible; pub mod nested; pub fn function() { println!("调用了 `my::function()`"); } fn private_function() { println!("调用了 `my::private_function()`"); } pub fn indirect_access() { print!("调用了 `my::indirect_access()`,它\n> "); private_function(); }

my/nested.rs 文件中:

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

my/inaccessible.rs 文件中:

#[allow(dead_code)] pub fn public_function() { println!("调用了 `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()`

Crates

在 Rust 中,crate 是一个编译单元。每当执行 rustc some_file.rs 时,some_file.rs 就被视为crate 文件。如果 some_file.rs 中包含 mod 声明,那么模块文件的内容会在编译器处理之前被插入到 crate 文件中 mod 声明的位置。换句话说,模块不会被单独编译,只有 crate 才会被编译。

一个 crate 可以被编译成二进制文件或库。默认情况下,rustc 会将 crate 编译成二进制文件。通过向 rustc 传递 --crate-type 标志并指定 lib,可以改变这一默认行为。

创建库

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

rary.rs 文件中:

pub fn public_function() { println!("调用了 rary 的 `public_function()`"); } fn private_function() { println!("调用了 rary 的 `private_function()`"); } pub fn indirect_access() { print!("调用了 rary 的 `indirect_access()`,它\n> "); private_function(); }
$ rustc --crate-type=lib rary.rs $ ls lib* library.rlib

库文件名会自动添加 "lib" 前缀,默认使用 crate 文件的名称。但可以通过向 rustc 传递 --crate-name 选项或使用 crate_name 属性 来覆盖这个默认名称。

使用库

要将 crate 链接到这个新库,可以使用 rustc--extern 标志。库中的所有项目都会被导入到一个与库同名的模块下。这个模块的行为通常与其他模块相同。

// extern crate rary; // Rust 2015 版本或更早版本可能需要此声明 fn main() { rary::public_function(); // 错误!`private_function` 是私有的 //rary::private_function(); rary::indirect_access(); }
# Where library.rlib is the path to the compiled library, assumed that it's # in the same directory here: $ rustc executable.rs --extern rary=library.rlib && ./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 生态系统标配了 cargocargo 可以为项目管理依赖。

要创建一个新的 Rust 项目,可以使用以下命令:

# 创建二进制项目 cargo new foo # 创建库项目 cargo new --lib bar

在本章的剩余部分,我们假设我们正在创建一个二进制项目,而不是一个库项目,但所有的概念都是相同的。

执行上述命令后,你应该会看到如下的文件结构:

. ├── bar │ ├── Cargo.toml │ └── src │ └── lib.rs └── foo ├── Cargo.toml └── src └── main.rs

main.rs 是你新建的 foo 项目的主源文件 —— 这一点没什么新鲜的。Cargo.toml 则是这个项目的 cargo 配置文件。如果你查看它的内容,应该会看到类似这样的内容:

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

[package] 下的 name 字段决定了项目的名称。如果你将 crate 发布到 crates.io(稍后会详细介绍),这个名称将被使用。同时,它也是编译时生成的二进制文件的名称。

version 字段是使用语义化版本控制的 crate 版本号。

authors 字段是发布 crate 时使用的作者列表。

[dependencies] 部分允许你为项目添加依赖。

举个例子,假设我们想让程序拥有一个出色的命令行界面(CLI)。你可以在 crates.io(Rust 官方包注册中心)上找到许多优秀的包。其中,clap 是一个广受欢迎的选择。在撰写本文时,clap 的最新发布版本是 2.27.1。要在我们的程序中添加这个依赖,只需在 Cargo.toml[dependencies] 下添加:clap = "2.27.1"。就这么简单!现在你就可以在程序中使用 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)。

瞧!就是这么简单!

约定

在上一章中,我们看到了如下目录结构:

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

那么,如果我们想在同一个项目中包含两个二进制文件,该怎么办呢?

cargo 实际上支持这种需求。如我们之前所见,默认的二进制文件名是 main,但你可以通过在 bin/ 目录中放置额外的文件来添加其他二进制文件:

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

如果要指示 cargo 只编译或运行特定的二进制文件,只需传递 --bin my_other_bin 标志,其中 my_other_bin 是我们想要处理的二进制文件的名称。

除了额外的二进制文件,cargo 还支持更多功能,如基准测试、测试和示例。

在下一章中,我们将更详细地探讨测试。

测试

众所周知,测试是任何软件不可或缺的一部分!Rust 为单元测试和集成测试提供了一流的支持(参见《Rust 程序设计语言》中的这一章)。

从上面链接的测试章节中,我们了解了如何编写单元测试和集成测试。在组织结构上,我们可以将单元测试放在它们所测试的模块中,而将集成测试放在专门的 tests/ 目录中:

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

tests 目录中的每个文件都是一个独立的集成测试,即旨在测试你的库,就像它被依赖的 crate 调用一样。

测试章节详细阐述了三种不同的测试风格:单元测试文档测试集成测试

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 4 tests test test_bar ... ok test test_baz ... ok test test_foo_bar ... ok test test_foo ... ok test result: ok. 4 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 可能会并发运行多个测试,因此请确保它们之间不会产生竞态条件。

并发可能导致问题的一个例子是,如果两个测试同时输出到同一个文件,如下所示:

#![allow(unused)] fn main() { #[cfg(test)] mod tests { // 导入必要的模块 use std::fs::OpenOptions; use std::io::Write; // 这个测试向文件写入内容 #[test] fn test_file() { // 打开 ferris.txt 文件,如果不存在则创建一个 let mut file = OpenOptions::new() .append(true) .create(true) .open("ferris.txt") .expect("无法打开 ferris.txt"); // 循环打印 "Ferris" 5 次 for _ in 0..5 { file.write_all("Ferris\n".as_bytes()) .expect("无法写入 ferris.txt"); } } // 这个测试尝试写入同一个文件 #[test] fn test_file_also() { // 打开 ferris.txt 文件,如果不存在则创建一个 let mut file = OpenOptions::new() .append(true) .create(true) .open("ferris.txt") .expect("无法打开 ferris.txt"); // 循环打印 "Corro" 5 次 for _ in 0..5 { file.write_all("Corro\n".as_bytes()) .expect("无法写入 ferris.txt"); } } } }

尽管预期结果应该是:

$ cat ferris.txt Ferris Ferris Ferris Ferris Ferris Corro Corro Corro Corro Corro

但实际写入 ferris.txt 的内容可能是这样的:

$ cargo test test_file && cat ferris.txt Corro Ferris Corro Ferris Corro Ferris Corro Ferris Corro Ferris

构建脚本

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

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

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

如果没有指定,Cargo 默认会在项目目录中查找 build.rs 文件。

如何使用构建脚本

构建脚本只是另一个 Rust 文件,它会在编译包中的其他内容之前被编译和调用。因此,它可以用来满足你的 crate 的先决条件。

Cargo 通过环境变量为脚本提供输入,这些环境变量可以被使用。具体参见这里的说明

脚本通过标准输出提供输出。所有打印的行都会被写入 target/debug/build/<pkg>/output。此外,以 cargo: 为前缀的行会被 Cargo 直接解释,因此可以用来为包的编译定义参数。

如需了解更多详细规范和示例,请参阅 Cargo 构建脚本规范

属性

属性是应用于模块、crate 或条目的元数据。这些元数据可用于以下目的:

属性的形式为 #[outer_attribute](外部属性)或 #![inner_attribute](内部属性),它们的区别在于应用的位置。

  • #[outer_attribute] 应用于紧随其后的条目。条目的例子包括:函数、模块声明、常量、结构体、枚举等。以下是一个示例,其中属性 #[derive(Debug)] 应用于结构体 Rectangle

    #![allow(unused)] fn main() { #[derive(Debug)] struct Rectangle { width: u32, height: u32, } }
  • #![inner_attribute] 应用于包含它的条目(通常是模块或 crate)。换句话说,这种属性被解释为应用于它所在的整个作用域。以下是一个示例,其中 #![allow(unused_variables)] 应用于整个 crate(如果放置在 main.rs 中):

    #![allow(unused_variables)] fn main() { let x = 3; // 这通常会警告未使用的变量。 }

属性可以使用不同的语法接受参数:

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

属性可以有多个值,也可以跨多行分隔:

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

dead_code

编译器提供了一个 dead_code lint,用于警告未使用的函数。可以使用属性来禁用这个 lint。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

注意,在实际程序中,你应该消除无用代码。在这些示例中,我们会在某些地方允许存在无用代码,这是因为这些示例具有交互性质。

Crates

crate_type 属性可用于告诉编译器一个 crate 是二进制文件还是库(甚至是哪种类型的库),而 crate_name 属性可用于设置 crate 的名称。

然而,需要注意的是,当使用 Rust 的包管理器 Cargo 时,crate_typecrate_name 属性完全不起作用。由于大多数 Rust 项目都使用 Cargo,这意味着 crate_typecrate_name 在实际使用中的应用相对有限。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

当使用 crate_type 属性时,我们就不再需要向 rustc 传递 --crate-type 标志。

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

cfg

配置条件检查可以通过两种不同的操作符实现:

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

前者启用条件编译,后者在运行时条件性地求值为 truefalse 字面量,允许在运行时进行检查。两者使用相同的参数语法。

cfg!#[cfg] 不同,它不会移除任何代码,只会求值为 true 或 false。例如,当 cfg! 用于条件时,if/else 表达式中的所有代码块都需要是有效的,无论 cfg! 正在评估什么。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

参考文档cfg!

自定义

一些条件(如 target_os)是由 rustc 隐式提供的,但自定义条件必须通过 --cfg 标志传递给 rustc

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

尝试运行这段代码,看看没有自定义 cfg 标志会发生什么。

使用自定义 cfg 标志:

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

泛型

泛型是一个关于将类型和功能泛化以适用于更广泛情况的主题。这在多方面都非常有用,可以大大减少代码重复,但可能需要相对复杂的语法。具体来说,使用泛型需要非常谨慎地指定泛型类型在哪些类型上是有效的。泛型最简单和最常见的用途是类型参数。

类型参数通过使用尖括号和大写驼峰命名法来指定为泛型:<Aaa, Bbb, ...>。"泛型类型参数"通常表示为 <T>。在 Rust 中,"泛型"也用来描述任何接受一个或多个泛型类型参数 <T> 的东西。任何被指定为泛型类型参数的类型都是泛型的,而其他所有类型都是具体的(非泛型)。

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

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

因为 T 已经使用 <T> 指定为泛型类型参数,所以在这里用作 (arg: T) 时被视为泛型。即使 T 之前已被定义为一个 struct,这种情况也成立。

下面的例子展示了一些泛型语法的实际应用:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

结构体

函数

同样的规则也适用于函数:当类型 T 前面加上 <T> 时,它就变成了泛型。

使用泛型函数有时需要明确指定类型参数。这种情况可能出现在函数返回类型是泛型时,或者编译器没有足够信息推断必要的类型参数时。

明确指定类型参数的函数调用看起来像这样:fun::<A, B, ...>()

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

函数结构体

实现

与函数类似,实现(impl)在涉及泛型时也需要谨慎处理。

#![allow(unused)] fn main() { struct S; // 具体类型 `S` struct GenericVal<T>(T); // 泛型类型 `GenericVal` // GenericVal 的实现,这里我们显式指定类型参数: impl GenericVal<f32> {} // 指定 `f32` impl GenericVal<S> {} // 指定上面定义的 `S` // `<T>` 必须放在类型前面以保持泛型 impl<T> GenericVal<T> {} }
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

返回引用的函数impl结构体

特质

当然,trait 也可以是泛型的。这里我们定义了一个泛型 trait,它重新实现了 Drop trait,用于释放自身和一个输入参数。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

Dropstructtrait

约束

在使用泛型时,类型参数通常需要使用 trait 作为约束,以规定类型应实现哪些功能。例如,下面的示例使用 Display trait 来打印,因此它要求 T 必须受 Display 约束;换句话说,T 必须实现 Display

// 定义一个函数 `printer`,它接受一个泛型类型 `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::fmtstructtrait

测试实例:空约束

约束的工作机制导致即使一个 trait 不包含任何功能,你仍然可以将其用作约束。std 库中的 EqCopy 就是这种 trait 的例子。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

std::cmp::Eqstd::marker::Copytrait

多重约束

可以使用 + 为单个类型指定多个约束。按照惯例,不同的类型用 , 分隔。

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

新类型惯用法

newtype 模式在编译时保证了程序接收到正确类型的值。

例如,一个检查年龄(以年为单位)的年龄验证函数,必须接收 Years 类型的值。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

取消最后一个 print 语句的注释,你会发现所提供的类型必须是 Years

要获取 newtype 的基本类型值,你可以使用元组语法或解构语法,如下所示:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

struct

关联项

"关联项"是指与各种类型的相关的一组规则。它是 trait 泛型的扩展,允许 trait 在内部定义新的项。

其中一种项被称为关联类型,当 trait 对其容器类型是泛型时,它提供了更简洁的使用模式。

另请参阅:

RFC

问题

对于容器类型是泛型的 trait,有类型规范要求 —— trait 的使用者必须指定所有的泛型类型。

在下面的例子中,Contains trait 允许使用泛型类型 AB。然后为 Container 类型实现该 trait,将 AB 指定为 i32,以便与 fn difference() 一起使用。

由于 Contains 是泛型的,我们不得不为 fn difference() 显式声明所有泛型类型。实际上,我们希望有一种方法来表达 AB 是由输入 C 决定的。正如你将在下一节中看到的,关联类型恰好提供了这种能力。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

structtrait

关联类型

使用"关联类型"通过将内部类型局部移动到 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

虚类型参数

虚类型参数是一种在运行时不会出现,但在编译时会进行静态检查的类型参数。

数据类型可以使用额外的泛型类型参数作为标记,或在编译时进行类型检查。这些额外的参数不占用存储空间,也没有运行时行为。

在下面的示例中,我们将 std::marker::PhantomData 与虚类型参数的概念结合,创建包含不同数据类型的元组。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

派生(Derive)结构体(struct)元组结构体(TupleStructs)

测试实例:单位澄清

通过使用虚类型参数实现 Add trait,我们可以探索一种有用的单位转换方法。下面我们来看看 Add trait:

// 这个结构会强制要求:`Self + RHS = Output` // 其中,如果在实现中未指定 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

另请参阅:

借用(&约束(X: Y枚举impl 和 self运算符重载reftrait(X for Y以及元组结构体

作用域规则

作用域在所有权、借用和生命周期中扮演着重要角色。它们向编译器指示借用何时有效、资源何时可以被释放,以及变量何时被创建或销毁。

RAII

Rust 中的变量不仅仅是在栈上保存数据:它们还拥有资源,例如 Box<T> 拥有堆上的内存。Rust 强制执行 RAII(资源获取即初始化),因此每当一个对象离开作用域时,它的析构函数就会被调用,它拥有的资源也会被释放。

这种行为可以防止资源泄漏错误,因此你再也不用手动释放内存或担心内存泄漏了!以下是一个简单示例:

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 提供的。当资源离开作用域时,析构函数会被调用。并非每种类型都需要实现这个 trait,只有当你需要为自己的类型实现特定的析构逻辑时才需要实现它。

运行下面的示例来了解 Drop trait 是如何工作的。当 main 函数中的变量离开作用域时,自定义的析构函数将被调用。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

Box

所有权和移动

由于变量负责释放它们自己的资源,资源只能有一个所有者。这可以防止资源被多次释放。请注意,并非所有变量都拥有资源(例如引用)。

当进行赋值(let x = y)或按值传递函数参数(foo(x))时,资源的所有权会被转移。在 Rust 中,这被称为移动(move)。

资源移动后,原所有者将无法再被使用。这避免了悬垂指针的产生。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

可变性

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

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

部分移动

Within the destructuring of a single variable, both by-move and by-reference pattern bindings can be used at the same time. Doing this will result in a partial move of the variable, which means that parts of the variable will be moved while other parts stay. In such a case, the parent variable cannot be used afterwards as a whole, however the parts that are only referenced (and not moved) can still be used. Note that types that implement the Drop trait cannot be partially moved from, because its drop method would use it afterwards as a whole.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

(在这个例子中,我们将 age 变量存储在堆上以说明部分移动:删除上面代码中的 ref 会导致错误,因为 person.age 的所有权会被移动到变量 age。如果 Person.age 存储在栈上,就不需要 ref,因为 age 的定义会从 person.age 复制数据而不是移动它。)

另请参阅:

解构

借用

大多数情况下,我们希望访问数据而不获取其所有权。为了实现这一点,Rust 使用了借用机制。对象可以通过引用(&T)传递,而不是按值(T)传递。

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

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

可变性

可变数据可以使用 &mut T 进行可变借用。这被称为可变引用,并给予借用者读写访问权。相比之下,&T 通过不可变引用借用数据,借用者可以读取数据但不能修改它:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

static

别名

数据可以被不可变借用任意次数,但在不可变借用期间,原始数据不能被可变借用。另一方面,同一时间只允许一个可变借用。只有在可变引用最后一次使用之后,原始数据才能再次被借用。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

ref 模式

在使用 let 绑定进行模式匹配或解构时,可以使用 ref 关键字来获取结构体或元组字段的引用。以下示例展示了几个这种用法有用的场景:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

生命周期

生命周期是编译器(更具体地说是其借用检查器)用来确保所有借用都有效的一种机制。具体来说,变量的生命周期从创建时开始,到销毁时结束。尽管生命周期和作用域经常被一同提及,但它们并不完全相同。

举个例子,当我们通过 & 借用一个变量时,这个借用的生命周期由其声明位置决定。因此,只要借用在出借者被销毁之前结束,它就是有效的。然而,借用的作用域则由引用使用的位置决定。

在接下来的例子和本节的其余部分中,我们将看到生命周期如何与作用域相关联,以及两者之间的区别。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

请注意,生命周期标签没有被赋予名称或类型。这限制了生命周期的使用方式,我们将在后面看到这一点。

显式注解

借用检查器使用显式生命周期注解来确定引用应该有效多长时间。在生命周期没有被省略1的情况下,Rust 需要显式注解来确定引用的生命周期。显式注解生命周期的语法使用撇号字符,如下所示:

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

类似于闭包,使用生命周期需要泛型。此外,这种生命周期语法表示 foo 的生命周期不能超过 'a 的生命周期。类型的显式注解形式为 &'a T,其中 'a 已经被引入。

在有多个生命周期的情况下,语法类似:

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

在这种情况下,foo 的生命周期不能超过 'a 'b 的生命周期。

请看下面的例子,展示了显式生命周期注解的使用:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
1

省略(elision)隐式地注解生命周期,因此与显式注解不同。

另请参阅:

泛型(generics)闭包(closures)

函数

排除省略(elision)的情况,带有生命周期的函数签名有以下几个约束:

  • 任何引用必须有一个标注的生命周期。
  • 任何被返回的引用必须具有与输入相同的生命周期或者是 static

此外,请注意,如果返回引用而没有输入会导致返回指向无效数据的引用,这是被禁止的。以下示例展示了一些带有生命周期的有效函数形式:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

函数

方法

方法的生命周期注解与函数类似:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

方法

结构体

结构体中生命周期的注解也与函数类似:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

struct(结构体)

特质

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

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

trait

约束

正如泛型类型可以被约束一样,生命周期(本身也是泛型)也可以使用约束。这里的 : 符号含义略有不同,但 + 的用法相同。请注意以下表达的含义:

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

下面的例子展示了上述语法在 where 关键字之后的实际应用:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

泛型泛型中的约束泛型中的多重约束

强制转换

一个较长的生命周期可以被强制转换为一个较短的生命周期,使其能在通常无法工作的作用域内工作。这种转换可以通过 Rust 编译器的推断自动完成,也可以通过声明不同的生命周期的形式实现:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

静态

Rust 有几个保留的生命周期名称。其中之一是 'static。你可能在两种情况下遇到它:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

这两种情况虽然相关但有微妙的区别,这也是学习 Rust 时常见的困惑来源。以下是每种情况的一些例子:

引用生命周期

作为引用生命周期,'static 表示该引用指向的数据在程序的整个剩余运行期间都有效。它仍然可以被强制转换为更短的生命周期。

有两种常见的方法可以创建具有 'static 生命周期的变量,它们都存储在二进制文件的只读内存中:

  • 使用 static 声明创建一个常量。
  • 创建一个字符串字面量,其类型为:&'static str

请看下面的例子,展示了这些方法:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Since 'static references only need to be valid for the remainder of a program's life, they can be created while the program is executed. Just to demonstrate, the below example uses Box::leak to dynamically create 'static references. In that case it definitely doesn't live for the entire duration, but only from the leaking point onward.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Trait 约束

作为 trait 约束时,它表示该类型不包含任何非静态引用。例如,接收者可以随意持有该类型,直到主动丢弃之前,它都不会变为无效。

理解这一点很重要:任何拥有所有权的数据总是满足 'static 生命周期约束,但对该数据的引用通常不满足:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

编译器会提示你:

error[E0597]: `i` does not live long enough --> src/lib.rs:15:15 | 15 | print_it(&i); | ---------^^-- | | | | | borrowed value does not live long enough | argument requires that `i` is borrowed for `'static` 16 | } | - `i` dropped here while still borrowed

另请参阅:

'static 常量

省略

有些生命周期模式非常常见,因此借用检查器允许省略它们以减少代码量并提高可读性。这被称为省略。Rust 中的省略存在的唯一原因是这些模式很常见。

以下代码展示了一些省略的例子。要更全面地了解省略,请参阅 Rust 程序设计语言中的生命周期省略章节。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

省略

特质

trait 是为未知类型 Self 定义的一组方法集合。这些方法可以访问同一 trait 中声明的其他方法。

trait 可以为任何数据类型实现。在下面的例子中,我们定义了 Animal,一组方法的集合。然后为 Sheep 数据类型实现 Animal trait,这样就可以对 Sheep 使用 Animal 中的方法。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

派生

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

以下是可派生的 trait 列表:

  • 比较 trait:EqPartialEqOrdPartialOrd
  • Clone,通过复制 &T 创建 T
  • Copy,使类型具备"复制语义"而不是"移动语义"。
  • Hash,从 &T 计算哈希值。
  • Default,创建一个数据类型的空实例。
  • Debug,使用 {:?} 格式化器来格式化一个值。
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

derive

使用 dyn 返回 trait

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

然而,有一个简单的解决方法。我们可以让函数返回一个包含 AnimalBox,而不是直接返回 trait 对象。Box 本质上是一个指向堆内存的引用。由于引用的大小是静态已知的,且编译器可以保证它指向堆上分配的 Animal,这样我们就能从函数中返回一个 trait 了!

Rust 在堆上分配内存时力求明确。因此,如果你的函数以这种方式返回一个指向堆上 trait 的指针,你需要在返回类型中使用 dyn 关键字,例如 Box<dyn Animal>

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

运算符重载

在 Rust 中,许多运算符可以通过 trait 进行重载。这意味着某些运算符可以根据输入参数执行不同的任务。之所以可能,是因为运算符实际上是方法调用的语法糖。例如,a + b 中的 + 运算符会调用 add 方法(相当于 a.add(b))。这个 add 方法是 Add trait 的一部分。因此,任何实现了 Add trait 的类型都可以使用 + 运算符。

可以在 core::ops 模块中找到用于重载运算符的 trait 列表,如 Add 等。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参见

Add语法索引

Drop

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

BoxVecStringFileProcess 是一些实现了 Drop trait 以释放资源的类型示例。你也可以为任何自定义数据类型手动实现 Drop trait。

下面的例子在 drop 函数中添加了一个控制台打印,用于宣告它被调用的时机。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

For a more practical example, here's how the Drop trait can be used to automatically clean up temporary files when they're no longer needed:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

迭代器

Iterator trait 用于实现对数组等集合的迭代器。

该 trait 只要求为 next 元素定义一个方法,这个方法可以在 impl 块中手动定义,也可以自动定义(如在数组和区间中)。

为了在常见情况下提供便利,for 结构使用 .into_iter() 方法将某些集合转换为迭代器。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

impl Trait

impl Trait 可以在两个位置使用:

  1. 作为参数类型
  2. 作为返回类型

作为参数类型

如果你的函数对某个 trait 是泛型的,但不关心具体类型,你可以使用 impl Trait 作为参数类型来简化函数声明。

例如,考虑以下代码:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

parse_csv_document 是泛型函数,可以接受任何实现了 BufRead 的类型,如 BufReader<File>[u8]。但具体的 R 类型并不重要,R 仅用于声明 src 的类型。因此,这个函数也可以写成:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

注意,使用 impl Trait 作为参数类型意味着你无法显式指定使用的函数形式。例如,parse_csv_document::<std::io::Empty>(std::io::empty()) 在第二个例子中将无法工作。

作为返回类型

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

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

更重要的是,某些 Rust 类型无法直接写出。例如,每个闭包都有自己的未命名具体类型。在 impl Trait 语法出现之前,你必须在堆上分配内存才能返回闭包。但现在你可以完全静态地做到这一点,像这样:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

你还可以使用 impl Trait 返回一个使用 mapfilter 闭包的迭代器!这使得使用 mapfilter 更加容易。由于闭包类型没有名称,如果你的函数返回带有闭包的迭代器,你无法写出显式的返回类型。但使用 impl Trait,你可以轻松实现:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

克隆

在处理资源时,默认行为是在赋值或函数调用期间转移它们。然而,有时我们也需要复制资源。

Clone trait 帮助我们实现这一点。最常见的是,我们可以使用 Clone trait 定义的 .clone() 方法。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

父特质

Rust 没有“继承”,但你可以将一个 trait 定义为另一个 trait 的超集。例如:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

《Rust 程序设计语言》中关于超 trait 的章节

消除重叠特质的歧义

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

好消息是:由于每个 trait 实现都有自己的 impl 块,因此很容易分清楚你正在实现哪个 trait 的 get 方法。

那么在调用这些方法时又该如何处理呢?为了消除它们之间的歧义,我们必须使用完全限定语法。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

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

macro_rules!

Rust 提供了一个强大的宏系统,支持元编程。正如你在前面章节中所看到的,宏看起来像函数,只是它们的名字以感叹号 ! 结尾。但与生成函数调用不同,宏会展开成源代码,然后与程序的其余部分一起编译。与 C 和其他语言中的宏不同,Rust 宏展开成抽象语法树,而不是进行字符串预处理,因此你不会遇到意外的优先级错误。

宏是使用 macro_rules! 宏来创建的。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

那么,为什么宏是有用的呢?

  1. 不要重复自己。在许多情况下,你可能需要在多个地方使用相似的功能,但类型不同。通常,编写宏是避免代码重复的有效方法。(稍后会详细介绍)

  2. 领域特定语言。宏允许你为特定目的定义专门的语法。(稍后会详细介绍)

  3. 可变参数接口。有时你可能想定义一个接受可变数量参数的接口。例如 println!,它可以根据格式字符串接受任意数量的参数。(稍后会详细介绍)

语法

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

指示符

宏的参数以美元符号 $ 为前缀,并用指示符来标注类型:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

以下是一些可用的指示符:

  • block
  • expr 用于表达式
  • ident 用于变量/函数名
  • item
  • literal 用于字面常量
  • pat模式 pattern
  • path
  • stmt语句 statement
  • tt标记树 token tree
  • ty类型 type
  • vis可见性限定符 visibility qualifier

完整列表请参阅 Rust 参考手册

重载

宏可以被重载以接受不同的参数组合。在这方面,macro_rules! 的工作方式类似于 match 块:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

重复

宏可以在参数列表中使用 + 来表示一个参数可能重复至少一次,或使用 * 来表示一个参数可能重复零次或多次。

在下面的例子中,用 $(...),+ 包围匹配器将匹配一个或多个由逗号分隔的表达式。另外请注意,最后一个情况的分号是可选的。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

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 代码,因为宏系统会将其展开为普通的 Rust 结构,但它看起来像一种小型语言。这使你能够为某些特定功能定义简洁或直观的语法(在一定范围内)。

假设我想定义一个简单的计算器 API。我希望提供一个表达式,并将计算结果打印到控制台。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

输出:

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

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

另外,注意宏中的两对大括号。外层的大括号是 macro_rules! 语法的一部分,除此之外还可以使用 ()[]

可变参数接口

可变参数接口可以接受任意数量的参数。例如,println! 可以接受任意数量的参数,这由格式字符串决定。

我们可以将上一节的 calculate! 宏扩展为可变参数的形式:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

输出:

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

错误处理

错误处理是处理可能出现失败情况的过程。例如,读取文件失败后继续使用那个错误的输入显然会导致问题。注意并明确管理这些错误可以使程序的其他部分避免各种陷阱。

Rust 中有多种处理错误的方法,这些方法将在接下来的小节中详细介绍。它们或多或少都有一些细微的差别和不同的使用场景。一般来说:

显式的 panic 主要用于测试和处理不可恢复的错误。在原型开发中,它可能会有用,例如在处理尚未实现的函数时。但在这些情况下,使用更具描述性的 unimplemented 会更好。在测试中,panic 是一种合理的显式失败方式。

Option 类型用于值是可选的情况,或者缺少值不构成错误条件的情况。例如目录的父目录 - /C: 就没有父目录。在处理 Option 时,对于原型设计和绝对确定有值的情况,使用 unwrap 是可以的。然而,expect 更有用,因为它允许你指定一个错误消息,以防万一出错。

当有可能出错且调用者必须处理问题时,请使用 Result。你也可以对它们使用 unwrapexpect(除非是测试或快速原型,否则请不要这样做)。

关于错误处理更详尽的讨论,请参阅官方文档中的错误处理章节。

panic

我们将看到的最简单的错误处理机制是 panic。它会打印一条错误消息,开始展开栈,并通常会退出程序。在这里,我们在错误条件下显式调用 panic

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

第一次调用 drink 正常执行。第二次调用会引发 panic,因此第三次调用永远不会被执行。

abortunwind

上一节介绍了错误处理机制 panic。可以根据 panic 设置有条件地编译不同的代码路径。当前可用的值有 unwindabort

基于之前的柠檬水示例,我们明确使用 panic 策略来执行不同的代码行。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

这里是另一个示例,重点是重写 drink() 函数并明确使用 unwind 关键字。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

可以通过命令行使用 abortunwind 来设置 panic 策略。

rustc lemonade.rs -C panic=abort

Optionunwrap

在上一个例子中,我们展示了如何主动引发程序失败。我们让程序在喝含糖柠檬水时触发 panic。但如果我们期望得到某种饮料却没有收到呢?这种情况同样糟糕,所以需要处理!

我们可以像处理柠檬水那样对空字符串("")进行测试。但既然我们使用的是 Rust,不如让编译器指出没有饮料的情况。

std 库中的 Option<T> 枚举用于处理可能存在缺失的情况。它表现为两个"选项"之一:

  • Some(T):找到了一个 T 类型的元素
  • None:没有找到元素

这些情况可以通过 match 显式处理,也可以用 unwrap 隐式处理。隐式处理要么返回内部元素,要么触发 panic

注意,可以使用 expect 手动自定义 panic,但 unwrap 相比显式处理会产生一个不太有意义的输出。在下面的例子中,显式处理产生了一个更可控的结果,同时保留了在需要时触发 panic 的选项。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

使用 ? 解包 Option

你可以使用 match 语句来解包 Option,但使用 ? 运算符通常更简便。如果 x 是一个 Option,那么求值 x? 将在 xSome 时返回其内部值,否则它将终止当前执行的函数并返回 None

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

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

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

组合器:map

match 是处理 Option 的有效方法。然而,频繁使用可能会让人感到繁琐,尤其是在只有输入时才有效的操作中。在这些情况下,可以使用组合器以模块化的方式管理控制流。

Option 有一个内置方法 map(),这是一个用于简单映射 Some -> SomeNone -> None 的组合器。多个 map() 调用可以链式使用,从而提供更大的灵活性。

在下面的例子中,process() 函数以简洁的方式替代了之前的所有函数。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

闭包OptionOption::map()

组合器:and_then

map() 被描述为一种可链式调用的方式来简化 match 语句。然而,在返回 Option<T> 的函数上使用 map() 会导致嵌套的 Option<Option<T>>。链式调用多个这样的函数可能会变得令人困惑。这时,另一个称为 and_then() 的组合器(在某些语言中称为 flatmap)就派上用场了。

and_then() 使用包装的值调用其函数输入并返回结果。如果 OptionNone,则直接返回 None

在下面的例子中,cookable_v3() 返回一个 Option<Food>。如果使用 map() 而不是 and_then(),将会得到一个 Option<Option<Food>>,这对于 eat() 函数来说是一个无效的类型。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

闭包OptionOption::and_then()Option::flatten()

解包 Option 和设置默认值

有多种方法可以解包 Option 并在其为 None 时使用默认值。为了选择满足我们需求的方法,我们需要考虑以下几点:

  • 我们需要立即求值还是惰性求值?
  • 我们需要保持原始的空值不变,还是就地修改它?

or() 可链式调用,立即求值,保持空值不变

or() 可以链式调用,并且会立即求值其参数,如下例所示。注意,由于 or 的参数是立即求值的,传递给 or 的变量会被移动。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

or_else() 可以链式调用,惰性求值,保持空值不变

另一种选择是使用 or_else,它同样支持链式调用,并且采用惰性求值。以下是一个示例:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

get_or_insert() 立即求值,原地修改空值

为确保 Option 包含一个值,我们可以使用 get_or_insert 来原地修改它,提供一个备选值。下面的例子展示了这一点。请注意,get_or_insert 会立即求值其参数,因此变量 apple 会被移动:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

get_or_insert_with() 惰性求值,原地修改空值

我们可以向 get_or_insert_with 传递一个闭包,而不是显式提供一个备选值。示例如下:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

闭包get_or_insertget_or_insert_with变量移动oror_else

Result

ResultOption 类型的增强版,它描述可能的错误而非可能的缺失

也就是说,Result<T, E> 可能有两种结果之一:

  • Ok(T):找到了一个 T 类型的元素
  • Err(E):发生了一个 E 类型的错误

按照惯例,预期的结果是 Ok,而意外的结果是 Err

Option 类似,Result 也有许多关联方法。例如,unwrap() 要么返回元素 T,要么触发 panic。对于情况处理,ResultOption 之间有许多重叠的组合子。

在使用 Rust 时,你可能会遇到返回 Result 类型的方法,比如 parse() 方法。将字符串解析为其他类型并非总是可行,因此 parse() 返回一个 Result 来表示可能的失败。

让我们看看成功和失败地 parse() 一个字符串会发生什么:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

在解析失败的情况下,parse() 会返回一个错误,导致 unwrap() 触发 panic。此外,panic 会终止程序并输出一条不友好的错误信息。

为了提高错误信息的质量,我们应该更明确地指定返回类型,并考虑显式地处理错误。

main 函数中使用 Result

如果显式指定,Result 类型也可以作为 main 函数的返回类型。通常,main 函数的形式如下:

fn main() { println!("Hello World!"); }

然而,main 函数也可以返回 Result 类型。如果 main 函数内发生错误,它将返回一个错误代码并打印该错误的调试表示(使用 Debug trait)。以下示例展示了这种情况,并涉及了下一节中讨论的内容。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Resultmap

在前面示例的 multiply 函数中使用 panic 并不能产生健壮的代码。通常,我们希望将错误返回给调用者,让它决定如何正确地处理错误。

首先,我们需要知道我们正在处理的错误类型。要确定 Err 类型,我们可以查看 parse() 方法,它是通过 FromStr trait 为 i32 实现的。因此,Err 类型被指定为 ParseIntError

在下面的示例中,直接使用 match 语句会导致整体代码更加繁琐。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

幸运的是,Optionmapand_then 以及许多其他组合器也为 Result 实现了。Result 文档中包含了完整的列表。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Result 的别名

如果我们想多次重用特定的 Result 类型,该怎么办?回想一下,Rust 允许我们创建别名。方便的是,我们可以为特定的 Result 定义一个别名。

在模块级别,创建别名特别有用。在特定模块中发现的错误通常具有相同的 Err 类型,因此单个别名可以简洁地定义所有相关的 Result。这非常实用,以至于 std 库甚至提供了一个:io::Result

这里有一个简单的例子来展示语法:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

io::Result

提前返回

在前面的例子中,我们使用组合器显式地处理了错误。处理这种情况分析的另一种方法是使用 match 语句和提前返回的组合。

也就是说,如果发生错误,我们可以简单地停止执行函数并返回错误。对某些人来说,这种形式的代码可能更容易阅读和编写。考虑使用提前返回重写的前面示例的这个版本:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

到目前为止,我们已经学会了使用组合器和提前返回来显式处理错误。虽然我们通常想避免 panic,但显式处理所有错误是很繁琐的。

在下一节中,我们将介绍 ? 运算符,用于我们只需要 unwrap 而不可能引发 panic 的情况。

引入 ?

有时我们只想要 unwrap 的简单性,而不希望有 panic 的可能。到目前为止,当我们真正想要的是获取变量时,unwrap 迫使我们不断地增加嵌套。这正是 ? 运算符的目的。

当遇到 Err 时,有两种可行的处理方式:

    1. 使用 panic!(我们已经决定尽可能避免这种方式)
    1. 使用 return(因为 Err 表示无法处理该错误)

? 运算符几乎1等同于在遇到 Err 时执行 return 而非 panicunwrap。让我们看看如何简化之前使用组合器的例子:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

try!

? 运算符出现之前,相同的功能是通过 try! 宏实现的。现在推荐使用 ? 运算符,但在查看旧代码时,你可能仍会遇到 try!。使用 try! 宏,前面例子中的 multiply 函数会是这样的:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
1

更多详情请参阅重新认识 ?

多种错误类型

前面的例子一直都很方便:ResultResult 交互,OptionOption 交互。

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

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

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

在接下来的章节中,我们将探讨几种处理此类问题的策略。

Option 中提取 Result

处理混合错误类型最基本的方法是将它们相互嵌套。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

有时我们希望在遇到错误时停止处理(例如使用 ?),但在 OptionNone 时继续执行。这时 transpose 函数就派上用场了,它可以方便地交换 ResultOption

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

定义错误类型

有时,用单一类型的错误来掩盖所有不同的错误可以简化代码。我们将通过自定义错误来演示这一点。

Rust 允许我们定义自己的错误类型。通常,一个"好的"错误类型应该:

  • 用同一类型表示不同的错误
  • 向用户展示友好的错误消息
  • 易于与其他类型进行比较
    • 好的示例:Err(EmptyVec)
    • 不好的示例:Err("请使用至少包含一个元素的向量".to_owned())
  • 能够保存错误的相关信息
    • 好的示例:Err(BadChar(c, position))
    • 不好的示例:Err("此处不能使用 +".to_owned())
  • 能够与其他错误很好地组合
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

使用 Box 将错误装箱

一种既能编写简洁代码又能保留原始错误信息的方法是使用 Box 将它们装箱。这种方法的缺点是底层错误类型只能在运行时确定,而不是静态确定的。

标准库通过让 Box 实现从任何实现了 Error trait 的类型到 trait 对象 Box<Error> 的转换来帮助我们装箱错误,这是通过 From 实现的。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

动态分发Error trait

? 的其他用途

注意在前面的例子中,我们对调用 parse 的直接反应是将库错误通过 map 转换为一个装箱的错误:

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

由于这是一个简单且常见的操作,如果能够省略就会很方便。可惜的是,由于 and_then 不够灵活,所以无法实现这一点。不过,我们可以使用 ? 来替代。

之前我们将 ? 解释为 unwrapreturn Err(err)。这只是大致正确。实际上,它的含义是 unwrapreturn Err(From::from(err))。由于 From::from 是不同类型之间的转换工具,这意味着如果你在错误可转换为返回类型的地方使用 ?,它将自动进行转换。

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

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

现在代码变得相当简洁了。与原来使用 panic 的版本相比,这种方法非常类似于用 ? 替换 unwrap 调用,只是返回类型变成了 Result。因此,需要在顶层对结果进行解构。

另请参阅:

From::from?运算符

包装错误

除了将错误装箱外,另一种方法是将它们包装在你自定义的错误类型中。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

这种方法增加了一些处理错误的样板代码,可能并非所有应用程序都需要。有一些库可以帮你处理这些样板代码。

另请参阅:

From::from枚举

处理错误的 crate

遍历 Result

Iter::map 操作可能会失败,例如:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

让我们逐步介绍处理这种情况的策略。

使用 filter_map() 忽略失败的项

filter_map 调用一个函数并过滤掉结果为 None 的项。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

使用 map_err()filter_map() 收集失败的项

map_err 会对错误调用一个函数,因此将其添加到之前的 filter_map 解决方案中,我们可以在迭代时将错误项保存到一旁。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

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

Result 实现了 FromIterator trait,因此结果的向量(Vec<Result<T, E>>)可以转换为包含向量的结果(Result<Vec<T>, E>)。一旦遇到 Result::Err,迭代就会终止。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

这种技巧同样适用于 Option

使用 partition() 收集所有有效值和失败项

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

当你查看结果时,你会注意到所有内容仍然被包装在 Result 中。这需要一些额外的样板代码。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

标准库类型

std 库提供了许多自定义类型,大大扩展了"原生类型"的功能。其中包括:

  • 可增长的 String,例如:"hello world"
  • 可增长的向量(vector):[1, 2, 3]
  • 可选类型:Option<i32>
  • 错误处理类型:Result<i32, i32>
  • 堆分配的指针:Box<i32>

另请参阅:

原生类型标准库

Box、栈和堆

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

可以使用 * 运算符解引用装箱的值,这会移除一层间接引用。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Vectors

向量(Vector)是可调整大小的数组。与切片(Slice)类似,它们的大小在编译时是未知的,但可以随时增长或缩小。向量由 3 个参数表示:

  • 指向数据的指针
  • 长度
  • 容量

容量表示为向量预留的内存量。只要长度小于容量,向量就可以增长。当需要超过这个阈值时,向量会被重新分配更大的容量。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

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

字符串

Rust 中最常用的两种字符串类型是 String&str

String 存储为字节向量(Vec<u8>),但保证始终是有效的 UTF-8 序列。String 在堆上分配,可增长,且不以 null 结尾。

&str 是一个切片(&[u8]),它始终指向一个有效的 UTF-8 序列,可以用来查看 String,就像 &[T]Vec<T> 的一个视图一样。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

更多 strString 的方法可以在 std::strstd::string 模块中找到

字面值和转义字符

有多种方式可以编写包含特殊字符的字符串字面值。所有方式都会产生类似的 &str,因此最好使用最方便编写的形式。同样,也有多种方式可以编写字节字符串字面值,它们都会产生 &[u8; N] 类型。

通常,特殊字符用反斜杠(\)进行转义。这样你可以在字符串中添加任何字符,包括不可打印的字符和你不知道如何输入的字符。如果你想要一个字面的反斜杠,用另一个反斜杠转义它:\\

出现在字面值内的字符串或字符字面值分隔符必须被转义:"\""'\''

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

有时需要转义的字符太多,或者直接按原样写出字符串会更方便。这时就可以使用原始字符串字面值。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

想要一个非 UTF-8 的字符串吗?(请记住,strString 必须是有效的 UTF-8)。或者你想要一个主要是文本的字节数组?字节字符串来帮忙!

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

对于字符编码之间的转换,请查看 encoding crate。

关于编写字符串字面值和转义字符的更详细说明,请参阅 Rust 参考手册的「标记」章节

Option

有时我们希望捕获程序某些部分的失败,而不是调用 panic!。这可以通过使用 Option 枚举来实现。

Option<T> 枚举有两个变体:

  • None:表示失败或缺少值,以及
  • Some(value):一个元组结构体,包装了类型为 Tvalue
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Result

我们已经看到 Option 枚举可以用作可能失败的函数的返回值,其中 None 用于表示失败。 然而,有时表达操作失败的原因很重要。为此,我们有 Result 枚举。

Result<T, E> 枚举有两个变体:

  • Ok(value):表示操作成功,并包装了操作返回的 value。(value 的类型为 T
  • Err(why):表示操作失败,并包装了 why,它(希望)解释了失败的原因。(why 的类型为 E
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

?

使用 match 链式处理结果可能会变得相当混乱;幸运的是,我们可以使用 ? 运算符来让代码变得整洁。? 运算符用在返回 Result 的表达式末尾,等效于一个 match 表达式。在这个表达式中,Err(err) 分支会展开为提前返回的 return Err(From::from(err)),而 Ok(ok) 分支则展开为 ok 表达式。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

请务必查阅 文档,其中包含了许多用于映射和组合 Result 的方法。

panic!

panic! 宏可用于生成一个 panic 并开始展开其栈。在展开过程中,运行时会通过调用该线程所有对象的析构函数来释放线程拥有的所有资源。

由于我们处理的是只有一个线程的程序,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 则通过键存储值。HashMap 的键可以是布尔值、整数、字符串,或任何其他实现了 EqHash trait 的类型。下一节将详细介绍这一点。

与向量类似,HashMap 也可以增长,但当有多余空间时,HashMap 还能自动收缩。你可以使用 HashMap::with_capacity(uint) 创建一个具有指定初始容量的 HashMap,或使用 HashMap::new() 来获得一个具有默认初始容量的 HashMap(推荐)。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

要了解更多关于哈希和哈希映射(有时称为哈希表)的工作原理,请参阅哈希表的维基百科页面

更改或自定义键类型

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

  • bool(虽然用处不大,因为只有两个可能的键值)
  • intuint 及其所有变体
  • String&str(专业提示:你可以使用 String 作为 HashMap 的键,并用 &str 调用 .get() 方法)

注意,f32f64 并未实现 Hash trait,这很可能是因为浮点数精度误差会导致将它们用作哈希映射的键时极易出错。

如果集合类中包含的类型分别实现了 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 的独特之处在于它保证不会有重复元素。这是所有集合类型都应满足的约定。HashSet 只是其中一种实现。(另请参阅:BTreeSet

如果你插入一个已存在于 HashSet 中的值(即新值等于现有值,且它们的哈希值相同),那么新值将替换旧值。

当你不希望某个元素出现多次,或者想知道是否已经拥有某个元素时,这非常有用。

但是集合的功能不仅限于此。

集合有 4 种主要操作(以下所有调用都返回一个迭代器):

  • 并集(union):获取两个集合中的所有唯一元素。

  • 差集(difference):获取存在于第一个集合但不在第二个集合中的所有元素。

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

  • 对称差(symmetric_difference):获取存在于其中一个集合中,但同时存在于两个集合中的所有元素。

在下面的例子中尝试这些操作:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

(示例改编自官方文档

Rc

当需要多重所有权时,可以使用 Rc(引用计数,Reference Counting)。Rc 会跟踪引用的数量,即包裹在 Rc 内部的值的所有者数量。

每当克隆一个 Rc 时,其引用计数就会增加 1;每当一个克隆的 Rc 离开作用域时,引用计数就会减少 1。当 Rc 的引用计数变为零(意味着没有剩余的所有者)时,Rc 及其包含的值都会被丢弃。

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

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

另请参阅:

std::rcstd::sync::arc

Arc

当需要在线程间共享所有权时,可以使用 Arc(原子引用计数,Atomically Reference Counted)。通过 Clone 实现,这个结构体可以为堆内存中值的位置创建一个引用指针,同时增加引用计数。由于它在线程间共享所有权,当指向某个值的最后一个引用指针超出作用域时,该变量就会被释放。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

标准库中的其他内容

标准库提供了许多其他类型来支持各种功能,例如:

  • 线程
  • 信道
  • 文件 I/O

这些类型扩展了基本类型所提供的功能。

另请参阅:

原生类型标准库

线程

Rust 通过 spawn 函数提供了一种生成原生操作系统线程的机制,该函数的参数是一个移动闭包。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

这些线程将由操作系统进行调度。

测试实例:map-reduce

Rust makes it very easy to parallelize data processing, without many of the headaches traditionally associated with such an attempt.

标准库提供了开箱即用的优秀线程原语。这些原语结合 Rust 的所有权概念和别名规则,自动防止了数据竞争。

The aliasing rules (one writable reference XOR many readable references) automatically prevent you from manipulating state that is visible to other threads. (Where synchronization is needed, there are synchronization primitives like Mutexes or Channels.)

在这个例子中,我们将计算一个数字块中所有数字的总和。我们通过将数字块分成小块并分配给不同的线程来完成这个任务。每个线程将计算其小块数字的总和,随后我们将汇总每个线程产生的中间结果。

注意,尽管我们在线程间传递引用,但 Rust 理解我们只是传递只读引用,因此不会发生不安全操作或数据竞争。此外,由于我们传递的引用具有 'static 生命周期,Rust 确保这些线程运行时数据不会被销毁。(当需要在线程间共享非 static 数据时,可以使用 Arc 等智能指针来保持数据存活并避免非 static 生命周期。)

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

练习

让线程数量依赖于用户输入的数据并不明智。如果用户决定插入大量空格,我们真的想要创建 2,000 个线程吗?修改程序,使数据始终被分割成固定数量的块,这个数量应由程序开头定义的静态常量来确定。

另请参阅:

信道

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

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

路径

Path 结构体表示底层文件系统中的文件路径。Path 有两种变体:用于类 UNIX 系统的 posix::Path 和用于 Windows 的 windows::Path。prelude 会导出适合特定平台的 Path 变体。

Path 可以从 OsStr 创建,并提供多种方法来获取路径所指向的文件或目录的信息。

Path 是不可变的。Path 的所有权版本是 PathBufPathPathBuf 之间的关系类似于 strStringPathBuf 可以原地修改,并且可以解引用为 Path

注意,Path 在内部并非表示为 UTF-8 字符串,而是存储为 OsString。因此,将 Path 转换为 &str 并非零开销操作,且可能失败(返回一个 Option)。然而,Path 可以自由地转换为 OsString&OsStr,分别使用 into_os_stringas_os_str 方法。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

请务必查看其他 Path 方法(posix::Pathwindows::Path)以及 Metadata 结构体。

另请参阅:

OsStrMetadata

文件 I/O

File 结构体表示一个已打开的文件(它封装了一个文件描述符),并提供对底层文件的读取和/或写入访问。

由于文件 I/O 操作可能会出现多种错误,所有 File 方法都返回 io::Result<T> 类型,这是 Result<T, io::Error> 的别名。

这使得所有 I/O 操作的失败都变得显式。得益于此,程序员可以看到所有可能的失败路径,并被鼓励主动处理这些情况。

open

open 函数可用于以只读模式打开文件。

File 拥有一个资源(即文件描述符),并在被 drop 时负责关闭文件。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

以下是预期的成功输出:

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

(建议您在不同的失败情况下测试上述示例:例如 hello.txt 不存在,或 hello.txt 不可读等。)

create

create 函数以只写模式打开文件。如果文件已存在,旧内容会被清除;否则,会创建一个新文件。

static LOREM_IPSUM: &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::fs::File; use std::io::prelude::*; use std::path::Path; fn main() { let path = Path::new("lorem_ipsum.txt"); let display = path.display(); // 以只写模式打开文件,返回 `io::Result<File>` let mut file = match File::create(&path) { Err(why) => panic!("无法创建 {}: {}", display, why), Ok(file) => file, }; // 将 `LOREM_IPSUM` 字符串写入 `file`,返回 `io::Result<()>` match file.write_all(LOREM_IPSUM.as_bytes()) { Err(why) => panic!("无法写入 {}: {}", display, why), Ok(_) => println!("成功写入 {}", display), } }

以下是预期的成功输出:

$ rustc create.rs && ./create successfully wrote to lorem_ipsum.txt $ cat 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.

(与前面的示例类似,建议您在失败情况下测试此示例。)

OpenOptions 结构体可用于配置文件的打开方式。

read_lines

一种简单的方法

对于初学者来说,这可能是从文件中读取行的一个合理的初步尝试。

#![allow(unused)] fn main() { use std::fs::read_to_string; fn read_lines(filename: &str) -> Vec<String> { let mut result = Vec::new(); for line in read_to_string(filename).unwrap().lines() { result.push(line.to_string()) } result } }

由于 lines() 方法返回文件中各行的迭代器,我们可以内联执行 map 并收集结果,从而得到一个更简洁流畅的表达式。

#![allow(unused)] fn main() { use std::fs::read_to_string; fn read_lines(filename: &str) -> Vec<String> { read_to_string(filename) .unwrap() // 遇到可能的文件读取错误时 panic .lines() // 将字符串分割成字符串切片的迭代器 .map(String::from) // 将每个切片转换为字符串 .collect() // 将它们收集到一个向量中 } }

注意,在上述两个示例中,我们都必须将 lines() 返回的 &str 引用转换为拥有所有权的 String 类型,分别使用 .to_string()String::from

一种更高效的方法

在这里,我们将打开的 File 的所有权传递给 BufReader 结构体。BufReader 使用内部缓冲区来减少中间分配。

我们还对 read_lines 函数进行了改进,使其返回一个迭代器,而不是为每行内容在内存中分配新的 String 对象。

use std::fs::File; use std::io::{self, BufRead}; use std::path::Path; fn main() { // 文件 hosts.txt 必须存在于当前路径下 if let Ok(lines) = read_lines("./hosts.txt") { // 消耗迭代器,返回一个(可选的)String for line in lines.map_while(Result::ok) { println!("{}", line); } } } // 输出被包装在 Result 中以便于错误匹配。 // 返回一个指向文件行读取器的迭代器。 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.txt $ rustc read_lines.rs && ./read_lines 127.0.0.1 192.168.0.1

(注意,由于 File::open 需要一个泛型 AsRef<Path> 作为参数,我们使用 where 关键字为 read_lines() 方法定义了相同的泛型约束。)

这种方法比在内存中创建包含整个文件内容的 String 更加高效。特别是在处理大文件时,后者可能会导致性能问题。

子进程

process::Output 结构体表示已完成子进程的输出,而 process::Command 结构体是一个进程构建器。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

(建议您尝试在上述示例中向 rustc 传递一个错误的标志)

管道

The std::process::Child struct represents a child process, and exposes the stdin, stdout and stderr handles for interaction with the underlying process via pipes.

use std::io::prelude::*; use std::process::{Command, Stdio}; static PANGRAM: &'static str = "the quick brown fox jumps over the lazy dog\n"; fn main() { // 启动 `wc` 命令 let mut cmd = if cfg!(target_family = "windows") { let mut cmd = Command::new("powershell"); cmd.arg("-Command").arg("$input | Measure-Object -Line -Word -Character"); cmd } else { Command::new("wc") }; let process = match cmd .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn() { Err(why) => panic!("无法启动 wc:{}", why), Ok(process) => process, }; // 向 `wc` 的 `stdin` 写入字符串。 // // `stdin` 的类型是 `Option<ChildStdin>`,但我们知道这个实例 // 必定存在,所以可以直接 `unwrap` 它。 match process.stdin.unwrap().write_all(PANGRAM.as_bytes()) { Err(why) => panic!("无法写入 wc 的标准输入:{}", why), Ok(_) => println!("已将 pangram 发送给 wc"), } // 由于 `stdin` 在上述调用后不再存活,它会被 `drop`, // 管道随之关闭。 // // 这一点非常重要,否则 `wc` 不会开始处理 // 我们刚刚发送的输入。 // `stdout` 字段的类型也是 `Option<ChildStdout>`,因此必须解包。 let mut s = String::new(); match process.stdout.unwrap().read_to_string(&mut s) { Err(why) => panic!("无法读取 wc 的标准输出:{}", why), Ok(_) => print!("wc 的响应为:\n{}", s), } }

等待

如果你想等待一个 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!("到达 main 函数末尾"); }
$ rustc wait.rs && ./wait # `wait` 会持续运行 5 秒,直到 `sleep 5` 命令执行完毕 reached end of main

文件系统操作

std::fs 模块包含多个用于处理文件系统的函数。

use std::fs; use std::fs::{File, OpenOptions}; use std::io; use std::io::prelude::*; #[cfg(target_family = "unix")] use std::os::unix; #[cfg(target_family = "windows")] use std::os::windows; 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::Result<()>` #[cfg(target_family = "unix")] { unix::fs::symlink("../b.txt", "a/c/b.txt").unwrap_or_else(|why| { println!("! {:?}", why.kind()); }); } #[cfg(target_family = "windows")] { windows::fs::symlink_file("../b.txt", "a/c/b.txt").unwrap_or_else(|why| { println!("! {:?}", why.to_string()); }); } 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 访问命令行参数,它返回一个迭代器,为每个参数生成一个 String

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
$ ./args 1 2 3 程序路径:./args 接收到 3 个参数:["1"、"2"、"3"]

Crates

此外,在开发命令行应用程序时,还有许多 crate 可以提供额外的功能。其中,clap 是一个广受欢迎的命令行参数处理 crate。

参数解析

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

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

如果你将程序命名为 match_args.rs 并使用 rustc match_args.rs 编译它,你可以按以下方式执行:

$ ./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 库交互的外部函数接口(FFI)。外部函数必须在 extern 块内声明,并使用 #[link] 属性标注外部库的名称。

use std::fmt; // 此 extern 块链接到 libm 库 #[cfg(target_family = "windows")] #[link(name = "msvcrt")] extern { // 这是一个外部函数 // 用于计算单精度复数的平方根 fn csqrtf(z: Complex) -> Complex; fn ccosf(z: Complex) -> Complex; } #[cfg(target_family = "unix")] #[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!("{:?} 的平方根是 {:?}", 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 还支持为测试指定额外的依赖项:

参见

单元测试

测试是 Rust 函数,用于验证非测试代码是否按预期方式运行。测试函数的主体通常包括一些准备工作,运行待测试的代码,然后断言结果是否符合预期。

大多数单元测试都放在带有 #[cfg(test)] 属性tests 模块中。测试函数用 #[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 { // 注意这个有用的惯用法:从外部作用域(对于 mod 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

测试与 ? 运算符

之前的单元测试示例都没有返回类型。但在 Rust 2018 版本中,你的单元测试可以返回 Result<()>,这使得你可以在测试中使用 ? 运算符!这可以使测试代码更加简洁。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

更多详情请参阅《版本指南》

测试 panic

要检查在某些情况下应该触发恐慌的函数,可以使用 #[should_panic] 属性。这个属性接受可选参数 expected = ,用于指定预期的恐慌消息文本。如果你的函数可能以多种方式触发 panic,这有助于确保你的测试正在检查正确的 panic 情况。

pub fn divide_non_zero_result(a: u32, b: u32) -> u32 { if b == 0 { panic!("除以零错误"); } else if a < b { panic!("除法结果为零"); } 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 = "除法结果为零")] 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 来运行这些被忽略的测试。

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 3 tests test tests::ignored_test ... ignored test tests::test_add ... ok test tests::test_add_hundred ... ok test result: ok. 2 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 项目的主要文档编写方式是通过在源代码中添加注释。文档注释使用 CommonMark Markdown 规范编写,并支持其中的代码块。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 } /// 文档注释通常包含"示例"、"异常"和"错误"等部分。 /// /// 下面的函数用于两数相除。 /// /// # 示例 /// /// ``` /// let result = doccomments::div(10, 2); /// assert_eq!(result, 5); /// ``` /// /// # 异常 /// /// 当第二个参数为零时,函数会触发异常。 /// /// ```rust,should_panic /// // 除以零会触发异常 /// doccomments::div(10, 0); /// ``` pub fn div(a: i32, b: i32) -> i32 { if b == 0 { panic!("除以零错误"); } 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 函数中 unwrap 它。听起来很复杂?这里有一个例子:

/// 在文档测试中使用隐藏的 `try_main` 函数。 /// /// ``` /// # // 以 `#` 开头的行在文档中是隐藏的,但它们仍然可以编译! /// # fn try_main() -> Result<(), String> { // 这行包装了文档中显示的函数体 /// let res = doccomments::try_div(10, 2)?; /// # Ok(()) // 从 try_main 返回 /// # } /// # fn main() { // 开始会调用 unwrap() 的 main 函数 /// # try_main().unwrap(); // 调用 try_main 并解包 /// # // 这样在出错时测试会触发 panic /// # } /// ``` pub fn try_div(a: i32, b: i32) -> Result<i32, String> { if b == 0 { Err(String::from("除以零错误")) } else { Ok(a / b) } }

参见

集成测试

单元测试每次只隔离测试一个模块:它们规模小,可以测试私有代码。集成测试则位于 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/mod.rs

pub fn setup() { // 一些设置代码,如创建必要的文件/目录,启动 // 服务器等。 }

测试文件:tests/integration_test.rs

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

将模块创建为 tests/common.rs 也可行,但不推荐,因为测试运行器会将该文件视为测试 crate 并尝试运行其中的测试。

开发依赖

有时我们需要仅用于测试(或示例、基准测试)的依赖项。这些依赖项添加在 Cargo.toml[dev-dependencies] 部分。这些依赖项不会传递给依赖于本包的其他包。

例如 pretty_assertions,它扩展了标准的 assert_eq!assert_ne! 宏,提供彩色差异对比。
文件 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; // 仅用于测试的 crate。不能在非测试代码中使用。 #[test] fn test_add() { assert_eq!(add(2, 3), 5); } }

参见

Cargo 文档中关于指定依赖项的说明。

不安全操作

作为本节的引言,借用官方文档的话说:"应该尽量减少代码库中不安全代码的数量。"牢记这一点,让我们开始吧!Rust 中的不安全标注用于绕过编译器设置的保护机制。具体来说,不安全主要用于以下四个方面:

  • 解引用裸指针
  • 调用被标记为 unsafe 的函数或方法(包括通过 FFI 调用函数,参见本书前面的章节
  • 访问或修改静态可变变量
  • 实现不安全特征

裸指针

裸指针 * 和引用 &T 的功能类似,但引用总是安全的,因为借用检查器保证它们指向有效数据。解引用裸指针只能在 unsafe 块中进行。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

调用不安全函数

某些函数可以被声明为 unsafe,这意味着确保其正确性是程序员的责任,而不是编译器的责任。一个例子是 std::slice::from_raw_parts,它根据指向第一个元素的指针和长度创建一个切片。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

对于 slice::from_raw_parts必须遵守的一个假设是:传入的指针指向有效内存,且指向的内存类型正确。如果这些不变量未被遵守,那么程序的行为将是未定义的,无法预知会发生什么。

内联汇编

Rust 通过 asm! 宏提供了内联汇编支持。它可以用于在编译器生成的汇编输出中嵌入手写的汇编代码。通常这不是必需的,但在无法通过其他方式实现所需性能或时序要求时可能会用到。访问底层硬件原语(例如在内核代码中)也可能需要这个功能。

注意:这里的示例使用 x86/x86-64 汇编,但也支持其他架构。

目前支持内联汇编的架构包括:

  • x86 和 x86-64
  • ARM
  • AArch64
  • RISC-V

基本用法

让我们从最简单的例子开始:

#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { use std::arch::asm; unsafe { asm!("nop"); } } }

这将在编译器生成的汇编代码中插入一条 NOP(无操作)指令。请注意,所有 asm! 调用都必须放在 unsafe 块内,因为它们可能插入任意指令并破坏各种不变量。要插入的指令以字符串字面量的形式列在 asm! 宏的第一个参数中。

输入和输出

插入一个什么都不做的指令相当无聊。让我们来做些实际操作数据的事情:

#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { use std::arch::asm; let x: u64; unsafe { asm!("mov {}, 5", out(reg) x); } assert_eq!(x, 5); } }

这将把值 5 写入 u64 类型的变量 x。你可以看到,我们用来指定指令的字符串字面量实际上是一个模板字符串。它遵循与 Rust 格式化字符串相同的规则。然而,插入到模板中的参数看起来可能与你熟悉的有些不同。首先,我们需要指定变量是内联汇编的输入还是输出。在这个例子中,它是一个输出。我们通过写 out 来声明这一点。我们还需要指定汇编期望变量在什么类型的寄存器中。这里我们通过指定 reg 将其放在任意通用寄存器中。编译器将选择一个合适的寄存器插入到模板中,并在内联汇编执行完成后从该寄存器读取变量的值。

让我们再看一个使用输入的例子:

#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { use std::arch::asm; let i: u64 = 3; let o: u64; unsafe { asm!( "mov {0}, {1}", "add {0}, 5", out(reg) o, in(reg) i, ); } assert_eq!(o, 8); } }

这段代码会将 5 加到变量 i 的值上,然后将结果写入变量 o。具体的汇编实现是先将 i 的值复制到输出寄存器,然后再加上 5

这个例子展示了几个要点:

asm! 宏支持多个模板字符串参数,每个参数都被视为独立的汇编代码行,就像它们之间用换行符连接一样。这使得格式化汇编代码变得简单。

其次,我们可以看到输入参数使用 in 声明,而不是 out

第三,我们可以像在任何格式字符串中一样指定参数编号或名称。这在内联汇编模板中特别有用,因为参数通常会被多次使用。对于更复杂的内联汇编,建议使用这种方式,因为它提高了可读性,并且允许在不改变参数顺序的情况下重新排列指令。

我们可以进一步优化上面的例子,避免使用 mov 指令:

#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { use std::arch::asm; let mut x: u64 = 3; unsafe { asm!("add {0}, 5", inout(reg) x); } assert_eq!(x, 8); } }

我们可以看到 inout 用于指定既作为输入又作为输出的参数。这与分别指定输入和输出不同,它保证将两者分配到同一个寄存器。

也可以为 inout 操作数的输入和输出部分指定不同的变量:

#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { use std::arch::asm; let x: u64 = 3; let y: u64; unsafe { asm!("add {0}, 5", inout(reg) x => y); } assert_eq!(y, 8); } }

延迟输出操作数

Rust 编译器在分配操作数时采取保守策略。它假设 out 可以在任何时候被写入,因此不能与其他参数共享位置。然而,为了保证最佳性能,使用尽可能少的寄存器很重要,这样就不必在内联汇编块前后保存和重新加载寄存器。为此,Rust 提供了 lateout 说明符。这可以用于任何在所有输入被消耗后才写入的输出。此外还有一个 inlateout 变体。

以下是一个在 release 模式或其他优化情况下 不能 使用 inlateout 的例子:

#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { use std::arch::asm; let mut a: u64 = 4; let b: u64 = 4; let c: u64 = 4; unsafe { asm!( "add {0}, {1}", "add {0}, {2}", inout(reg) a, in(reg) b, in(reg) c, ); } assert_eq!(a, 12); } }

在未优化的情况下(如 Debug 模式),将上述例子中的 inout(reg) a 替换为 inlateout(reg) a 仍能得到预期结果。但在 release 模式或其他优化情况下,使用 inlateout(reg) a 可能导致最终值 a = 16,使断言失败。

这是因为在优化情况下,编译器可以为输入 bc 分配相同的寄存器,因为它知道它们具有相同的值。此外,当使用 inlateout 时,ac 可能被分配到同一个寄存器,这种情况下,第一条 add 指令会覆盖从变量 c 初始加载的值。相比之下,使用 inout(reg) a 可以确保为 a 分配一个单独的寄存器。

然而,以下示例可以使用 inlateout,因为输出仅在读取所有输入寄存器后才被修改:

#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { use std::arch::asm; let mut a: u64 = 4; let b: u64 = 4; unsafe { asm!("add {0}, {1}", inlateout(reg) a, in(reg) b); } assert_eq!(a, 8); } }

如你所见,即使 ab 被分配到同一个寄存器,这段汇编代码片段仍能正确运行。

显式寄存器操作数

某些指令要求操作数必须位于特定寄存器中。因此,Rust 内联汇编提供了一些更具体的约束说明符。虽然 reg 通常适用于任何架构,但显式寄存器高度依赖于特定架构。例如,对于 x86 架构,通用寄存器如 eaxebxecxedxebpesiedi 等可以直接通过名称进行寻址。

#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { use std::arch::asm; let cmd = 0xd1; unsafe { asm!("out 0x64, eax", in("eax") cmd); } } }

在这个例子中,我们调用 out 指令将 cmd 变量的内容输出到端口 0x64。由于 out 指令只接受 eax(及其子寄存器)作为操作数,我们必须使用 eax 约束说明符。

注意:与其他操作数类型不同,显式寄存器操作数不能在模板字符串中使用。你不能使用 {},而应直接写入寄存器名称。此外,它们必须出现在操作数列表的末尾,位于所有其他操作数类型之后。

考虑以下使用 x86 mul 指令的例子:

#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { use std::arch::asm; fn mul(a: u64, b: u64) -> u128 { let lo: u64; let hi: u64; unsafe { asm!( // x86 的 mul 指令将 rax 作为隐式输入, // 并将乘法的 128 位结果写入 rax:rdx。 "mul {}", in(reg) a, inlateout("rax") b => lo, lateout("rdx") hi ); } ((hi as u128) << 64) + lo as u128 } } }

这里使用 mul 指令将两个 64 位输入相乘,得到一个 128 位的结果。唯一的显式操作数是一个寄存器,我们用变量 a 填充它。第二个操作数是隐式的,必须是 rax 寄存器,我们用变量 b 填充它。结果的低 64 位存储在 rax 中,用于填充变量 lo。高 64 位存储在 rdx 中,用于填充变量 hi

被破坏的寄存器

在许多情况下,内联汇编会修改不需要作为输出的状态。这通常是因为我们必须在汇编中使用临时寄存器,或者因为指令修改了我们不需要进一步检查的状态。这种状态通常被称为"被破坏"。我们需要告知编译器这一点,因为它可能需要在内联汇编块前后保存和恢复这种状态。

use std::arch::asm; #[cfg(target_arch = "x86_64")] fn main() { // 三个条目,每个四字节 let mut name_buf = [0_u8; 12]; // 字符串按顺序以 ASCII 格式存储在 ebx、edx、ecx 中 // 由于 ebx 是保留寄存器,汇编需要保留其值 // 因此我们在主要汇编代码前后执行 push 和 pop 操作 // 64 位处理器的 64 位模式不允许对 32 位寄存器(如 ebx)进行 push/pop 操作 // 所以我们必须使用扩展的 rbx 寄存器 unsafe { asm!( "push rbx", "cpuid", "mov [rdi], ebx", "mov [rdi + 4], edx", "mov [rdi + 8], ecx", "pop rbx", // 我们使用指向数组的指针来存储值,以简化 Rust 代码 // 虽然这会增加几条汇编指令,但更清晰地展示了汇编的工作方式 // 相比于使用显式寄存器输出(如 `out("ecx") val`) // *指针本身*只是一个输入,尽管它在背后被写入 in("rdi") name_buf.as_mut_ptr(), // 选择 cpuid 0,同时指定 eax 为被修改寄存器 inout("eax") 0 => _, // cpuid 也会修改这些寄存器 out("ecx") _, out("edx") _, ); } let name = core::str::from_utf8(&name_buf).unwrap(); println!("CPU 制造商 ID:{}", name); } #[cfg(not(target_arch = "x86_64"))] fn main() {}

在上面的示例中,我们使用 cpuid 指令读取 CPU 制造商 ID。该指令将最大支持的 cpuid 参数写入 eax,并按顺序将 CPU 制造商 ID 的 ASCII 字节写入 ebxedxecx

尽管 eax 从未被读取,我们仍需要告知编译器该寄存器已被修改,这样编译器就可以保存汇编前这些寄存器中的任何值。我们通过将其声明为输出来实现这一点,但使用 _ 而非变量名,表示输出值将被丢弃。

这段代码还解决了 LLVM 将 ebx 视为保留寄存器的限制。这意味着 LLVM 假定它对该寄存器拥有完全控制权,并且必须在退出汇编块之前将其恢复到原始状态。因此,ebx 不能用作输入或输出,除非编译器将其用于满足通用寄存器类(如 in(reg))。这使得在使用保留寄存器时,reg 操作数变得危险,因为我们可能会在不知情的情况下破坏输入或输出,原因是它们共享同一个寄存器。

为了解决这个问题,我们采用以下策略:使用 rdi 存储输出数组的指针;通过 push 保存 ebx;在汇编块内从 ebx 读取数据到数组中;然后通过 popebx 恢复到原始状态。pushpop 操作使用完整的 64 位 rbx 寄存器版本,以确保整个寄存器被保存。在 32 位目标上,代码会在 push/pop 操作中使用 ebx

这种技术还可以与通用寄存器类一起使用,以获得一个临时寄存器在汇编代码内使用:

#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { use std::arch::asm; // 使用移位和加法将 x 乘以 6 let mut x: u64 = 4; unsafe { asm!( "mov {tmp}, {x}", "shl {tmp}, 1", "shl {x}, 2", "add {x}, {tmp}", x = inout(reg) x, tmp = out(reg) _, ); } assert_eq!(x, 4 * 6); } }

符号操作数和 ABI 破坏

默认情况下,asm! 假定汇编代码会保留所有未指定为输出的寄存器的内容。asm!clobber_abi 参数告诉编译器根据给定的调用约定 ABI 自动插入必要的破坏操作数:任何在该 ABI 中未完全保留的寄存器都将被视为被破坏。可以提供多个 clobber_abi 参数,所有指定 ABI 的破坏都将被插入。

#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { use std::arch::asm; extern "C" fn foo(arg: i32) -> i32 { println!("arg = {}", arg); arg * 2 } fn call_foo(arg: i32) -> i32 { unsafe { let result; asm!( "call {}", // 要调用的函数指针 in(reg) foo, // 第一个参数在 rdi 中 in("rdi") arg, // 返回值在 rax 中 out("rax") result, // 将所有不被 "C" 调用约定保留的寄存器 // 标记为被破坏 clobber_abi("C"), ); result } } } }

寄存器模板修饰符

在某些情况下,需要对寄存器名称插入模板字符串时的格式进行精细控制。当一个架构的汇编语言对同一个寄存器有多个名称时,这种控制尤为必要。每个名称通常代表寄存器的一个子集"视图"(例如,64 位寄存器的低 32 位)。

默认情况下,编译器总是会选择引用完整寄存器大小的名称(例如,在 x86-64 上是 rax,在 x86 上是 eax 等)。

可以通过在模板字符串操作数上使用修饰符来覆盖这个默认设置,类似于格式字符串的用法:

#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { use std::arch::asm; let mut x: u16 = 0xab; unsafe { asm!("mov {0:h}, {0:l}", inout(reg_abcd) x); } assert_eq!(x, 0xabab); } }

在这个例子中,我们使用 reg_abcd 寄存器类来限制寄存器分配器只使用 4 个传统的 x86 寄存器(axbxcxdx)。这些寄存器的前两个字节可以独立寻址。

假设寄存器分配器选择将 x 分配到 ax 寄存器。h 修饰符将生成该寄存器高字节的名称,而 l 修饰符将生成低字节的名称。因此,汇编代码将被展开为 mov ah, al,这条指令将值的低字节复制到高字节。

如果你对操作数使用较小的数据类型(例如 u16)并忘记使用模板修饰符,编译器将发出警告并建议使用正确的修饰符。

内存地址操作数

有时汇编指令需要通过内存地址或内存位置传递操作数。你必须手动使用目标架构指定的内存地址语法。例如,在使用 Intel 汇编语法的 x86/x86_64 架构上,你应该用 [] 包裹输入/输出,以表明它们是内存操作数:

#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { use std::arch::asm; fn load_fpu_control_word(control: u16) { unsafe { asm!("fldcw [{}]", in(reg) &control, options(nostack)); } } } }

标签

重复使用命名标签(无论是局部的还是其他类型的)可能导致汇编器或链接器错误,或引起其他异常行为。命名标签的重用可能以多种方式发生,包括:

  • 显式重用:在一个 asm! 块中多次使用同一标签,或在多个块之间重复使用。
  • 通过内联隐式重用:编译器可能会创建 asm! 块的多个副本,例如当包含该块的函数在多处被内联时。
  • 通过 LTO 隐式重用:链接时优化(LTO)可能导致其他 crate 的代码被放置在同一代码生成单元中,从而可能引入任意标签。

因此,你应该只在内联汇编代码中使用 GNU 汇编器的数字局部标签。在汇编代码中定义符号可能会由于重复的符号定义而导致汇编器和/或链接器错误。

此外,在 x86 架构上使用默认的 Intel 语法时,由于一个 LLVM 的 bug,你不应使用仅由 01 组成的标签,如 011101010,因为它们可能被误解为二进制值。使用 options(att_syntax) 可以避免这种歧义,但这会影响_整个_ asm! 块的语法。(关于 options 的更多信息,请参见下文的选项。)

#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { use std::arch::asm; let mut a = 0; unsafe { asm!( "mov {0}, 10", "2:", "sub {0}, 1", "cmp {0}, 3", "jle 2f", "jmp 2b", "2:", "add {0}, 2", out(reg) a ); } assert_eq!(a, 5); } }

这段代码会将 {0} 寄存器的值从 10 递减到 3,然后加 2 并将结果存储在 a 中。

这个例子展示了几个要点:

  • 首先,同一个数字可以在同一个内联块中多次用作标签。
  • Second, that when a numeric label is used as a reference (as an instruction operand, for example), the suffixes “b” (“backward”) or ”f” (“forward”) should be added to the numeric label. It will then refer to the nearest label defined by this number in this direction.

选项

默认情况下,内联汇编块的处理方式与具有自定义调用约定的外部 FFI 函数调用相同:它可能读写内存,产生可观察的副作用等。然而,在许多情况下,我们希望向编译器提供更多关于汇编代码实际行为的信息,以便编译器能够进行更好的优化。

让我们回顾一下之前 add 指令的例子:

#![allow(unused)] fn main() { #[cfg(target_arch = "x86_64")] { use std::arch::asm; let mut a: u64 = 4; let b: u64 = 4; unsafe { asm!( "add {0}, {1}", inlateout(reg) a, in(reg) b, options(pure, nomem, nostack), ); } assert_eq!(a, 8); } }

可以将选项作为可选的最后一个参数传递给 asm! 宏。在这个例子中,我们指定了三个选项:

  • pure:表示汇编代码没有可观察的副作用,其输出仅依赖于输入。这使得编译器优化器能够减少内联汇编的调用次数,甚至完全消除它。
  • nomem:表示汇编代码不读取或写入内存。默认情况下,编译器会假设内联汇编可以读写任何它可访问的内存地址(例如通过作为操作数传递的指针或全局变量)。
  • nostack:表示汇编代码不会向栈中压入任何数据。这允许编译器使用诸如 x86-64 上的栈红区等优化技术,以避免栈指针调整。

这些选项使编译器能够更好地优化使用 asm! 的代码,例如消除那些输出未被使用的纯 asm! 块。

有关可用选项的完整列表及其效果,请参阅参考文档

兼容性

The Rust language is evolving rapidly, and because of this certain compatibility issues can arise, despite efforts to ensure forwards-compatibility wherever possible.

原始标识符

Rust 和许多编程语言一样,有"关键字"的概念。这些标识符在语言中具有特殊含义,因此你不能在变量名、函数名等地方使用它们。原始标识符允许你在通常不允许使用关键字的地方使用它们。这在 Rust 引入新关键字,而使用旧版本 Rust 的库中有与新版本引入的关键字同名的变量或函数时特别有用。

例如,假设有一个使用 Rust 2015 版编译的 crate foo,它导出了一个名为 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 为用户生成库文档。
  • Playground:在文档中集成 Rust Playground。

文档

使用 cargo doctarget/doc 目录下构建文档。运行 cargo doc --open 将自动在浏览器中打开文档。

使用 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 的文档 pub mod bar { /// Bar 的文档 pub struct Bar; }

no_inline

用于防止链接到单独页面或其他任何地方。

// libcore/prelude 中的示例 #[doc(no_inline)] pub use crate::mem::drop;

hidden

使用此属性告诉 rustdoc 不要在文档中包含此内容:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

在文档生成方面,rustdoc 被社区广泛使用。它是用来生成 标准库文档 的工具。

另请参阅:

Playground

Rust Playground 是一个通过网页界面体验 Rust 代码的平台。

mdbook 中使用 Playground

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

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

这不仅允许读者运行你的代码示例,还能修改和调整它。关键是在代码块标记中添加 editable 关键字,用逗号分隔。

```rust,editable //...place your code here ```

此外,如果你希望 mdbook 在构建和测试时跳过某段代码,可以添加 ignore 关键字。

```rust,editable,ignore //...place your code here ```

在文档中使用 Playground

你可能注意到在一些官方 Rust 文档中有一个"运行"按钮,点击后会在 Rust Playground 的新标签页中打开代码示例。要启用此功能,需要使用 #[doc] 属性中的 html_playground_url

#![doc(html_playground_url = "https://play.rust-lang.org/")] //! ``` //! println!("Hello World"); //! ```

另请参阅: