首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【笔记】Comprehensive Rust语言学习

【笔记】Comprehensive Rust语言学习

原创
作者头像
于顾而言SASE
发布2025-10-23 14:38:57
发布2025-10-23 14:38:57
15200
代码可运行
举报
文章被收录于专栏:网络安全随笔网络安全随笔
运行总次数:0
代码可运行

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官网的安装方式安装非常容易失败,即使不失败也非常非常慢 如果用国内的镜像则可以分分钟就搞定

代码语言:javascript
代码运行次数:0
运行
复制
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验证一哈

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

4. 令人印象深刻的语法

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

4.1. 变量默认是不可变的

代码语言:javascript
代码运行次数:0
运行
复制
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

代码语言:javascript
代码运行次数:0
运行
复制
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 都可以选择接受一个标签参数,用来 终止嵌套循环

代码语言:javascript
代码运行次数:0
运行
复制
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)执行操作但不返回值,其类型永远是单元类型 ()

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

代码语言:javascript
代码运行次数:0
运行
复制
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。
代码语言:javascript
代码运行次数:0
运行
复制
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.

代码语言:javascript
代码运行次数:0
运行
复制
fn main() {
    let mut a: [i8; 10] = [42; 10];
    a[5] = 0;
    println!("a: {a:?}");
}

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

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

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

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

代码语言:javascript
代码运行次数:0
运行
复制
// ❌ 如果不用 &,会发生所有权转移
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 仍然可以使用
}

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

代码语言:javascript
代码运行次数:0
运行
复制
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 的借用检查器严格执行“独占”规则。

代码语言:javascript
代码运行次数:0
运行
复制
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)
}

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

代码语言:javascript
代码运行次数:0
运行
复制
// 使用共享引用作为参数:只读,不获取所有权
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++中的类

代码语言:javascript
代码运行次数:0
运行
复制
// 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仍然是必不可少的选择,因为它强制进行穷尽性检查

代码语言:javascript
代码运行次数:0
运行
复制
// 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。这是一种意图的明确声明,代码可读性更强,编译器也能更早地捕获错误。

代码语言:javascript
代码运行次数:0
运行
复制
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 名称或方法调用中暴露额外的类型参数

代码语言:javascript
代码运行次数:0
运行
复制
// 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()关联函数,返回一个所有字段均为默认值的实例。

代码语言:javascript
代码运行次数:0
运行
复制
#[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 泛型编程的核心概念之一,用于约束泛型类型必须实现某些特定的行为(即特征)。

代码语言:javascript
代码运行次数:0
运行
复制
// 要求类型 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的情况,从而避免了这类运行时错误

代码语言:javascript
代码运行次数:0
运行
复制
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 中用于处理可恢复错误的核心枚举类型。它强制开发者显式处理操作可能成功或失败的场景

代码语言:javascript
代码运行次数:0
运行
复制
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

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

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

代码语言:javascript
代码运行次数:0
运行
复制
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,整型啥的:

代码语言:javascript
代码运行次数:0
运行
复制
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)。

代码语言:javascript
代码运行次数:0
运行
复制
// 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 的一部分进行编译和执行。
代码语言:javascript
代码运行次数:0
运行
复制
#![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. 尝试运算符

代码语言:javascript
代码运行次数:0
运行
复制
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会自动进行类型转换 。

代码语言:javascript
代码运行次数:0
运行
复制
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("...")]为每个错误变体定义人类可读的错误信息,并支持字符串插值

代码语言:javascript
代码运行次数:0
运行
复制
#[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)给它,闭包中的代码将在新线程中运行,并且不阻塞主线程

代码语言:javascript
代码运行次数:0
运行
复制
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 的所有权系统可以防止悬垂引用等内存安全问题

代码语言:javascript
代码运行次数:0
运行
复制
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

代码语言:javascript
代码运行次数:0
运行
复制
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

代码语言:javascript
代码运行次数:0
运行
复制
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,即你可以安全地将一个不可变引用发送到另一个线程使用。

代码语言:javascript
代码运行次数:0
运行
复制
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,方便直接操作数据并自动释放锁。

代码语言:javascript
代码运行次数:0
运行
复制
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,可以安全地将所有权转移到线程中。

代码语言:javascript
代码运行次数:0
运行
复制
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 核心被充分利用

定时器

处理延迟、超时和周期性任务

一个精准的“异步闹钟”

同步原语

为异步环境提供互斥锁、信道等工具

让多个异步任务可以安全地共享和传递数据

代码语言:javascript
代码运行次数:0
运行
复制
// 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库

代码语言:javascript
代码运行次数:0
运行
复制
#[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数量计算工作线程:

代码语言:javascript
代码运行次数:0
运行
复制
// 去重但保持原有顺序
    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这个函数:

代码语言:javascript
代码运行次数:0
运行
复制
// 创建具有特定工作线程数的 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模块

代码语言:javascript
代码运行次数:0
运行
复制
// 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

代码语言:javascript
代码运行次数:0
运行
复制
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
  • 网络模块 network.rs

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

代码语言:javascript
代码运行次数:0
运行
复制
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
代码语言:javascript
代码运行次数:0
运行
复制
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条记录
  • 底部显示错误信息
代码语言:javascript
代码运行次数:0
运行
复制
// 函数接受帧对象、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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 核心工具链
  • 1.1. Cargo
  • 1.2. rustc
  • 1.3. rustup
  • 2. VSCode开发环境搭建
  • 3. Rust优势
  • 4. 令人印象深刻的语法
  • 4.1. 变量默认是不可变的
  • 4.2. 变量类型是明确的
  • 4.3. Lables
  • 4.4. 分号: 表达式和语句分不清楚?
  • 4.5. 宏:可变参数的函数
  • 4.6. 数组的长度也是类型的一部分
  • 4.7. 所有权转移,独占引用和共享引用
  • 4.8. 不一样的枚举enum
  • 4.9. trait特征
  • 4.10. 派生功能是通过宏实现的
  • 4.11. 特征边界
  • 4.12. 重要的内置enum类型
  • 4.13. 内部可变性Cell和 RefCell
  • 4.14. 生命周期
  • 4.15. 文档测试
  • 4.16. 尝试运算符
  • 4.17. anyhow和thiserror宏
  • 4.18. 线程thread
  • 4.19. Send和Sync trait
  • 4.20. Mutex
  • 4.21. 异步Tokio
  • 5. 小巧的Rust开源项目-Nping源码阅读
  • 6. Reference
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档