函数与输入输出
📚 Rust 课程系列
函数是组织代码的基本单位,输入输出则是程序与外界交互的窗口。本篇介绍 Rust 函数的定义、参数传递(按值、引用、可变引用)与进阶用法(自动解引用、重新借用、模式解构、可变参数的多种实现、所有权策略),并覆盖标准库的 I/O 能力——控制台、文件、缓冲读写与路径处理。
函数
| 要点 | 说明 |
|---|---|
| 定义 | fn name(param: T) -> R { ... },无返回值省略 -> |
| 返回值 | 末尾表达式即返回值,无需 return;提前退出用 return expr; |
| 发散函数 | 返回类型 ! 表示永不返回(如 panic!、无限循环) |
1 | |
💡 提示:Rust 函数体由语句和表达式组成——表达式无分号即成为返回值,加分号则变为语句返回
()。这是初学者最常犯的错误之一。
🔄 对比:Rust 的
!类型类似于 C++ 的[[noreturn]]或 GCC 的__attribute__((noreturn)),但 Rust 的!可以强制类型转换为任意类型,使发散函数可在任何位置使用。
引用参数
Rust 函数参数默认按值传递(移动或复制)。使用引用避免所有权转移:
| 参数形式 | 语义 | 调用方式 |
|---|---|---|
&T | 不可变引用,只读 | func(&var) |
&mut T | 可变引用,可修改 | func(&mut var) |
T | 获取所有权(移动或复制) | func(var) |
1 | |
1 | |
借用规则(详见《所有权、借用与生命周期》):
- 同一时刻:任意多个
&T,或仅一个&mut T,二者互斥
⚠️ 注意:函数同时持有
&mut T和&T参数时,若在函数体内通过可变引用读取不可变引用指向的数据,编译器会拒绝。解决方案:将只需读取的参数改为按值传递T(若T: Copy),或调整调用顺序。
1 | |
返回引用需标注生命周期:
1 | |
💡 提示:
'a表示返回值的生命周期不超过两个输入中较短的那个。生命周期标注不改变引用的实际寿命,只是编译器用来验证安全性的契约。完整讨论见《所有权、借用与生命周期》。
可变参数
Rust 不支持 C 风格可变参数(va_list),但有以下替代方案:
| 方式 | 适用场景 | 类型灵活性 |
|---|---|---|
&[T] 切片 | 同类型、数量可变 | 单一类型 |
泛型迭代器 I: Iterator<Item=T> | 同类型、支持多种迭代源 | 单一类型 |
| 宏 | 不同类型、不同数量 | 完全灵活 |
1 | |
1 | |
🔄 对比:Go 用
...T可变参数、C 用va_list、Java 用可变参数T...——它们都是语言级特性。Rust 选择宏方案,因为类型安全与零成本抽象难以兼得。
引用参数的进阶用法
自动解引用与强制转换
Rust 的 Deref coercion 自动将 &String → &str、&Box<T> → &T,简化函数调用:
1 | |
💡 提示:Deref coercion 仅发生在"显然安全"的场合——从拥有者到借用者的单向转换。它不会反向进行(
&str不会自动变成&String),因为后者需要分配内存。
🔄 对比:C++ 的隐式转换运算符可自定义任意转换路径,容易产生意外行为;Rust 的 Deref coercion 只沿
Deref/DerefMut链走,编译器控制转换方向,杜绝隐式开销。
引用参数的重新借用(Reborrowing)
可变引用在每次使用间会自动重新借用,允许同一 &mut T 在顺序调用中多次传递:
1 | |
⚠️ 注意:重新借用仅在顺序调用时自动生效。若将
&mut引用存入变量并跨语句持有,则在该变量生命周期结束前无法再次借用——这是 NLL(Non-Lexical Lifetimes)的规则:引用的生命周期持续到最后一次使用而非作用域结尾。
引用与模式匹配
函数参数中可直接用模式解构引用:
1 | |
💡 提示:参数模式中的
&表示"我期望一个引用,请帮我解构其内容"。&(x, y)解构引用指向的元组,x和y是i32值(非引用)。
可变参数的更多实现方式(进阶)
使用 IntoIterator trait
比 Iterator 更灵活——接受任何可转换为迭代器的类型(Vec、数组、Range 等):
1 | |
💡 提示:
IntoIterator消费原集合(into_iter()),若需保留原数据请用&I的IntoIterator实现(大多数集合为&Vec<T>实现了IntoIterator<Item=&T>)。
使用 FromIterator 构建集合
配合 collect(),可将任意迭代器收集到目标容器:
1 | |
可变参数的宏模式(进阶)
支持键值对参数的宏模式:
1 | |
🔬 进阶:
$(,)?是 Rust 宏中的可选尾逗号模式,允许name: "Alice",末尾带逗号。宏的递归展开在编译期完成,不影响运行时性能。
函数参数的所有权策略
接收器模式(Receiver Patterns)
根据函数对 self 的处理方式选择合适的接收器——这是 Rust API 设计的核心决策:
| 接收器 | 语义 | 调用后 self | 典型用途 |
|---|---|---|---|
&self | 只读借用 | 仍可用 | getter |
&mut self | 可变借用 | 仍可用(已修改) | setter / mutation |
self | 获取所有权 | 不可用 | 消费转换(into_*) |
mut self | 获取+返回所有权 | 不可用(返回新值) | builder 链式调用 |
1 | |
💡 提示:
mut self与self的区别仅在于函数体内是否可修改——调用后原值均不可用。mut self常用于 builder 模式:Data::new().with_name("x").with_age(30)。
🔄 对比:C++ 的值/引用/指针参数选择靠约定,Rust 通过
self/&self/&mut self在类型系统层面强制执行,编译器保证不会在借用期间意外销毁或修改。
AsRef 与 AsMut 参数
使用 trait bound 让函数接受多种拥有/借用形式:
1 | |
💡 提示:
AsRef<T>是"我需要一个&T视图"的通用表达。标准库为String实现了AsRef<str>和AsRef<[u8]>,为Vec<T>实现了AsRef<[T]>等。优先使用AsRef<str>而非&String,使用AsRef<[T]>而非&Vec<T>。
Cow(写时克隆)作为参数(进阶)
避免不必要的克隆——仅在需要修改时才分配内存:
1 | |
🔬 进阶:
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 | |
输入输出(I/O)
Rust 标准库的 I/O 操作返回 Result,必须处理可能的错误。错误处理的完整讨论见《错误处理与 Panic 恢复》。
标准输入输出
1 | |
💡 提示:
read_line保留末尾换行符,需.trim()去除。格式化输出的完整语法见《模块、属性与宏》。
文件 I/O
1 | |
⚠️ 注意:
read_to_string将整个文件载入内存,大文件请用BufReader+ 逐行/逐块读取,避免 OOM。
缓冲读写
BufReader / BufWriter 减少系统调用次数,对频繁小量读写效果显著:
1 | |
💡 提示:
BufWriter在缓冲区满或flush()时才真正写入。若程序在flush()前崩溃,缓冲区数据可能丢失。在关键路径上务必显式flush()。
🔄 对比:C 的
FILE*默认带缓冲,Go 的bufio.Reader/Writer用法类似。Rust 的File是无缓冲的原始句柄,必须手动包装BufReader/BufWriter——这使零拷贝等高级场景更可控。
错误处理
I/O 操作返回 Result<T, std::io::Error>。生产代码用 ? 传播错误,简单脚本可用 expect/unwrap:
1 | |
⚠️ 注意:
?只能在返回Result/Option的函数中使用。main函数可声明为fn main() -> Result<(), Box<dyn Error>>以支持?。完整错误处理策略见《错误处理与 Panic 恢复》。
路径处理
使用 Path / PathBuf 进行跨平台路径操作——Path 是借用型(类似 &str),PathBuf 是拥有型(类似 String):
1 | |
🔄 对比:Go 的
filepath包、Python 的pathlib功能类似。Rust 的Path/PathBuf区分借用与拥有,与str/String的设计一致。
I/O 要点速查:
- 生产代码避免
expect/unwrap,用?传播错误 - 大文件用
BufReader流式读取,避免read_to_stringOOM - 跨平台路径用
Path/PathBuf,不要字符串拼接 - 网络 I/O 使用异步编程(详见《并发与异步编程》)


