Rust 基础

安装

  1. 官方提供安装脚本

安装细节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Rustup 元数据和工具链将安装到 Rustup 的
主目录,位于:
/home/fu01/.rustup
可以使用 RUSTUP_HOME 环境变量修改此目录。

Cargo 的主目录位于:
/home/fu01/.cargo
可以使用 CARGO_HOME 环境变量修改此目录。

cargo、rustc、rustup 和其他命令将被添加到 Cargo 的 bin 目录,该目录位于:
/home/fu01/.cargo/bin
然后,通过修改以下配置文件,将此路径添加到您的 PATH 环境变量中:
修改以下配置文件:
/home/fu01/.profile
/home/fu01/.bashrc
您可以随时使用 rustup self uninstall 命令卸载 Cargo,

基础示例

1
2
3
fn main() {
println!("Hello World!");
}

$ rustc runoob.rs # 编译 runoob.rs 文件

说明

构建和管理项目都是使用cargo

cargo 命令

  • build, b Compile the current package
  • check, c Analyze the current package and report errors, but don't build object files
  • clean Remove the target directory
  • doc, d Build this package's and its dependencies' documentation
  • new Create a new cargo package
  • init Create a new cargo package in an existing directory
  • add Add dependencies to a manifest file
  • remove Remove dependencies from a manifest file
  • run, r Run a binary or example of the local package
  • test, t Run the tests
  • bench Run the benchmarks
  • update Update dependencies listed in Cargo.lock
  • search Search registry for crates
  • publish Package and upload this package to the registry
  • install Install a Rust binary
  • uninstall Uninstall a Rust binary
  • … See all commands with --list

变量

rust 是强类型语言,能自动判断变量类型。变量定义后,所能接受的数值类型就会固定下来。

关键字 类型要求 内存特性 作用域与生命周期
**let / let mut** 可自动推导类型 存储在栈/堆中 局部作用域,超出作用域自动释放
const 必须显式指定类型 无固定内存地址(内联替换) 编译期确定,全程序周期可用
static 必须显式指定类型 有固定内存地址 全局存在(整个程序运行期间)
1
2
3
4
5
6
7
8
9
let a = 123; // 不可变变量
let mut b = 10; // 可变变量
const MAX_POINTS: u32 = 100_000;

// 绝对不可变的全局静态变量
static HELLO_WORLD: &str = "Hello, world!";

// 可变的全局静态变量(极不推荐,且读取/修改必须在 unsafe 块内)(不让用)
static mut COUNTER: u32 = 0;

运算符

  • 加法+ | 减法- | 乘法* | 除法(整除)/ | 取余%

  • 相等== | 不相等!= | 大于> | 小于< | 大于等于>= | 小于等于<=

  • 逻辑与(AND)&& | 逻辑或(OR)|| | 逻辑非(NOT)!

  • 按位与& | 按位或| | 按位异或^ | 按位取反! | 左移<< | 右移>>

  • 赋值= | 加并赋值+= | 减并赋值-= | 乘并赋值*= | 除并赋值/= | 取余并赋值%= | 位运算复合赋值&= |= ^= <<= >>=

  • 范围(不含右端).. | 范围(含右端)..= | 类型转换as | 错误传播(在 Result 中)? | 解引用* | 取引用& | 绑定为引用ref

    1. 0..5 产生 0 到 4
    2. 0..=5 产生 0 到 5
    3. 5 as f32
    4. some()?;
    5. *ptr
    6. &x
    7. let ref y = x;

数据类型

长度 有符号 (i) 无符号 (u)
8-bit i8 u8
16-bit i16 u16
32-bit i32 (默认) u32
64-bit i64 u64
128-bit i128 u128
取决于架构 isize usize

isize 和 usize 的位数由运行代码的计算机架构决定(64 位系统上是 64 位,32 位系统上是 32 位)

f32:单精度浮点数。 f64:双精度浮点数。

bool:true 和 false

char: 字符型(Unicode)

注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 这是第一种注释方式

/* 这是第二种注释方式 */

/*
* 多行注释
* 多行注释
* 多行注释
*/

说明文档的注释

/// Adds one to the number given.
///
/// # Examples
///
/// ```
/// let x = add(1, 2);
///
/// ```

函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn another_function() {
println!("Hello, runoob!");
}

fn another_function(x: i32, y: i32) {
println!("x 的值为 : {}", x);
println!("y 的值为 : {}", y);
}

函数体表达式:
let y = {
let x = 3;
x + 1
};
等价于 自动使用return `x + 1` 返回值给 y

-> 来声明函数返回值的类型
fn add(a: i32, b: i32) -> i32 {
return a + b;
}

条件语句

Rust 中的 if 不存在单语句不用加 {} 的规则

  1. 条件表达式必须是 bool 类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if number < 5 {
println!("条件为 true");
} else {
println!("条件为 false");
}

if a > 0 {
b = 1;
}
else if a < 0 {
b = -1;
}
else {
b = 0;
}

类似于三元条件运算表达式 (A ? B : C)
let number = if a > 0 { 1 } else { -1 };

循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
while number != 4 {
println!("{}", number);
number += 1;
}


let a = [10, 20, 30, 40, 50];
for i in a.iter() {
println!("值为 : {}", i);
}


let s = ['R', 'U', 'N', 'O', 'O', 'B'];
let mut i = 0;
loop {
let ch = s[i];
if ch == 'O' {
break;
}
println!("\'{}\'", ch);
i += 1;
}

// loop break 可以返回数值
let location = loop {
let ch = s[i];
if ch == 'O' {
break i;
}
i += 1;
};

迭代器

创建方法 每次迭代拿到的类型 说明 原始集合是否还能用?
.iter() &T(不可变引用) 只读借用元素 能,集合不受影响
.iter_mut() &mut T(可变引用) 借用并允许修改元素 能,集合被修改
.into_iter() T(转移所有权) 将元素直接移出集合 不能,集合被消费丢弃
1
2
3
4
5
6
7
8
9
10
11
12
13
let mut numbers = vec![1, 2, 3];

for num in numbers.iter() {
println!("读取: {}", num); // num 的类型是 &i32
}

for num in numbers.iter_mut() {
*num *= 10; // 解引用并修改值,numbers 变成了 [10, 20, 30]
}

for num in numbers.into_iter() {
println!("消费: {}", num); // num 的类型是 i32
}

将多个适配器组合在一起,构建一个复杂的处理流水线,最后调用消费器(如 collect())触发执行。

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// 需求:找出所有偶数,将它们乘以 10,然后存入一个新的 Vec 中
let result: Vec<i32> = numbers
.iter() // 1. 创建迭代器
.filter(|&x| x % 2 == 0) // 2. 过滤:只保留偶数
.map(|&x| x * 10) // 3. 映射:每个元素乘以 10
.collect(); // 4. 消费器:将结果收集回 Vec

println!("{:?}", result); // 输出: [20, 40, 60, 80, 100]
}

只有在调用 .collect() 时,前面的 filter 和 map 才会真正按需开始计算。

其他消费器

1
2
3
4
5
6
7
8
9
10
11
12
13
let numbers = vec![10, 20, 30, 40, 50];

// 1. .sum():求和
let total: i32 = numbers.iter().sum();
println!("总和: {}", total); // 150

// 2. .find():查找满足条件的第一个元素(返回 Option)
let first_large = numbers.iter().find(|&&x| x > 25);
println!("第一个大于 25 的数: {:?}", first_large); // Some(&30)

// 3. .any() / .all():断言检查
let has_even = numbers.iter().any(|&x| x % 2 == 0);
println!("是否有偶数: {}", has_even); // true

带索引的迭代 (.enumerate())

1
2
3
4
5
let fruits = vec!["apple", "banana", "orange"];

for (index, fruit) in fruits.iter().enumerate() {
println!("第 {} 个水果是: {}", index, fruit);
}

由于迭代器是“惰性”的,可以定义一个无限递增的序列,只在需要时截取部分元素:

1
2
3
4
5
6
7
8
9
fn main() {
// (1..) 生成从 1 开始到无穷大的范围
let squares: Vec<i32> = (1..)
.map(|x| x * x) // 计算平方
.take(5) // 消费器/限制器:只拿前 5 个元素(拿到 5 个就终止)
.collect();

println!("{:?}", squares); // 输出: [1, 4, 9, 16, 25]
}

自定义迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 定义一个计数器结构体
struct Counter {
count: u32,
max: u32,
}

impl Counter {
fn new(max: u32) -> Counter {
Counter { count: 0, max }
}
}

// 为 Counter 实现 Iterator 特征
impl Iterator for Counter {
type Item = u32; // 迭代器产出的元素类型

// 核心方法:每次调用 next() 返回下一个元素
fn next(&mut self) -> Option<Self::Item> {
if self.count < self.max {
self.count += 1;
Some(self.count) // 有值返回 Some
} else {
None // 迭代结束返回 None
}
}
}

fn main() {
let counter = Counter::new(3);

// 可以直接使用 for 循环遍历你的自定义结构体!
for num in counter {
println!("自定义计数: {}", num);
}
// 输出:
// 自定义计数: 1
// 自定义计数: 2
// 自定义计数: 3
}

闭包(语法糖)

本质上就是可以捕获周围环境变量的“匿名函数” 和普通函数(fn)最大的区别在于:它能直接使用在它定义之前的外部变量**。

  1. 基础

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //使用竖线 `|` 来包裹形参
    fn main() {
    let a = 200;
    // 1. 普通闭包(带类型标注)
    let add_one = |x: i32| -> i32 { x + a };
    println!("{}", add_one(5)); // 输出 6

    // 2. 简洁闭包(Rust 会自动推导类型)
    let add_two = |x| x + a;
    println!("{}", add_two(5)); // 输出 7
    }

  2. 只读取外部变量,不修改。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    fn main() {
    let greeting = "Hello";

    // 闭包捕获了外部的 greeting 变量
    let print_msg = || println!("{}", greeting);

    print_msg(); // 输出: Hello
    println!("{}", greeting); // greeting 依然有效,因为只是借用
    }

  3. 需要修改外部变量时,闭包必须声明为 mut。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    fn main() {
    let mut count = 0;

    // 闭包内部修改了 count,因此闭包本身需要是 mut
    let mut increment = || {
    count += 1;
    println!("Count: {}", count);
    };

    increment(); // 输出 Count: 1
    increment(); // 输出 Count: 2
    }

  4. 将外部变量的所有权直接移入闭包内(使用 move 关键字)。多用于跨线程传输数据。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    fn main() {
    let list = vec![1, 2, 3];

    // 加 move 强制转移 list 的所有权给 closure
    let closure = move || println!("From closure: {:?}", list);

    closure();
    // println!("{:?}", list); // ❌ 编译报错!list 的所有权已经不在了
    }

  5. 作为函数参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    fn main() {
    let numbers = vec![1, 2, 3, 4, 5];

    // 使用闭包过滤偶数,并将每个元素乘以 2
    let result: Vec<i32> = numbers
    .into_iter()
    .filter(|&x| x % 2 == 0) // 闭包 1:判断条件
    .map(|x| x * 2) // 闭包 2:元素转换
    .collect();

    println!("{:?}", result); // 输出 [4, 8]
    }

所有权

  • Rust 中的每个值都有一个所有者(Owner)。
  • 同一时刻,一个值只能有一个所有者。
  • 当所有者离开作用域(Scope)时,这个值就会被自动销毁(释放内存)。
  • 只有堆相关变量才会有所有权转移(栈内存上,复制成本极低,默认会对它们自动进行Copy)
  1. 所有权转移

    1
    2
    3
    4
    5
    6
    7
    8
    9
    fn main() {
    let s1 = String::from("hello");

    // 所有权从 s1 转移(Move)给了 s2
    let s2 = s1;

    // println!("{}", s1); // ❌ 编译报错!s1 已经失去所有权,不能再使用了
    println!("{}", s2); // ✅ 正常输出: hello
    }

  2. 函数传参也会转移所有权

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    fn main() {
    let s = String::from("hello");

    take_ownership(s); // s 的所有权被移交进了函数内部

    // println!("{}", s); // ❌ 编译报错!s 的所有权已经不在当前作用域了
    }

    fn take_ownership(some_string: String) {
    println!("{}", some_string); // some_string 成为新主人
    } // 函数结束,some_string 离开作用域,内存被自动释放!

  3. 不拿走所有权,只借用它的访问权(在变量前面加 & 符号,就表示“借用”)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    fn main() {
    let s1 = String::from("hello");

    // 传入 &s1,只借不拿
    let len = calculate_length(&s1);

    // ✅ s1 依然拥有所有权,可以正常使用!
    println!("'{}' 的长度是 {}.", s1, len);
    }

    // 接收参数类型为 &String,代表只借用,不拥有
    fn calculate_length(s: &String) -> usize {
    s.len()
    } // 函数结束,s 只是归还了借用权,什么都不会被释放

  4. 如果是基本数据类型(如 i32, bool, f64, char 等),赋值时不会转移所有权(自动拷贝)

    1
    2
    3
    4
    5
    6
    fn main() {
    let x = 5;
    let y = x; // 发生了复制(Copy),而不是转移(Move)

    println!("x = {}, y = {}", x, y); // ✅ x 和 y 都可以正常使用!
    }

数组

  1. 原生固定数组(栈)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    fn main() {
    // 1. 显式指定类型和长度:[类型; 长度]
    let board_row: [i32; 10] = [0; 10]; // 快速填充 10 个 0

    // 2. 数组的赋值(Copy 行为)
    let a = [1, 2, 3];
    let b = a; // 在栈上整体复制了一份

    // ✅ a 和 b 都可以继续使用,没有任何所有权问题
    println!("a: {:?}, b: {:?}", a, b);

    // 3. 修改元素(必须声明为 mut)
    let mut shape = [1, 1, 0, 0];
    shape[2] = 1;
    println!("修改后的形状: {:?}", shape);
    }

  2. 动态数组(堆)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    fn main() {
    // 1. 使用 vec! 宏快速创建动态数组
    let mut scores = vec![100, 200, 300];

    // 2. 动态增删元素
    scores.push(400); // 末尾添加
    println!("当前分数列表: {:?}, 长度: {}", scores, scores.len());

    scores.pop(); // 弹出末尾元素 (400)

    // 3. 所有权转移(Move)示例
    let other_scores = scores; // 发生 Move!scores 失去所有权

    // println!("{:?}", scores); // ❌ 如果取消注释这行,编译器会报错!
    println!("新所有者: {:?}", other_scores); // ✅ 只能用 other_scores
    }

  3. 数组切片

    • 切片不是真正独立存储数据的数组,而是对已有数组(原生数组或 Vec)某一段连续内存的“借用(视图)”。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 定义一个接受切片作为参数的函数
    // 它既能接收 &[i32; 5] 也能接收 &Vec<i32>
    fn print_sub_data(slice: &[i32]) {
    println!("切片内容: {:?}, 元素个数: {}", slice, slice.len());
    }

    fn main() {
    let arr = [10, 20, 30, 40, 50]; // 原生数组
    let vec_data = vec![1, 2, 3, 4, 5]; // 动态数组

    // 1. 截取原生数组的区间 [1..4](包含索引 1, 2, 3,不含 4)
    let arr_slice: &[i32] = &arr[1..4];
    print_sub_data(arr_slice); // 输出 [20, 30, 40]

    // 2. 截取 Vec 的整个区间
    print_sub_data(&vec_data[..]); // 输出 [1, 2, 3, 4, 5]
    }

  4. 堆上的固定数组 Box<[T]>

    • 通过 Box 把一个固定长度的数组强行放到堆上。
    • 保存在堆上(不会占用有限的栈空间),但长度固定(不能像 Vec 那样 push)。赋值时会发生所有权转移(Move)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    fn main() {
    // 1. 先用 vec! 创建,然后转为堆上的固定切片 Box<[i32]>
    let huge_map: Box<[i32]> = vec![0; 1000].into_boxed_slice();

    println!("堆上定长数组的长度: {}", huge_map.len());
    println!("第一个元素: {}", huge_map[0]);

    // huge_map.push(1); // ❌ 编译报错!堆上定长数组不能增加元素

    // 2. 赋值时会发生 Move
    let new_map_owner = huge_map;
    // println!("{}", huge_map[0]); // ❌ 报错!huge_map 已经 Move 给 new_map_owner
    }

字符串

  1. String 示例:可变、拼接、转移所有权(堆)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    fn main() {
    // 1. 创建 String 的两种最常见方式
    let mut game_title = String::from("俄罗斯");
    let mut status = "Game Over".to_string();

    // 2. 追加字符与字符串(修改数据,必须有 mut)
    game_title.push_str("方块"); // 追加 &str
    game_title.push('!'); // 追加单个 char
    println!("游戏标题: {}", game_title); // 输出: 俄罗斯方块!

    // 3. 所有权转移(Move)行为
    let player_name = String::from("Player1");
    let current_player = player_name; // 发生 Move!player_name 的所有权转移给了 current_player

    // println!("{}", player_name); // ❌ 如果取消注释这行,编译器会报错:Use of moved value
    println!("当前玩家: {}", current_player); // ✅ 只能使用新所有者
    }

  2. &str 是对字符串的借用视图,不占用额外内存分配,赋值或传参时不会转移所有权。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // 最佳实践:函数参数优先使用 &str
    // 这样无论传 &str 还是 &String,都能完美兼容!
    fn print_banner(text: &str) {
    println!("=== {} ===", text);
    }

    fn main() {
    // 1. 字面量字符串,类型默认是 &'static str
    let default_msg: &str = "Press Start";

    // 2. 从已有 String 中“切”出一部分(借用切片)
    let full_text = String::from("Tetris Rust");
    let sub_text: &str = &full_text[0..6]; // 借用前 6 个字节 ("Tetris")

    println!("子字符串: {}", sub_text);

    // 3. 验证&str 作为函数参数的灵活性
    print_banner(default_msg); // ✅ 传入 &str
    print_banner(&full_text); // ✅ 传入 &String(Rust 会自动解引用隐式转换成 &str)

    // 4. 不会发生 Move
    let s1: &str = "Ready";
    let s2 = s1; // 只是复制了指针和长度
    println!("s1: {}, s2: {}", s1, s2); // ✅ s1 和 s2 都可以继续正常使用!
    }

  3. 两者互转速查表

转换方向 代码写法 说明
**&str → String** String::from("hello") 或 "hello".to_string() 在堆上分配一块新内存,把字符串深拷贝过去。
**String → &str** &my_string 或 &my_string[..] 不分配内存,直接借用 String 的内存区域。

切片

因为切片不持有数据的所有权,所以切片本身离开作用域时不会释放底层内存。

  1. 范围切片语法(Range Syntax)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    fn main() {
    let arr = [10, 20, 30, 40, 50];

    let s1 = &arr[1..4]; // [20, 30, 40] 左闭右开 (含索引 1,不含 4)
    let s2 = &arr[..3]; // [10, 20, 30] 省略开头 (从 0 开始到索引 3,不含 3)
    let s3 = &arr[2..]; // [30, 40, 50] 省略结尾 (从索引 2 一直到末尾)
    let s4 = &arr[..]; // [10, 20, 30, 40, 50] 借用整个数组
    let s5 = &arr[1..=3]; // [20, 30, 40] 闭区间 ..= (包含索引 3)

    println!("s1: {:?}, len: {}", s1, s1.len());
    }

  2. 不可变数组切片(只读视图)

    • 从原生数组 [T; N] 或动态数组 Vec<T> 截取切片,只读不改。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    fn main() {
    let vec_data = vec![100, 200, 300, 400, 500];

    // 从 Vec 截取索引 1 到 3 的只读切片
    let slice: &[i32] = &vec_data[1..4];

    println!("第一个切片元素: {}", slice[0]); // 输出 200 (对应原 vec 的索引 1)

    // 遍历切片
    for elem in slice {
    println!("切片元素: {}", elem);
    }
    }

  3. 可变数组切片(修改原数据) &mut [T]

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    fn main() {
    let mut grid = [0, 0, 0, 0, 0];

    // 获取索引 1 到 3 的可变切片
    let slice: &mut [i32] = &mut grid[1..4];

    // 通过切片直接修改底层数据
    slice[0] = 9;
    slice[1] = 9;

    // 原数组已经被成功改变!
    println!("修改后的原数组: {:?}", grid); // 输出: [0, 9, 9, 0, 0]
    }

  4. 字符串切片是以 字节(Byte) 为单位进行截取的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    fn main() {
    let s = String::from("Hello World");

    // 截取前 5 个字节 ("Hello")
    let word1: &str = &s[0..5];
    // 截取 6 到末尾的字节 ("World")
    let word2: &str = &s[6..];

    println!("word1: {}, word2: {}", word1, word2);

    // ⚠️ 注意:如果包含中文字符,切片下标必须落在正确的 UTF-8 字符边界上!
    let cn_str = String::from("Rust编程");
    let cn_word = &cn_str[0..4]; // "Rust" 占 4 字节
    // let err = &cn_str[0..5]; // ❌ 如果下标落在中文中间,运行时会 Panic 崩溃!
    println!("中文切片: {}", cn_word);
    }

  5. 切片最重要的用途是作为函数的输入参数。它可以让你的 API 兼顾极致的灵活性与性能。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 最佳实践:入参写 &[i32],既支持原生数组,也支持 Vec,还支持子切片
    fn calculate_sum(numbers: &[i32]) -> i32 {
    numbers.iter().sum()
    }

    fn main() {
    let array = [1, 2, 3, 4, 5];
    let vector = vec![10, 20, 30];

    // 1. 传原生数组切片
    let sum1 = calculate_sum(&array);

    // 2. 传 Vec 切片
    let sum2 = calculate_sum(&vector);

    // 3. 传原数据的一部分
    let sum3 = calculate_sum(&array[1..3]); // 计算 [2, 3]

    println!("sum1: {}, sum2: {}, sum3: {}", sum1, sum2, sum3);
    }

  6. 备忘表

切片类型 常用语法 是否能修改数据? 底层包含什么? 赋值/传参行为
不可变数组切片 &[T] ❌ 只读 指针 (8B) + 长度 (8B) 借用(不转移所有权)
可变数组切片 &mut [T] ✅ 可修改原数组 指针 (8B) + 长度 (8B) 可变借用(独占)
字符串切片 &str ❌ 只读(通常) 指针 (8B) + 长度 (8B) 借用(不转移所有权)

元组(Tuple)

  • 异构性:一个元组内可以包含多种不同类型的数据(如 i32、bool、String 混装)。
  • 固定长度:元组一旦定义,其长度是固定的,不能动态增加或删除元素。
  • 类型签名:元组的类型由其包含的每个元素的类型和顺序共同决定。例如 (i32, &str) 和 (&str, i32) 在 Rust 中是两种完全不同的类型。
  1. 基础语法与声明

    1
    2
    3
    4
    5
    6
    7
    8
    9
    fn main() {
    // 语法 1:自动推导类型
    let tup1 = (500, 6.4, "Tetris");

    // 语法 2:显式指定类型标签 (i32, f64, &str)
    let tup2: (i32, f64, &str) = (500, 6.4, "Tetris");

    println!("元组 1: {:?}", tup1); // 使用 {:?} 打印整个元组
    }

  2. “点号 + 从 0 开始的数字”来获取指定位置的值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    fn main() {
    let point = (10, 20, "ScreenCenter");

    let x = point.0;
    let y = point.1;
    let name = point.2;

    println!("坐标: ({}, {}), 名称: {}", x, y, name);
    }

  3. 模式匹配 / 解构 使用 let (x, y, z) = tuple 语法,一次性将元组拆包赋值给多个新变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    fn main() {
    let player_info = ("Player1", 9999, true);

    // 模式匹配:解构元组
    let (name, score, is_active) = player_info;

    println!("玩家: {}, 得分: {}, 是否在线: {}", name, score, is_active);

    // 如果只想提取部分值,可以使用下划线 _ 忽略不关心的元素
    let (_, final_score, _) = player_info;
    println!("只关注得分: {}", final_score);
    }

  4. 如果元组被声明为 mut,你可以修改其中的元素,但不能更改该位置的类型或改变元组长度。

    1
    2
    3
    4
    5
    6
    7
    8
    fn main() {
    let mut tile_pos = (5, 10);

    tile_pos.1 += 1; // 修改 y 坐标,使其向下移动一步
    println!("新坐标: {:?}", tile_pos); // 输出: (5, 11)

    // tile_pos.0 = "hello"; // ❌ 编译报错!位置 0 只能是 i32,不能变成字符串
    }

  5. 元组的赋值行为(Copy 还是 Move)取决于它内部包裹的所有元素的类型:

    • 如果元组内部所有元素都实现了 Copy**(如 i32、bool、char),那么这个元组整体也是 Copy 的,赋值不会发生 Move**。
    • 如果元组内部包含任何非 Copy 元素(如 String、Vec),那么这个元组赋值时会触发 Move(所有权转移)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    fn main() {
    // 场景 A:全都是 Copy 类型 -> 发生 Copy
    let a = (1, 2);
    let b = a;
    println!("a: {:?}, b: {:?}", a, b); // ✅ a 依然有效!

    // 场景 B:包含 String (非 Copy) -> 发生 Move
    let c = (1, String::from("hello"));
    let d = c;
    // println!("{:?}", c); // ❌ 报错!c 中的字符串所有权已经移交给 d 了
    println!("d: {:?}", d);
    }

  6. 空元组 (不给元组放入任何元素 ())

    • 它不占用任何内存空间(大小为 0 字节)。
    • 类似于 C/C++ 中的 void,用于表达“无返回值”或“空操作”。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 当函数没有写返回值时,默认隐式返回 ()
    fn log_message(msg: &str) -> () {
    println!("[Log]: {}", msg);
    }

    fn main() {
    let unit_val: () = log_message("Game Started");
    println!("Unit 类型占用内存大小: {} 字节", std::mem::size_of_val(&unit_val)); // 输出: 0
    }

  7. 定义一个只包含 1 个元素的元组,必须在元素后面加上一个**额外的逗号 ,**。

    1
    2
    3
    4
    5
    6
    fn main() {
    let not_a_tuple = (5); // 这是一个普通的数字 i32 (带了圆括号)
    let single_tuple = (5,); // 这是一个单元素元组,类型是 (i32,)

    println!("元组类型第一个元素: {}", single_tuple.0);
    }

  8. 应用场景 用作坐标 / 向量

    1
    let pos: (usize, usize) = (10, 5); // 棋盘上的 (col, row)

  9. 应用场景 函数返回多个值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 检查方块移动:返回 (是否碰撞, 新坐标)
    fn try_move_down(x: i32, y: i32) -> (bool, (i32, i32)) {
    let next_y = y + 1;
    if next_y >= 20 {
    (true, (x, y)) // 碰到底部,停止移动,返回原坐标
    } else {
    (false, (x, next_y)) // 未碰撞,返回新坐标
    }
    }

    fn main() {
    let (is_collided, new_pos) = try_move_down(5, 19);
    println!("是否碰撞: {}, 最新位置: {:?}", is_collided, new_pos);
    }

  10. 总结

概念 示例 核心特征
标准元组 (1, 3.14, "hi") 异构组合,固定长度
访问方式 tup.0 或 let (x, y) = tup 点号数字 或 模式匹配解构
单元素元组 (100,) 必须加末尾逗号 ,
单元类型 (Unit) () 零字节内存,等价于 void
典型用途 2D/3D坐标 (x, y)、多返回值 临时把关联的数据打包传输

结构体

  1. 三种不同样式的结构体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 1. 经典命名字段结构体(最常用,类似于面向对象的类)
    struct Tile {
    x: i32,
    y: i32,
    color: String,
    }

    // 2. 元组结构体(Tuple Struct):没有字段名,只有类型
    struct Point(i32, i32); // 适合用来做类型包装,如坐标 (x, y)

    // 3. 单元结构体(Unit-like Struct):没有任何字段
    struct GameEngine; // 不占内存空间,常用于实现某些全局行为/Trait

  2. 经典结构体:定义、实例化与字段访问

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    struct Player {
    name: String,
    score: u32,
    is_active: bool,
    }

    fn main() {
    // 创建结构体实例(注意:Rust 要求必须初始化所有字段)
    let mut p1 = Player {
    name: String::from("Player1"),
    score: 100,
    is_active: true,
    };

    // 1. 访问与修改(只有声明为 mut,其内部字段才能修改)
    p1.score += 50;
    println!("玩家 {} 的当前得分: {}", p1.name, p1.score);

    // 2. 字段初始化简写(Field Init Shorthand)
    let name = String::from("Player2");
    let score = 200;
    // 当变量名和结构体字段名一致时,可以省略 name: name
    let p2 = Player { name, score, is_active: false };

    // 3. 结构体更新语法(Struct Update Syntax)
    // 基于 p1 创建 p3,仅修改 name,其余字段直接从 p1 拷贝/转移
    let p3 = Player {
    name: String::from("Player3"),
    ..p1 // 必须放在最后
    };
    }

    ⚠️ 所有权注意:在上面的 ..p1 中,因为 Player.name 是 String(非 Copy 类型),p1.name 的所有权被转移(Move)给了 p3,因此之后 p1 就不能整体使用了!

  3. 方法(Methods)与关联函数(impl 块)

    • Rust 没有 class 关键字,它把数据定义(struct) 和 行为定义(impl) 彻底分离开来。
    • self / &self / &mut self**:作为第一个参数时,代表这是一个实例方法**。
    • 没有 self**:代表这是一个关联函数**(静态方法),通常用来做构造函数(如 new)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    struct Tetromino {
    x: i32,
    y: i32,
    shape_type: char,
    }

    impl Tetromino {
    // 1. 关联函数(类似静态方法,常用于构造实例)
    fn new(x: i32, y: i32, shape_type: char) -> Self {
    Self { x, y, shape_type }
    }

    // 2. 只读实例方法(借用 &self)
    fn print_info(&self) {
    println!("方块 '{}' 位于 ({}, {})", self.shape_type, self.x, self.y);
    }

    // 3. 可变实例方法(借用 &mut self,修改内部数据)
    fn move_down(&mut self) {
    self.y += 1; // 下落一步
    }
    }

    fn main() {
    // 调用关联函数创建对象
    let mut piece = Tetromino::new(5, 0, 'T');

    piece.print_info(); // 输出: 方块 'T' 位于 (5, 0)
    piece.move_down(); // 修改属性
    piece.print_info(); // 输出: 方块 'T' 位于 (5, 1)
    }

  4. 想为数据赋予明确的业务含义,又不想繁琐地命名每一个字段时,可以使用元组结构体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 给坐标和颜色赋予独立的类型,防止传参混淆
    struct Position(i32, i32);
    struct Color(u8, u8, u8);

    fn set_pixel(pos: Position, color: Color) {
    println!("在 ({}, {}) 渲染颜色 RGB({},{},{})", pos.0, pos.1, color.0, color.1, color.2);
    }

    fn main() {
    let pos = Position(10, 20);
    let red = Color(255, 0, 0);

    // set_pixel(red, pos); // ❌ 编译报错!虽然底层都是数字,但类型不匹配,极度安全!
    set_pixel(pos, red); // ✅ 正确
    }

  5. 备忘表

概念 代码语法 核心作用
经典结构体 struct Point { x: i32, y: i32 } 具名字段,构建复杂数据模型
元组结构体 struct Color(u8, u8, u8) 无字段名,用于轻量包装和类型安全
单元结构体 struct Marker; 无数据,用于状态标记或实现行为
关联函数 fn new() -> Self 用 Struct::func() 调用,常用于构造对象
只读方法 fn show(&self) 读取内部数据,不发生 Move,不修改数据
可变方法 fn update(&mut self) 修改内部数据(要求对象本身声明为 mut)

枚举

  • 在 Rust 中,枚举是一种代数数据类型(Sum Type)。这意味着:Rust 的每个枚举成员都可以捆绑不同类型、不同数量的数据!
  • 和 match 模式匹配结合在一起时,构成了 Rust 处理复杂状态、错误控制和逻辑跳转的最强利器。
  1. 一个枚举类型中,你可以混合定义无数据成员、元组数据成员和结构体数据成员

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    enum GameState {
    // 1. 无数据成员(类似于经典 C 语言枚举)
    Menu,
    Paused,

    // 2. 绑定元组数据(不同成员可以绑定完全不同数量/类型的数据)
    Playing(u32), // 保存当前得分
    Position(i32, i32), // 保存当前坐标 (x, y)

    // 3. 绑定命名结构体数据
    GameOver { final_score: u32, is_high_score: bool },
    }

  2. 基础定义与 match 模式匹配

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    enum TetrominoType {
    I, J, L, O, S, T, Z,
    }

    fn get_color(piece: TetrominoType) -> &'static str {
    // 必须覆盖每一个分支
    match piece {
    TetrominoType::I => "Cyan (青色)",
    TetrominoType::J => "Blue (蓝色)",
    TetrominoType::L => "Orange (橙色)",
    TetrominoType::O => "Yellow (黄色)",
    TetrominoType::S => "Green (绿色)",
    TetrominoType::T => "Purple (紫色)",
    TetrominoType::Z => "Red (红色)",
    }
    }

    fn main() {
    let current_piece = TetrominoType::T;
    println!("当前方块颜色: {}", get_color(current_piece));
    }

  3. 解构枚举中的数据(通过 match )

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    enum Command {
    Quit,
    Move { x: i32, y: i32 },
    Rotate(bool), // true: 顺时针, false: 逆时针
    }

    fn process_command(cmd: Command) {
    match cmd {
    Command::Quit => println!("退出游戏"),
    Command::Move { x, y } => println!("移动到坐标: ({}, {})", x, y),
    Command::Rotate(clockwise) => {
    if clockwise {
    println!("顺时针旋转");
    } else {
    println!("逆时针旋转");
    }
    }
    }
    }

    fn main() {
    process_command(Command::Move { x: 5, y: 10 });
    process_command(Command::Rotate(true));
    }

  4. 如果只对枚举中的某种特定状态感兴趣,用 match 写全所有分支会显得臃肿,此时用 if let

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    enum GameEvent {
    KeyDown(char),
    Tick,
    WindowResize,
    }

    fn main() {
    let event = GameEvent::KeyDown('R');

    // 只关注 KeyDown 事件,忽略其他事件
    if let GameEvent::KeyDown(key) = event {
    println!("玩家按下了按键: {}", key);
    }
    }

  5. 和结构体一样,枚举也可以拥有自己的 impl 块,定义方法和关联函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    // 1. 加上 #[derive(Debug)],让 Rust 编译器自动生成打印格式化代码
    #[derive(Debug)]
    enum Direction {
    Up,
    Down,
    Left,
    Right,
    }

    impl Direction {
    // 实例方法:获取相反的方向
    fn opposite(&self) -> Self {
    match self {
    Direction::Up => Direction::Down,
    Direction::Down => Direction::Up,
    Direction::Left => Direction::Right,
    Direction::Right => Direction::Left,
    }
    }
    }

    fn main() {
    let dir = Direction::Left;
    let opp = dir.opposite();

    // 2. 使用 {:?} 占位符进行 Debug 格式化打印
    println!("原始方向: {:?}", dir);
    println!("相反方向: {:?}", opp);
    }

  6. Rust 语言本身没有空指针(null),也没有异常抛出机制(exception)。所有的空值处理和错误处理,全靠标准库中的这两个枚举来实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    //`Option<T>` —— 替代 `null`
    // 标准库定义(已被自动引入全局,可以直接用):
    // enum Option<T> {
    // Some(T), // 有值,包裹着 T 类型的数据
    // None, // 没有值(相当于 null)
    // }

    fn find_line_to_clear(grid: &[[i32; 10]; 20]) -> Option<usize> {
    // 假设找到第 19 行满了
    Some(19) // 如果没找到就返回 None
    }

    fn main() {
    let result = find_line_to_clear(&[[0; 10]; 20]);

    match result {
    Some(row) => println!("消去第 {} 行!", row),
    None => println!("没有可以消去满行"),
    }
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    //`Result<T, E>` —— 替代错误抛出(Exception)
    // 标准库定义:
    // enum Result<T, E> {
    // Ok(T), // 操作成功,包含结果 T
    // Err(E), // 操作失败,包含错误信息 E
    // }

    fn load_high_score() -> Result<u32, String> {
    // 假设读取文件失败
    Err(String::from("最高分文件不存在"))
    }

    fn main() {
    match load_high_score() {
    Ok(score) => println!("历史最高分: {}", score),
    Err(err) => println!("加载失败,原因: {}", err),
    }
    }

  7. 总结

概念 语法示例 核心特征与用途
无数据成员 enum State { Playing, Paused } 纯状态标记(类似经典 C 枚举)
带数据成员 enum Event { Key(char), Move(i32, i32) } 捆绑不同类型数据(代数数据类型)
match 匹配 match event { Event::Key(k) => ... } 强迫穷尽处理所有情况,安全解构
if let 语法 if let Event::Key(k) = event { ... } 仅关心单一模式时的代码简化语法糖
Option<T> Some(val) / None 取代 null,强制安全处理空值
Result<T, E> Ok(val) / Err(err) 取代 Exception,显式进行错误恢复

库

1

  1. 创建一个库项目

    1
    cargo new my_tetris_lib --lib

    创建成功后,项目的目录结构如下:

    1
    2
    3
    4
    my_tetris_lib/
    ├── Cargo.toml # 项目配置文件
    └── src/
    └── lib.rs # 库的入口文件(注意:不是 main.rs!)

  2. 在 src/lib.rs 中编写库代码

    • 在库文件中,默认所有数据、结构体和函数都是私有的(Private)。
    • 如果你希望外部能够调用它们,必须使用 pub(Public) 关键字显式暴露出来。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    // 1. 导出一个枚举
    #[derive(Debug, PartialEq)]
    pub enum Direction {
    Left,
    Right,
    Down,
    }

    // 2. 导出一个结构体
    pub struct Board {
    pub width: usize, // 字段也需要 pub 才能被外部直接访问
    pub height: usize,
    }

    impl Board {
    // 导出构造方法
    pub fn new(width: usize, height: usize) -> Self {
    Self { width, height }
    }

    // 导出方法
    pub fn is_valid_pos(&self, x: i32, y: i32) -> bool {
    x >= 0 && (x as usize) < self.width && y >= 0 && (y as usize) < self.height
    }
    }

    // 3. 导出一个普通函数
    pub fn add_score(current: u32, lines_cleared: u32) -> u32 {
    current + lines_cleared * 100
    }

  3. 库本身不能直接 cargo run(因为它没有 main 入口),但可以通过以下方式进行验证

    • 方式 1:在 src/lib.rs 底部直接写测试代码,然后运行 cargo test

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #[cfg(test)]
    mod tests {
    use super::*; // 引入上方定义的库内容

    #[test]
    fn test_board_creation() {
    let board = Board::new(10, 20);
    assert_eq!(board.width, 10);
    assert!(board.is_valid_pos(5, 5));
    assert!(!board.is_valid_pos(-1, 5));
    }
    }

    • 方式 2:添加一个 examples 文件夹

    1
    2
    3
    4
    5
    6
    my_tetris_lib/
    ├── Cargo.toml
    ├── src/
    │ └── lib.rs
    └── examples/
    └── demo.rs # 示例入口

    • 编辑 examples/demo.rs:

    1
    2
    3
    4
    5
    6
    use my_tetris_lib::{Board, Direction}; // 引入你自己的库

    fn main() {
    let board = Board::new(10, 20);
    println!("Board 校验结果: {}", board.is_valid_pos(3, 3));
    }

    运行示例程序:

    1
    cargo run --example demo

  4. 其他本地项目中调用这个库

    • 在 tetris_game/Cargo.toml 中,通过 相对路径 引入本地库:

    1
    2
    3
    [dependencies]
    # 指向你本地库的相对路径或绝对路径
    my_tetris_lib = { path = "../my_tetris_lib" }

    • 然后在 tetris_game 的代码里就可以像使用第三方库一样直接使用它了:

    1
    2
    3
    4
    5
    use my_tetris_lib::Board;

    fn main() {
    let game_board = Board::new(10, 20);
    }

  5. 导出为 C 语言可调用的动态库/静态库?(.so / .dll / .a)

    • 编辑 Cargo.toml:

    1
    2
    3
    [lib]
    name = "my_tetris_lib"
    crate-type = ["cdylib", "staticlib"] # 编译为 C 动态库(.so/.dll) 和 C 静态库(.a)

    • 在 src/lib.rs 中,使用 #[no_mangle] 和 extern "C" 导出 C 兼容函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #[no_mangle]
    pub extern "C" fn calculate_score(lines: u32) -> u32 {
    lines * 100
    }

    //新版本rust
    // 修改前:#[no_mangle]
    // 修改后:
    #[unsafe(no_mangle)]
    pub extern "C" fn calculate_score(lines: u32) -> u32 {
    lines * 100
    }

    运行编译命令:

    1
    cargo build --release

    • 编译产物会生成在 target/release/ 目录下(如 libmy_tetris_lib.so 或 my_tetris_lib.dll),可以直接供其他语言加载。

2 把当前项目的代码拆分、模块化

  1. 方式一:在已有项目内部直接拆分库/模块(最推荐)

    1
    2
    3
    4
    my_tetris/
    ├── Cargo.toml
    └── src/
    └── main.rs # 里面写了 500 行代码,越来越乱

    • 直接在 src/ 目录下新建文件(比如 board.rs 或 piece.rs),它们就自动变成了当前项目内部的库模块!

    • 新建 src/board.rs 文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // src/board.rs
    pub struct Board {
    pub width: usize,
    pub height: usize,
    }

    impl Board {
    pub fn new(width: usize, height: usize) -> Self {
    Self { width, height }
    }
    }

    • 在 src/main.rs 中声明并使用它:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // src/main.rs

    // 使用 mod 关键字声明引入同目录下的 board.rs 模块
    mod board;

    // 引入模块里的结构体
    use board::Board;

    fn main() {
    let b = Board::new(10, 20);
    println!("地图宽度: {}", b.width);
    }

  2. 方式二:在同一个项目根目录下,包含一个 lib.rs 和一个 main.rs(单项目双入口)

    • Rust 的 Cargo 非常聪明,允许你在同一个文件夹里既写库逻辑,又写可执行程序!

    • 只要你在 src/ 目录下同时放这两个文件:

    1
    2
    3
    4
    5
    my_tetris/
    ├── Cargo.toml
    └── src/
    ├── lib.rs # 这里写核心的核心算法、数据结构(库)
    └── main.rs # 这里写游戏启动主循环、渲染逻辑(可执行程序)

    • src/lib.rs 里的代码会被 Cargo 自动打包成库,库的名字就是你 Cargo.toml 里的项目名(比如 my_tetris)。
    • 在 src/main.rs 里,你可以直接像调用第三方库一样调用 lib.rs 里的内容:

    1
    2
    3
    4
    5
    6
    // src/main.rs
    use my_tetris::Board; // 直接引入同一项目 lib.rs 里 pub 出来的结构体

    fn main() {
    let board = Board::new(10, 20);
    }

3 把当前项目的代码拆分、模块化(子目录)

  1. 假设你想在 src/ 下建一个 game 文件夹,把 board.rs(棋盘)和 piece.rs(方块)都放进去

    1
    2
    3
    4
    5
    6
    7
    8
    my_tetris/
    ├── Cargo.toml
    └── src/
    ├── main.rs # 游戏主入口
    └── game/ # 1. 创建子目录
    ├── mod.rs # 2. 子目录的“总出口文件”(关键!)
    ├── board.rs # 3. 具体的棋盘库代码
    └── piece.rs # 4. 具体的方块库代码

    • src/game/board.rs:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    pub struct Board {
    pub width: usize,
    pub height: usize,
    }

    impl Board {
    pub fn new(width: usize, height: usize) -> Self {
    Self { width, height }
    }
    }

    • src/game/piece.rs:

    1
    2
    3
    4
    pub struct Piece {
    pub x: i32,
    pub y: i32,
    }

  2. mod.rs 是 game 这个子目录的枢纽入口文件。它负责把目录里的子模块组织起来,并决定对外暴露什么。

    • src/game/mod.rs:

    1
    2
    3
    4
    5
    6
    7
    // 1. 声明同目录下的子文件 board.rs 和 piece.rs
    pub mod board;
    pub mod piece;

    // 2. (可选) 重新导出(Re-export),方便外部调用,不用写冗长的层级
    pub use board::Board;
    pub use piece::Piece;

  3. 在 src/main.rs 中调用

    • src/main.rs:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 声明引入 game 子目录模块
    mod game;

    // 方式 A:如果不重新导出,层级是这样的:
    // use game::board::Board;

    // 方式 B:因为在 mod.rs 里写了 pub use,这里可以直接简洁引入:
    use game::{Board, Piece};

    fn main() {
    let board = Board::new(10, 20);
    let piece = Piece { x: 5, y: 0 };

    println!("地图宽度: {}, 当前方块 X: {}", board.width, piece.x);
    }

  4. 较新版本的 Rust 中,如果不喜欢在每个子文件夹里都放一个名字叫 mod.rs 的文件,Rust 提供了一种更优雅的替代写法:

    • 可以直接在 src/ 目录下建一个与文件夹同名的 .rs 文件!

    1
    2
    3
    4
    5
    6
    src/
    ├── main.rs
    ├── game.rs <-- 替代 game/mod.rs,直接放在 src/ 下
    └── game/ <-- 存放具体代码的子目录
    ├── board.rs
    └── piece.rs

    这种架构下,src/game.rs 的内容和上面的 mod.rs 完全一模一样:

    1
    2
    3
    4
    5
    6
    // src/game.rs
    pub mod board;
    pub mod piece;

    pub use board::Board;
    pub use piece::Piece;

错误处理

与 C++ / Java 使用 try-catch 异常机制不同,Rust 没有异常(Exceptions)。

Rust 将错误分为两大类:

  1. 不可恢复错误(Unrecoverable Errors):遇到严重 Bug(如数组越界、内存溢出),直接使用 panic! 挂起/终止程序。
  2. 可恢复错误(Recoverable Errors):预期可能发生的错误(如文件找不到、网络超时),使用 Result<T, E> 枚举显式表达,要求开发者必须显式处理。

0

  1. 不可恢复错误:panic!

    • 当程序遇到无法继续运行的致命问题时,可以使用 panic! 宏。

    1
    2
    3
    4
    5
    6
    7
    8
    fn main() {
    // 手动触发 panic
    // panic!("数据库连接断开,程序无法继续!");

    // 常见隐式触发 panic 的场景:数组越界
    let v = vec![1, 2, 3];
    // v[99]; // 这行代码会导致程序 panic 并打印调用栈
    }

  2. 可恢复错误:Result<T, E> 标准枚举

    • 标准库定义的 Result 枚举结构如下:

    1
    2
    3
    4
    enum Result<T, E> {
    Ok(T), // 成功,包含类型为 T 的值
    Err(E), // 失败,包含类型为 E 的错误信息
    }

    • 基础用法:匹配处理 (match), 当调用一个可能失败的函数(比如打开文件)时,Rust 会返回 Result,强迫你处理两种可能:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    use std::fs::File;
    use std::io::ErrorKind;

    fn main() {
    let file_result = File::open("config.json");

    // 使用 match 显式解包和匹配错误类型
    let file = match file_result {
    Ok(f) => f,
    Err(error) => match error.kind() {
    // 如果是因为文件不存在,则创建文件
    ErrorKind::NotFound => match File::create("config.json") {
    Ok(fc) => fc,
    Err(e) => panic!("创建文件失败: {:?}", e),
    },
    // 其他错误直接 panic
    other_error => panic!("打开文件失败: {:?}", other_error),
    },
    };
    }

    • 简化处理:快捷方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    use std::fs::File;

    fn main() {
    // 1. unwrap(): 成功则返回值,失败则直接 panic(适合写 Demo 或确定绝不会报错的场景)
    // let f1 = File::open("hello.txt").unwrap();

    // 2. expect(): 类似 unwrap(),但可以在 panic 时自定义错误提示
    // let f2 = File::open("hello.txt").expect("找不到 hello.txt 配置文件");

    // 3. unwrap_or_else(): 失败时执行闭包逻辑,避免 panic
    let f3 = File::open("hello.txt").unwrap_or_else(|err| {
    println!("警告: 打开失败 ({:?}),改用默认逻辑...", err);
    File::create("hello.txt").expect("创建失败")
    });
    }

  3. 用 ? 操作符传播错误

    • 如果子函数报错,最常见的做法是把错误向上抛给调用方处理。
    • 如果表达式的结果是 Ok(val),? 会自动将 val 解包出来;如果是 Err(err),? 会立即中断当前函数,并将 Err(err) 返回给上层调用者。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    use std::fs::File;
    use std::io::{self, Read};

    // 读取文件内容的函数,返回 Result<String, io::Error>
    fn read_username_from_file() -> Result<String, io::Error> {
    // 打开文件,失败则直接从当前函数 return Err(file_err)
    let mut file = File::open("username.txt")?;

    let mut username = String::new();
    // 读取内容,失败则直接从当前函数 return Err(read_err)
    file.read_to_string(&mut username)?;

    // 全部成功,包装在 Ok 中返回
    Ok(username)
    }

    fn main() {
    match read_username_from_file() {
    Ok(name) => println!("用户名是: {}", name),
    Err(e) => println!("获取用户名失败: {}", e),
    }
    }

    链式调用的终极简化:上面的 read_username_from_file 甚至可以连写成一行: std::fs::read_to_string("username.txt")

  4. 社区标准级的错误处理 Crate: anyhow(最适合应用层/客户端开发)

    • anyhow::Result<T> 可以接收任何实现了 std::error::Error trait 的错误,非常适合写应用程序逻辑,不用为各种复杂的错误类型头疼。

    1
    2
    3
    # Cargo.toml
    [dependencies]
    anyhow = "1.0"

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    use anyhow::{Context, Result};
    use std::fs;

    // 使用 anyhow::Result,可以同时用 ? 抛出不同类型的错误
    fn process_avatar() -> Result<()> {
    // .context() 可以在报错时附加上下文提示信息
    let content = fs::read_to_string("config.json")
    .context("读取配置文件 config.json 失败")?;

    let number: i32 = content.trim().parse()
    .context("配置内容无法解析为整数")?;

    println!("读取到的数值为: {}", number);
    Ok(())
    }

    fn main() {
    if let Err(e) = process_avatar() {
    // e 会打印出精细的错误链条和上下文信息
    println!("程序出错: {:?}", e);
    }
    }

  5. 社区标准级的错误处理 Crate: thiserror(最适合写独立库/SDK)

    • 在写一个通用的网络/解析 Core 库,需要为调用方暴露精准的错误类型,可以用 thiserror 通过宏自动生成自定义错误枚举

    1
    2
    3
    4
    # Cargo.toml
    [dependencies]
    thiserror = "1.0"
    reqwest = "0.11"

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    use thiserror::Error;

    // 定义你的自定义错误类型
    #[derive(Error, Debug)]
    pub enum AppError {
    #[error("网络请求失败: {0}")]
    Network(#[from] reqwest::Error),

    #[error("图片解码错误")]
    ImageDecode,

    #[error("配额不足,还剩 {0} 字节")]
    QuotaExceeded(usize),
    }

    fn main() {
    // 模拟触发一个配额不足的自定义错误
    let err = AppError::QuotaExceeded(1024);

    // 借助 thiserror,这里会自动格式化输出:配额不足,还剩 1024 字节
    println!("捕获到的错误是: {}", err);
    }

match

  • 如果把其他语言的 switch-case 比作“只能匹配固定数字或字符串的分支选择器”,那么 Rust 的 match 就是全能的解构与类型匹配利器。它不仅能判断值,还能直接把复杂数据结构里的内容“剥出来”使用,同时由编译器在背后强制把关,防止漏掉任何边缘情况。
  1. 穷尽性(Exhaustiveness):必须覆盖所有可能的分支,少写一种情况编译器直接报错。
  2. 表达式(Expression):每个分支处理完可以返回值,并且可以直接赋给变量。
  3. 解构(Destructuring):可以直接提取枚举、结构体、元组中的内部数据。

硬性规则: 所有分支 => 右边返回的值类型必须完全一致! 因为 Rust 是强类型语言,整个 match 表达式最终只能输出一种类型的数据

  1. 基础值与通配符匹配(替代传统 switch)

    • 用 _ 代表“其他所有情况”(相当于 default)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    fn main() {
    let number = 3;

    // match 作为表达式直接返回值给 status
    let status = match number {
    1 => "起点",
    2 | 3 | 5 | 7 => "质数", // 用 | 匹配多个值
    10..=20 => "在 10 到 20 之间", // 用 ..= 匹配闭区间范围
    _ => "普通数字", // 通配符,处理所有剩余情况
    };

    println!("数字状态: {}", status); // 输出: 质数
    }

  2. 枚举解构(提取内部数据)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    fn process_score(score: Option<i32>) {
    match score {
    // 匹配到 Some 时,顺便把里面的数值解包并赋给 val
    Some(val) => println!("最终得分: {}", val),
    // 匹配到 None 时,代表没有数据
    None => println!("暂无成绩记录"),
    }
    }

    fn main() {
    process_score(Some(95)); // 输出: 最终得分: 95
    process_score(None); // 输出: 暂无成绩记录
    }

  3. 元组与结构体解构

    • 直接在匹配的同时将复合类型打散:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    struct Point {
    x: i32,
    y: i32,
    }

    fn main() {
    let point = Point { x: 0, y: 10 };

    match point {
    // 匹配 x 为 0 的情况,并把 y 的值提取到 y 变量中
    Point { x: 0, y } => println!("在 Y 轴上,Y = {}", y),
    // 匹配 y 为 0 的情况,提取 x
    Point { x, y: 0 } => println!("在 X 轴上,X = {}", x),
    // 其他普通坐标
    Point { x, y } => println!("普通坐标点: ({}, {})", x, y),
    }
    }

  4. 在模式后面跟上一个 if 条件语句,进行二次精细化筛选

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    fn main() {
    let pair = (2, -2);

    match pair {
    // 只有当 x 和 y 相等时才进入这个分支
    (x, y) if x == y => println!("两个数字相等: {}", x),
    // 只有当 x + y == 0 时才进入
    (x, y) if x + y == 0 => println!("两个数字互为相反数"),
    // 剩余的普通元组
    (x, y) => println!("普通组合: ({}, {})", x, y),
    }
    }

  5. @ 绑定(既匹配范围,又保留原始值)

    • 当你既想检查数据是否落在特定范围内,又想在后续代码里使用这个数据时:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    enum Message {
    Hello { id: i32 },
    }

    fn main() {
    let msg = Message::Hello { id: 5 };

    match msg {
    // id_val @ 匹配 3..=7 范围,同时把具体的 id 值绑定到 id_val 变量上
    Message::Hello { id: id_val @ 3..=7 } => {
    println!("找到了在 3 到 7 之间的 ID: {}", id_val)
    }
    Message::Hello { id: 10..=12 } => {
    println!("找到了在 10 到 12 之间的 ID")
    }
    Message::Hello { id } => {
    println!("其他 ID: {}", id)
    }
    }
    }

符号 : ::

  • ::(双冒号):用来找东西(“去哪个模块/类/枚举里把某个方法或成员找出来”)。
  • :(单冒号):用来做绑定(“把这个变量/字段绑定到某种类型、某个数值或某个匹配条件上”)。

双冒号 ::

双冒号 :: —— 用于“路径与空间定位”

  1. 调用类型的关联函数 / 构造函数

    1
    2
    let mut s = String::new();      // 调用 String 类型的静态函数 new
    let f = File::open("a.txt")?; // 调用 File 类型的 open 函数

  2. 指定枚举的具体变体(Variant)

    1
    2
    let msg = Message::Hello { id: 5 }; // 访问 Message 枚举下的 Hello 变体
    let res: Result<i32, &str> = Ok(200); // 实际上是 Result::Ok(200) 的简写

  3. 访问模块(Module)或导入路径

    1
    2
    use std::fs::File;              // 导入 std 模块下的 fs 模块下的 File
    let err = std::io::Error::last_os_error(); // 完整路径访问

  4. 泛型调用的“涡轮鱼”语法(Turbofish ::<T>)

    1
    2
    // 显式告诉 parse 函数把它解析成 i32 类型
    let num = "42".parse::<i32>().unwrap();

单冒号 :

单冒号 : —— 用于“类型约束与字段映射”

  1. 变量声明时的“类型注解”(Type Annotation)

    1
    2
    let age: u32 = 18;             // 变量 age 的类型是 u32
    let name: String = String::from("Alice");

  2. 结构体 / 枚举定义与初始化中的“字段赋值”

    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct User {
    name: String, // 定义字段及其类型
    age: u32,
    }

    let u = User {
    name: String::from("Tom"), // 初始化赋值
    age: 20,
    };

  3. 模式匹配 / 解构中的“模式映射”

    1
    2
    3
    4
    5
    6
    7
    match msg {
    // 字段 id : 对应匹配模式 (3..=7)
    Message::Hello { id: 3..=7 } => { ... }

    // 字段 id : 提取并重命名给新变量 my_id
    Message::Hello { id: my_id } => { ... }
    }

泛型

给类型打占位符

  1. 把类型参数 T 放在函数名后面的 <T> 里

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // <T> 声明:T 是一个类型占位符
    // 只要类型 T 实现了 Copy 特性(比如数值类型),就可以用这段逻辑
    fn swap<T: Copy>(a: &mut T, b: &mut T) {
    let temp = *a;
    *a = *b;
    *b = temp;
    }

    fn main() {
    let mut x = 10;
    let mut y = 20;
    swap(&mut x, &mut y); // T 被自动推导为 i32
    println!("x = {}, y = {}", x, y); // x = 20, y = 10

    let mut a = 3.14;
    let mut b = 6.28;
    swap(&mut a, &mut b); // T 被自动推导为 f64
    println!("a = {}, b = {}", a, b); // a = 6.28, b = 3.14
    }

  2. 泛型结构体(Struct)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 支持同一类型的 x 和 y
    struct Point<T> {
    x: T,
    y: T,
    }

    // 如果 x 和 y 类型可能不同,可以定义多个泛型占位符(如 T 和 U)
    struct FlexiblePoint<T, U> {
    x: T,
    y: U,
    }

    fn main() {
    let integer_point = Point { x: 5, y: 10 }; // Point<i32>
    let float_point = Point { x: 1.0, y: 4.0 }; // Point<f64>
    let mixed_point = FlexiblePoint { x: 5, y: 4.5 }; // FlexiblePoint<i32, f64>
    }

  3. 泛型方法(Impl)

    • 为泛型结构体实现方法时,impl 后面也必须声明 <T>

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    struct Point<T> {
    x: T,
    y: T,
    }

    // 注意:impl 后面的 <T> 是声明,Point<T> 是使用
    impl<T> Point<T> {
    // 方法:获取 x 的引用
    fn x(&self) -> &T {
    &self.x
    }
    }

    // 甚至可以专门为特定类型(如 f64)实现专属方法!
    impl Point<f64> {
    fn distance_from_origin(&self) -> f64 {
    (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
    }

    fn main() {
    let p = Point { x: 3.0, y: 4.0 };
    println!("x = {}", p.x()); // 输出: 3.0
    println!("距离原点: {}", p.distance_from_origin()); // 输出: 5.0
    }

  4. 泛型枚举(Enum)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 标准库里 Option 的源码实现简写:
    enum Option<T> {
    Some(T), // 包裹类型为 T 的值
    None, // 啥也没有
    }

    // 标准库里 Result 的源码实现简写:
    enum Result<T, E> {
    Ok(T), // 成功时,包裹类型为 T 的结果
    Err(E), // 失败时,包裹类型为 E 的错误
    }

  5. 特性约束

    • 在泛型中,如果直接对 T 进行操作(比如比较大小 a > b),编译器会报错,因为并不是所有类型都能比较大小。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    use std::cmp::PartialOrd;

    // <T: PartialOrd> 告诉编译器:T 必须是“支持比较大小”的类型!
    fn largest<T: PartialOrd>(a: T, b: T) -> T {
    if a > b {
    a
    } else {
    b
    }
    }

    fn main() {
    println!("较大的数字是: {}", largest(10, 20)); // 输出: 20
    println!("较大的字符是: {}", largest('a', 'z')); // 输出: z
    }

  6. 总结

语法 含义
fn foo<T>(x: T) 定义泛型函数,T 代指任意类型
struct Foo<T, U> 定义含两个独立泛型的结构体
impl<T> Foo<T> 为泛型结构体实现方法(注意 impl 后面也要带 <T>)
<T: Display> 特性约束:限制 T 必须实现 Display(能被打印)特性

宏

  1. 可变参数:普通函数参数数量必须固定;而 println!、vec! 可以传任意数量的参数。
  2. 在编译期操作代码结构:比如 #[derive(Debug)] 能在编译时自动分析结构体字段,帮你不费吹灰之力地生成几百行打印代码。
  3. 减少重复的模版代码(Boilerplate):可以在编译前将重复的模式自动展开。
1
2
3
4
5
6
7
               ┌── 声明宏 (Declarative Macros) ─── macro_rules! (类似模式匹配)
│
Rust 中的宏 ───┤
│ ┌── 类属性宏 (Attribute-like): #[route(GET, "/")]
└── 派生/过程宏 ───┼── 自定义派生 (Custom Derive): #[derive(MyTrait)]
(Procedural) └── 类函数宏 (Function-like): sql!("SELECT * ...")

  1. 声明宏

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // macro_rules! 用来定义声明宏
    macro_rules! my_vec {
    // 匹配规则:$( $x:expr ),* 代表匹配“由逗号分隔的零个或多个表达式”
    ( $( $x:expr ),* ) => {
    {
    let mut temp_vec = Vec::new();
    // $( ... )* 会对匹配到的每个表达式重复展开
    $(
    temp_vec.push($x);
    )*
    temp_vec
    }
    };
    }

    fn main() {
    // 像调用标准库一样使用我们的宏
    let v = my_vec![1, 2, 3 + 1, 5];
    println!("生成动态数组: {:?}", v); // 输出: [1, 2, 4, 5]
    }

  2. 重载模式匹配

    • 像 match 分支一样,一个宏可以写多个匹配规则

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    macro_rules! say_hello {
    // 规则 1:无参数调用
    () => {
    println!("Hello!");
    };
    // 规则 2:接收一个名字字符串
    ($name:expr) => {
    println!("Hello, {}!", $name);
    };
    }

    fn main() {
    say_hello!(); // 输出: Hello!
    say_hello!("Rust Learner");// 输出: Hello, Rust Learner!
    }

  3. 过程宏

    • 过程宏比声明宏更硬核,它接受一段 Rust 编译期代码作为输入,经过复杂计算后,输出一段全新的 Rust 代码。

    • 自定义派生宏(作用在 struct 或 enum 上,自动为它们生成代码):

    1
    2
    3
    4
    5
    6
    // 在结构体上挂上派生宏
    #[derive(Debug, Error)] // 这里的 Error 就是 thiserror 库提供的过程宏
    enum AppError {
    #[error("文件未找到")]
    NotFound,
    }

    • thiserror 的过程宏在编译时读取了这个 enum 的结构,自动为你生成了 impl std::fmt::Display for AppError 的全部代码,免去了你手动写的繁琐过程。

    • 类属性宏:

    1
    2
    3
    4
    #[get("/api/user")] // 属性宏:在编译期提取路径参数并自动注册路由
    async fn get_user() -> &'static str {
    "User Data"
    }

    • 可以给函数或结构体加上自定义属性标签,常用于 Web 框架(如 actix-web 或 axum)

过程宏实现

Rust 规定:过程宏不能直接写在普通的 .rs 文件或普通项目里,它必须独占一个独立的编译 crate(独立子项目),并且必须在 Cargo.toml 中明确声明 proc-macro = true。

实现一个名为 #[derive(HelloMacro)] 的过程宏。只要在任何结构体上面挂上它,就会自动为这个结构体生成一个 hello_macro() 方法,打印自己的名字。

  1. 创建一个专用的过程宏子项目

    1
    2
    3
    # 必须是一个独立的项目/文件夹,名称随意
    cargo new hello_macro_derive --lib
    cd hello_macro_derive

  2. 修改 Cargo.toml

    1
    2
    3
    4
    5
    6
    [lib]
    proc-macro = true # 核心:声明这是一个过程宏项目!

    [dependencies]
    syn = "2.0" # 负责把输入的 Rust 代码文本解析成“抽象语法树 (AST)”
    quote = "1.0" # 负责把写好的 Rust 代码重新拼装回 TokenStream

  3. 在 src/lib.rs 中编写过程宏代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    use proc_macro::TokenStream;
    use quote::quote;
    use syn::{parse_macro_input, DeriveInput};

    // 1. 用 #[proc_macro_derive(名字)] 标记这个函数是派生宏
    #[proc_macro_derive(HelloMacro)]
    pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // 2. 将输入的代码 TokenStream 解析成 syn 的语法树结构 (AST)
    let ast = parse_macro_input!(input as DeriveInput);

    // 3. 提取这个结构体/枚举的名称(比如叫 "Pancake")
    let name = &ast.ident;

    // 4. 使用 quote! 宏构建要生成的全新 Rust 代码
    // #name 会被替换成上面提取出来的真实名称
    let expanded = quote! {
    impl #name {
    pub fn hello_macro() {
    println!("你好!我是由过程宏生成的代码,我的类型名字叫: {}", stringify!(#name));
    }
    }
    };

    // 5. 将生成的代码转换回 TokenStream 输出给编译器
    TokenStream::from(expanded)
    }

  4. 本地项目 加载使用

    1
    2
    3
    [dependencies]
    # 引用你刚才创建的过程宏项目
    hello_macro_derive = { path = "../hello_macro_derive" }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    use hello_macro_derive::HelloMacro;

    // 挂上我们自己写的过程宏!
    #[derive(HelloMacro)]
    struct Pancake;

    #[derive(HelloMacro)]
    struct User {
    id: i32,
    }

    fn main() {
    // 这个 hello_macro() 方法完全是我们刚才用 quote! 自动生成的!
    Pancake::hello_macro(); // 输出: 你好!我是由过程宏生成的代码,我的类型名字叫: Pancake
    User::hello_macro(); // 输出: 你好!我是由过程宏生成的代码,我的类型名字叫: User
    }

智能指针

  1. Box<T> —— 最直接的堆内存借用

    • Rust 默认把变量存在栈(Stack)上,用 Box::new() 可以把数据强制放到堆(Heap)上,而栈上只留一个固定大小的指针。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 递归定义链表:Cons(当前值, 指向下一个节点的 Box 指针)
    #[derive(Debug)]
    enum List {
    Cons(i32, Box<List>),
    Nil,
    }

    use List::{Cons, Nil};

    fn main() {
    // 构造链表:1 -> 2 -> Nil
    let list = Cons(1, Box::new(Cons(2, Box::new(Nil))));
    println!("链表结构: {:?}", list);

    // 当 list 离开作用域时,Box 会递归自动释放堆上所有的内存,无需手动 free
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    // 1. 定义一个能力接口:只要实现我,就必须能调用 draw() 绘制自己
    trait Draw {
    fn draw(&self);
    }

    // 2. 这是一个具体的控件:按钮
    struct Button;

    // 3. 按钮签署了 Draw 合约,实现了绘制逻辑
    impl Draw for Button {
    fn draw(&self) {
    println!("绘制按钮");
    }
    }

    // 4. 这个函数说:“我要返回一个【能画出来的控件】,但我不想在返回值类型里把它是按钮还是输入框给死!”
    fn get_component() -> Box<dyn Draw> {
    // 创建一个真实的 Button,用 Box 把它打包到堆上,隐式转换为 Box<dyn Draw> 扔出去
    Box::new(Button)
    }

    fn main() {
    // 拿到这个装在盒子里、只知道能画画的组件
    let component: Box<dyn Draw> = get_component();

    // 不管里面具体是 Button 还是别的,直接画!
    component.draw(); // 输出: 绘制按钮
    }

  2. Rc<T> —— 单线程下的共享所有权(引用计数)

    • Rust 默认遵守“单一所有权”原则(一个值只能有一个主人)。但在图数据结构、多节点共享等场景中,一份数据需要被多个地方同时拥有,这就需要用到 Rc<T>。

    • 机制:每克隆一次 Rc::clone(&a),引用计数 +1;某个指针离开作用域,计数 -1;当计数清零时,堆内存自动释放。

    • 限制:只适用于单线程,且内部数据默认是不可变的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    use std::rc::Rc;

    fn main() {
    // 创建一个存放在堆上的字符串,初始计数为 1
    let data = Rc::new(String::from("共享的核心数据"));

    println!("初始引用计数: {}", Rc::strong_count(&data)); // 输出: 1

    {
    // Rc::clone 只是增加计数,不深拷贝数据,性能极高
    let b = Rc::clone(&data);
    let c = Rc::clone(&data);
    println!("创建 b, c 后计数: {}", Rc::strong_count(&data)); // 输出: 3
    println!("b 读取到的内容: {}", b);
    } // b 和 c 离开作用域,计数自动 -2

    println!("b, c 离开作用域后计数: {}", Rc::strong_count(&data)); // 输出: 1
    }

  3. Arc<T> —— 多线程下的安全共享所有权

    • 内部使用原子操作更新引用计数,可以安全地在多个线程之间传递(传递到 thread::spawn 闭包中)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    use std::sync::Arc;
    use std::thread;

    fn main() {
    // 使用 Arc 包裹字符串
    let data = Arc::new(String::from("多线程共享数据"));

    let mut handles = vec![];

    for i in 0..3 {
    let data_clone = Arc::clone(&data); // 克隆 Arc 指针传给新线程
    let handle = thread::spawn(move || {
    println!("线程 {} 收到数据: {}", i, data_clone);
    });
    handles.push(handle);
    }

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

  4. RefCell<T> —— 运行时借用检查(内部可变性)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    use std::cell::RefCell;

    fn main() {
    // x 本身是一个不可变变量,但里面包着 RefCell
    let x = RefCell::new(10);

    {
    // borrow_mut() 在运行时获取可变借用
    let mut mut_ref = x.borrow_mut();
    *mut_ref += 50;
    } // mut_ref 在这里离开作用域,释放可变借用

    // borrow() 在运行时获取不可变借用
    println!("修改后的数值: {}", x.borrow()); // 输出: 60
    }

  5. 组合模式:Rc<RefCell<T>>

    • 既需要“多个地方共享所有权”,又需要“在任意地方修改数据”,单用 Rc(不可变)或 RefCell(不可共享)都做不到。将它们叠加组合使用,是 Rust 中实现图、双向链表或多节点对象的经典写法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    use std::rc::Rc;
    use std::cell::RefCell;

    #[derive(Debug)]
    struct Node {
    value: i32,
    next: Option<Rc<RefCell<Node>>>,
    }

    fn main() {
    // 创建一个共享且可变的节点
    let node1 = Rc::new(RefCell::new(Node { value: 10, next: None }));

    // 另一个变量克隆所有权
    let node1_owner = Rc::clone(&node1);

    // 通过其中一个所有者修改节点内部的值!
    node1_owner.borrow_mut().value = 99;

    // 两个所有者看得到修改后的最新值
    println!("修改后的节点: {:?}", node1.borrow()); // value 显示 99
    }

  6. 总结

智能指针 存储位置 借用检查时机 适用场景
Box<T> 堆内存 编译期 递归类型、大数据转移所有权、Trait 动态分发
Rc<T> 堆内存 编译期(单线程) 单线程下的多节点共享读取数据
Arc<T> 堆内存 编译期(多线程) 多线程下的数据共享读取
RefCell<T> 栈/堆 运行期 表面上是不可变引用,但需要在内部修改数据
Rc<RefCell<T>> 堆内存 运行期 单线程下实现多地方共享 + 可变修改的数据结构(如树/图/双向链表)

Rust 基础
https://fu01.github.io/posts/e4a59e90/
作者
Fu01
发布于
2026年9月15日
许可协议