Rust Result 详解:可靠的错误处理机制

《Rust 编程实战》系列第 22 篇

在上一篇文章中,我们学习了:

r 复制代码
Option<T>

它用于表示:

一个值可能存在,也可能不存在。

例如:

vbnet 复制代码
HashMap::get()
Vec::get()

返回:

scss 复制代码
Some(value)
None

但是实际开发中,很多问题不仅仅是"有没有值",还需要知道:

为什么失败?

例如:

  • 文件不存在

  • 数据格式错误

  • 网络连接失败

  • 数据库查询失败

  • 用户输入非法

  • 权限不足

这时候 Rust 提供了:

r 复制代码
Result<T, E>

它是 Rust 错误处理的核心类型。

本文将介绍:

  • 什么是 Result

  • Ok 与 Err

  • 使用 match 处理错误

  • unwrap 与 expect

  • ? 运算符

  • 错误传播

  • Option 与 Result 区别

  • 自定义错误

  • 实战案例


什么是 Result?

Result 是 Rust 标准库中的枚举类型。

定义类似:

scss 复制代码
enum Result<T, E> {
    Ok(T),
    Err(E),
}

其中:

  • T 表示成功时返回的数据类型

  • E 表示失败时错误类型

例如:

成功:

scss 复制代码
Ok("文件内容")

失败:

scss 复制代码
Err("文件不存在")

Result 的基本使用

示例:

rust 复制代码
fn divide(
    a: i32,
    b: i32,
) -> Result<i32, String> {

    if b == 0 {
        return Err(
            String::from("不能除以0")
        );
    }

    Ok(a / b)
}


fn main() {

    let result = divide(10, 2);

    println!("{:?}", result);
}

输出:

scss 复制代码
Ok(5)

如果:

scss 复制代码
divide(10, 0)

返回:

scss 复制代码
Err("不能除以0")

使用 match 处理 Result

最完整的处理方式:

scss 复制代码
fn main() {

    let result = divide(10, 0);

    match result {

        Ok(value) => {
            println!(
                "结果:{}",
                value
            );
        }

        Err(error) => {
            println!(
                "错误:{}",
                error
            );
        }
    }
}

输出:

复制代码
错误:不能除以0

Rust 强制要求处理成功和失败两种情况。


Result 与 Option 的区别

Option:

scss 复制代码
Some(value)
None

表示:

复制代码
有数据 / 没数据

例如:

arduino 复制代码
map.get()

Result:

scss 复制代码
Ok(value)
Err(error)

表示:

复制代码
成功 / 失败

并且失败时携带原因。

例如:

文件读取:

成功:

scss 复制代码
Ok("hello")

失败:

scss 复制代码
Err("文件不存在")

简单理解:

类型 用途
Option 可能没有值
Result 可能发生错误

使用 unwrap()

如果确定一定成功,可以使用:

scss 复制代码
unwrap()

例如:

rust 复制代码
fn main() {

    let result: Result<i32, &str>
        = Ok(100);

    let value = result.unwrap();

    println!("{}", value);
}

输出:

复制代码
100

但是如果:

rust 复制代码
let result: Result<i32, &str>
    = Err("失败");

执行:

ini 复制代码
result.unwrap();

程序会 panic。

因此:

unwrap 适合测试代码,不适合处理用户输入和外部数据。


使用 expect()

expect()unwrap() 类似,但可以提供错误说明。

rust 复制代码
fn main() {

    let value =
        "123"
        .parse::<i32>()
        .expect(
            "数字解析失败"
        );

    println!("{}", value);
}

如果失败:

错误信息:

复制代码
数字解析失败

相比:

scss 复制代码
unwrap()

更容易定位问题。


使用 unwrap_or()

提供默认值:

rust 复制代码
fn main() {

    let result:
        Result<i32, &str>
        = Err("失败");


    let value =
        result.unwrap_or(0);


    println!("{}", value);
}

输出:

复制代码
0

如果成功:

scss 复制代码
Ok(100)

则返回:

复制代码
100

使用 ? 运算符

Rust 中处理错误最常见的方式:

复制代码
?

它可以自动传播错误。

例如:

rust 复制代码
use std::fs;


fn read_file()
    -> Result<String, std::io::Error>
{

    let content =
        fs::read_to_string(
            "data.txt"
        )?;

    Ok(content)
}

这段代码:

复制代码
?

会自动完成:

如果成功:

scss 复制代码
Ok(content)

继续执行。

如果失败:

scss 复制代码
Err(error)

立即返回。


不使用 ? 的写法

上面的代码等价于:

rust 复制代码
use std::fs;


fn read_file()
    -> Result<String, std::io::Error>
{

    let result =
        fs::read_to_string(
            "data.txt"
        );


    match result {

        Ok(content) => {
            Ok(content)
        }

        Err(error) => {
            Err(error)
        }
    }
}

可以看到:

?

只是 Rust 提供的错误传播简写。


多个错误操作

实际项目中,经常有多个可能失败的步骤:

例如:

  1. 读取文件
  2. 解析数字
  3. 计算结果

使用:

复制代码
?

代码会更加清晰:

rust 复制代码
use std::fs;


fn calculate()
    -> Result<i32, Box<dyn std::error::Error>>
{

    let text =
        fs::read_to_string(
            "number.txt"
        )?;


    let number:
        i32 =
        text.trim()
            .parse()?;


    Ok(number * 2)
}

每一步失败都会自动返回错误。


Result 与 Option 转换

有时候需要在两者之间转换。

Result 转 Option

rust 复制代码
let result:
    Result<i32, String>
    = Ok(100);


let option =
    result.ok();

得到:

scss 复制代码
Some(100)

如果是:

scss 复制代码
Err(...)

得到:

css 复制代码
None

Option 转 Result

vbnet 复制代码
let option:
    Option<i32>
    = Some(100);


let result =
    option.ok_or(
        "没有数据"
    );

得到:

scss 复制代码
Ok(100)

自定义错误类型

实际项目中,不建议所有错误都使用:

arduino 复制代码
String

通常会定义自己的错误类型。

例如:

markdown 复制代码
#[derive(Debug)]
enum AppError {

    UserNotFound,

    InvalidPassword,

    DatabaseError,
}

函数:

scss 复制代码
fn login(
    username: &str
)
-> Result<(), AppError>
{

    if username.is_empty() {

        return Err(
            AppError::UserNotFound
        );
    }


    Ok(())
}

这样错误类型更加明确。


实战:用户注册校验

scss 复制代码
#[derive(Debug)]
enum RegisterError {

    EmptyName,

    InvalidAge,
}


fn register(
    name: &str,
    age: u32,
)
-> Result<(), RegisterError>
{

    if name.is_empty() {

        return Err(
            RegisterError::EmptyName
        );
    }


    if age < 18 {

        return Err(
            RegisterError::InvalidAge
        );
    }


    Ok(())
}


fn main() {

    match register(
        "Tom",
        20
    ) {

        Ok(_) => {
            println!(
                "注册成功"
            );
        }

        Err(error) => {
            println!(
                "注册失败:{:?}",
                error
            );
        }
    }
}

输出:

复制代码
注册成功

实战:解析配置文件

假设配置:

ini 复制代码
port=8080

解析:

rust 复制代码
fn parse_port(
    text: &str
)
-> Result<u16, String>
{

    let parts:
        Vec<&str>
        =
        text.split('=')
            .collect();


    if parts.len() != 2 {

        return Err(
            String::from(
                "配置格式错误"
            )
        );
    }


    let port =
        parts[1]
        .parse::<u16>()
        .map_err(|_| {

            String::from(
                "端口格式错误"
            )

        })?;


    Ok(port)
}


fn main() {

    match parse_port(
        "port=8080"
    ) {

        Ok(port) => {
            println!(
                "端口:{}",
                port
            );
        }

        Err(error) => {
            println!(
                "{}",
                error
            );
        }
    }
}

输出:

复制代码
端口:8080

常见错误

滥用 unwrap()

错误:

ini 复制代码
let user =
    find_user()
    .unwrap();

如果数据来自:

  • 用户输入

  • 网络请求

  • 文件

  • 数据库

不要直接 unwrap。


用错误字符串代替错误类型

不推荐:

rust 复制代码
Result<User, String>

大型项目更推荐:

sql 复制代码
Result<User, UserError>

错误类型更加明确。


忽略 Result

例如:

ini 复制代码
file.write(data);

编译器会提醒:

rust 复制代码
unused Result

因为写入可能失败。

应该处理:

ini 复制代码
file.write(data)?;

或者:

scss 复制代码
match file.write(data) {
    Ok(_) => {}
    Err(e) => {}
}

最佳实践

库代码优先返回 Result

例如:

rust 复制代码
fn load_config()
 -> Result<Config, ConfigError>

让调用者决定如何处理错误。


应用入口可以处理最终错误

例如:

rust 复制代码
fn main()
 -> Result<(), Box<dyn std::error::Error>>
{
    run()?;

    Ok(())
}

使用 ? 简化错误传播

推荐:

ini 复制代码
let data = read_file()?;

而不是大量:

lua 复制代码
match

嵌套。


自定义业务错误

大型项目建议:

typescript 复制代码
enum Error {
    Database,
    Network,
    Permission,
}

便于统一管理。


本章小结

Result<T, E> 是 Rust 错误处理的核心。

通过本文,我们学习了:

  • Result 的结构

  • Ok 和 Err

  • match 处理错误

  • unwrap 和 expect

  • unwrap_or 默认值

  • ? 错误传播

  • Option 与 Result 转换

  • 自定义错误类型

  • 配置解析和注册校验案例

可以记住:

Option 解决"有没有值"。
Result 解决"成功还是失败,以及为什么失败"。

Rust 不隐藏错误,而是要求程序明确处理错误,这也是 Rust 程序稳定、安全的重要原因。


下一篇预告

下一篇将学习:

Rust panic 详解:不可恢复错误与程序崩溃处理

内容包括:

  • 什么是 panic

  • panic! 宏

  • panic 发生场景

  • backtrace 调试

  • unwrap 引发 panic

  • panic 与 Result 的区别

  • recover 思想

  • abort 与 unwind

  • 什么时候应该 panic

  • 实战错误定位

相关推荐
程序员爱钓鱼1 小时前
GOPATH 与 Go Modules:Go 项目依赖管理的演变
后端·go
鱼樱前端1 小时前
别再"学工具"了,先搭你的 AI 工作流
前端·ai编程·前端工程化
凤山老林3 小时前
从美团全栈化看 AI 冲击:前端转全栈,是自救还是必然
前端·人工智能·状态模式
牛奔9 小时前
Go 如何打印调试深层或嵌套的结构体
开发语言·后端·golang
红烧大青虫10 小时前
HarmonyOS应用开发实战:小事记 - 数据迁移策略:RDB 表结构变更的版本号管理与 onUpgrade 回调
后端·华为·harmonyos·鸿蒙系统
geovindu10 小时前
go: Iterative Algorithms
开发语言·后端·算法·golang·迭代算法
777VG10 小时前
PostgreSQL +martin将多张表输出成一个 MVT
前端·数据库·postgresql
mayaairi11 小时前
JS循环语句深度解析:嵌套for、while与do...while
开发语言·前端·javascript
To_OC11 小时前
啃完流式输出:从一个卡顿的 LLM 接口开始,我搞懂了数据流到底怎么 “流”
前端·javascript·llm