【笔记】Comprehensive Rust语言学习

1. 核心工具链

|---------------|----------------------|-----------------------------------|
| 工具名 | 主要功能 | 常用命令示例 |
| Cargo | 包管理、构建、测试、发布 | cargo new, cargo build, cargo run |
| rustc | Rust 编译器 | rustc main.rs |
| rustup | 工具链安装与管理 | rustup update |
| Clippy | 代码 lint,发现常见错误和优化点 | cargo clippy |
| Rustfmt | 代码格式化 | cargo fmt |
| rust-analyzer | IDE 语言服务器,提供代码补全、跳转等 | (通常集成在编辑器中) |

1.1. Cargo

Cargo 是 Rust 的构建系统和包管理器,是日常开发中最常用的工具。它帮助你:

创建项目cargo new project_name创建一个新项目,cargo new --lib project_name创建一个库项目

管理依赖 :在 Cargo.toml[dependencies]下添加包(crate)和版本,Cargo 会从 crates.io(Rust 官方包注册中心)或 Git 仓库等下载并编译它们

构建项目

cargo build:编译项目

cargo build --release:进行发布构建,启用优化,生成更小更快的二进制文件,但编译时间更长

运行与检查

cargo run:编译并运行项目

cargo check:快速检查代码能否通过编译,而不生成可执行文件,速度很快

运行测试cargo test会运行项目中所有的测试函数

生成文档cargo doc会为你的项目和其依赖生成 HTML 文档

1.2. rustc

rustc是 Rust 的编译器,负责将 Rust 源代码编译成可执行文件或库。虽然我们通常通过 Cargo 来调用 rustc,但直接使用它可以帮助你理解编译过程或进行一些简单的编译任务

1.3. rustup

rustup用于安装和管理多个 Rust 工具链

你可以:

  • 安装 stable(稳定版)、beta(测试版)和 nightly(夜间版) 三种发布通道的 Rust
  • 轻松地更新 Rust:rustup update
  • 为不同的项目切换不同的工具链版本
  • 安装不同平台的标准库,用于交叉编译。

2. VSCode开发环境搭建

Ubuntu 22.04.5 LTS + vscode ssh

由于rustup官方服务器在国外
如果直接按照rust官网的安装方式安装非常容易失败,即使不失败也非常非常慢
如果用国内的镜像则可以分分钟就搞定

复制代码
1. curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rust.sh

2. vim rust.sh
// RUSTUP_UPDATE_ROOT编辑为
// RUSTUP_UPDATE_ROOT="https://mirrors.ustc.edu.cn/rust-static/rustup"
// 这是用来下载 rustup-init 的, 修改后通过国内镜像下载

3. export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup
// 这让 rustup-init从国内进行下载rust的组件,提高速度

4. bash rust.sh
// 默认选1

5. vi $HOME/.cargo/env
// 末尾新增
// RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup
// 为了以后都从国内镜像源下载包

6. source $HOME/.cargo/env

7. rustc --version
// rustc 1.89.0 (29483883e 2025-08-04)

vscode上面安装如下插件

vscode验证一哈

复制代码
$ cargo new hello_world
     Created binary (application) `hello_world` package

$ cd hello_world
$ cargo run
   Compiling hello_world v0.1.0 (/home/skylink/code/hello_world)
    Finished dev [unoptimized + debuginfo] target(s) in 0.75s
     Running `target/debug/hello_world`
Hello, world!

3. Rust优势

  • Rust 是一门静态编译语言,其功能定位与 C++ 相似
    • rustc 使用 LLVM 作为它的后端。
  • Rust 支持多种平台和架构:
    • x86、ARM、WebAssembly......
    • Linux、Mac、Windows......
  • Rust 被广泛用于各种设备中:
    • 固件和引导程序,
    • 智能显示器,
    • 手机,
    • 桌面,
    • 服务器。

独有的优势如下:

  • 你可以达到堪比 C 和 C++ 的性能,而没有内存不安全的问题
复制代码
 ○ 不存在未初始化的变量。
 ○ 不存在“双重释放”。
 ○ 不存在“释放后使用”。
 ○ 不存在 NULL 指针。
 ○ 不存在被遗忘的互斥锁。
 ○ 不存在线程之间的数据竞争。
 ○ 不存在迭代器失效。
  • 没有未定义的运行时行为:每个 Rust 语句的行为都有明确定义
复制代码
 ○ 数组访问有边界检查。
 ○ 整数溢出有明确定义(panic 或回绕)。
  • 现代语言功能:具有与高级语言一样丰富且人性化的表达能力
复制代码
○ 枚举和模式匹配。
○ 泛型。
○ 无额外开销的外部函数接口(FFI)。
○ 零成本抽象。
○ 强大的编译器错误提示。----后面我们就能看到了,真的强
○ 内置依赖管理器。
○ 对测试的内置支持。
○ 优秀的语言服务协议(Language Server Protocol)支持。

4. 令人印象深刻的语法

由于本人的技术栈是C/C++/Go/Python,所以这里只介绍一些Rust让我吃惊的语法 ,高度相似的语法我就不介绍了,详细的请看这个:https://www.bookstack.cn/read/comprehensive-rust-202412-zh/c01514b4d45220e0.md

4.1. 变量默认是不可变的

复制代码
fn main() {
    let x: i32 = 10;
    println!("x: {x}");
    x = 20;
    println!("x: {x}");
}

root@aqnlc-ubuntu-134-93:/home/skylink/code/hello_world# cargo run
   Compiling hello_world v0.1.0 (/home/skylink/code/hello_world)
error[E0384]: cannot assign twice to immutable variable `x`
 --> src/main.rs:4:5
  |
2 |     let x: i32 = 10;
  |         - first assignment to `x`
3 |     println!("x: {x}");
4 |     x = 20;
  |     ^^^^^^ cannot assign twice to immutable variable
  |
help: consider making this binding mutable
  |
2 |     let mut x: i32 = 10;
  |         +++

  // 编译报错了,并且编译器还给出了提示,让我们用mut修饰可变变量

4.2. 变量类型是明确的

虽然你可以不写,让编译器去推导,但是推导出来的所生成的机器码与明确类型声明完全相同。整形默认是i32,浮点字面量默认为 f64

复制代码
fn main() {
    let x = 3.14;
    let y = 20;
    assert_eq!(x, y);
}

error[E0277]: can't compare `{float}` with `{integer}`
 --> src/main.rs:4:5
  |
4 |     assert_eq!(x, y);
  |     ^^^^^^^^^^^^^^^^ no implementation for `{float} == {integer}`

4.3. Lables

continuebreak 都可以选择接受一个标签参数,用来 终止嵌套循环

复制代码
fn main() {
    // 定义一个 3x3 的二维数组 `s`
    let s = [[5, 6, 7], [8, 9, 10], [21, 15, 32]];
    // 计数器,记录搜索过的元素个数
    let mut elements_searched = 0;
    // 要查找的目标值
    let target_value = 10;
    
    // 使用带标签的 'outer 循环来遍历行(i 从 0 到 2)
    'outer: for i in 0..=2 {
        // 内层循环遍历列(j 从 0 到 2)
        for j in 0..=2 {
            // 每检查一个元素,计数器加 1
            elements_searched += 1;
            // 判断当前元素是否等于目标值
            if s[i][j] == target_value {
                // 如果找到目标值,立即跳出外层循环('outer)
                break 'outer;
            }
        }
    }
    // 打印最终搜索过的元素数量
    println!("elements searched: {elements_searched}");
}

4.4. 分号: 表达式和语句分不清楚?

表达式(Expression)会计算并产生一个值

语句 (Statement)执行操作但不返回值,其类型永远是单元类型 ()

在代码块中,如果最后一行以分号结尾,它就成了语句,如果最后一行没有分号,它就是一个表达式

复制代码
fn main() {
    let z = 13;
    let x = {
        let y = 10;
        println!("y: {y}");
        z - y; // 应该是 z-y
    };
    println!("x: {x}");
}

error[E0277]: `()` doesn't implement `std::fmt::Display`
 --> src/main.rs:8:19
  |
6 |         z - y;
  |              - help: remove this semicolon
7 |     };
8 |     println!("x: {x}");
  |                  -^-
  |                  ||
  |                  |`()` cannot be formatted with the default formatter
  |                  required by this formatting parameter

4.5. 宏:可变参数的函数

rust函数始终采用固定数量的参数。不支持默认参数。宏可用于支持可变函数。

  • println!(format, ..) prints a line to standard output, applying formatting described in std::fmt.
  • format!(format, ..) 的用法与 println! 类似,但它以字符串形式返回结果。
  • dbg!(expression) 会记录表达式的值并返回该值。
  • todo!() 用于标记尚未实现的代码段。如果执行该代码段,则会触发 panic。
  • unreachable!() 用于标记无法访问的代码段。如果执行该代码段,则会触发 panic。
复制代码
fn factorial(n: u32) -> u32 {
    let mut product = 1;
    for i in 1..=n {
        product *= dbg!(i);
    }
    product
}
fn fizzbuzz(n: u32) -> u32 {
    todo!()
}
fn main() {
    let n = 4;
    println!("{n}! = {}", factorial(n));
}

[src/main.rs:4:20] i = 1
[src/main.rs:4:20] i = 2
[src/main.rs:4:20] i = 3
[src/main.rs:4:20] i = 4
4! = 24

4.6. 数组的长度也是类型的一部分

[u8; 3] and [u8; 4] are considered two different types.

复制代码
fn main() {
    let mut a: [i8; 10] = [42; 10];
    a[5] = 0;
    println!("a: {a:?}");
}

4.7. 所有权转移,独占引用和共享引用

Rust 通过共享引用 (&T)独占引用 (&mut T) 的严格区分,在编译期就杜绝了数据竞争的可能性。

  • 当你只需要读取 数据,并且希望多个部分都能同时访问时,使用共享引用
  • 当你需要修改 数据时,必须使用独占引用,并且要遵守其"独占"的规则。

如果不加&符号,这意味着函数会获取数据的所有权,调用后原始数据就不能再使用了。

复制代码
// ❌ 如果不用 &,会发生所有权转移
fn magnitude(v: [f64]) -> f64 { ... }

fn main() {
    let arr = [1.0, 2.0, 3.0];
    let result = magnitude(arr);  // arr 的所有权被转移给函数
    // println!("{:?}", arr);     // ❌ 编译错误!arr 已经不能使用
}

// ✅ 使用 & 进行借用
fn magnitude(v: &[f64]) -> f64 { ... }

fn main() {
    let arr = [1.0, 2.0, 3.0];
    let result = magnitude(&arr); // 只是借用,不转移所有权
    println!("{:?}", arr);        // ✅ arr 仍然可以使用
}

共享引用不允许修改数据,而独占引用允许。

复制代码
fn main() {
    let mut x = 5;

    // 共享引用 - 只读
    let r1 = &x;
    let r2 = &x; // 可以同时有多个共享引用
    println!("r1 = {}, r2 = {}", r1, r2); // 允许
    // *r1 = 10; // 错误!不能通过共享引用修改数据 [2](@ref)

    // 独占引用 - 可读写
    let m1 = &mut x;
    *m1 = 10; // 允许修改
    println!("x = {}", x); // x 现在为 10

    // let m2 = &mut x; // 错误!同一作用域内不能有第二个独占引用 [1,2](@ref)
    // println!("r1 = {}", r1); // 错误!在存在可变引用后,之前的共享引用也不能再使用
}

Rust 的借用检查器严格执行"独占"规则。

复制代码
fn main() {
    let mut data = vec![1, 2, 3];
    
    // 创建一个共享引用
    let shared_ref = &data[0];
    println!("First element: {}", shared_ref);
    
    // 在共享引用最后一次使用后,可以创建独占引用
    let mutable_ref = &mut data;
    mutable_ref.push(4); // 允许修改
    
    // println!("First element: {}", shared_ref);
    // 错误!在可变借用后不能再使用之前的共享引用 [6](@ref)
}

在函数签名中明确使用引用可以避免所有权的转移。

复制代码
// 使用共享引用作为参数:只读,不获取所有权
fn calculate_length(s: &String) -> usize {
    s.len()
} // 这里 s 离开作用域,但由于它是引用,不拥有所有权,所以不会丢弃任何数据 [7](@ref)

// 使用独占引用作为参数:可修改,不获取所有权
fn modify_string(s: &mut String) {
    s.push_str(", world!");
}

fn main() {
    let s = String::from("hello");
    let len = calculate_length(&s); // 传递共享引用
    println!("The length of '{}' is {}.", s, len); // s 仍然有效

    let mut s_mut = String::from("hello");
    modify_string(&mut s_mut); // 传递独占引用
    println!("Modified string: {}", s_mut); // 输出 "hello, world!"
}

4.8. 不一样的枚举enum

和C++的命名整型常量完全不一样,更像是 C++中的类

复制代码
// C++
enum class Color { Red, Green, Blue }; // 每个成员仅代表一个整数值
// Color myColor = Color::Red;

// RUST
enum Message {
    Quit, // 无关联数据
    Move { x: i32, y: i32 }, // 匿名结构体
    Write(String), // 一个 String
    ChangeColor(i32, i32, i32), // 三个 i32
}
let msg = Message::Write(String::from("Hello"));

这种设计让 Rust 的枚举非常适合用于构建状态机、处理事件或消息

  1. 模式匹配

选择 if let :当你只关心一种匹配情况,并且对其他情况不感兴趣或只需简单处理时

选择 let else :当你需要解构一个值并获取其内部数据 ,但如果解构失败需要立即报告错误或提前返回时。它能有效减少嵌套,让主逻辑更突出

选择 while let :当你需要持续地从某个操作(如迭代器、弹出栈等)中提取值,直到该操作无法再产生匹配模式的值时

别忘了 match :当你需要处理所有可能的情况 时,match仍然是必不可少的选择,因为它强制进行穷尽性检查

复制代码
// match
#[rustfmt::skip]
fn main() {
    let input = 'x';
    match input {
        'q'                       => println!("Quitting"),
        'a' | 's' | 'w' | 'd'     => println!("Moving around"),
        '0'..='9'                 => println!("Number input"),
        key if key.is_lowercase() => println!("Lowercase: {key}"),
        _                         => println!("Something else"),
    }
}

// if let
fn sleep_for(secs: f32) {
    if let Ok(dur) = Duration::try_from_secs_f32(secs) {
        std::thread::sleep(dur);
        println!("slept for {:?}", dur);
    }
}

// let else 
fn hex_or_die_trying(maybe_string: Option<String>) -> Result<u32, String> {
    let s = if let Some(s) = maybe_string {
        s
    } else {
        return Err(String::from("got None"));
    };
    
}

// while let
// Some(c)是 ​Option枚举的一个变体,表示“有一个值,这个值是 c”。
fn main() {
    let mut name = String::from("Comprehensive Rust 🦀");
    while let Some(c) = name.pop() {
        println!("character: {c}");
    }
    // (There are more efficient ways to reverse a string!)
}

4.9. trait特征

和golang的接口很像,区别是Rust :必须显式 使用 impl Trait for Type来声明一个类型实现了某个 trait。这是一种意图的明确声明,代码可读性更强,编译器也能更早地捕获错误。

复制代码
trait Pet {
    fn talk(&self) -> String;
    fn greet(&self) {
        println!("Oh you're a cutie! What's your name? {}", self.talk());
    }
}
struct Dog {
    name: String,
    age: i8,
}
impl Pet for Dog {
    fn talk(&self) -> String {
        format!("Woof, my name is {}!", self.name)
    }
}

关联类型

和泛型很像,区别就是更简洁,无需在 trait 名称或方法调用中暴露额外的类型参数

复制代码
// trait 定义:使用关联类型 Item
trait Contains {
    type Item; // 关联类型占位符
    fn contains(&self, element: Self::Item) -> bool;
}

struct MyContainer(i32);

// 为 MyContainer 实现 Contains trait
impl Contains for MyContainer {
    type Item = i32; // 指定关联类型为 i32

    fn contains(&self, element: Self::Item) -> bool {
        self.0 == element
    }
}


// trait 定义:使用泛型参数 E (Element)
trait Contains<E> {
    fn contains(&self, element: E) -> bool;
}

// 一个简单的结构体,包装一个 i32
struct MyContainer(i32);

// 为 MyContainer 实现 Contains trait,指定泛型 E 为 i32
impl Contains<i32> for MyContainer {
    fn contains(&self, element: i32) -> bool {
        self.0 == element
    }
}

4.10. 派生功能是通过宏实现的

Rust 中的 #[derive(...)]是一种属性宏(Attribute Macro),用于让编译器自动为你的类型(结构体、枚举或联合体)实现指定的 trait。它极大地简化了代码,避免了手动编写重复的样板代码。

|---------|------------------------------------------------------------------------|
| Trait | 作用 |
| Debug | 生成用于调试输出的代码,允许使用 {:?}或 {:#?}格式化符号打印结构体内容。 |
| Clone | 提供 .clone()方法,用于创建值的深拷贝(deep copy)。这意味着会完整复制结构体及其所有数据(包括 String等堆上数据)。 |
| Default | 提供 ::default()关联函数,返回一个所有字段均为默认值的实例。 |

复制代码
#[derive(Debug, Clone, Default)]
struct Player {
    name: String,
    strength: u8,
    hit_points: u8,
}

fn main() {
    let p1 = Player::default(); // Default trait adds `default` constructor.
    let mut p2 = p1.clone(); // Clone trait adds `clone` method.
    p2.name = String::from("EldurScrollz");
    // Debug trait adds support for printing with `{:?}`.
    println!("{:?} vs. {:?}", p1, p2);
}
// Player { name: "", strength: 0, hit_points: 0 } vs. Player { name: "EldurScrollz", strength: 0, hit_points: 0 }

4.11. 特征边界

特征边界是 Rust 泛型编程的核心概念之一,用于约束泛型类型必须实现某些特定的行为(即特征)。

复制代码
// 要求类型 T 必须实现 Clone trait

// 写法1
fn duplicate<T: Clone>(a: T) -> (T, T) { 
    (a.clone(), a.clone())
}

// 写法2
fn duplicate<T>(a: T) -> (T, T)
where
    T: Clone, // 使用 where 子句声明约束
{
    (a.clone(), a.clone())
}

// 写法3
fn duplicate(a: impl Clone) -> (impl Clone, impl Clone) { // 参数和返回值位置都可以用
    (a.clone(), a.clone())
}

4.12. 重要的内置enum类型

Option和Result

在许多编程语言中,经常用 nullnil来表示"无值"。但直接访问一个 null引用常常导致运行时错误(空指针异常)。Rust 通过 Option<T>类型,在编译阶段就强制你必须处理值可能为 None的情况,从而避免了这类运行时错误

复制代码
let name = "Löwe 老虎 Léopard Gepardi";
let mut position: Option<usize> = name.find('é'); // 查找字符返回 Option<usize>
println!("find returned {position:?}"); // 打印: find returned Some(14)
assert_eq!(position.unwrap(), 14); // 通过 unwrap 取出 Some 中的值

position = name.find('Z'); // 查找一个不存在的字符
println!("find returned {position:?}"); // 打印: find returned None
// assert_eq!(position.expect("Character not found"), 0); // 如果为 None,expect 会 panic 并附带错误信息


match position {
    Some(idx) => println!("Found at index: {}", idx),
    None => println!("Character not found."),
}

Result 是 Rust 中用于处理可恢复错误的核心枚举类型。它强制开发者显式处理操作可能成功或失败的场景

复制代码
use std::fs::File;
use std::io::Read;

fn main() {
    let file: Result<File, std::io::Error> = File::open("diary.txt");
    match file {
        Ok(mut file) => { // 文件打开成功,file 绑定为 File 类型
            let mut contents = String::new();
            if let Ok(bytes) = file.read_to_string(&mut contents) {
                println!("Dear diary: {contents} ({bytes} bytes)");
            } else {
                println!("Could not read file content");
            }
        }
        Err(err) => { // 文件打开失败,err 绑定为 std::io::Error 类型
            println!("The diary could not be opened: {err}");
        }
    }
}

4.13. 内部可变性CellRefCell

内部可变性是一种设计模式,它允许你在仅持有数据的不可变引用时,也能修改数据本身 。这听起来似乎违反了 Rust 的基本借用规则,但 CellRefCell通过一些巧妙的机制(或是在编译时确保安全,或是在运行时动态检查)使得这一操作变得安全。

个典型的场景是实现某个外部定义的 Trait 。假设一个库定义了一个消息发送接口,出于设计考虑,它要求 send方法接收不可变引用 &self

复制代码
// 外部库定义的Trait,我们无法修改
pub trait Messenger {
    fn send(&self, msg: String); // 注意:这里是 &self,不是 &mut self
}

但你在实现这个Trait时,需要将消息加入一个内部缓存队列。这时,你就需要在 &self的方法内部修改数据。如果没有内部可变性,这是不可能的。而 RefCell就派上了用场 :

复制代码
use std::cell::RefCell;

struct MyMessenger {
    message_cache: RefCell<Vec<String>>, // 使用RefCell包裹缓存
}

impl Messenger for MyMessenger {
    fn send(&self, msg: String) {
        // 在不可变引用&self的方法内,通过borrow_mut获取可变引用并修改数据
        self.message_cache.borrow_mut().push(msg);
    }
}

Cell的话适用于简单的数据结构,类似于bool,整型啥的:

复制代码
use std::cell::Cell;

struct Person {
    age: Cell<u32>, // 即使Person实例不可变,age字段也能变
}

impl Person {
    fn have_birthday(&self) { // 注意:这里接收的是不可变引用 &self
        let current_age = self.age.get();
        self.age.set(current_age + 1); // 但可以修改Cell内部的字段
    }
}

fn main() {
    let person = Person { age: Cell::new(30) }; // person本身不是mut
    person.have_birthday();
    println!("Age after birthday: {}", person.age.get());
}

4.14. 生命周期

Rust 的生命周期是其所有权系统的重要组成部分,核心目标是在编译期确保引用始终有效 ,从而避免悬垂指针等内存安全问题。当编译器无法自动推断引用关系时,需要显式标注生命周期 。生命周期参数以撇号开头,通常使用短小的名称(如 'a)。

复制代码
// case 1
// Highlight 注释会强制包含 &str 的底层数据的生命周期, 
// 至少与使用该数据的任何 Highlight 实例一样长。
// 如果 text 在 fox(或 dog)的生命周期结束前被消耗,借用检查器将抛出一个错误。
    
#[derive(Debug)]
struct Highlight<'doc>(&'doc str);
fn erase(text: String) {
    println!("Bye {text}!");
}
fn main() {
    let text = String::from("The quick brown fox jumps over the lazy dog.");
    let fox = Highlight(&text[4..19]);
    let dog = Highlight(&text[35..43]);
    // erase(text);
    println!("{fox:?}");
    println!("{dog:?}");
}

// case 2
// 返回值的生命周期与它真正依赖的数据源(即 points切片)的生命周期关联起来。
fn nearest<'a>(points: &'a [Point], query: &Point) -> Option<&'a Point> {
    // ... 函数体 ...
}

// case 3
fn main() {
    let r;
    {
        let x = 5;
        r = &x; // 错误:`x` 的生命周期不够长
    }
    println!("r: {}", r);
}

4.15. 文档测试

  • /// 注释中的代码块会自动被视为 Rust 代码。
  • 代码会作为 cargo test 的一部分进行编译和执行。
复制代码
#![allow(unused)]
fn main() {
/// Shortens a string to the given length.
///
/// ```
/// # use playground::shorten_string;
/// assert_eq!(shorten_string("Hello World", 5), "Hello");
/// assert_eq!(shorten_string("Hello World", 20), "Hello World");
/// ```
pub fn shorten_string(s: &str, length: usize) -> &str {
    &s[..std::cmp::min(length, s.len())]
}
}

4.16. 尝试运算符

复制代码
match some_expression {
    Ok(value) => value,
    Err(err) => return Err(err),
}
等同于
some_expression?

比如:
let username_file_result = fs::File::open(path);
    let mut username_file = match username_file_result {
        Ok(file) => file,
        Err(err) => return Err(err),
    };


let mut username_file = fs::File::open(path)?; // 使用 ? 操作符简化文件打开的错误处理

4.17. anyhow和thiserror宏

anyhow的设计初衷是让应用程序中的错误处理变得无比简单。它提供了一个通用的错误类型 anyhow::Error,使得你无需在每个函数签名中声明具体的错误类型。可以无缝使用 ?来传播错误,anyhow会自动进行类型转换 。

复制代码
use anyhow::{Context, Result};

fn read_user_data(user_id: u32) -> Result<String> {
    let path = format!("data/{}.json", user_id);
    // 使用 ? 传播错误,并使用 .context() 添加上下文
    let data = std::fs::read_to_string(&path)
        .with_context(|| format!("Failed to read user data file for user {}", user_id))?;

    // 解析JSON,同样使用 ? 传播错误(例如来自 serde_json 的错误)
    let value: serde_json::Value = serde_json::from_str(&data)
        .context("Failed to parse user data as JSON")?;

    Ok(value.to_string())
}

fn main() -> Result<()> {
    let data = read_user_data(42)?;
    println!("User data: {}", data);
    Ok(())
}

thiserror的核心价值在于让你能轻松地定义结构清晰、信息丰富的自定义错误类型,特别适合在库开发中使用,因为它为库的使用者提供了精确的错误信息,方便他们进行不同的处理。使用 #[error("...")]为每个错误变体定义人类可读的错误信息,并支持字符串插值

复制代码
#[derive(Debug, Error)]
enum ParserError {
    #[error("Tokenizer error: {0}")]
    TokenizerError(#[from] TokenizerError),
    #[error("Unexpected end of input")]
    UnexpectedEOF,
    #[error("Unexpected token {0:?}")]
    UnexpectedToken(Token),
    #[error("Invalid number")]
    InvalidNumber(#[from] std::num::ParseIntError),
}

4.18. 线程thread

thread::spawn的基本使用非常简单:你传递一个闭包(closure)给它,闭包中的代码将在新线程中运行,并且不阻塞主线程

复制代码
use std::thread;
use std::time::Duration;

fn main() {
    // 创建一个新线程
    let handle = thread::spawn(|| {
        for i in 1..5 {
            println!("子线程: {}", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    // 主线程继续执行自己的任务
    for i in 1..3 {
        println!("主线程: {}", i);
        thread::sleep(Duration::from_millis(1));
    }

    // 等待新线程执行完毕
    handle.join().unwrap();
}

在线程闭包中如果需要使用外部变量,常常需要配合 move关键字来转移变量的所有权。这是因为新线程的生命周期可能长于创建它的函数,Rust 的所有权系统可以防止悬垂引用等内存安全问题

复制代码
use std::thread;

fn main() {
    let data = vec![1, 2, 3, 4, 5];

    // 使用 move 将 data 的所有权转移到新线程的闭包中
    let handle = thread::spawn(move || {
        // 现在 data 属于这个新线程
        for num in data {
            println!("处理数字: {}", num);
        }
        // data 在这里被丢弃
    });

    // 在主线程中不能再使用 data,因为所有权已经转移
    // println!("{:?}", data); // 这行代码会导致编译错误

    handle.join().unwrap(); 输出:线程返回的结果
}

线程间的通信使用channel

复制代码
use std::thread;
use std::sync::mpsc; // mpsc: Multiple Producer, Single Consumer

fn main() {
    // 创建一个通道
    let (tx, rx) = mpsc::channel();

    // 在新线程中发送消息
    thread::spawn(move || {
        let message = String::from("你好,主线程!");
        tx.send(message).unwrap(); // 发送消息
        // 注意:发送后,message 的所有权也转移了
    });

    // 在主线程中接收消息
    // recv() 会阻塞,直到收到消息
    let received = rx.recv().unwrap();
    println!("收到: {}", received); // 输出:收到: 你好,主线程!
}

4.19. Send和Sync trait

SendSync是 Rust 语言中保证线程安全的核心标记 trait(marker trait),它们在编译期通过静态检查来防止数据竞争。简单来说,它们定义了数据能否安全地在线程间"移动"或"共享"。

Send 关乎所有权移动。它回答"这个数据能安全地交给另一个线程吗?"

Sync 关乎引用共享。它回答"这个数据的只读引用能安全地同时给多个线程使用吗?"

Send标记一个类型可以安全地将其所有权从一个线程移动到另一个线程。绝大多数 Rust 标准库类型都实现了 Send

复制代码
use std::thread;
use std::rc::Rc; // Rc<T> 没有实现 Send

fn main() {
    let data = vec![1, 2, 3, 4, 5]; // Vec<T> 实现了 Send

    // let data = Rc::new(42); // Rc<T> 没有实现 Send,因此会报错
    // 这是因为 Rc的引用计数更新不是原子操作,
    // 线程同时修改会导致计数错误。此时应使用线程安全的 Arc(原子引用计数)

    // 使用 move 关键字将 data 的所有权转移到新线程
    let handle = thread::spawn(move || {
        println!("Data in new thread: {:?}", data);
        // 在这里,data 属于这个新线程
    });

    handle.join().unwrap();
    // 此后,主线程不能再使用 data,因为所有权已经转移
}

Sync标记一个类型可以安全地被多个线程同时共享其不可变引用 (&T)。更准确地说,T: Sync意味着 &T: Send,即你可以安全地将一个不可变引用发送到另一个线程使用。

复制代码
use std::sync::Arc;
use std::thread;

fn main() {
    let data = Arc::new(42); // Arc<T> 实现了 Sync(当 T: Send + Sync 时)

    let mut handles = vec![];

    for i in 0..5 {
        let data_clone = Arc::clone(&data); // 克隆 Arc,增加引用计数
        let handle = thread::spawn(move || {
            // 多个线程可以同时安全地读取 data_clone 指向的数据
            println!("Thread {}: {}", i, data_clone);
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }
}

use std::thread;
use std::cell::RefCell; // RefCell<T> 没有实现 Sync

fn main() {
    let data = RefCell::new(42);

    let handle = thread::spawn(move || { // 这里会触发编译错误!
        println!("{}", data.borrow());
    });

    handle.join().unwrap();
}

4.20. Mutex

在 Rust 的并发编程中,Mutex(互斥锁)是保护共享数据、防止数据竞争的核心工具之一。

Mutex<T>本身是一个智能指针,对其调用 lock方法会返回一个 MutexGuard<T>,该守卫实现了 DerefDroptrait,方便直接操作数据并自动释放锁。

复制代码
use std::sync::Mutex;

fn main() {
    // 创建一个保护 i32 类型数据的 Mutex,初始值为 5
    let mux = Mutex::new(5);

    {
        // 获取锁,返回一个 MutexGuard
        // lock() 会阻塞当前线程,直到获取到锁
        // 使用 unwrap() 在成功获取锁后解出守卫,如果锁中毒(poisoned)则会 panic
        let mut num = mux.lock().unwrap();
        *num = 10; // 通过 Deref 解引用修改数据
        println!("Value inside mutex: {}", *num);
        // 作用域结束,MutexGuard 被 drop,锁自动释放
    }

    // 再次获取锁,确认值已被修改
    println!("Now mux = {:?}", mux); // 输出: Mutex { data: 10, poisoned: false, .. }
}

要在多个线程间共享同一个 Mutex,需要使用 Arc(原子引用计数智能指针),因为它实现了 SendSync,可以安全地将所有权转移到线程中。

复制代码
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    // 使用 Arc 包装 Mutex,以实现多线程间的共享所有权
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        // 克隆 Arc指针,增加引用计数,让另一个线程也获得指向同一个 Mutex的权限。
        // 绝不是复制数据或锁本身。
        // 克隆 Arc 的引用计数,每个线程持有其一份克隆
        // 它让多个线程都能“拥有”访问同一份数据的权利。
        let counter_clone = Arc::clone(&counter);
        let handle = thread::spawn(move || { // 将克隆的所有权移入线程
            let mut num = counter_clone.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }

    // 等待所有线程结束
    for handle in handles {
        handle.join().unwrap();
    }

    // 打印最终结果
    println!("Final counter: {}", *counter.lock().unwrap()); // 输出: Final counter: 10
}

有一些注意事项:

  1. 锁中毒(Poisoning) :如果一个线程在持有 Mutex锁时发生 panic,Mutex会标记自己为"中毒"状态。后续尝试 lock时会返回 Err。你可以选择处理这个错误或直接 unwrap让当前线程也 panic,这取决于你的应用场景
  2. 死锁(Deadlock):如果两个或多个线程各自持有一些锁,并同时等待对方释放锁,就可能发生死锁,所有相关线程都会被无限期阻塞。避免死锁需要仔细设计锁的获取顺序
  3. 性能考量Mutex的锁操作有一定开销。应尽量缩小锁的持有时间,即获取锁后尽快完成操作并释放锁,避免在持有锁的情况下执行耗时操作或等待I/O

4.21. 异步Tokio

Tokio 是 Rust 生态中最主流的异步运行时 ,它让你能够编写高性能、高并发的网络应用程序。简单来说,Tokio 为 Rust 的 async/await语法提供了底层的执行引擎,让你能用同步代码的书写风格,获得异步非阻塞的性能优势。

|--------|------------------------|-----------------------------|
| 核心组件 | 主要作用 | 通俗理解 |
| 异步 I/O | 提供非阻塞的网络(TCP/UDP)和文件操作 | 让程序在等待数据时不去"干等",而是去处理其他任务 |
| 任务调度器 | 管理并执行大量的异步任务(Future) | 一个高效的"任务指挥官",确保 CPU 核心被充分利用 |
| 定时器 | 处理延迟、超时和周期性任务 | 一个精准的"异步闹钟" |
| 同步原语 | 为异步环境提供互斥锁、信道等工具 | 让多个异步任务可以安全地共享和传递数据 |

复制代码
// 1. 顺序执行
use tokio::time::{sleep, Duration};

// 定义一个异步函数
async fn say_after(delay: u64, msg: &str) {
    sleep(Duration::from_secs(delay)).await; // 使用 .await 等待异步操作,不会阻塞线程
    println!("{}", msg);
}

#[tokio::main] // 这个宏将 main 函数转换为异步函数并启动运行时
async fn main() {
    // 使用 `.await` 按顺序执行
    say_after(1, "Hello").await;
    say_after(2, "World").await;
}

// 2. 并发执行
#[tokio::main]
async fn main() {
    // 使用 `spawn` 并发执行两个任务
    let task1 = tokio::spawn(async {
        say_after(2, "Task 1 completed").await;
    });
    let task2 = tokio::spawn(async {
        say_after(1, "Task 2 completed").await; // 这个任务先完成
    });

    // 等待两个任务都完成
    let _ = tokio::join!(task1, task2);
    println!("All tasks done!");
}

// 3.异步环境下的数据共享
use tokio::sync::Mutex;
use std::sync::Arc;

#[tokio::main]
async fn main() {
    // 创建一个受保护的可变计数器
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        // 创建10个并发任务,每个任务对计数器加1
        let handle = tokio::spawn(async move {
            let mut num = counter.lock().await; // 异步获取锁,不会阻塞线程
            *num += 1;
        });
        handles.push(handle);
    }

    // 等待所有任务完成
    for handle in handles {
        handle.await.unwrap();
    }

    // 打印最终结果
    println!("Final count: {}", *counter.lock().await); // 输出:Final count: 10
}

与标准库的同步 std::sync::Mutex不同,tokio::sync::Mutexlock方法是异步的。在等待锁时,它会让出线程控制权,允许运行时执行其他任务,从而极大提升程序的并发吞吐量,特别适合存在锁竞争但锁持有时间很短的场景。

5. 小巧的Rust开源项目-Nping源码阅读

Nping 是一个基于 Rust 开发的终端可视化 Ping 工具, 支持多地址并发 Ping, 可视化图表展示, 数据实时更新等特性。

让我们看看他是如何实现的,核心架构如下:

  1. 主流程 main.rs

命令行参数解析,使用了clap库

复制代码
#[derive(Parser, Debug)]
#[command(
    version = "v0.5.0",
    author = "hanshuaikang<https://github.com/hanshuaikang>",
    about = "🏎  Nping mean NB Ping, A Ping Tool in Rust with Real-Time Data and Visualizations"
)]
struct Args {
    /// Target IP address or hostname to ping
    #[arg(help = "target IP address or hostname to ping", required = true)]
    target: Vec<String>,

    /// Number of pings to send
    #[arg(short, long, default_value_t = 65535, help = "Number of pings to send")]
    count: usize,

    /// Interval in seconds between pings
    #[arg(short, long, default_value_t = 0, help = "Interval in seconds between pings")]
    interval: i32,

    #[clap(long = "force_ipv6", default_value_t = false, short = '6', help = "Force using IPv6")]
    pub force_ipv6: bool,

    // ... 其他参数
}

要ping哪些ip,首先先去重,然后根据ip数量计算工作线程:

复制代码
// 去重但保持原有顺序
    let mut seen = HashSet::new();
    let targets: Vec<String> = args.target.into_iter()
        .filter(|item| seen.insert(item.clone()))
        .collect();

// 根据 IP 数量计算工作线程数
let ip_count = if targets.len() == 1 && args.multiple > 0 {
    args.multiple as usize
} else {
    targets.len()
};
let worker_threads = (ip_count + 1).max(1);

使用tokio库,创建多线程异步执行run_app这个函数:

复制代码
// 创建具有特定工作线程数的 tokio 运行时
let rt = Builder::new_multi_thread()
    .worker_threads(worker_threads)
    .enable_all()
    .build()?;

// 运行异步应用
let res = rt.block_on(run_app(targets, args.count, args.interval, running.clone(), args.force_ipv6, args.multiple, args.view_type, args.output));

多线程并发执行的核心在run_app里面
for (i, ip) in ips.iter().enumerate() {
    let ip = ip.clone();
    let running = running.clone();
    let errs = errs.clone();
    let task = task::spawn({  // 这里创建异步任务
        // 每个 IP 地址的任务,去执行ping命令,使用的是pinger库
        async move {
            send_ping(addr, ip, errs.clone(), count, interval, running.clone(), ping_event_tx).await.unwrap();
        }
    });
    tasks.push(task)
}

使用channel将ping结果发给数据处理模块,然后数据处理模块再发给ui模块

复制代码
// ping event channel (network -> data processor)
let (ping_event_tx, ping_event_rx) = mpsc::sync_channel::<PingEvent>(0);

// ui data channel (data processor -> ui)
let (ui_data_tx, ui_data_rx) = mpsc::sync_channel::<IpData>(0);

let ping_event_tx = Arc::new(ping_event_tx);

启动两个线程任务,一个数据处理,一个UI

复制代码
start_data_processor(
    ping_event_rx,
    ui_data_tx,
    targets_for_processor,
    view_type.clone(),
    running.clone(),
);

let ui_task = task::spawn(async move {
    let mut guard = terminal_guard_for_ui.lock().unwrap();
    draw::draw_interface_with_updates(
        &mut guard.terminal.as_mut().unwrap(),
        &view_type_for_ui,
        &ip_data_for_ui,
        ui_data_rx,
        running_for_ui,
        errs_for_ui,
        output_file,
    ).ok();
});

所以线程都可以通过running: Arc<Mutex<bool>> 进行控制退出
一般是ctrl + c 会使 running =false

send_ping的具体实现,利用ping库:

复制代码
let stream = ping(options)?;
 match stream.recv() {
    Ok(result) => {
        match result {
            ... ...

 // result枚举如下           
 pub enum PingResult {
    Pong(Duration, String),
    Timeout(String),
    Unknown(String),
    PingExited(ExitStatus, String),
}

// 发给数据处理
let _ = tx.send(PingResult::PingExited(result.status, decoded_stderr));
  • 数据处理模块 data_processor.rs
复制代码
pub fn process_event(&mut self, event: PingEvent) -> Option<IpData> {
    match event {
        PingEvent::Success { addr, ip, rtt, .. } => {
            let key = format!("{}_{}", addr, ip);
            if let Some(data) = self.data_map.get_mut(&key) {
                Self::update_success_stats(data, rtt, self.point_num);
                Some(data.clone())
            } else {
                None
            }
        },
        PingEvent::Timeout { addr, ip, .. } => {
            let key = format!("{}_{}", addr, ip);
            if let Some(data) = self.data_map.get_mut(&key) {
                Self::update_timeout_stats(data, self.point_num);
                Some(data.clone())
            } else {
                None
            }
        },
    }
}

更新这个解构
pub struct IpData {
    pub(crate) addr: String,
    pub(crate) ip: String,
    pub(crate) rtts: VecDeque<f64>,
    pub(crate) last_attr: f64,
    pub(crate) min_rtt: f64,
    pub(crate) max_rtt: f64,
    pub(crate) timeout: usize,
    pub(crate) received: usize,
    pub(crate) pop_count: usize,
}
  • ui模块 ui目录

所有视图都使用ratatui库构建终端用户界面:

  • 使用Layout进行布局管理
  • 使用Constraint定义各部分大小比例
  • 使用不同颜色表示不同状态(绿色正常、黄色警告、红色错误)
  • 支持实时更新和键盘交互(q/Esc/Ctrl+C退出)

以graph为例:

这是默认视图,主要特点:

  • 每行显示最多5个目标的详细信息
  • 每个目标显示:
    • 目标地址和IP
    • 最后、平均、最大、最小RTT延迟
    • 抖动(Jitter)和丢包率(Loss)
    • 实时延迟图表(使用线条图)
    • 最近5条记录
  • 底部显示错误信息
复制代码
// 函数接受帧对象、IP 数据和错误信息作为参数
// 计算需要显示的行数(每行最多显示5个目标)
// 初始化布局块容器
pub fn draw_graph_view<B: Backend>(
    f: &mut Frame,
    ip_data: &[IpData],
    errs: &[String]) {
    let size = f.area();
    let rows = (ip_data.len() as f64 / 5.0).ceil() as usize;
    let mut chunks = Vec::new();
    
// 这部分循环处理每一行,每行最多显示5个目标的监控信息。
for (row, vertical_chunk) in vertical_chunks.iter().enumerate().take(rows) {
    let start = row * 5;
    let end = (start + 5).min(ip_data.len());
    let row_data = &ip_data[start..end];    

// 水平布局约束
let horizontal_constraints: Vec<Constraint> = if row_data.len() == 5 {
    row_data.iter().map(|_| Constraint::Percentage(20)).collect()
} else {
    // when the number of targets is less than 5, we need to adjust the size of each target
    let mut size = 100;
    if ip_data.len() > 5 {
        size = row_data.len() * 20;
    }
    row_data.iter().map(|_| Constraint::Percentage(size as u16 / row_data.len() as u16)).collect()
};

// 单目标渲染

// 计算延迟    
et loss_pkg = if data.timeout > 0 {
    (data.timeout as f64 / (data.received as f64 + data.timeout as f64)) * 100.0
} else {
    0.0
};

let loss_pkg_color = if loss_pkg > 50.0 {
    Color::Red
} else if loss_pkg > 0.0 {
    Color::Yellow
} else {
    Color::Green
};

// 计算其他指标
 let base_metric_text = Line::from(vec![
       Span::styled("Last: ", Style::default()),
       Span::styled(
           if data.last_attr == 0.0 {
               "< 0.01ms".to_string()
           } else if data.last_attr == -1.0 {
               "0.0ms".to_string()
           } else {
               format!("{:?}ms", data.last_attr)
           },
           Style::default().fg(Color::Green)
       ),
       // ... 其他指标
   ]);

6. Reference

https://www.bookstack.cn/read/comprehensive-rust-202412-zh/c01514b4d45220e0.md

rust 使用国内镜像,快速安装方法

https://github.com/hanshuaikang/Nping

相关推荐
芥子沫3 小时前
Docker安装思源笔记&使用指南
笔记·docker·容器·思源笔记
递归不收敛3 小时前
三、检索增强生成(RAG)技术体系
人工智能·笔记·自然语言处理
im_AMBER3 小时前
React 06
前端·javascript·笔记·学习·react.js·前端框架
autism_cx4 小时前
TCP/IP协议栈
服务器·网络·笔记·网络协议·tcp/ip·ios·osi
报错小能手4 小时前
C++笔记(面向对象)对于对象返回方式的讲解
笔记
zyq99101_15 小时前
树与二叉树的奥秘全解析
c语言·数据结构·学习·1024程序员节
微露清风5 小时前
系统性学习C++-第七讲-string类
java·c++·学习
Olrookie5 小时前
StreamX部署详细步骤
大数据·笔记·flink
报错小能手5 小时前
项目——基于C/S架构的预约系统平台(3)
linux·开发语言·笔记·学习·架构·1024程序员节