Rust 有许多功能可以让你管理代码的组织,包括哪些细节可以被公开,哪些细节作为私有部分,以及程序中各个作用域中有哪些名称。这些特性,有时被统称为 “模块系统(the module system)”,包括:
- 包(Packages):Cargo 的一个功能,它允许你构建、测试和分享 crate。
- Crates:一个模块树,可以产生一个库或可执行文件。
- 模块(Modules)和 use:允许你控制作用域和路径的私有性。
- 路径(path):一个为例如结构体、函数或模块等项命名的方式。
包和 Crate
crate 是 Rust 编译器每次处理的最小代码单位。即使你用 rustc 而不是 cargo 来编译单个源代码文件,编译器也会把那个文件视为一个 crate。crate 可以包含模块,而这些模块也可以定义在其他文件中,并与该 crate 一起编译
crate 有两种形式:二进制 crate 和库 crate。
二进制 crate(Binary crates)可以被编译为可执行程序,比如命令行程序或者服务端。它们必须有一个名为 main 函数来定义当程序被执行的时候所需要做的事情。目前我们所创建的 crate 都是二进制 crate。
库 crate(Library crates)并没有 main 函数,它们也不会编译为可执行程序。相反它们定义了可供多个项目复用的功能模块。
crate root 是一个源文件,Rust 编译器以它为起始点,并构成你的 crate 的根模块
包 (package)是提供一系列功能的一个或者多个 crate 的捆绑。一个包会包含一个 Cargo.toml 文件,阐述如何去构建这些 crate。Cargo 实际上就是一个包,它包含了用于构建你代码的命令行工具的二进制 crate。其他项目也依赖 Cargo 库来实现与 Cargo 命令行程序一样的逻辑。
包中可以包含至多一个库 crate(library crate)。包中可以包含任意多个二进制 crate(binary crate),但是必须至少包含一个 crate(无论是库的还是二进制的)。
(Cheat Sheet)
的绝佳参考。
从 crate 根节点开始: 当编译一个 crate, 编译器首先在 crate 根文件(通常,对于一个库 crate 而言是 src/lib.rs,对于一个二进制 crate 而言是 src/main.rs)中寻找需要被编译的代码。
声明模块
: 在 crate 根文件中,你可以声明一个新模块;比如,用
mod garden;声明了一个叫做garden的模块。编译器会在下列路径中寻找模块代码:- 内联,用大括号替换
mod garden后跟的分号 - 在文件
src/garden.rs - 在文件
src/garden/mod.rs(较旧的风格,但仍然受支持)
- 内联,用大括号替换
声明子模块
: 在除了 crate 根节点以外的任何文件中,你可以定义子模块。比如,你可能在
src/garden.rs中声明mod vegetables;编译器会在以父模块命名的目录中寻找子模块代码:- 内联,直接在
mod vegetables后方不是一个分号而是一个大括号 - 在文件
src/garden/vegetables.rs
rustsrc/ garden.rs # 主模块 garden/ # 子模块 vegetables.rs xxx.rs- 在文件
src/garden/vegetables/mod.rs(较旧的风格,但仍然受支持)
- 内联,直接在
模块中的代码路径: 一旦一个模块是你 crate 的一部分,你可以在隐私规则允许的前提下,从同一个 crate 内的任意地方,通过代码路径引用该模块的代码。举例而言,一个 garden vegetables 模块下的
Asparagus类型可以通过crate::garden::vegetables::Asparagus访问。私有 vs 公用: 一个模块里的代码默认对其父模块私有。为了使一个模块公用,应当在声明时使用
pub mod替代mod。为了使一个公用模块内部的成员公用,应当在声明前使用pub。use关键字: 在一个作用域内,use关键字创建了一个项的快捷方式,用来减少长路径的重复。在任何可以引用crate::garden::vegetables::Asparagus的作用域,你可以通过use crate::garden::vegetables::Asparagus;创建一个快捷方式,然后你就可以在作用域中只写Asparagus来使用该类型。
如果一个模块 A 被包含在模块 B 中,我们将模块 A 称为模块 B 的 子(child)模块,模块 B 则是模块 A 的 父(parent)模块。整个模块树都植根于名为 crate 的隐式模块下。
引用模块树中项的路径
路径有两种形式:
- 绝对路径(absolute path)是以 crate 根(root)开头的完整路径;对于外部 crate 的代码,是以 crate 名开头的绝对路径,对于当前 crate 的代码,则以字面值
crate开头。 - 相对路径(relative path)从当前模块开始,以
self、super或当前模块中的某个标识符开头。
绝对路径和相对路径都后跟一个或多个由双冒号(::)分割的标识符。
super 开始的相对路径
我们可以通过在路径的开头使用 super ,从父模块开始构建相对路径,而不是从当前模块或者 crate 根开始。这类似以 .. 语法开始一个文件系统路径。
fn deliver_order() {}
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::deliver_order();
}
fn cook_order() {}
}创建公有的结构体和枚举
一个结构体定义的前面使用了 pub,这个结构体会变成公有的,但是这个结构体的字段仍然是私有的。可以根据情况决定每个字段是否公有。
mod back_of_house {
pub struct Breakfast {
pub toast: String,
seasonal_fruit: String,
}
impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}
pub fn eat_at_restaurant() {
// 在夏天订购一个黑麦土司作为早餐
let mut meal = back_of_house::Breakfast::summer("Rye");
// 改变主意更换想要面包的类型
meal.toast = String::from("Wheat");
println!("I'd like {} toast please", meal.toast);
// 如果取消下一行的注释代码不能编译;
// 不允许查看或修改早餐附带的季节水果
// meal.seasonal_fruit = String::from("blueberries");
}use 关键字将路径引入作用域
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting::add_to_waitlist;
pub fn eat_at_restaurant() {
add_to_waitlist();
}使用 as 关键字提供新的名称
使用 use 将两个同名类型引入同一作用域这个问题还有另一个解决办法:在这个类型的路径后面,我们使用 as 指定一个新的本地名称或者别名。
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
// --snip--
}
fn function2() -> IoResult<()> {
// --snip--
}使用 pub use 重导出名称
重导出(re-exporting),因为在把某个项目导入当前作用域的同时,也将其暴露给其他作用域。
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}使用外部包
文件名:Cargo.toml
Cargo.toml 中加入 rand 依赖告诉了 Cargo 要从 crates.io 下载 rand 和其依赖,并使其可在项目代码中使用。
rand = "0.9.3"使用:
use rand::Rng;
fn main() {
let secret_number = rand::thread_rng().gen_range(1..=100);
}使用嵌套路径将相同的项在一行中引入作用域
use std::cmp::Ordering;
use std::io;
// to
use std::{cmp::Ordering, io};
use std::io;
use std::io::Write;
// to
use std::io::{self, Write};glob 运算符导入项
如果希望将一个路径下所有公有项引入作用域,可以指定路径后跟 * glob 运算符:
use std::collections::*;集合
序中非常常用的三种集合:
- 向量(vector)允许你把数量可变的值一个挨一个地存放起来。
- 字符串(string)是字符的集合。此前我们已经提到过
String类型,不过本章会更深入地讨论它。 - 哈希映射(hash map)允许你把某个值与特定的键关联起来。它是更通用的数据结构 map 的一种具体实现。
vector
vector 会把值彼此相邻地存放在内存中,所以如果末尾追加一个新元素,而当前存放位置又没有足够空间容纳所有元素,程序就可能需要分配一块新内存,并把旧元素复制到新空间里去,所以对于原先元素的引用需要在添加元素之前
// 创建一个新的空 vector
let v: Vec<i32> = Vec::new();
// 用初始值创建 Vec<T>,而 Rust 会推断出你想存储的值的类型
let v = vec![1, 2, 3];
// 更新
v.push(5);
// 读取 vector 的元素
// [] 方法会让程序 panic
let third: &i32 = &v[2];
// get 方法的索引超出了 vector 的范围时,它不会 panic,而是返回 None
let third: Option<&i32> = v.get(2);
match third {
Some(third) => println!("The third element is {third}"),
None => println!("There is no third element."),
}遍历 vector 中的元素
let v = vec![100, 32, 57];
for i in &v {
println!("{i}");
}
// 可变引用遍历
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}字符串
很多 Vec<T> 上可用的操作在 String 中同样可用,Rust 的核心语言中只有一种字符串类型,字符串 slice str,它通常以被借用的形式出现,&str。它们是一些对储存在别处的 UTF-8 编码字符串数据的引用。
字符串(String)类型由 Rust 标准库提供,而不是编入核心语言,它是一种可增长、可变、可拥有、UTF-8 编码的字符串类型。
// 新建字符串
let mut s = String::new();
let s = "initial contents".to_string();
// push_str 方法向 String 附加字符串 slice
let mut s1 = String::from("foo");
let s2 = "bar";
s1.push_str(s2);
// 使用 push 将一个字符加入 String 值中
let mut s = String::from("lo");
s.push('l');
// 使用 + 运算符拼接字符串
et s1 = String::from("Hello, ");
let s2 = String::from("world!");
// 注意 s1 被移动了,不能继续使用, S3 获取 S1 所有权
let s3 = s1 + &s2;
// 使用 format! 宏拼接字符串,宏 format! 生成的代码使用引用因此不会获取任何参数的所有权。
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{s1}-{s2}-{s3}");索引字符串
Rust 不允许使用索引获取 String 字符
字符串存储字节不同
索引操作预期总是需要常数时间(O(1))。但是对于
String不可能保证这样的性能,因为 Rust 必须从开头到索引位置遍历来确定有多少有效的字符。
字符串 slice
s 会是一个 &str,它包含字符串的头四个字节。早些时候,我们提到了这些字母都是两个字节长的,所以这意味着 s 将会是 Зд。
如果尝试用类似 &hello[0..1] 的方式对字符的部分字节进行 slice,Rust 会在运行时 panic,就跟访问 vector 中的无效索引时一样:
let hello = "Здравствуйте";
let s = &hello[0..4];遍历字符串
for c in "Зд".chars() {
println!("{c}");
}
// З д
for b in "Зд".bytes() {
println!("{b}");
}
/*
208
151
208
180
*/Hash Map 储存键值对
// 新建
use std::collections::HashMap;
let mut scores = HashMap::new();
// 插入
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
// 获取
let team_name = String::from("Blue");
let score = scores.get(&team_name).copied().unwrap_or(0);
// 遍历
for (key, value) in &scores {
println!("{key}: {value}");
}get 方法返回 Option<&V>,如果某个键在哈希 map 中没有对应的值,get 会返回 None。程序中通过调用 copied 方法来获取一个 Option<i32> 而不是 Option<&i32>,接着调用 unwrap_or 在 scores 中没有该键所对应的项时将其设置为零。
更新哈希 map
覆盖一个值
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);只在键尚不存在时插入键值对
entry,它接收你想检查的键作为参数。entry 方法的返回值是一个名为 Entry 的枚举,它表示一个可能存在、也可能不存在的值
scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);错误处理
Rust 将错误分成两大类:可恢复的(recoverable)和不可恢复的(unrecoverable)错误。对于可恢复错误,例如“文件未找到”这样的错误,我们多半只想把问题报告给用户,然后重试这次操作。不可恢复错误则总是 bug 的征兆,比如试图访问数组末尾之外的位置,因此我们希望立刻停止程序。
使用 Result<T, E> 类型来处理可恢复错误,使用 panic! 宏在程序遇到不可恢复错误时停止执行。
用 panic! 处理不可恢复的错误
fn main() {
panic!("crash and burn");
}当出现 panic 时,程序默认会开始 展开(unwinding),这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。另一种选择是直接 终止(abort),这会不清理数据就退出程序。
那么程序所使用的内存需要由操作系统来清理。如果你需要项目的最终二进制文件越小越好,panic 时通过在 Cargo.toml 的 [profile] 部分增加 panic = 'abort',可以由展开切换为终止。例如,如果你想要在 release 模式中 panic 时直接终止,可添加:
[profile.release]
panic = 'abort'尝试访问超越 vector 结尾的元素,会造成 panic!
$ cargo run Compiling panic v0.1.0 (file:///projects/panic) Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s Running `target/debug/panic` thread 'main' panicked at src/main.rs:4:6: index out of bounds: the len is 3 but the index is 99 note: run with `RUST_BACKTRACE=1` environment variable to display a backtracenote: 这一行说可以设置 RUST_BACKTRACE 环境变量来得到 backtrace。backtrace 是一份到达当前执行点之前所有被调用函数的列表。
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace把 RUST_BACKTRACE 环境变量设成除 0 之外的任意值
$ RUST_BACKTRACE=1 cargo run
thread 'main' panicked at src/main.rs:4:6:
index out of bounds: the len is 3 but the index is 99
stack backtrace:
...
6: panic::main
at ./src/main.rs:4:6
7: core::ops::function::FnOnce::call_once
at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.用 Result 处理可恢复的错误
打开文件
对不同的错误原因采取不同的行为:如果 File::open 因为文件不存在而失败,我们希望创建这个文件并返回新文件的句柄。如果 File::open 因为任何其他原因失败 – 例如没有打开文件的权限则 panic!
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {e:?}"),
},
_ => {
panic!("Problem opening the file: {error:?}");
}
},
};
}使用闭包(closure)处理
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Problem creating the file: {error:?}");
})
} else {
panic!("Problem opening the file: {error:?}");
}
});
}失败时 panic 的快捷方式
use std::fs::File;
// 使用默认的 panic! 信息
fn main() {
let greeting_file = File::open("hello.txt").unwrap();
}
// expect 在调用 panic! 时使用的错误信息是传递给 expect 的参数
fn main() {
let greeting_file = File::open("hello.txt")
.expect("hello.txt should be included in this project");传播错误
当函数的实现中调用了可能会失败的操作时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。这被称为传播(propagating)错误
函数使用 match 将错误返回给代码调用者
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let username_file_result = File::open("hello.txt");
let mut username_file = match username_file_result {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut username = String::new();
match username_file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(e) => Err(e),
}
}? 运算符快捷方式
如果 Result 的值是 Ok,这个表达式就会返回 Ok 中的值,程序继续执行。如果值是 Err,Err 就会像使用了 return 关键字一样,作为整个函数的返回值提前返回,这样错误值就被传播给了调用者。
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username_file = File::open("hello.txt")?;
let mut username = String::new();
username_file.read_to_string(&mut username)?;
Ok(username)
}match 表达式和 ? 运算符还有一点不同:被 ? 作用的错误值会经过 from 函数。这个函数定义在标准库的 From trait 中,用于把一种类型的值转换成另一种类型。当 ? 运算符调用 from 函数时,接收到的错误类型会被转换成当前函数返回类型里定义的错误类型。当一个函数用单一错误类型来表示它所有可能的失败方式时,这会非常有用,即使函数内部的不同部分可能会因为很多不同的原因而失败。
可以在 ? 之后直接使用链式方法调用来进一步简化代码
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username = String::new();
File::open("hello.txt")?.read_to_string(&mut username)?;
Ok(username)
}泛型
方法定义中的泛型
必须在 impl 后面声明 T,这样就可以在 Point<T> 上实现的方法中使用 T 了。通过在 impl 之后声明泛型 T,Rust 就知道 Point 的尖括号中的类型是泛型而不是具体类型。
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("p.x = {}", p.x());
}定义方法时也可以为泛型指定限制(constraint)
Point<f32> 类型会有一个方法 distance_from_origin,而其他 T 不是 f32 类型的 Point<T> 实例则没有定义此方法。
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}Rust 在编译时对泛型代码进行单态化(monomorphization)。单态化就是把泛型代码转换成具体代码的过程,方法是用编译时实际用到的具体类型去填充泛型代码。
Trait
trait 定义了某个特定类型拥有可能与其他类型共享的功能。可以通过 trait 以一种抽象的方式定义共同行为。
定义 trait
pub trait Summary {
fn summarize(&self) -> String;
}为类型实现 trait
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}trait 必须和类型一起引入作用域以便使用额外的 trait 方法。
use aggregator::{SocialPost, Summary};
fn main() {
let post = SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
repost: false,
};
println!("1 new post: {}", post.summarize());
}使用默认实现
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}默认实现允许调用相同 trait 中的其他方法,哪怕这些方法没有默认实现。
一旦定义了 summarize_author,我们就可以对 SocialPost 结构体的实例调用 summarize 了,而 summarize 的默认实现会调用我们提供的 summarize_author 定义。因为实现了 summarize_author,Summary trait 就提供了 summarize 方法的功能,且无需编写更多的代码。
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}为了使用这个版本的 Summary,只需在为类型实现 trait 时定义 summarize_author 即可:
impl Summary for SocialPost {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}使用 trait 作为参数
该参数支持任何实现了指定 trait 的类型。在 notify 函数体中,可以调用任何来自 Summary trait 的方法,比如 summarize。
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}Trait Bound-语法
impl Trait 语法更直观,但它实际上是更长形式的 trait bound 语法的语法糖。它看起来像:
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}通过 + 语法指定多个 trait bound
pub fn notify(item: &(impl Summary + Display)) {
pub fn notify<T: Summary + Display>(item: &T) {通过 where 简化 trait bound
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{返回实现了 trait 的类型
fn returns_summarizable() -> impl Summary {
SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
repost: false,
}
}使用 trait bound 有条件地实现方法
通过使用带有 trait bound 的泛型参数的 impl 块,可以有条件地只为那些实现了特定 trait 的类型实现方法。
use std::fmt::Display;
struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}生命周期
生命周期注解并不改变任何引用的生命周期的长短。相反它们描述了多个引用生命周期相互的关系,而不影响其生命周期。
生命周期注解有着一个不太常见的语法:生命周期参数名称必须以撇号(')开头,其名称通常全是小写,类似于泛型其名称非常短。大多数人使用 'a 作为第一个生命周期注解。
生命周期符号 'a 主要还是帮助Rust编译器的,通过引入生命周期符号标注,Rust能实现精准地分析引用的有效期。
&i32 // 引用
&'a i32 // 带有显式生命周期的引用
&'a mut i32 // 带有显式生命周期的可变引用为了在函数签名中使用生命周期注解,需要在函数名和参数列表间的尖括号中声明泛型生命周期(lifetime)参数,就像泛型类型(type)参数一样。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}静态生命周期
'static,其生命周期能够存活于整个程序期间。
所有的字符串字面值都拥有 'static 生命周期,也可以选择像下面这样标注出来:
let s: &'static str = "I have a static lifetime.";在结构体定义中
这个结构体只有一个字段 part,它存放了一个字符串 slice,这是一个引用。类似于泛型参数类型,必须在结构体名称后面的尖括号中声明泛型生命周期参数,以便在结构体定义中使用生命周期参数。这个注解意味着 ImportantExcerpt 的实例不能比其 part 字段中的引用存在的更久。
novel 的数据在 ImportantExcerpt 实例创建之前就存在。直到 ImportantExcerpt 离开作用域之后 novel 都不会离开作用域,所以 ImportantExcerpt 实例中的引用是有效的。
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let i = ImportantExcerpt {
part: first_sentence,
};
}
//
struct Url<'a> {
protocol: &'a str,
// ...
}
struct Request<'a> {
url: Url<'a>,
body: String,
// ...
}类型方法中的引用
类型的方法只是第一个参数为Self的所有权或引用类型的函数。
如果返回的值是Self本身或本身一部分的引用,就不用手动写 'a 生命周期符号,Rust会自动帮我们在方法返回值引用的生命周期和Self的scope之间进行绑定,这是一条默认的规则。
struct A {
foo: String,
}
impl A {
fn play(&self, a: &str, b: &str) -> &str {
&self.foo
}
}泛型类型参数、trait bounds 和生命周期
在同一函数中指定泛型类型参数、trait bounds 和生命周期的语法
生命周期也是泛型,所以生命周期参数 'a 和泛型类型参数 T 都位于函数名后的同一尖括号列表中。
use std::fmt::Display;
fn longest_with_an_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: Display,
{
println!("Announcement! {ann}");
if x.len() > y.len() { x } else { y }
}测试
为了将一个函数变成测试函数,需要在 fn 行之前加上 #[test]。当使用 cargo test 命令运行测试时,Rust 会构建一个测试执行程序用来调用被标注的函数,并报告每一个测试是通过还是失败。
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}使用 assert! 宏来检查结果
assert! 宏由标准库提供,在希望确保测试中一些条件为 true 时非常有用。需要向 assert! 宏提供一个求值为布尔值的参数。如果值是 true,assert! 什么也不做,同时测试会通过。如果值为 false,assert! 调用 panic! 宏,这会导致测试失败。
assert!(larger.can_hold(&smaller));
// 自定义失败信息
assert!(
result.contains("Carol"),
"Greeting did not contain name, value was `{result}`"
);使用 assert_eq! 和 assert_ne! 宏测试相等
assert_eq! 和 assert_ne!。这两个宏分别比较两个值是相等还是不相等。当断言失败时它们也会打印出这两个值具体是什么,以便于观察测试为什么失败,而 assert! 只会打印出它从 == 表达式中得到了 false 值,而不是打印导致 false 的具体值。
assert_eq!(result, 4);使用 should_panic 检查 panic
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn greater_than_100() {
Guess::new(200);
}
}
// 增加一个可选的 expected 参数。测试工具会确保错误信息中包含其提供的文本。
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "less than or equal to 100")]
fn greater_than_100() {
Guess::new(200);
}
}