Skip to content

Rust 有许多功能可以让你管理代码的组织,包括哪些细节可以被公开,哪些细节作为私有部分,以及程序中各个作用域中有哪些名称。这些特性,有时被统称为 “模块系统(the module system)”,包括:

  • Packages):Cargo 的一个功能,它允许你构建、测试和分享 crate。
  • Crates:一个模块树,可以产生一个库或可执行文件。
  • 模块Modules)和 use:允许你控制作用域和路径的私有性。
  • 路径path):一个为例如结构体、函数或模块等项命名的方式。

包和 Crate

crate 是 Rust 编译器每次处理的最小代码单位。即使你用 rustc 而不是 cargo 来编译单个源代码文件,编译器也会把那个文件视为一个 crate。crate 可以包含模块,而这些模块也可以定义在其他文件中,并与该 crate 一起编译

crate 有两种形式:二进制 crate 和库 crate。

二进制 crateBinary crates)可以被编译为可执行程序,比如命令行程序或者服务端。它们必须有一个名为 main 函数来定义当程序被执行的时候所需要做的事情。目前我们所创建的 crate 都是二进制 crate。

库 crateLibrary 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
    rust
    src/
      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)从当前模块开始,以 selfsuper 或当前模块中的某个标识符开头。

绝对路径和相对路径都后跟一个或多个由双冒号(::)分割的标识符。

super 开始的相对路径

我们可以通过在路径的开头使用 super ,从父模块开始构建相对路径,而不是从当前模块或者 crate 根开始。这类似以 .. 语法开始一个文件系统路径。

rust
fn deliver_order() {}

mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();
        super::deliver_order();
    }

    fn cook_order() {}
}

创建公有的结构体和枚举

一个结构体定义的前面使用了 pub,这个结构体会变成公有的,但是这个结构体的字段仍然是私有的。可以根据情况决定每个字段是否公有。

rust
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 关键字将路径引入作用域

rust
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 指定一个新的本地名称或者别名

rust
use std::fmt::Result;
use std::io::Result as IoResult;

fn function1() -> Result {
    // --snip--
}

fn function2() -> IoResult<()> {
    // --snip--
}

使用 pub use 重导出名称

重导出re-exporting),因为在把某个项目导入当前作用域的同时,也将其暴露给其他作用域。

rust
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 和其依赖,并使其可在项目代码中使用。

toml
rand = "0.9.3"

使用:

rust
use rand::Rng;

fn main() {
    let secret_number = rand::thread_rng().gen_range(1..=100);
}

使用嵌套路径将相同的项在一行中引入作用域

rust
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 运算符:

rust
use std::collections::*;

集合

序中非常常用的三种集合:

  • 向量vector)允许你把数量可变的值一个挨一个地存放起来。
  • 字符串string)是字符的集合。此前我们已经提到过 String 类型,不过本章会更深入地讨论它。
  • 哈希映射hash map)允许你把某个值与特定的键关联起来。它是更通用的数据结构 map 的一种具体实现。

vector

vector 会把值彼此相邻地存放在内存中,所以如果末尾追加一个新元素,而当前存放位置又没有足够空间容纳所有元素,程序就可能需要分配一块新内存,并把旧元素复制到新空间里去,所以对于原先元素的引用需要在添加元素之前

rust
// 创建一个新的空 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 中的元素

rust
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 编码的字符串类型。

rust
// 新建字符串
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 中的无效索引时一样:

rust
let hello = "Здравствуйте";

let s = &hello[0..4];

遍历字符串

rust
for c in "Зд".chars() {
    println!("{c}");
}
// З д

for b in "Зд".bytes() {
    println!("{b}");
}
/*
    208
    151
    208
    180
*/

Hash Map 储存键值对

rust
// 新建
 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_orscores 中没有该键所对应的项时将其设置为零。

更新哈希 map

覆盖一个值

rust
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);

只在键尚不存在时插入键值对

entry,它接收你想检查的键作为参数。entry 方法的返回值是一个名为 Entry 的枚举,它表示一个可能存在、也可能不存在的值

rust
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! 处理不可恢复的错误

rust
fn main() {
    panic!("crash and burn");
}

当出现 panic 时,程序默认会开始 展开unwinding),这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。另一种选择是直接 终止abort),这会不清理数据就退出程序。

那么程序所使用的内存需要由操作系统来清理。如果你需要项目的最终二进制文件越小越好,panic 时通过在 Cargo.toml 的 [profile] 部分增加 panic = 'abort',可以由展开切换为终止。例如,如果你想要在 release 模式中 panic 时直接终止,可添加:

toml
[profile.release]
panic = 'abort'

尝试访问超越 vector 结尾的元素,会造成 panic!

rust
$ 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 backtrace

note: 这一行说可以设置 RUST_BACKTRACE 环境变量来得到 backtrace。backtrace 是一份到达当前执行点之前所有被调用函数的列表。

rust
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

RUST_BACKTRACE 环境变量设成除 0 之外的任意值

bash
$ 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!

rust
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)处理

rust
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 的快捷方式

rust
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 将错误返回给代码调用者

rust
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 中的值,程序继续执行。如果值是 ErrErr 就会像使用了 return 关键字一样,作为整个函数的返回值提前返回,这样错误值就被传播给了调用者。

rust
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 函数时,接收到的错误类型会被转换成当前函数返回类型里定义的错误类型。当一个函数用单一错误类型来表示它所有可能的失败方式时,这会非常有用,即使函数内部的不同部分可能会因为很多不同的原因而失败。

可以在 ? 之后直接使用链式方法调用来进一步简化代码

rust
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 的尖括号中的类型是泛型而不是具体类型。

rust
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> 实例则没有定义此方法。

rust
impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

Rust 在编译时对泛型代码进行单态化monomorphization)。单态化就是把泛型代码转换成具体代码的过程,方法是用编译时实际用到的具体类型去填充泛型代码。

Trait

trait 定义了某个特定类型拥有可能与其他类型共享的功能。可以通过 trait 以一种抽象的方式定义共同行为。

定义 trait

rust
pub trait Summary {
    fn summarize(&self) -> String;
}

为类型实现 trait

rust
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 方法。

rust
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());
}

使用默认实现

rust
pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

默认实现允许调用相同 trait 中的其他方法,哪怕这些方法没有默认实现。

一旦定义了 summarize_author,我们就可以对 SocialPost 结构体的实例调用 summarize 了,而 summarize 的默认实现会调用我们提供的 summarize_author 定义。因为实现了 summarize_authorSummary trait 就提供了 summarize 方法的功能,且无需编写更多的代码。

rust
pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

为了使用这个版本的 Summary,只需在为类型实现 trait 时定义 summarize_author 即可:

rust
impl Summary for SocialPost {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
}

使用 trait 作为参数

该参数支持任何实现了指定 trait 的类型。在 notify 函数体中,可以调用任何来自 Summary trait 的方法,比如 summarize

rust
pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

Trait Bound-语法

impl Trait 语法更直观,但它实际上是更长形式的 trait bound 语法的语法糖。它看起来像:

rust
pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

通过 + 语法指定多个 trait bound

rust
pub fn notify(item: &(impl Summary + Display)) {
    
pub fn notify<T: Summary + Display>(item: &T) {

通过 where 简化 trait bound

rust
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 的类型

rust
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 的类型实现方法。

rust
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能实现精准地分析引用的有效期。

rust
&i32        // 引用
&'a i32     // 带有显式生命周期的引用
&'a mut i32 // 带有显式生命周期的可变引用

为了在函数签名中使用生命周期注解,需要在函数名和参数列表间的尖括号中声明泛型生命周期(lifetime)参数,就像泛型类型(type)参数一样。

rust
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

静态生命周期

'static,其生命周期能够存活于整个程序期间。

所有的字符串字面值都拥有 'static 生命周期,也可以选择像下面这样标注出来:

rust
let s: &'static str = "I have a static lifetime.";

在结构体定义中

这个结构体只有一个字段 part,它存放了一个字符串 slice,这是一个引用。类似于泛型参数类型,必须在结构体名称后面的尖括号中声明泛型生命周期参数,以便在结构体定义中使用生命周期参数。这个注解意味着 ImportantExcerpt 的实例不能比其 part 字段中的引用存在的更久。

novel 的数据在 ImportantExcerpt 实例创建之前就存在。直到 ImportantExcerpt 离开作用域之后 novel 都不会离开作用域,所以 ImportantExcerpt 实例中的引用是有效的。

rust
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之间进行绑定,这是一条默认的规则。

rust
struct A {
    foo: String,
}

impl A {
    fn play(&self, a: &str, b: &str) -> &str {
        &self.foo
    }
}

泛型类型参数、trait bounds 和生命周期

在同一函数中指定泛型类型参数、trait bounds 和生命周期的语法

生命周期也是泛型,所以生命周期参数 'a 和泛型类型参数 T 都位于函数名后的同一尖括号列表中。

rust
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 会构建一个测试执行程序用来调用被标注的函数,并报告每一个测试是通过还是失败。

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! 宏提供一个求值为布尔值的参数。如果值是 trueassert! 什么也不做,同时测试会通过。如果值为 falseassert! 调用 panic! 宏,这会导致测试失败。

rust
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 的具体值。

rust
assert_eq!(result, 4);

使用 should_panic 检查 panic

rust
#[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);
    }
}

Released under the GPL License.