文章目录
- [1 函数](#1 函数)
-
- [1.1 函数和代码块](#1.1 函数和代码块)
- [1.2 闭包](#1.2 闭包)
-
- [1.2.1 简介](#1.2.1 简介)
- [1.2.2 捕获外部变量](#1.2.2 捕获外部变量)
- [1.2.3 闭包 Trait:Fn / FnMut / FnOnce](#1.2.3 闭包 Trait:Fn / FnMut / FnOnce)
- [1.2.4 闭包作为参数和返回值](#1.2.4 闭包作为参数和返回值)
-
- [1.2.4.1 闭包作为参数](#1.2.4.1 闭包作为参数)
- [1.2.4.2 闭包作为返回值](#1.2.4.2 闭包作为返回值)
- [2 错误处理](#2 错误处理)
-
- [2.1 简介](#2.1 简介)
- [2.2 不可恢复错误:panic!](#2.2 不可恢复错误:panic!)
- [2.3 可恢复错误:Result<T, E>](#2.3 可恢复错误:Result<T, E>)
-
- [2.3.1 使用 match 处理 Result](#2.3.1 使用 match 处理 Result)
- [2.3.2 unwrap 和 expect 快速但危险](#2.3.2 unwrap 和 expect 快速但危险)
- [2.4 错误传播:? 运算符](#2.4 错误传播:? 运算符)
-
- [2.4.1 手动传播 vs ? 运算符](#2.4.1 手动传播 vs ? 运算符)
- [2.4.2 在 main 中使用 ?](#2.4.2 在 main 中使用 ?)
- [2.5 自定义错误](#2.5 自定义错误)
-
- [2.5.1 自定义类型与分类处理](#2.5.1 自定义类型与分类处理)
- [2.5.2 自定义错误类型](#2.5.2 自定义错误类型)
- [2.5.3 三方库](#2.5.3 三方库)
1 函数
1.1 函数和代码块
Rust 函数的基本形式:fn <函数名> ( <参数> ) <函数体>,需要注意的是 Rust 函数名称 的命名风格是小写字母以下划线分割,函数最后一行如果没return没有分号那么最后一行值作为返回,如果没return但有分号则返回值是空元组
代码块:创建新作用域,这样变量在块内定义,块外不可见,块的最后一行表达式 可以作为返回值(没有分号返回最后一行值,有分号则返回值是空元组),资源释放在块结束时自动调用 drop
rust
fn add(a: i32, b: i32) -> i32 {
a + b // 没有分号,返回 a+b
}
// 等同于
fn add(a: i32, b: i32) -> i32 {
return a + b; // 有分号 有return
}
fn print_sum(a: i32, b: i32) {
println!("{}", a + b); // 有分号,返回 ()
}
let x = {
let a = 1;
let b = 2;
a + b // ← 没有分号,这是返回值
// return a + b; // 报错!不能在代码块里用 return
}; // x = 3
let y = {
let a = 1;
let b = 2;
a + b; // ← 有分号,变成语句,返回值是 ()
}; // y = ()
1.2 闭包
1.2.1 简介
闭包是一种匿名函数,它可以捕获并存储其所在环境中的变量,并把这些变量"带"到它被调用的地方去使用。闭包被广泛应用于函数式编程、并发编程和事件驱动编程等领域。
闭包的基本语法:let closure_name = |参数列表| 表达式或语句块;
闭包和普通函数都能封装一段逻辑,但闭包可以捕获外部环境中的变量
| 特性 | 闭包 | 函数 |
|---|---|---|
| 匿名性 | 没有名称,通常赋值给变量 | 有固定的 fn 名称 |
| 环境捕获 | 可以捕获外部变量 | 不能捕获外部变量 |
| 定义方式 | ` | 参数 |
| 类型推导 | 参数和返回值类型通常可以省略 | 必须显式指定参数和返回值类型 |
| 存储与传递 | 可以作为变量、参数、返回值 | 同样支持 |
示例
rust
// 闭包:省略类型注解,编译器自动推断
let add_one = |x| x + 1;
println!("add_one(4) = {}", add_one(4)); // 输出: 5
// 闭包:显式标注参数和返回值类型 需要语句块
let multiply = |a: i32, b: i32| -> i32 { a * b };
println!("multiply(3, 7) = {}", multiply(3, 7)); // 输出: 21
// 多行闭包需要加大括号
let greet = |name: &str| {
let greeting = format!("Hello, {}!", name);
greeting
};
println!("{}", greet("test")); // 输出: Hello, test!
1.2.2 捕获外部变量
闭包最核心的特性是能够捕获其所在作用域中的变量,这是闭包与普通函数的本质区别
rust
let x = 10;
// 报错!普通函数无法访问外部变量 x 只能用自己的参数和全局变量
fn print_x() {
println!("{}", x); // 报错:找不到 x
}
//闭包可以"捕获" x
let print_x = || {
println!("{}", x); // x 是从外部捕获的
};
print_x();
闭包可以通过三种方式捕获外部变量:
| 方式 | 写法 | 捕获形式 | 原变量还能用? |
|---|---|---|---|
| 不可变借用 | 默认(只读) | &T |
能 |
| 可变借用 | 修改值 | &mut T |
闭包定义后直到闭包销毁前不能用 |
| 转移所有权 | move |
T |
不能 |
不可变借用(默认)
rust
let name = String::from("Alice");
let greet = || {
println!("Hello, {}", name); // 只读,不可变借用
};
greet(); // Hello, Alice
println!("{}", name); // 还能用
可变借用,外部变量必须是 mut,闭包变量也要 mut
rust
let mut count = 0;
let mut increment = || {
count += 1; // 修改,可变借用
println!("{}", count);
};
increment(); // 1
//println!("{}", count); // 报错!此时 increment 后面还要被调用,它的生命周期还没结束
increment(); // 2
println!("{}", count); // 报错!闭包持有可变借用
转移所有权(move)
rust
let name = String::from("Alice");
let consume = move || {
println!("{}", name); // name 被 move 进闭包
};
consume();
// println!("{}", name); // 报错!name 所有权已转移
注意:对于实现了 Copy trait 的类型(如 i32、bool),move 只会复制一份值,外部变量仍然可用
| 类型 | move 闭包的行为 |
|---|---|
i32、bool、f64 等 |
复制,原变量还能用 |
String、Vec、自定义结构体 |
转移所有权,原变量不能用 |
rust
let x = 10; // i32 实现了 Copy
let c = move || {
println!("{}", x); // move 但 x 是 Copy,只复制一份
};
c();
println!("{}", x); // 还能用!因为 x 被复制了,不是转移
1.2.3 闭包 Trait:Fn / FnMut / FnOnce
Rust 编译器会根据闭包捕获变量的方式,自动为闭包实现对应的 trait。理解这三个 trait 是使用闭包作为函数参数或返回值的关键。
| Trait | 捕获方式 | 可调用次数 | 典型场景 |
|---|---|---|---|
| Fn | 不可变借用(&T) | 多次 | 只读访问捕获变量 |
| FnMut | 可变借用(&mut T) | 多次 | 需要修改捕获变量 |
| FnOnce | 获取所有权(T) | 仅一次 | 消耗捕获变量,之后不可再调用 |
三者的继承关系为:Fn 是 FnMut 的子 trait,FnMut 是 FnOnce 的子 trait。也就是说,一个实现了 Fn 的闭包,自动也实现了 FnMut 和 FnOnce。
注意:move 关键字只是强制获取所有权,并不意味着闭包一定是 FnOnce,如果闭包体中并没有消耗捕获的值,那么即使加了 move,闭包仍可能实现 Fn(可多次调用)
rust
可以重复调用
fn test_02() {
let name = String::from("Alice");
let consume = move || {
println!("{}", name); // name 被 move 进闭包
};
consume();
consume();
}
使用drop真正消耗了那个值,它才会降级为 FnOnce
rust
fn test_02() {
let name = String::from("Alice");
let consume = move || {
println!("{}", name); // name 被 move 进闭包
drop(name);
};
consume();
//consume(); //编译报错 不能重复调用
}
1.2.4 闭包作为参数和返回值
闭包可以作为函数参数传入,也可以作为函数的返回值返回,这是闭包在实际开发中最常见的两种用法。
1.2.4.1 闭包作为参数
将闭包作为参数时,需要用泛型约束指定闭包的 trait 类型。以下示例中,参数 F 被约束为 Fn(i32) -> i32,表示接收一个 i32 参数并返回 i32 的闭包。
rust
// 定义一个函数,接受闭包作为参数
fn apply<F>(val: i32, f: F) -> i32
where
F: Fn(i32) -> i32, // F 必须实现 Fn(i32) -> i32
{
f(val)
}
fn main() {
let double = |x| x * 2;
let result = apply(5, double);
println!("Result: {}", result); // 输出: Result: 10
// 也可以直接传入匿名闭包
let result2 = apply(3, |x| x + 100);
println!("Result2: {}", result2); // 输出: Result2: 103
}
1.2.4.2 闭包作为返回值
由于闭包的类型是匿名的,返回闭包时需要使用 impl Trait 或 Box<dyn Trait> 来描述返回类型。
使用 impl Fn 返回闭包,当返回的闭包类型在编译期可以确定时,使用 impl Fn 即可,无需堆分配。
| 方式 | 分配位置 | 适用场景 | 灵活性 |
|---|---|---|---|
impl Fn |
栈 | 编译期可确定闭包类型,性能更优 | 只能返回一种固定的类型 |
Box<dyn Fn> |
堆 | 运行时动态选择闭包,或需要跨函数存储闭包 | 可以返回不同的类型 |
rust
// 返回一个闭包,捕获参数 x 并与传入值相加
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
// 必须 move,否则 x 是局部引用,闭包返回后 x 已失效
move |y| x + y
}
fn main() {
let add_five = make_adder(5);
println!("5 + 3 = {}", add_five(3)); // 输出: 5 + 3 = 8
let add_ten = make_adder(10);
println!("10 + 2 = {}", add_ten(2)); // 输出: 10 + 2 = 12
}
当需要在运行时动态选择不同闭包返回时,使用 Box<dyn Fn> 将闭包分配到堆上。
rust
fn make_adder(x: i32) -> Box<dyn Fn(i32) -> i32> {
Box::new(move |y| x + y)
}
fn main() {
let add_ten = make_adder(10);
println!("10 + 2 = {}", add_ten(2)); // 输出: 10 + 2 = 12
}
灵活性示例:
rust
// 报错!编译器不允许在同一个函数里返回两种不同的匿名闭包
fn make_adder(flag: bool) -> impl Fn(i32) -> i32 {
if flag {
|x| x + 1
} else {
|x| x + 2
}
}
// 正确!通过装箱,可以返回不同的闭包
fn make_adder(flag: bool) -> Box<dyn Fn(i32) -> i32> {
if flag {
Box::new(|x| x + 1)
} else {
Box::new(|x| x + 2)
}
}
2 错误处理
2.1 简介
Rust 采用了一种独特的错误处理机制,没有异常(Exception),没有 try/catch。它将错误分为两类,用不同的方式处理:
- 不可恢复错误(
Unrecoverable):程序逻辑出了严重问题,使用panic!宏终止程序 - 可恢复错误(
Recoverable):操作可能失败但可以处理,使用Result<T, E>枚举
这种设计迫使开发者在编译期就处理可能的错误,而不是在运行时才发现遗漏。
2.2 不可恢复错误:panic!
panic! 宏用于表示程序遇到了无法继续执行的严重错误。调用时会:打印错误信息和发生位置,展开(unwind)调用栈并清理资源,终止程序
rust
fn main() {
panic!("发生了严重错误");
// 下面的代码永远不会执行
println!("Hello, Rust");
}
运行结果:
thread 'test_01' (29876) panicked at tests\error.rs:3:5:
发生严重错误
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
输出的两行信息:
第一行:panic 的位置(文件名和行号)以及错误信息
第二行:提示如何查看完整的调用栈回溯
查看调用栈回溯(Backtrace):设置 RUST_BACKTRACE=full 显示完整 backtrace(包含系统库帧)
回溯输出会列出从 panic 发生位置到 main 函数的完整调用链。当 panic 发生在深层调用中时,这个信息非常有用。
何时使用 panic!:代码中出现了不应该发生的逻辑错误(bug),比如访问越界、解引用空指针、违反了不可变的契约。对于外部输入导致的可预期错误,应使用 Result。
2.3 可恢复错误:Result<T, E>
Result 是 Rust 标准库中用于表示可能失败的操作的枚举:
rust
enum Result<T, E>{
Ok(T), // 操作成功,包含结果值
Err(E), // 操作失败,包含错误信息
}
Rust 标准库中所有可能失败的函数都返回 Result。
2.3.1 使用 match 处理 Result
rust
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
match f {
Ok(file) => {
println!("文件打开成功: {:?}", file);
}
Err(error) => {
println!("文件打开失败: {}", error);
}
}
}
如果只关心成功的情况时,if let 比 match 更简洁:
rust
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
if let Ok(file) = f {
println!("文件打开成功");
// 在这里使用 file ...
} else {
println!("文件打开失败");
}
}
2.3.2 unwrap 和 expect 快速但危险
如果能确定操作不会失败(或在原型阶段不想处理错误),可以用这两个快捷方法:
| 方法 | 行为 | 失败时的 panic 信息 |
|---|---|---|
.unwrap() |
成功返回 T,失败直接 panic! | 使用默认的错误信息 |
.expect("msg") |
成功返回 T,失败直接 panic! | 使用自定义的错误信息(更易调试) |
rust
use std::fs::File;
fn main() {
// unwrap:失败时 panic,使用默认信息
// thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: ...'
let f1 = File::open("hello.txt").unwrap();
// expect:失败时 panic,使用自定义信息(推荐)
// thread 'main' panicked at '无法打开配置文件: ...'
let f2 = File::open("hello.txt").expect("无法打开配置文件");
}
建议:生产代码中优先使用 expect 而非 unwrap,因为自定义的错误信息能在 panic 时快速定位问题。更好的做法是使用 ? 运算符将错误传播给调用者。
2.4 错误传播:? 运算符
在实际开发中,函数遇到错误时往往不想自己处理,而是将错误传播给调用者。Rust 提供了 ? 运算符来简化这一操作。
注意:? 运算符只能用在返回 Result(或 Option)的函数中。从 Rust 1.39 起,main 函数也可以返回 Result。
2.4.1 手动传播 vs ? 运算符
手动传播的写法------冗长但清晰:
rust
use std::fs::File;
use std::io::{self, Read};
// 手动传播错误(繁琐)
fn read_file_manual(path: &str) -> Result<String, io::Error> {
let f = File::open(path);
// 打开失败则返回 Err
let mut file = match f {
Ok(file) => file,//继续执行,file 是 File 类型
Err(e) => return Err(e), // 提前返回错误 提前返回 Result::Err,直接退出函数
};
let mut content = String::new();
// 读取失败则返回 Err
match file.read_to_string(&mut content) {
Ok(_) => Ok(content),
Err(e) => Err(e),
}
}
使用 ? 运算符,同样的逻辑可以简化为:
rust
use std::fs::File;
use std::io::{self, Read};
// 使用 ? 运算符(简洁)
fn read_file(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?; // 失败自动返回 Err
let mut content = String::new();
file.read_to_string(&mut content)?; // 失败自动返回 Err
Ok(content)
}
还可以链式调用,进一步简化:
rust
fn read_file(path: &str) -> Result<String, io::Error> {
let mut content = String::new();
File::open(path)?.read_to_string(&mut content)?;
Ok(content)
}
2.4.2 在 main 中使用 ?
默认的 main 函数返回 (),不能使用 ?。但可以让 main 返回 Result:
rust
use std::fs::File;
use std::io::{self, Read};
fn read_file(path: &str) -> Result<String, io::Error> {
let mut content = String::new();
File::open(path)?.read_to_string(&mut content)?;
Ok(content)
}
// main 返回 Result,这样就能在 main 中使用 ? 了
fn main() -> Result<(), Box<dyn std::error::Error>> {
let content = read_file("hello.txt")?; // ? 在 main 中也能用了
println!("{}", content);
Ok(())
}
2.5 自定义错误
2.5.1 自定义类型与分类处理
在实际项目中,通常需要根据不同的错误类型做不同的处理。Rust 通过 kind() 方法实现这一点
io::ErrorKind 常用的变体:
NotFound:文件或目录不存在PermissionDenied:权限不足AlreadyExists:文件已存在(创建时)ConnectionRefused:连接被拒绝TimedOut:操作超时InvalidInput:参数无效
rust
use std::fs::File;
use std::io::{self, Read};
// 将文件读取封装为独立函数,用 ? 传播错误
fn read_text_from_file(path: &str) -> Result<String, io::Error> {
let mut f = File::open(path)?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
fn main() {
match read_text_from_file("hello.txt") {
Ok(content) => println!("文件内容:\n{}", content),
Err(e) => {
// 根据错误类型做不同处理
match e.kind() {
io::ErrorKind::NotFound => {
println!("文件不存在,请检查路径");
}
io::ErrorKind::PermissionDenied => {
println!("没有权限读取该文件");
}
_ => {
println!("读取文件时发生错误: {}", e);
}
}
}
}
}
2.5.2 自定义错误类型
在项目中,通常需要定义自己的错误类型来表示业务逻辑中的错误
rust
use std::fmt;
use std::num::ParseIntError;
// 定义自定义错误枚举
#[derive(Debug)]
enum AppError {
IoError(std::io::Error),
ParseError(ParseIntError),
CustomError(String),
}
// 实现 Display trait,用于格式化输出
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::IoError(e) => write!(f, "IO 错误: {}", e),
AppError::ParseError(e) => write!(f, "解析错误: {}", e),
AppError::CustomError(msg) => write!(f, "业务错误: {}", msg),
}
}
}
// 实现 From trait,让 ? 运算符自动转换错误类型
impl From<std::io::Error> for AppError {
fn from(error: std::io::Error) -> Self {
AppError::IoError(error)
}
}
impl From<ParseIntError> for AppError {
fn from(error: ParseIntError) -> Self {
AppError::ParseError(error)
}
}
// 现在可以在同一个函数中用 ? 处理不同类型的错误
fn process_config(path: &str) -> Result<i32, AppError> {
let content = std::fs::read_to_string(path)?; // io::Error → AppError
let value: i32 = content.trim().parse()?; // ParseIntError → AppError
if value < 0 {
return Err(AppError::CustomError("配置值不能为负数".into()));
}
Ok(value)
}
fn main() {
match process_config("config.txt") {
Ok(val) => println!("配置值: {}", val),
Err(e) => println!("错误: {}", e),
}
}
这里实现 fmt:Display,是因为 fmt 方法不是直接调用的,是 Rust 的格式化系统在背后自动调用的,比如:
| 场景 | 实际调用 |
|---|---|
println!("{}", error) |
自动调用 error.fmt(...) |
format!("{}", error) |
自动调用 |
error.to_string() |
内部调用 fmt |
panic!("{}", error) |
自动调用 |
2.5.3 三方库
用 thiserror 宏
rust
use thiserror::Error;
#[derive(Error, Debug)]
enum MyError {
#[error("未找到")]
NotFound,
#[error("非法输入: {0}")]
InvalidInput(String),
#[error("IO错误")]
Io(#[from] std::io::Error),
}
用 anyhow(快速开发,不自定义)
rust
use anyhow::{Result, Context};
fn do_something() -> Result<()> {
std::fs::read("file.txt").context("读取失败")?;
Ok(())
}