函数与输入输出
📚 Rust 课程系列
本篇介绍 Rust 函数的定义、参数传递(按值、引用、可变引用)与进阶用法(自动解引用、重新借用、模式解构、可变参数实现、所有权策略),并覆盖标准库的 I/O 能力——控制台、文件、缓冲读写与路径处理。
函数
| 要点 | 说明 |
|---|---|
| 定义 | fn name(param: T) -> R { ... },无返回值省略 -> |
| 返回值 | 末尾表达式即返回值,无需 return;提前退出用 return expr; |
| 发散函数 | 返回类型 ! 表示永不返回(如 panic!、无限循环) |
1 | |
💡 提示:Rust 函数体由语句和表达式组成——表达式无分号即成为返回值,加分号则变为语句返回
()。
🔄 对比:Rust 的
!类型类似于 C++ 的[[noreturn]]或 GCC 的__attribute__((noreturn)),但 Rust 的!可强制转换为任意类型,使发散函数能在任何位置使用。
局部函数
Rust 允许在函数内部再定义函数,称为局部函数(嵌套函数)。它是一个普通的 fn 项,不是闭包–这条区别决定了它的所有限制。
1 | |
使用限制:
| 限制 | 说明 |
|---|---|
| 不能捕获外层局部变量 | fn 项是普通函数,非闭包,无法读写外层函数的 let 绑定(E0434) |
| 作用域限于外层函数 | 仅外层函数内部可见,兄弟函数与外部模块均无法调用 |
1 | |
⚠️ 注意:局部函数能访问的只有项(items)–外层及模块中的
const、static、类型别名与其他函数–唯独不能访问let绑定的运行时变量。需要外层运行时值时,要么作为参数传入,要么改用闭包。
1 | |
1 | |
🔄 对比:局部
fn与闭包是一组互补的取舍–fn项能自递归但不能捕获,闭包能捕获但不能自递归(闭包无法在体内引用自身的let绑定)。需要“既捕获又递归”时,通常把捕获值作为参数传给一个递归的内部fn。
| 能力 | 局部 fn 项 | 闭包 |
|---|---|---|
| 捕获外层变量 | ❌ | ✅ |
| 自递归 | ✅ | ❌ |
💡 提示:局部函数的名称在外层函数内全程可见(即使定义在调用之后),这是项的提升(item hoisting)特性;但它不会泄漏到外层函数之外。
引用参数
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表示返回值的生命周期不超过两个输入中较短的那个。生命周期标注不改变引用的实际寿命,只是编译器验证安全性的契约。完整讨论见《所有权、借用与生命周期》。
切片参数与借用视图
&[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] 允许原地修改元素,但长度固定–只能改值(sort、fill、swap),不能增删(无 push/pop):
1 | |
| 参数形式 | 改值 | 增删长度 | 实参来源 |
|---|---|---|---|
&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_u8、b"lit"、&bytes | &Vec<u8> -> &[u8] |
&Path | &str、&string、&path_buf | &str/&PathBuf -> &Path |
1 | |
💡 提示:
&[1, 2, 3]与&[]是数组字面量的引用,类型分别为&[i32; 3]与&[i32; 0],再强转为&[i32]。空切片&[]常用于表示「无额外数据」,免去Option<&[T]>的累赘。
数组引用 &[T; N] -> 切片 &[T] 是单向强转–长度已知的数组引用总能当切片传入;反向需运行期检查:
1 | |
按数据性质选视图类型:
| 数据性质 | 参数类型 | 说明 |
|---|---|---|
| 文本 | &str | UTF-8 保证,接 &String/字面量 |
| 原始字节 | &[u8] | 无编码保证,接 &Vec<u8>/b"..." |
| 通用序列 | &[T] | 元素类型任意的连续序列 |
| 路径 | &Path | 跨平台,接 &str/&PathBuf |
💡 提示:只读序列参数一律优先切片视图,而非
&Vec<T>/&String–更通用、零拷贝、与字面量天然兼容。需要同时接纳&str与&[u8]等异构视图时,再上AsRef(见后文「AsRef与AsMut参数」)。可变序列的取舍见上文「可变切片视图」。
可变参数
Rust 不支持 C 风格可变参数(va_list),但有以下替代方案:
| 方式 | 适用场景 | 类型灵活性 |
|---|---|---|
&[T] 切片 | 同类型、数量可变 | 单一类型 |
泛型迭代器 I: Iterator<Item=T> | 同类型、支持多种迭代源 | 单一类型 |
| 宏 | 不同类型、不同数量 | 完全灵活 |
1 | |
1 | |
🔄 对比:Go 用
...T可变参数、C 用va_list、Java 用可变参数T...——它们都是语言级特性。Rust 选择宏方案,因类型安全与零成本抽象难以兼得。
引用参数的进阶用法
显式 & 与 *:解引用与自动引用
& 取引用、* 解引用,是引用操作符的两面。Rust 的特别之处在于:很多场景编译器会自动补上它们,唯独写入必须手写 *。先看必须显式的场景:
1 | |
写入时 * 不可省略–若写成 mr = 20,编译器会当作对 mr 本身的赋值(mr: &mut i32 与 20: i32 类型不符而报错),而非修改它指向的 y。读取则宽松得多:*r 虽合法,但方法调用、字段访问等场景下编译器会自动解引用,下文展开。
最常见的自动场景是方法调用。编译器根据方法接收器签名(self/&self/&mut self)自动补上 &、&mut 或 *,这套机制称为自动引用(auto-ref):
1 | |
auto-ref 让 obj.method() 在 obj 为 T、&T、&mut T 时都能直接调用,省去手动 &。接收器形式的选择见后文「接收器模式」。
引用还会链式自动解引用(auto-deref)–多层引用层层穿透,直到够得着目标方法或字段:
1 | |
字段访问同理,box.field、ref.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 强制转换,它依赖Dereftrait,而非简单的*解引用。
自动解引用与强制转换
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 使用异步编程(详见《并发与异步编程》)



















