Rust 基础
安装
- 官方提供安装脚本
安装细节:
1 | |
基础示例
1 | |
$ 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 | |
运算符
加法
+| 减法-| 乘法*| 除法(整除)/| 取余%相等
==| 不相等!=| 大于>| 小于<| 大于等于>=| 小于等于<=逻辑与(AND)
&&| 逻辑或(OR)||| 逻辑非(NOT)!按位与
&| 按位或|| 按位异或^| 按位取反!| 左移<<| 右移>>赋值
=| 加并赋值+=| 减并赋值-=| 乘并赋值*=| 除并赋值/=| 取余并赋值%=| 位运算复合赋值&=|=^=<<=>>=范围(不含右端)
..| 范围(含右端)..=| 类型转换as| 错误传播(在 Result 中)?| 解引用*| 取引用&| 绑定为引用ref- 0..5 产生 0 到 4
- 0..=5 产生 0 到 5
- 5 as f32
- some()?;
- *ptr
- &x
- 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 | |
函数
1 | |
条件语句
Rust 中的 if 不存在单语句不用加 {} 的规则
- 条件表达式必须是 bool 类型
1 | |
循环
1 | |
迭代器
| 创建方法 | 每次迭代拿到的类型 | 说明 | 原始集合是否还能用? |
|---|---|---|---|
| .iter() | &T(不可变引用) | 只读借用元素 | 能,集合不受影响 |
| .iter_mut() | &mut T(可变引用) | 借用并允许修改元素 | 能,集合被修改 |
| .into_iter() | T(转移所有权) | 将元素直接移出集合 | 不能,集合被消费丢弃 |
1 | |
将多个适配器组合在一起,构建一个复杂的处理流水线,最后调用消费器(如 collect())触发执行。
1 | |
只有在调用 .collect() 时,前面的 filter 和 map 才会真正按需开始计算。
其他消费器
1 | |
带索引的迭代 (.enumerate())
1 | |
由于迭代器是“惰性”的,可以定义一个无限递增的序列,只在需要时截取部分元素:
1 | |
自定义迭代器
1 | |
闭包(语法糖)
本质上就是可以捕获周围环境变量的“匿名函数” 和普通函数(
fn)最大的区别在于:它能直接使用在它定义之前的外部变量**。
基础
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
}只读取外部变量,不修改。
1
2
3
4
5
6
7
8
9
10fn main() {
let greeting = "Hello";
// 闭包捕获了外部的 greeting 变量
let print_msg = || println!("{}", greeting);
print_msg(); // 输出: Hello
println!("{}", greeting); // greeting 依然有效,因为只是借用
}需要修改外部变量时,闭包必须声明为
mut。1
2
3
4
5
6
7
8
9
10
11
12
13fn main() {
let mut count = 0;
// 闭包内部修改了 count,因此闭包本身需要是 mut
let mut increment = || {
count += 1;
println!("Count: {}", count);
};
increment(); // 输出 Count: 1
increment(); // 输出 Count: 2
}将外部变量的所有权直接移入闭包内(使用
move关键字)。多用于跨线程传输数据。1
2
3
4
5
6
7
8
9
10fn main() {
let list = vec![1, 2, 3];
// 加 move 强制转移 list 的所有权给 closure
let closure = move || println!("From closure: {:?}", list);
closure();
// println!("{:?}", list); // ❌ 编译报错!list 的所有权已经不在了
}作为函数参数
1
2
3
4
5
6
7
8
9
10
11
12
13fn 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
2
3
4
5
6
7
8
9fn main() {
let s1 = String::from("hello");
// 所有权从 s1 转移(Move)给了 s2
let s2 = s1;
// println!("{}", s1); // ❌ 编译报错!s1 已经失去所有权,不能再使用了
println!("{}", s2); // ✅ 正常输出: hello
}函数传参也会转移所有权
1
2
3
4
5
6
7
8
9
10
11
12fn 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 离开作用域,内存被自动释放!不拿走所有权,只借用它的访问权(在变量前面加
&符号,就表示“借用”)1
2
3
4
5
6
7
8
9
10
11
12
13
14fn 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 只是归还了借用权,什么都不会被释放如果是基本数据类型(如
i32,bool,f64,char等),赋值时不会转移所有权(自动拷贝)1
2
3
4
5
6fn main() {
let x = 5;
let y = x; // 发生了复制(Copy),而不是转移(Move)
println!("x = {}, y = {}", x, y); // ✅ x 和 y 都可以正常使用!
}
数组
原生固定数组(栈)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16fn 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);
}动态数组(堆)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16fn 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
}数组切片
- 切片不是真正独立存储数据的数组,而是对已有数组(原生数组或 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]
}堆上的固定数组
Box<[T]>- 通过
Box把一个固定长度的数组强行放到堆上。 - 保存在堆上(不会占用有限的栈空间),但长度固定(不能像
Vec那样push)。赋值时会发生所有权转移(Move)。
1
2
3
4
5
6
7
8
9
10
11
12
13fn 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
}- 通过
字符串
String示例:可变、拼接、转移所有权(堆)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17fn 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); // ✅ 只能使用新所有者
}&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 都可以继续正常使用!
}两者互转速查表
| 转换方向 | 代码写法 | 说明 |
|---|---|---|
**&str →
String** |
String::from("hello") 或
"hello".to_string() |
在堆上分配一块新内存,把字符串深拷贝过去。 |
**String →
&str** |
&my_string 或 &my_string[..] |
不分配内存,直接借用 String 的内存区域。 |
切片
因为切片不持有数据的所有权,所以切片本身离开作用域时不会释放底层内存。
范围切片语法(Range Syntax)
1
2
3
4
5
6
7
8
9
10
11fn 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());
}不可变数组切片(只读视图)
- 从原生数组
[T; N]或动态数组Vec<T>截取切片,只读不改。
1
2
3
4
5
6
7
8
9
10
11
12
13fn 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);
}
}- 从原生数组
可变数组切片(修改原数据)
&mut [T]1
2
3
4
5
6
7
8
9
10
11
12
13fn 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]
}字符串切片是以 字节(Byte) 为单位进行截取的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16fn 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);
}切片最重要的用途是作为函数的输入参数。它可以让你的 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);
}备忘表
| 切片类型 | 常用语法 | 是否能修改数据? | 底层包含什么? | 赋值/传参行为 |
|---|---|---|---|---|
| 不可变数组切片 | &[T] |
❌ 只读 | 指针 (8B) + 长度 (8B) | 借用(不转移所有权) |
| 可变数组切片 | &mut [T] |
✅ 可修改原数组 | 指针 (8B) + 长度 (8B) | 可变借用(独占) |
| 字符串切片 | &str |
❌ 只读(通常) | 指针 (8B) + 长度 (8B) | 借用(不转移所有权) |
元组(Tuple)
- 异构性:一个元组内可以包含多种不同类型的数据(如
i32、bool、String混装)。 - 固定长度:元组一旦定义,其长度是固定的,不能动态增加或删除元素。
- 类型签名:元组的类型由其包含的每个元素的类型和顺序共同决定。例如
(i32, &str)和(&str, i32)在 Rust 中是两种完全不同的类型。
基础语法与声明
1
2
3
4
5
6
7
8
9fn main() {
// 语法 1:自动推导类型
let tup1 = (500, 6.4, "Tetris");
// 语法 2:显式指定类型标签 (i32, f64, &str)
let tup2: (i32, f64, &str) = (500, 6.4, "Tetris");
println!("元组 1: {:?}", tup1); // 使用 {:?} 打印整个元组
}“点号 + 从 0 开始的数字”来获取指定位置的值
1
2
3
4
5
6
7
8
9fn main() {
let point = (10, 20, "ScreenCenter");
let x = point.0;
let y = point.1;
let name = point.2;
println!("坐标: ({}, {}), 名称: {}", x, y, name);
}模式匹配 / 解构 使用
let (x, y, z) = tuple语法,一次性将元组拆包赋值给多个新变量1
2
3
4
5
6
7
8
9
10
11
12fn 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);
}如果元组被声明为
mut,你可以修改其中的元素,但不能更改该位置的类型或改变元组长度。1
2
3
4
5
6
7
8fn main() {
let mut tile_pos = (5, 10);
tile_pos.1 += 1; // 修改 y 坐标,使其向下移动一步
println!("新坐标: {:?}", tile_pos); // 输出: (5, 11)
// tile_pos.0 = "hello"; // ❌ 编译报错!位置 0 只能是 i32,不能变成字符串
}元组的赋值行为(Copy 还是 Move)取决于它内部包裹的所有元素的类型:
- 如果元组内部所有元素都实现了
Copy**(如i32、bool、char),那么这个元组整体也是Copy的,赋值不会发生 Move**。 - 如果元组内部包含任何非
Copy元素(如String、Vec),那么这个元组赋值时会触发 Move(所有权转移)。
1
2
3
4
5
6
7
8
9
10
11
12fn 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);
}- 如果元组内部所有元素都实现了
空元组 (不给元组放入任何元素
())- 它不占用任何内存空间(大小为 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
}定义一个只包含 1 个元素的元组,必须在元素后面加上一个**额外的逗号
,**。1
2
3
4
5
6fn main() {
let not_a_tuple = (5); // 这是一个普通的数字 i32 (带了圆括号)
let single_tuple = (5,); // 这是一个单元素元组,类型是 (i32,)
println!("元组类型第一个元素: {}", single_tuple.0);
}应用场景 用作坐标 / 向量
1
let pos: (usize, usize) = (10, 5); // 棋盘上的 (col, row)应用场景 函数返回多个值
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);
}总结
| 概念 | 示例 | 核心特征 |
|---|---|---|
| 标准元组 | (1, 3.14, "hi") |
异构组合,固定长度 |
| 访问方式 | tup.0 或 let (x, y) = tup |
点号数字 或 模式匹配解构 |
| 单元素元组 | (100,) |
必须加末尾逗号 , |
| 单元类型 (Unit) | () |
零字节内存,等价于 void |
| 典型用途 | 2D/3D坐标 (x, y)、多返回值 |
临时把关联的数据打包传输 |
结构体
三种不同样式的结构体
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经典结构体:定义、实例化与字段访问
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
31struct 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就不能整体使用了!方法(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
31struct 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)
}- Rust 没有
想为数据赋予明确的业务含义,又不想繁琐地命名每一个字段时,可以使用元组结构体
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); // ✅ 正确
}备忘表
| 概念 | 代码语法 | 核心作用 |
|---|---|---|
| 经典结构体 | 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
2
3
4
5
6
7
8
9
10
11
12enum GameState {
// 1. 无数据成员(类似于经典 C 语言枚举)
Menu,
Paused,
// 2. 绑定元组数据(不同成员可以绑定完全不同数量/类型的数据)
Playing(u32), // 保存当前得分
Position(i32, i32), // 保存当前坐标 (x, y)
// 3. 绑定命名结构体数据
GameOver { final_score: u32, is_high_score: bool },
}基础定义与
match模式匹配1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21enum 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));
}解构枚举中的数据(通过
match)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24enum 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));
}如果只对枚举中的某种特定状态感兴趣,用
match写全所有分支会显得臃肿,此时用if let1
2
3
4
5
6
7
8
9
10
11
12
13
14enum GameEvent {
KeyDown(char),
Tick,
WindowResize,
}
fn main() {
let event = GameEvent::KeyDown('R');
// 只关注 KeyDown 事件,忽略其他事件
if let GameEvent::KeyDown(key) = event {
println!("玩家按下了按键: {}", key);
}
}和结构体一样,枚举也可以拥有自己的
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);
}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),
}
}总结
| 概念 | 语法示例 | 核心特征与用途 |
|---|---|---|
| 无数据成员 | 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
cargo new my_tetris_lib --lib创建成功后,项目的目录结构如下:
1
2
3
4my_tetris_lib/
├── Cargo.toml # 项目配置文件
└── src/
└── lib.rs # 库的入口文件(注意:不是 main.rs!)在
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
}库本身不能直接
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
6my_tetris_lib/
├── Cargo.toml
├── src/
│ └── lib.rs
└── examples/
└── demo.rs # 示例入口- 编辑
examples/demo.rs:
1
2
3
4
5
6use 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- 方式 1:在
其他本地项目中调用这个库
- 在
tetris_game/Cargo.toml中,通过 相对路径 引入本地库:
1
2
3[dependencies]
# 指向你本地库的相对路径或绝对路径
my_tetris_lib = { path = "../my_tetris_lib" }- 然后在
tetris_game的代码里就可以像使用第三方库一样直接使用它了:
1
2
3
4
5use my_tetris_lib::Board;
fn main() {
let game_board = Board::new(10, 20);
}- 在
导出为 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
2
3
4my_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);
}方式二:在同一个项目根目录下,包含一个
lib.rs和一个main.rs(单项目双入口)Rust 的 Cargo 非常聪明,允许你在同一个文件夹里既写库逻辑,又写可执行程序!
只要你在
src/目录下同时放这两个文件:
1
2
3
4
5my_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 把当前项目的代码拆分、模块化(子目录)
假设你想在
src/下建一个game文件夹,把board.rs(棋盘)和piece.rs(方块)都放进去1
2
3
4
5
6
7
8my_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
10pub 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
4pub struct Piece {
pub x: i32,
pub y: i32,
}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;在
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);
}较新版本的 Rust 中,如果不喜欢在每个子文件夹里都放一个名字叫
mod.rs的文件,Rust 提供了一种更优雅的替代写法:- 可以直接在
src/目录下建一个与文件夹同名的.rs文件!
1
2
3
4
5
6src/
├── 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 将错误分为两大类:
- 不可恢复错误(Unrecoverable Errors):遇到严重
Bug(如数组越界、内存溢出),直接使用
panic!挂起/终止程序。 - 可恢复错误(Recoverable
Errors):预期可能发生的错误(如文件找不到、网络超时),使用
Result<T, E>枚举显式表达,要求开发者必须显式处理。
0
不可恢复错误:
panic!- 当程序遇到无法继续运行的致命问题时,可以使用
panic!宏。
1
2
3
4
5
6
7
8fn main() {
// 手动触发 panic
// panic!("数据库连接断开,程序无法继续!");
// 常见隐式触发 panic 的场景:数组越界
let v = vec![1, 2, 3];
// v[99]; // 这行代码会导致程序 panic 并打印调用栈
}- 当程序遇到无法继续运行的致命问题时,可以使用
可恢复错误:
Result<T, E>标准枚举- 标准库定义的
Result枚举结构如下:
1
2
3
4enum 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
20use 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
15use 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("创建失败")
});
}- 标准库定义的
用
?操作符传播错误- 如果子函数报错,最常见的做法是把错误向上抛给调用方处理。
- 如果表达式的结果是
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
22use 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")社区标准级的错误处理 Crate:
anyhow(最适合应用层/客户端开发)anyhow::Result<T>可以接收任何实现了std::error::Errortrait 的错误,非常适合写应用程序逻辑,不用为各种复杂的错误类型头疼。
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
22use 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);
}
}社区标准级的错误处理 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
22use 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);
}- 在写一个通用的网络/解析 Core
库,需要为调用方暴露精准的错误类型,可以用
match
- 如果把其他语言的
switch-case比作“只能匹配固定数字或字符串的分支选择器”,那么 Rust 的match就是全能的解构与类型匹配利器。它不仅能判断值,还能直接把复杂数据结构里的内容“剥出来”使用,同时由编译器在背后强制把关,防止漏掉任何边缘情况。
- 穷尽性(Exhaustiveness):必须覆盖所有可能的分支,少写一种情况编译器直接报错。
- 表达式(Expression):每个分支处理完可以返回值,并且可以直接赋给变量。
- 解构(Destructuring):可以直接提取枚举、结构体、元组中的内部数据。
硬性规则: 所有分支 => 右边返回的值类型必须完全一致! 因为 Rust 是强类型语言,整个 match 表达式最终只能输出一种类型的数据
基础值与通配符匹配(替代传统 switch)
- 用
_代表“其他所有情况”(相当于default)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14fn main() {
let number = 3;
// match 作为表达式直接返回值给 status
let status = match number {
1 => "起点",
2 | 3 | 5 | 7 => "质数", // 用 | 匹配多个值
10..=20 => "在 10 到 20 之间", // 用 ..= 匹配闭区间范围
_ => "普通数字", // 通配符,处理所有剩余情况
};
println!("数字状态: {}", status); // 输出: 质数
}- 用
枚举解构(提取内部数据)
1
2
3
4
5
6
7
8
9
10
11
12
13fn 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); // 输出: 暂无成绩记录
}元组与结构体解构
- 直接在匹配的同时将复合类型打散:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17struct 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),
}
}在模式后面跟上一个
if条件语句,进行二次精细化筛选1
2
3
4
5
6
7
8
9
10
11
12fn 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),
}
}@绑定(既匹配范围,又保留原始值)- 当你既想检查数据是否落在特定范围内,又想在后续代码里使用这个数据时:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20enum 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
2let mut s = String::new(); // 调用 String 类型的静态函数 new
let f = File::open("a.txt")?; // 调用 File 类型的 open 函数指定枚举的具体变体(Variant)
1
2let msg = Message::Hello { id: 5 }; // 访问 Message 枚举下的 Hello 变体
let res: Result<i32, &str> = Ok(200); // 实际上是 Result::Ok(200) 的简写访问模块(Module)或导入路径
1
2use std::fs::File; // 导入 std 模块下的 fs 模块下的 File
let err = std::io::Error::last_os_error(); // 完整路径访问泛型调用的“涡轮鱼”语法(Turbofish
::<T>)1
2// 显式告诉 parse 函数把它解析成 i32 类型
let num = "42".parse::<i32>().unwrap();
单冒号 :
单冒号
:—— 用于“类型约束与字段映射”
变量声明时的“类型注解”(Type Annotation)
1
2let age: u32 = 18; // 变量 age 的类型是 u32
let name: String = String::from("Alice");结构体 / 枚举定义与初始化中的“字段赋值”
1
2
3
4
5
6
7
8
9struct User {
name: String, // 定义字段及其类型
age: u32,
}
let u = User {
name: String::from("Tom"), // 初始化赋值
age: 20,
};模式匹配 / 解构中的“模式映射”
1
2
3
4
5
6
7match msg {
// 字段 id : 对应匹配模式 (3..=7)
Message::Hello { id: 3..=7 } => { ... }
// 字段 id : 提取并重命名给新变量 my_id
Message::Hello { id: my_id } => { ... }
}
泛型
给类型打占位符
把类型参数
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
}泛型结构体(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>
}泛型方法(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
25struct 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
}- 为泛型结构体实现方法时,
泛型枚举(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 的错误
}特性约束
- 在泛型中,如果直接对
T进行操作(比如比较大小a > b),编译器会报错,因为并不是所有类型都能比较大小。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15use 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
}- 在泛型中,如果直接对
总结
| 语法 | 含义 |
|---|---|
fn foo<T>(x: T) |
定义泛型函数,T 代指任意类型 |
struct Foo<T, U> |
定义含两个独立泛型的结构体 |
impl<T> Foo<T> |
为泛型结构体实现方法(注意 impl 后面也要带
<T>) |
<T: Display> |
特性约束:限制 T 必须实现
Display(能被打印)特性 |
宏
- 可变参数:普通函数参数数量必须固定;而
println!、vec!可以传任意数量的参数。 - 在编译期操作代码结构:比如
#[derive(Debug)]能在编译时自动分析结构体字段,帮你不费吹灰之力地生成几百行打印代码。 - 减少重复的模版代码(Boilerplate):可以在编译前将重复的模式自动展开。
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]
}重载模式匹配
- 像
match分支一样,一个宏可以写多个匹配规则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15macro_rules! say_hello {
// 规则 1:无参数调用
() => {
println!("Hello!");
};
// 规则 2:接收一个名字字符串
($name:expr) => {
println!("Hello, {}!", $name);
};
}
fn main() {
say_hello!(); // 输出: Hello!
say_hello!("Rust Learner");// 输出: Hello, Rust Learner!
}- 像
过程宏
过程宏比声明宏更硬核,它接受一段 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
2
3# 必须是一个独立的项目/文件夹,名称随意
cargo new hello_macro_derive --lib
cd hello_macro_derive修改
Cargo.toml1
2
3
4
5
6[lib]
proc-macro = true # 核心:声明这是一个过程宏项目!
[dependencies]
syn = "2.0" # 负责把输入的 Rust 代码文本解析成“抽象语法树 (AST)”
quote = "1.0" # 负责把写好的 Rust 代码重新拼装回 TokenStream在
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
26use 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)
}本地项目 加载使用
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
16use 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
}
智能指针
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(); // 输出: 绘制按钮
}- Rust 默认把变量存在栈(Stack)上,用
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
18use 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
}Arc<T>—— 多线程下的安全共享所有权- 内部使用原子操作更新引用计数,可以安全地在多个线程之间传递(传递到
thread::spawn闭包中)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21use 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();
}
}- 内部使用原子操作更新引用计数,可以安全地在多个线程之间传递(传递到
RefCell<T>—— 运行时借用检查(内部可变性)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15use 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
}组合模式:
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
22use 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
}- 既需要“多个地方共享所有权”,又需要“在任意地方修改数据”,单用
总结
| 智能指针 | 存储位置 | 借用检查时机 | 适用场景 |
|---|---|---|---|
Box<T> |
堆内存 | 编译期 | 递归类型、大数据转移所有权、Trait 动态分发 |
Rc<T> |
堆内存 | 编译期(单线程) | 单线程下的多节点共享读取数据 |
Arc<T> |
堆内存 | 编译期(多线程) | 多线程下的数据共享读取 |
RefCell<T> |
栈/堆 | 运行期 | 表面上是不可变引用,但需要在内部修改数据 |
Rc<RefCell<T>> |
堆内存 | 运行期 | 单线程下实现多地方共享 + 可变修改的数据结构(如树/图/双向链表) |