Rust基础之函数和错误

文章目录

  • [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 闭包的行为
i32boolf64 复制,原变量还能用
StringVec、自定义结构体 转移所有权,原变量不能用
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 TraitBox<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>

ResultRust 标准库中用于表示可能失败的操作的枚举:

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&lt;String, io::Error&gt; {
    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(())
}
相关推荐
脱胎换骨-军哥1 小时前
C++ 代码规范与格式化指南
开发语言·c++·代码规范
立心者02 小时前
SpringBoot中使用TOTP实现MFA(多因素认证)
java·spring boot·后端
枕星而眠2 小时前
C++ STL Map容器完全指南:从有序红黑树到无序哈希表
java·开发语言
爱吃牛肉的大老虎3 小时前
Rust对象之结构体,枚举,特性
开发语言·后端·rust
bbq粉刷匠3 小时前
HashMap 底层原理深度拆解(二):putVal 完整链路解析(懒加载 · 链表遍历 · 尾插法)
java·开发语言·哈希算法
Sam_Deep_Thinking3 小时前
一个靠谱的C端网关服务应该包含什么内容
java·程序员·系统架构
@三十一Y3 小时前
C++:红黑树的实现
开发语言·c++
杨运交3 小时前
[055][调度模块]Spring动态任务调度框架的设计与实现
java·后端·spring
白狐_7984 小时前
408 数据结构算法题 01:线性表暴力求解保分指南
java·数据结构·算法
流云鹤4 小时前
03Java学习day(3)
java·学习