📚 Rust 课程系列

  1. 课程概览
  2. 基础语法
  3. 函数与输入输出(本文)
  4. 所有权、借用与生命周期
  5. 结构体、枚举与模式匹配
  6. 类型系统:泛型、trait 与多态
  7. 集合与容器
  8. 错误处理与 Panic 恢复
  9. 模块、属性与宏
  10. 智能指针、迭代器与闭包
  11. 并发与异步编程
  12. Unsafe Rust 与常用 trait 详解
  13. 工具链、Cargo 与外部 crate
  14. 最佳实践、性能与调试

函数是组织代码的基本单位,输入输出则是程序与外界交互的窗口。本篇介绍 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 函数参数默认按值传递(移动或复制)。使用引用避免所有权转移:

参数形式语义调用方式
&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 表示返回值的生命周期不超过两个输入中较短的那个。生命周期标注不改变引用的实际寿命,只是编译器用来验证安全性的契约。完整讨论见《所有权、借用与生命周期》

可变参数

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 的 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 使用异步编程(详见《并发与异步编程》