Rust(2)进阶语法

时间:2024-10-07 14:13:19

Rust(2)进阶语法


Author: Once Day Date: 2024年10月3日

一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许终点只是一场白日梦…

漫漫长路,有人对你微笑过嘛…

全系列文章可参考专栏: 源码分析_Once-Day的博客-****博客

参考文章:

  • Rust 程序设计语言 - Rust 程序设计语言 简体中文版 (kaisery.github.io)
  • 简介 - Rust 参考手册 中文版 (rustwiki.org)

文章目录

  • Rust(2)进阶语法
        • 1. 进阶语法
          • 1.1 panic错误处理
          • 1.2 Result错误处理
          • 1.3 泛型数据类型
          • 1.4 Trait(共同特性)
          • 1.5 生命周期
          • 1.6 自动化测试
          • 1.7 迭代器
          • 1.8 文档化注释
          • 1.9 Box智能指针
          • 1.10 RC智能指针
          • 1.11 RefCell智能指针
          • 1.12 多线程编程
          • 1.13 模式匹配

1. 进阶语法
1.1 panic错误处理

在 Rust 中,panic 是一种非常重要的错误处理机制。当程序遇到不可恢复的错误时,例如访问数组越界、整数除零等,Rust 会主动调用 panic 来终止程序。这是 Rust 保证内存安全的重要手段之一。

当 panic 发生时,Rust 会展开(unwind)调用栈并清理每个函数中的数据。这个过程有点类似其他语言中的异常抛出。如果 panic 没有被捕获处理,整个程序会终止并返回一个非零的错误码。

在代码中,可以使用 panic! 宏来主动触发一个 panic,例如:

fn divide(x: i32, y: i32) -> i32 {
    if y == 0 {
        panic!("attempt to divide by zero");
    }
    x / y
}

这里如果传入的除数 y 为 0,divide 函数会调用 panic! 宏,阻止程序继续运行,这可以避免后续的除零错误。

panic 主要用于处理不可恢复的错误状况。对于可以恢复的错误,更推荐使用 Result 或 Option 来优雅地传递和处理。

还可以使用 catch_unwind 函数来捕获 panic,避免程序直接终止:

use std::panic;

let result = panic::catch_unwind(|| {
    // 可能会 panic 的代码
    println!("hello!");
});

这里的闭包内如果发生了 panic,会被 catch_unwind 捕获,返回一个 Result,而不是让程序崩溃。

1.2 Result错误处理

Result 是 Rust 标准库中定义的一个枚举类型,用于表示可能出错的操作结果。它有两个变体:

  • Ok(T) 代表操作成功,内部包含一个类型为 T 的值;
  • Err(E) 代表操作失败,内部包含一个类型为 E 的错误值。

其定义如下:

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Result 在 Rust 中被广泛用于错误处理和传播。任何可能出错的函数,都可以返回一个 Result,让调用者明确地处理可能出现的错误。例如,一个解析字符串为整数的函数可以这样定义:

fn parse_int(s: &str) -> Result<i32, ParseIntError> {
    // ...
}

这里 parse_int 函数返回一个 Result,成功时内含解析得到的 i32 数字,失败时内含一个 ParseIntError 错误。调用者必须显式地处理这个 Result。

Rust 提供了一系列方便的组合子函数,用于处理和传播 Result:

  • map: 对 Result 的 Ok 值进行映射转换,保持 Err 值不变。
  • and_then: 类似 map,但映射函数本身也返回一个 Result。
  • map_err: 对 Result 的 Err 值进行映射转换,保持 Ok 值不变。
  • unwrap: 对 Ok 值进行解封,如遇 Err 则 panic。
  • expect: 类似 unwrap,但可以指定 panic 时的错误信息。
  • ?运算符: 如果 Result 是 Err,则直接返回该 Err;如果是 Ok,则解封其中的值。

例如,我们可以使用 ? 运算符来传播错误:

fn read_username_from_file() -> Result<String, io::Error> {
    let mut file = File::open("username.txt")?;
    let mut username = String::new();
    file.read_to_string(&mut username)?;
    Ok(username)
}

这里 ? 会自动将 Ok 中的值解封,如遇 Err 则提前返回。这让错误传播的代码非常简洁。

1.3 泛型数据类型

泛型是 Rust 中一个非常强大和重要的特性,它允许我们编写适用于多种类型的代码,增强了代码的重用性和表达力。Rust 中的泛型主要有以下几种形式:

(1) 泛型函数,可以在函数定义中使用泛型类型参数,使函数能够处理不同类型的参数。例如:

fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut largest = list[0];
    for &item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

这里的 largest 函数使用了泛型类型 T,可以找出任何实现了 PartialOrd 和 Copy trait 的类型的切片中的最大值。

(2) 泛型结构体,可以在结构体定义中使用泛型类型参数,使结构体能够包含不同类型的字段。例如:

struct Point<T> {
    x: T,
    y: T,
}

这里的 Point 结构体有一个泛型类型参数 T,可以表示二维平面上任意类型的点。

(3) 泛型枚举,可以在枚举定义中使用泛型类型参数,使枚举能够包含不同类型的值。最典型的例子就是标准库中的 Option 和 Result:

enum Option<T> {
    Some(T),
    None,
}

enum Result<T, E> {
    Ok(T),
    Err(E),
}

(4) 泛型方法,可以在 impl 块中定义泛型方法,使方法能够处理不同类型的 self 参数或其他参数。例如:

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

这里为 Point<T> 实现了一个 x 方法,返回 x 坐标的引用。

(5) 泛型 trait,可以定义带有泛型类型参数的 trait,描述一批类型的共同行为。例如:

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

impl<T: Display> Summary for T {
    fn summarize(&self) -> String {
        format!("(Display) {}", self)
    }
}

这里定义了一个 Summary trait,并为所有实现了 Display 的类型提供了一个默认的 summarize 实现。

泛型让 Rust 具有了很强的表达力,可以编写出抽象层次更高、适用范围更广的代码。同时 Rust 编译器会对泛型代码进行单态化(monomorphization),生成具体类型的代码,保证了运行时的高效性。

1.4 Trait(共同特性)

Trait 是 Rust 中一个非常重要和强大的特性,它用于定义和抽象类型的共同行为。Trait 类似于其他语言中的接口(Interface),但更加灵活和强大。

(1) 定义 Trait,可以使用 trait 关键字来定义一个 Trait。Trait 内部可以包含方法签名、关联类型、常量等。

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

这里定义了一个 Summary Trait,要求实现者必须提供一个 summarize 方法,该方法借用 &self,返回一个 String。

(2) 为类型实现 Trait,可以使用 impl 关键字来为一个具体的类型实现某个 Trait。

struct NewsArticle {
    headline: String,
    location: String,
    author: String,
    content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

这里为 NewsArticle 结构体实现了 Summary Trait,提供了具体的 summarize 方法。

(3) Trait Bound,可以使用 Trait Bound 来限制泛型类型参数必须实现某些 Trait。这样可以在泛型函数内部调用这些 Trait 的方法。

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

这里的泛型类型参数 T 被限制为必须实现 Summary Trait,因此可以在函数内部调用 summarize 方法。

(4) Trait 作为函数参数和返回值,可以将 Trait 用作函数参数或返回值的类型,表示函数接受或返回任何实现了该 Trait 的类型。

fn returns_summarizable() -> impl Summary {
    NewsArticle {
        headline: String::from("Penguins win the Stanley Cup Championship!"),
        location: String::from("Pittsburgh, PA, USA"),
        author: String::from("Iceburgh"),
        content: String::from("The Pittsburgh Penguins once again are the best hockey team in the NHL."),
    }
}

这里的函数返回值类型是 impl Summary,表示返回任何实现了 Summary Trait 的类型。

(5) Trait 的默认实现,在定义 Trait 时,可以为其中的某些方法提供默认实现。

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

这样,如果某个类型实现了 Summary 但没有提供自己的 summarize 实现,就会使用这个默认实现。

(6) Trait 的继承,一个 Trait 可以继承另一个 Trait,这样前者就包含了后者的所有方法。

trait Display {
    fn display(&self) -> String;
}

trait Summary: Display {
    fn summarize(&self) -> String {
        format!("(Read more... {})", self.display())
    }
}

这里 Summary Trait 继承了 Display Trait,因此任何实现 Summary 的类型也必须实现 Display。

Trait 是 Rust 中实现抽象和多态的基础,也是 Rust 泛型编程的重要组成部分。通过 Trait,我们可以定义一套统一的接口,让不同的类型共享相同的行为。同时 Trait Bound 让我们能够对泛型类型参数进行灵活约束,保证了类型安全。

1.5 生命周期

生命周期是 Rust 所独有的一个概念,它用于表示引用的有效范围。在 Rust 中,每一个引用都有一个生命周期,它决定了这个引用在何时有效。生命周期的主要目的是避免悬垂引用(Dangling References),即引用在其引用的数据被释放后仍然存在的情况。

(1) 生命周期的语法,生命周期在语法上用一个撇号 ’ 加上一个名字来表示,例如 'a, 'b, 'c 等。最常见的生命周期是 'static,它表示引用的数据在整个程序的运行期间都有效。

(2) 函数中的生命周期,当一个函数有引用类型的参数或返回值时,我们需要为这些引用指定生命周期。

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

这里的 longest 函数有两个引用参数 x 和 y,返回值也是一个引用。我们使用泛型生命周期参数 'a 来表示这三个引用的生命周期必须相同。这保证了返回的引用的生命周期与传入的引用的生命周期一致,避免了悬垂引用。

(3) 结构体中的生命周期,当结构体中包含引用类型的字段时,每个引用字段都需要一个生命周期注解。

struct Excerpt<'a> {
    part: &'a str,
}

这里的 Excerpt 结构体有一个引用字段 part,为其指定了生命周期 'a。这表示 Excerpt 实例的生命周期不能超过其 part 字段引用的数据的生命周期。

(4) 生命周期省略(Elision)规则,在某些情况下,Rust 编译器可以自动推断生命周期,无需显式注解,这称为生命周期省略规则。主要有以下三条规则:

  • 每一个引用参数都有它自己的生命周期参数。
  • 如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数。
  • 如果方法有 &self 或 &mut self 参数,那么 self 的生命周期被赋给所有输出生命周期参数。

(5) 静态生命周期,'static 生命周期表示引用的数据在整个程序的运行期间都有效。字符串字面量就拥有 'static 生命周期:

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

(6) 生命周期约束,有时我们需要为泛型类型参数指定生命周期约束,表示类型参数中的引用必须满足某些生命周期关系。

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
    }
}

这里我们为泛型类型参数 T 指定了一个 Display Trait 约束,同时也为引用参数 x 和 y 指定了生命周期 'a。

1.6 自动化测试

Rust 内置了强大的测试支持,可以方便地编写和运行单元测试和集成测试。Rust 的测试系统与语言深度集成,不需要额外的测试框架,非常易于使用。

(1) 单元测试,单元测试用于测试单个模块的功能,通常在与被测代码相同的文件中。

要编写单元测试,我们需要使用 #[test] 属性来标记测试函数,并使用 assert!assert_eq! 等宏来检查测试结果。

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
}

这里我们在一个名为 tests 的模块中定义了一个测试函数 it_works。#[cfg(test)] 属性表示这个模块只在运行测试时编译。

我们可以使用 cargo test 命令来运行单元测试。测试通过时没有任何输出,测试失败时会打印相关的失败信息。

(2) 集成测试,用于测试多个模块间的交互,通常在专门的 tests 目录下。

要编写集成测试,我们需要在项目根目录下创建一个 tests 目录,并在其中创建测试文件。测试文件中的每个函数都是一个独立的集成测试。

use adder;

#[test]
fn it_adds_two() {
    assert_eq!(4, adder::add_two(2));
}

这里我们在 tests/integration_test.rs 文件中编写了一个集成测试,测试了 adder 模块的 add_two 函数。

我们可以使用 cargo test --test integration_test 命令来运行这个集成测试。

(3) 测试的组织结构,Rust 的测试系统支持在测试函数中使用 #[should_panic] 属性来测试某些代码是否会如期 panic:

#[test]
#[should_panic(expected = "Divide by zero")]
fn test_divide_by_zero() {
    let _ = 1 / 0;
}

我们还可以使用 #[ignore] 属性来暂时忽略某些测试:

#[test]
#[ignore]
fn expensive_test() {
    // 耗时较长的测试代码...
}

被忽略的测试在普通的 cargo test 运行中会被跳过,但可以使用 cargo test -- --ignored 来专门运行这些测试。

(4) 测试的最佳实践,为了编写出高质量的 Rust 测试,应该遵循以下最佳实践:

  • 测试代码应该简单、可读,避免过度复杂的逻辑。
  • 每个测试应该专注于测试一个特定的功能点,避免在一个测试中测试多个不相关的内容。
  • 测试应该独立运行,不应该依赖于其他测试的运行结果或顺序。
  • 测试应该能够稳定重复运行,避免依赖于随机性或外部环境。
  • 对于复杂的功能,应该从多个角度编写测试,覆盖各种可能的输入和边界条件。
1.7 迭代器

迭代器是 Rust 中一个非常重要和强大的概念,它提供了一种通用、高效、灵活的方式来遍历和操作集合中的元素。Rust 的迭代器深受函数式编程的影响,支持链式调用、惰性求值等特性,可以显著提高代码的可读性和性能。

(1) 迭代器的定义,在 Rust 中,迭代器是实现了 Iterator trait 的任何类型。Iterator trait 定义了一个 next 方法,用于返回迭代器的下一个元素:

pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    // 其他方法...
}

其中,Item 是一个关联类型,表示迭代器产生的元素的类型。next 方法返回一个 Option<Self::Item>,表示迭代器可能产生下一个元素(Some)或者已经结束(None)。

(2) 创建迭代器,可以通过调用集合类型的 iter, iter_mut 或 into_iter 方法来创建不同类型的迭代器:

  • iter 方法创建一个产生不可变引用的迭代器。
  • iter_mut 方法创建一个产生可变引用的迭代器。
  • into_iter 方法创建一个获取集合所有权的迭代器。
let v = vec![1, 2, 3];
let iter = v.iter();

这里我们从一个 vector 创建了一个不可变引用的迭代器。

(3) 使用迭代器,可以使用 for 循环来遍历迭代器中的元素:

for item in iter {
    println!("{}", item);
}

这会打印出 vector 中的每一个元素。也可以使用 next 方法手动遍历迭代器:

assert_eq!(iter.next(), Some(&1));
assert_eq!(iter.next(), Some(&2));
assert_eq!(iter.next(), Some(&3));
assert_eq!(iter.next(), None);

(4) 迭代器适配器,迭代器最强大的功能之一是它提供了大量的适配器方法,可以用于转换和过滤迭代器。这些适配器是惰性的,只有在需要时才会执行。常用的适配器包括:

  • map: 对迭代器中的每个元素应用一个函数,返回一个新的迭代器。
  • filter: 根据一个谓词函数过滤迭代器中的元素,返回一个新的迭代器。
  • take: 从迭代器的开头获取指定数量的元素,返回一个新的迭代器。
  • skip: 跳过迭代器开头指定数量的元素,返回一个新的迭代器。
  • zip: 将两个迭代器的元素配对,返回一个产生元组的新迭代器。
let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
assert_eq!(v2, vec![2, 3, 4]);

这里使用 map 适配器将 v1 中的每个元素加 1,然后使用 collect 方法将结果收集到一个新的 vector 中。

(5) 消费迭代器,除了适配器,迭代器还提供了一些消费方法,用于对迭代器进行最终的计算。这些方法会真正触发迭代器的执行,常用的消费方法包括:

  • collect: 将迭代器中的元素收集到一个集合中,常用于将迭代器转换为 vector、hashmap 等类型。
  • sum: 对迭代器中的元素求和,返回一个汇总值。
  • product: 对迭代器中的元素求积,返回一个汇总值。
  • min, max: 找出迭代器中的最小或最大元素。
  • find: 根据一个谓词函数查找迭代器中的第一个元素,返回一个 Option。
  • fold: 使用一个初始值和一个累加函数对迭代器中的元素进行归约,返回一个最终值。
let v1: Vec<i32> = vec![1, 2, 3];
let v1_sum: i32 = v1.iter().sum();
assert_eq!(v1_sum, 6);

这里使用 sum 方法对 v1 中的元素求和,得到最终结果 6。

1.8 文档化注释

文档注释是 Rust 中一种特殊的注释,它们可以用于生成项目的 API 文档。Rust 的文档注释支持 Markdown 语法,可以方便地编写富文本格式的文档。Rust 编译器和 Cargo 工具对文档注释有内置的支持,可以自动提取文档注释并生成美观、实用的 HTML 格式文档。

(1) 文档注释的语法,Rust 中的文档注释以三个斜杠 /// 开头,可以放在 crate、模块、函数、类型、trait、结构体、枚举等项的前面。

/// This is a documentation comment for a function.
///
/// It can have multiple lines and use **Markdown** syntax.
fn my_function() {
    // ...
}

对于多行文档注释,通常在第一行写一个简要的概述,然后用一个空行分隔详细的描述。

除了 ///,Rust 还支持以 //! 开头的文档注释,它们用于描述包含项(如 crate、模块)而不是被包含项。

(2) 文档注释中的 Markdown 语法,Rust 的文档注释支持常用的 Markdown 语法,包括:

(1) *斜体*和**粗体**
(2) `代码`和```代码块```
(3) [链接](https://www.rust-lang.org/)
(4) 标题、列表、引用等
/// # Examples
///
/// ```
/// let result = my_function(42);
/// assert_eq!(result, Some(42));
/// ```
fn my_function(x: i32) -> Option<i32> {
    Some(x)
}

这里我们在文档注释中使用了二级标题和代码块来提供一个使用示例。

(3) 文档测试,Rust 支持在文档注释中编写可执行的测试代码,称为文档测试(Doc-tests)。文档测试可以确保文档中的示例代码是正确和最新的。要编写文档测试,只需在文档注释的代码块中编写普通的 Rust 代码和断言即可。

/// ```
/// let result = my_function(42);
/// assert_eq!(result, Some(42));
/// ```
fn my_function(x: i32) -> Option<i32> {
    Some(x)
}

可以使用 cargo test 命令来运行文档测试,就像运行普通的单元测试一样。

(4) 生成 API 文档,可以使用 cargo doc 命令来生成项目的 API 文档。这个命令会提取所有的文档注释,并生成一个 HTML 格式的文档网站。在项目根目录下运行:

cargo doc --open

这会在 target/doc 目录下生成文档,并自动在浏览器中打开文档的首页。

生成的文档网站包括了模块、类型、函数等项的详细信息,以及它们的文档注释。文档中的代码块会被高亮显示,Markdown 语法会被正确渲染。文档网站还提供了搜索、导航等功能,方便用户查找和浏览 API。

(5) 常用的文档注释惯例,为了编写出高质量的 API 文档,应该遵循一些常用的文档注释惯例:

  • 在文档注释的第一行提供一个简洁的概述,总结项的功能或目的。
  • 对于函数,描述它的参数、返回值和可能的错误情况。
  • 对于类型,描述它的属性、方法和使用场景。
  • 提供具体的使用示例,最好是可以直接运行的代码。
  • 使用 Markdown 语法来组织和格式化文档,提高可读性。
  • 保持文档的简洁、准确和最新,避免冗余或过时的信息。

文档注释是 Rust 项目的重要组成部分,它们不仅提供了 API 的使用指南,也体现了项目的设计思路和质量。作为 Rust 程序员,应该重视文档注释的编写,将其作为开发过程的一部分,而不是事后的补充。好的文档注释可以显著提高项目的可用性和可维护性,吸引更多的用户和贡献者。

1.9 Box智能指针

Box 是 Rust 标准库提供的一种智能指针类型,它允许我们在堆上分配值并通过指针来操作它们。Box 通常用于以下场景:

  • 当我们需要在堆上分配一个值,而不是在栈上。
  • 当我们需要一个指向 trait 对象的指针。
  • 当一个值的大小在编译时无法确定,但我们需要一个固定大小的值时。

(1) 创建 Box,可以使用 Box::new 函数来创建一个 Box 指针:

let x = Box::new(5);

这会在堆上分配一个值 5,并返回一个指向该值的 Box 指针。Box 指针的类型是 Box<T>,其中 T 是被指向的值的类型。

(2) 解引用 Box,可以使用解引用操作符 * 来访问 Box 指针指向的值:

let x = Box::new(5);
assert_eq!(*x, 5);

这会返回 Box 指向的值的引用。

(3) Box 与所有权,Box 与普通指针不同,它拥有指向的值的所有权。当 Box 指针离开作用域时,它指向的值也会被自动释放。这避免了手动内存管理的需要。

{
    let x = Box::new(5);
} // x 离开作用域,它指向的值被释放

(4) Box 与 trait 对象,Box 经常用于创建指向 trait 对象的指针。trait 对象允许我们在运行时使用动态分发,对不同的类型进行抽象。

trait Draw {
    fn draw(&self);
}

struct Circle;
impl Draw for Circle {
    fn draw(&self) {
        println!(