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。

二进制 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
    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)从当前模块开始,以 self、super 或当前模块中的某个标识符开头。

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

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_or 在 scores 中没有该键所对应的项时将其设置为零。

更新哈希 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 中的值,程序继续执行。如果值是 Err,Err 就会像使用了 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_author,Summary 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! 宏提供一个求值为布尔值的参数。如果值是 true,assert! 什么也不做,同时测试会通过。如果值为 false,assert! 调用 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.