📚 Rust 课程系列

  1. 课程概览
  2. 基础语法(一):变量、数据类型与字符串
  3. 切片(Slice):序列的借用视图
  4. 基础语法(二):运算符、表达式与控制流
  5. 函数与输入输出(本文)
  6. 所有权、借用与生命周期
  7. 结构体
  8. 枚举
  9. 模式匹配
  10. 类型系统:泛型、trait 与多态
  11. 集合与容器
  12. 错误处理与 Panic 恢复
  13. 模块、属性与宏
  14. 智能指针、迭代器与闭包
  15. 并发与异步编程
  16. Unsafe Rust 与常用 trait 详解
  17. 工具链、Cargo 与外部 crate
  18. 最佳实践、性能与调试

本篇介绍 Rust 函数的定义、参数传递(按值、引用、可变引用)与进阶用法(自动解引用、重新借用、模式解构、可变参数实现、所有权策略),并覆盖标准库的 I/O 能力——控制台、文件、缓冲读写与路径处理。

函数

要点说明
定义fn name(param: T) -> R { ... },无返回值省略 ->
返回值末尾表达式即返回值,无需 return;提前退出用 return expr;
发散函数返回类型 ! 表示永不返回(如 panic!、无限循环)
1
2
fn add(a: i32, b: i32) -> i32 { a + b }  // expression return
fn diverge() -> ! { panic!("never returns!"); }

💡 提示:Rust 函数体由语句和表达式组成——表达式无分号即成为返回值,加分号则变为语句返回 ()

🔄 对比:Rust 的 ! 类型类似于 C++ 的 [[noreturn]] 或 GCC 的 __attribute__((noreturn)),但 Rust 的 ! 可强制转换为任意类型,使发散函数能在任何位置使用。

局部函数

Rust 允许在函数内部再定义函数,称为局部函数(嵌套函数)。它是一个普通的 fn 项,不是闭包–这条区别决定了它的所有限制。

1
2
3
4
5
6
7
fn main() {
// nested fn: a regular fn item, not a closure
fn fib(n: u32) -> u32 {
if n < 2 { n } else { fib(n - 1) + fib(n - 2) }
}
println!("{}", fib(10)); // 55
}

使用限制

限制说明
不能捕获外层局部变量fn 项是普通函数,非闭包,无法读写外层函数的 let 绑定(E0434)
作用域限于外层函数仅外层函数内部可见,兄弟函数与外部模块均无法调用
1
2
3
4
5
6
7
8
9
fn main() {
let factor = 3;

fn scale(n: i32) -> i32 {
// n * factor // ERROR E0434: fn item can't capture
n * 2
}
println!("{}", scale(5)); // 10
}

⚠️ 注意:局部函数能访问的只有项(items)–外层及模块中的 conststatic、类型别名与其他函数–唯独不能访问 let 绑定的运行时变量。需要外层运行时值时,要么作为参数传入,要么改用闭包。

1
2
3
4
5
6
7
8
fn outer() {
const FACTOR: i32 = 3; // item: const

fn scale(n: i32) -> i32 {
n * FACTOR // OK: items are accessible
}
println!("{}", scale(5)); // 15
}
1
2
3
4
5
6
7
8
9
10
11
fn main() {
let factor = 3;

// Option A: pass outer value as a parameter
fn scale(n: i32, f: i32) -> i32 { n * f }
println!("{}", scale(5, factor)); // 15

// Option B: use a closure to capture (can't self-recurse)
let scale_cl = |n: i32| n * factor;
println!("{}", scale_cl(5)); // 15
}

🔄 对比:局部 fn 与闭包是一组互补的取舍–fn能自递归但不能捕获,闭包能捕获但不能自递归(闭包无法在体内引用自身的 let 绑定)。需要“既捕获又递归”时,通常把捕获值作为参数传给一个递归的内部 fn

能力局部 fn闭包
捕获外层变量
自递归

💡 提示:局部函数的名称在外层函数内全程可见(即使定义在调用之后),这是项的提升(item hoisting)特性;但它不会泄漏到外层函数之外。

引用参数

Rust 函数参数默认按值传递(移动或复制)。使用引用避免所有权转移:

参数形式语义调用方式
&T不可变引用,只读func(&var)
&mut T可变引用,可修改func(&mut var)
T获取所有权(移动或复制)func(var)
1
2
3
4
5
6
7
fn print_length(s: &str) { println!("长度: {}", s.len()); }

fn main() {
let s = String::from("hello");
print_length(&s); // borrow, ownership stays
println!("{}", s); // s still valid
}
1
2
3
4
5
6
7
fn append_world(s: &mut String) { s.push_str(" world"); }

fn main() {
let mut s = String::from("hello");
append_world(&mut s);
println!("{}", s); // "hello world"
}

借用规则(详见《所有权、借用与生命周期》):

  • 同一时刻:任意多个 &T,或仅一个 &mut T,二者互斥

⚠️ 注意:函数同时持有 &mut T&T 参数时,若在函数体内通过可变引用读取不可变引用指向的数据,编译器会拒绝。解决方案:将只需读取的参数改为按值传递 T(若 T: Copy),或调整调用顺序。

1
2
3
4
5
6
fn process(data: &mut Vec<i32>, item: &i32) {
// data.push(*item); // ERROR: &mut and & conflict
}
fn correct_process(data: &mut Vec<i32>, item: i32) {
data.push(item); // OK: item passed by value
}

返回引用需标注生命周期

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

💡 提示'a 表示返回值的生命周期不超过两个输入中较短的那个。生命周期标注不改变引用的实际寿命,只是编译器验证安全性的契约。完整讨论见《所有权、借用与生命周期》

切片参数与借用视图

&[T]&str&[u8]&Path 都是不拥有数据的借用视图参数–它们是携带「指针 + 长度」的胖指针(详见《切片(Slice)》),让一个签名同时接纳 Vec、数组、String、字面量等多种调用形式。

核心好处:切片视图把「读一段数据」做到零拷贝、不夺所有权、且一个签名通吃多种实参。

参数形式所有权拷贝开销调用后原数据
Vec<T>(按值)获取移动或 clone失效
&Vec<T>借用可用
&[T]借用可用,且实参最广

💡 提示:切片视图是胖指针(指针 + 长度,固定 16 字节),传递开销恒定、与数据量无关。子切片(&v[1..4]&s[..n])仍是切片,可继续传给其他切片函数,天然支持流水线式组合。

🔄 对比:C 传数组会退化为裸指针,丢失长度需额外传参;Go 的 slice header 自带长度,但 append 可能换底层数组、共享数据有隐患。Rust 的 &[T] 胖指针自带长度且为不可变借用,零开销且编译期防越界、防数据竞争。

可变切片视图 &mut [T]:与只读 &[T] 对应,&mut [T] 允许原地修改元素,但长度固定–只能改值(sortfillswap),不能增删(无 push/pop):

1
2
3
4
5
fn sort_in_place(xs: &mut [i32]) { xs.sort(); }

let mut v = vec![3, 1, 2];
sort_in_place(&mut v); // &mut Vec<i32> -> &mut [i32]
sort_in_place(&mut [3, 1, 2]); // 数组也可
参数形式改值增删长度实参来源
&mut Vec<T>&mut vec
&mut [T]&mut vec/&mut arr

💡 提示:只读与可变的取舍方向相反–只读优先 &[T](更通用、实参最广),可变优先 &mut Vec<T>(能力更全、可增删)。仅当明确承诺「不改长度」(就地排序、ASCII 归一化、掩码)时,&mut [T] 的表达力才有优势。

调用侧强转矩阵–同一参数类型,调用方传什么都能接:

参数类型可接受的调用形式强转路径
&[T]&vec&arr&[a, b, c]&[]&Vec<T>/&[T; N] -> &[T]
&str&string"lit"&s[..]&String -> &str
&[u8]&vec_u8b"lit"&bytes&Vec<u8> -> &[u8]
&Path&str&string&path_buf&str/&PathBuf -> &Path
1
2
3
4
5
6
7
8
9
// 一个 &[T] 签名,四种调用方式
fn sum(xs: &[i32]) -> i32 { xs.iter().sum() }

let v = vec![1, 2, 3];
let a = [1, 2, 3];
assert_eq!(sum(&v), 6); // &Vec<i32> -> &[i32]
assert_eq!(sum(&a), 6); // &[i32; 3] -> &[i32]
assert_eq!(sum(&[1, 2, 3]), 6); // 数组字面量
assert_eq!(sum(&[]), 0); // 空切片,无需 Option

💡 提示&[1, 2, 3]&[] 是数组字面量的引用,类型分别为 &[i32; 3]&[i32; 0],再强转为 &[i32]。空切片 &[] 常用于表示「无额外数据」,免去 Option<&[T]> 的累赘。

数组引用 &[T; N] -> 切片 &[T] 是单向强转–长度已知的数组引用总能当切片传入;反向需运行期检查:

1
2
3
fn first3(xs: &[i32]) -> Option<&[i32; 3]> {
xs.try_into().ok() // 长度不足 3 返回 None
}

按数据性质选视图类型

数据性质参数类型说明
文本&strUTF-8 保证,接 &String/字面量
原始字节&[u8]无编码保证,接 &Vec<u8>/b"..."
通用序列&[T]元素类型任意的连续序列
路径&Path跨平台,接 &str/&PathBuf

💡 提示:只读序列参数一律优先切片视图,而非 &Vec<T>/&String–更通用、零拷贝、与字面量天然兼容。需要同时接纳 &str&[u8] 等异构视图时,再上 AsRef(见后文「AsRefAsMut 参数」)。可变序列的取舍见上文「可变切片视图」。

可变参数

Rust 不支持 C 风格可变参数(va_list),但有以下替代方案:

方式适用场景类型灵活性
&[T] 切片同类型、数量可变单一类型
泛型迭代器 I: Iterator<Item=T>同类型、支持多种迭代源单一类型
不同类型、不同数量完全灵活
1
2
3
4
5
6
7
8
// Slice: same type, variable count
fn sum(numbers: &[i32]) -> i32 { numbers.iter().sum() }
// Usage: sum(&[1, 2, 3, 4, 5])

// Generic iterator: accepts Vec, Range, etc.
fn sum_iter<I>(iter: I) -> i32
where I: Iterator<Item = i32> { iter.sum() }
// Usage: sum_iter(1..=5), sum_iter(vec![1,2,3].into_iter())
1
2
3
4
5
6
7
8
9
// Macro: different types, variable count
macro_rules! print_all {
() => {};
($first:expr $(, $rest:expr)*) => {
print!("{} ", $first);
print_all!($($rest),*);
};
}
// Usage: print_all!(1, "hello", 3.14, true);

🔄 对比:Go 用 ...T 可变参数、C 用 va_list、Java 用可变参数 T...——它们都是语言级特性。Rust 选择宏方案,因类型安全与零成本抽象难以兼得。

引用参数的进阶用法

显式 &*:解引用与自动引用

& 取引用、* 解引用,是引用操作符的两面。Rust 的特别之处在于:很多场景编译器会自动补上它们,唯独写入必须手写 *。先看必须显式的场景:

1
2
3
4
5
6
7
8
9
let x = 5;
let r = &x; // 显式 &:取引用
assert_eq!(*r, 5); // 显式 *:读取引用指向的值

let mut y = 10;
let mr = &mut y;
*mr = 20; // 显式 *:通过引用写入(不可省略)
*mr += 5; // 复合赋值同样需要 *
assert_eq!(y, 25);

写入时 * 不可省略–若写成 mr = 20,编译器会当作对 mr 本身的赋值(mr: &mut i3220: i32 类型不符而报错),而非修改它指向的 y。读取则宽松得多:*r 虽合法,但方法调用、字段访问等场景下编译器会自动解引用,下文展开。

最常见的自动场景是方法调用。编译器根据方法接收器签名(self/&self/&mut self)自动补上 &&mut*,这套机制称为自动引用(auto-ref)

1
2
3
4
5
let mut s = String::from("hi");
s.push_str(", world"); // push_str(&mut self, ..) -> 自动 (&mut s).push_str(..)
s.len(); // len(&self) -> 自动 (&s).len()
// 显式等价写法(通常无需这么写):
// String::push_str(&mut s, ", world");

auto-ref 让 obj.method()objT&T&mut T 时都能直接调用,省去手动 &。接收器形式的选择见后文「接收器模式」。

引用还会链式自动解引用(auto-deref)–多层引用层层穿透,直到够得着目标方法或字段:

1
2
3
4
5
let s = String::from("hi");
let r: &String = &s;
let rr: &&String = &r;
assert_eq!(rr.len(), 2); // 自动穿透两层引用到达 String
assert_eq!(String::len(&**rr), 2); // 完全显式:手动解两层 + 手动 &

字段访问同理,box.fieldref.field 会自动穿透引用与智能指针。下表归纳「显式」与「自动」的分工:

操作显式写法自动场景
取引用&x / &mut x方法接收器自动 &(auto-ref)
解引用读取*r方法调用、字段访问自动 deref
解引用写入*r = v / *r += n无(必须显式)
多层穿透String::len(&**rr)rr.len() 自动逐层 deref

💡 提示:判断原则–读取与方法调用通常自动,写入与取引用传参通常显式。注意普通函数不走 auto-ref:fn f(x: &T)x: T 时调用处仍需手写 f(&x),auto-ref 只作用于方法的接收器。

🔬 进阶:以上自动机制处理的是同类型引用间的 &/*(如 &&T -> &T -> T)。跨类型转换(如 &String -> &str)走的是下一节的 Deref 强制转换,它依赖 Deref trait,而非简单的 * 解引用。

自动解引用与强制转换

Rust 的 Deref coercion 自动将 &String&str&Box<T>&T,简化函数调用:

1
2
3
4
5
6
7
8
9
fn greet(name: &str) { println!("Hello, {}!", name); }

fn main() {
let s = String::from("Alice");
greet(&s); // &String -> &str (auto)

let boxed = Box::new(String::from("Bob"));
greet(&boxed); // &Box<String> -> &String -> &str (chain)
}

💡 提示:Deref coercion 仅发生在"显然安全"的场合——从拥有者到借用者的单向转换。它不会反向进行(&str 不会自动变成 &String),因后者需分配内存。

🔄 对比:C++ 的隐式转换运算符可自定义任意转换路径,容易产生意外行为;Rust 的 Deref coercion 只沿 Deref/DerefMut 链走,编译器控制转换方向,杜绝隐式开销。

引用参数的重新借用(Reborrowing)

可变引用在每次使用间会自动重新借用,允许同一 &mut T 在顺序调用中多次传递:

1
2
3
4
5
6
7
8
9
fn push_if_positive(v: &mut Vec<i32>, n: i32) {
if n > 0 { v.push(n); }
}

fn process(data: &mut Vec<i32>) {
push_if_positive(data, 5); // first borrow
push_if_positive(data, -3); // auto reborrow
push_if_positive(data, 10); // auto reborrow
}

⚠️ 注意:重新借用仅在顺序调用时自动生效。若将 &mut 引用存入变量并跨语句持有,则在该变量生命周期结束前无法再次借用——这是 NLL(Non-Lexical Lifetimes)的规则:引用的生命周期持续到最后一次使用而非作用域结尾。

引用与模式匹配

函数参数中可直接用模式解构引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Destructure tuple reference
fn process_point(&(x, y): &(i32, i32)) {
println!("坐标: ({}, {})", x, y);
}

// Destructure struct reference
struct Point { x: f64, y: f64 }
fn process_point_ref(&Point { x, y }: &Point) {
println!("x = {}, y = {}", x, y);
}

// Destructure slice reference
fn get_first_two(&[first, second, ..]: &[i32]) -> Option<(i32, i32)> {
Some((first, second))
}

💡 提示:参数模式中的 & 表示"我期望一个引用,请帮我解构其内容"。&(x, y) 解构引用指向的元组,xyi32 值(非引用)。

可变参数的更多实现方式(进阶)

使用 IntoIterator trait

Iterator 更灵活——接受任何可转换为迭代器的类型(Vec、数组、Range 等):

1
2
3
4
5
6
7
8
fn sum_all<I, T>(items: I) -> T
where
I: IntoIterator<Item = T>,
T: std::iter::Sum,
{
items.into_iter().sum()
}
// sum_all(vec![1, 2, 3]), sum_all([1, 2, 3]), sum_all(1..=5)

💡 提示IntoIterator 消费原集合(into_iter()),若需保留原数据请用 &IIntoIterator 实现(大多数集合为 &Vec<T> 实现了 IntoIterator<Item=&T>)。

使用 FromIterator 构建集合

配合 collect(),可将任意迭代器收集到目标容器:

1
2
3
4
5
6
7
8
fn collect_to<C, T, I>(items: I) -> C
where
I: IntoIterator<Item = T>,
C: FromIterator<T>,
{
items.into_iter().collect()
}
// let set: HashSet<i32> = collect_to(vec![1, 2, 2, 3]);

可变参数的宏模式(进阶)

支持键值对参数的宏模式:

1
2
3
4
5
6
7
8
macro_rules! create_person {
($($key:ident: $value:expr),* $(,)?) => {{
let mut person = std::collections::HashMap::new();
$(person.insert(stringify!($key), $value);)*
person
}};
}
// let p = create_person! { name: "Alice", age: 30, };

🔬 进阶$(,)? 是 Rust 宏中的可选尾逗号模式,允许 name: "Alice", 末尾带逗号。宏的递归展开在编译期完成,不影响运行时性能。

函数参数的所有权策略

接收器模式(Receiver Patterns)

根据函数对 self 的处理方式选择合适的接收器——这是 Rust API 设计的核心决策:

接收器语义调用后 self典型用途
&self只读借用仍可用getter
&mut self可变借用仍可用(已修改)setter / mutation
self获取所有权不可用消费转换(into_*
mut self获取+返回所有权不可用(返回新值)builder 链式调用
1
2
3
4
5
6
7
8
9
10
11
struct Data { content: String }

impl Data {
fn get_content(&self) -> &str { &self.content } // borrow
fn update_content(&mut self, new: &str) { self.content = new.to_string(); } // mut borrow
fn into_content(self) -> String { self.content } // consume
fn with_prefix(mut self, prefix: &str) -> Self { // consume + return
self.content = format!("{}{}", prefix, self.content);
self
}
}

💡 提示mut selfself 的区别仅在于函数体内是否可修改——调用后原值均不可用。mut self 常用于 builder 模式:Data::new().with_name("x").with_age(30)

🔄 对比:C++ 的值/引用/指针参数选择靠约定,Rust 通过 self/&self/&mut self 在类型系统层面强制执行,编译器保证不会在借用期间意外销毁或修改。

AsRefAsMut 参数

使用 trait bound 让函数接受多种拥有/借用形式:

1
2
3
4
5
fn print_bytes<T: AsRef<[u8]>>(data: T) {
let bytes = data.as_ref();
println!("字节数: {}, 内容: {:?}", bytes.len(), bytes);
}
// print_bytes(b"hello"), print_bytes(vec![1u8,2,3]), print_bytes("world")

💡 提示AsRef<T> 是"我需要一个 &T 视图"的通用表达。标准库为 String 实现了 AsRef<str>AsRef<[u8]>,为 Vec<T> 实现了 AsRef<[T]> 等。优先用 AsRef<str> 而非 &String,用 AsRef<[T]> 而非 &Vec<T>

Cow(写时克隆)作为参数(进阶)

避免不必要的克隆——仅在需要修改时才分配内存:

1
2
3
4
5
6
7
8
9
10
11
use std::borrow::Cow;

fn normalize<'a>(input: Cow<'a, str>) -> Cow<'a, str> {
if input.contains('\t') {
Cow::Owned(input.replace('\t', " ")) // needs mutation → clone
} else {
input // no mutation → return borrowed
}
}
// normalize(Cow::Borrowed("hello world")) // zero-alloc
// normalize(Cow::Borrowed("hello\tworld")) // allocates new String

🔬 进阶Cow<'a, B>Borrowed(&'a B)Owned(B::Owned) 的枚举。调用者传入 Cow::Borrowed(&str) 时,若函数不需要修改则零分配返回;若需修改则 into_owned() 触发克隆。这在解析器、字符串处理等场景可显著减少堆分配。

🔄 对比Cow 类似 C++ 的 copy-on-write 智能指针,但 Rust 的 Cow 在类型层面区分借用与拥有,编译器确保不会意外修改借用数据。

函数设计要点速查

  • 优先 &T / &mut T,仅在需要所有权时用值传递
  • 接受 &str 而非 &String&[T] 而非 &Vec<T>——更通用
  • 库函数返回 Result<T, E>,避免内部 panic!
  • 函数指针:fn(i32, i32) -> i32 可作为参数或存储
  • 高阶函数:Fn/FnMut/FnOnce 闭包 trait(详见《智能指针、迭代器与闭包》
1
2
3
4
5
6
7
8
// Function pointer
let op: fn(i32, i32) -> i32 = add;
let result = op(5, 3);

// Higher-order function
fn apply_twice<F>(f: F, x: i32) -> i32
where F: Fn(i32) -> i32 { f(f(x)) }
let result = apply_twice(|x| x + 1, 5); // 7

输入输出(I/O)

Rust 标准库的 I/O 操作返回 Result,必须处理可能的错误。错误处理的完整讨论见《错误处理与 Panic 恢复》

标准输入输出

1
2
3
4
5
6
7
8
9
10
11
12
// Output
println!("Hello, {}!", "Rust"); // auto newline
print!("Enter name: "); // no newline

// Input
use std::io;
let mut input = String::new();
io::stdin().read_line(&mut input).expect("read failed");
println!("You entered: {}", input.trim());

// Formatted output (more in ch08)
println!("x = {:04}, y = {:.2}", 42, 3.14159); // x = 0042, y = 3.14

💡 提示read_line 保留末尾换行符,需 .trim() 去除。格式化输出的完整语法见《模块、属性与宏》

文件 I/O

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

// Read entire file
let content = fs::read_to_string("hello.txt").expect("read failed");

// Write file
fs::write("output.txt", "Hello, file!").expect("write failed");

// Line-by-line (for large files)
use std::fs::File;
use std::io::{BufRead, BufReader};
let reader = BufReader::new(File::open("data.txt").expect("open failed"));
for line in reader.lines() {
println!("{}", line.expect("read line failed"));
}

⚠️ 注意read_to_string 将整个文件载入内存,大文件请用 BufReader + 逐行/逐块读取,避免 OOM。

缓冲读写

BufReader / BufWriter 减少系统调用次数,对频繁小量读写效果显著:

1
2
3
4
5
6
7
8
9
10
11
12
use std::io::{BufReader, BufWriter, Read, Write};
use std::fs::File;

// Buffered read
let mut reader = BufReader::new(File::open("large.txt")?);
let mut buffer = String::new();
reader.read_to_string(&mut buffer)?;

// Buffered write
let mut writer = BufWriter::new(File::create("output.txt")?);
writer.write_all(b"Hello, buffered world!")?;
writer.flush()?; // ensure data reaches disk

💡 提示BufWriter 在缓冲区满或 flush() 时才真正写入。若程序在 flush() 前崩溃,缓冲区数据可能丢失。在关键路径上务必显式 flush()

🔄 对比:C 的 FILE* 默认带缓冲,Go 的 bufio.Reader/Writer 用法类似。Rust 的 File 是无缓冲的原始句柄,必须手动包装 BufReader/BufWriter——这使零拷贝等高级场景更可控。

错误处理

I/O 操作返回 Result<T, std::io::Error>。生产代码用 ? 传播错误,简单脚本可用 expect/unwrap

1
2
3
4
5
6
7
8
use std::fs::File;
use std::io::Read;

fn read_file(path: &str) -> Result<String, std::io::Error> {
let mut content = String::new();
File::open(path)?.read_to_string(&mut content)?;
Ok(content)
}

⚠️ 注意? 只能在返回 Result/Option 的函数中使用。main 函数可声明为 fn main() -> Result<(), Box<dyn Error>> 以支持 ?。完整错误处理策略见《错误处理与 Panic 恢复》

路径处理

使用 Path / PathBuf 进行跨平台路径操作——Path 是借用型(类似 &str),PathBuf 是拥有型(类似 String):

1
2
3
4
5
6
7
8
9
use std::path::{Path, PathBuf};

let path = Path::new("/home/user/file.txt");
path.exists(); // bool
path.parent(); // Option<&Path>
path.extension(); // Option<&OsStr>

let mut buf = PathBuf::from("/home/user");
buf.push("file.txt"); // cross-platform: uses OS separator

🔄 对比:Go 的 filepath 包、Python 的 pathlib 功能类似。Rust 的 Path/PathBuf 区分借用与拥有,与 str/String 的设计一致。

I/O 要点速查

  • 生产代码避免 expect/unwrap,用 ? 传播错误
  • 大文件用 BufReader 流式读取,避免 read_to_string OOM
  • 跨平台路径用 Path/PathBuf,不要字符串拼接
  • 网络 I/O 使用异步编程(详见《并发与异步编程》