Rust 错误处理入门和进阶

Rust 错误处理入门和进阶

在这里插入图片描述

引用 Rust Book 的话,“错误是软件中不可避免的事实”。这篇文章讨论了如何处理它们。

在讨论 可恢复错误和 Result 类型之前,我们首先来谈谈 不可恢复错误 - 又名恐慌(panic)。

不可恢复错误

恐慌(panic)是程序可能抛出的异常。它停止执行当前线程。当发生 恐慌 时,它会返回 错误简短描述以及有关恐慌位置的信息。

fn main() {
    panic!("error!");
    println!("Never reached :(");
}

运行以上代码将会抛出:

thread 'main' panicked at 'error!', examples\panics.rs:2:5

它们与 JavaScript 和其他语言中的 throw 类似,因为它们不需要在函数上添加注释来运行,并且可以穿过函数边界。然而在 Rust 中,恐慌无法恢复,没有办法在当前线程中接收恐慌。

fn send_message(s: String) {
    if s.is_empty() {
       panic!("Cannot send empty message");
    } else {
        // ...
    }
}

send_message 函数是容易出错的(可能会出错)。如果使用 空消息 调用此函数,则程序将停止运行。被调用者无法跟踪已发生的错误。

对于可恢复的错误,Rust 在标准库中有一个用于错误处理的类型,称为 Result 。它是一种通用类型,这意味着result和error变体基本上可以是您想要的任何内容。

pub enum Result<T, E> {
    Ok(T),
    Err(E),
}

基本错误处理

目前我们的 send_message 函数没有返回任何内容。这意味着调用方无法接收到任何信息。我们可以更改定义以返回 Result ,而不是恐慌,可以提前返回 Result::Err

fn send_message(s: String) -> Result<(), &'static str> {
    if s.is_empty() {
        // Note the standard prelude includes `Err` so the `Result::Err` and `Err` are equivalent
        return Result::Err("message is empty")
    } else {
        // ...
    }
    Ok(())
}

现在我们的函数实际上返回有关出了什么问题的信息,我们可以在调用它时处理它:

if let Err(send_error) = send_message(message) {
    show_user_error(send_error);
}

处理未使用的结果

在上面的示例中,我们检查项目的值并对其进行分支。然而,如果我们没有检查和处理返回的结果,那么 Rust 编译器会给我们一个有用的警告,这样你就不会忘记显式处理程序中的错误。

|     send_message();
|     ^^^^^^^^^^^^^^^
= note: `#[warn(unused_must_use)]` on by default
= note: this `Result` may be an `Err` variant, which should be handled

Result 类型可以在大多数库中找到。我最喜欢的示例之一是 FromStr::from_str trait 方法的返回类型。使用 str::parse (使用 FromStr 特征),我们可以执行以下操作:

fn main() {
    let mut input = String::new();
    std::io::stdin().read_line(&mut input).unwrap();

    match input.trim_end().parse::<f64>() {
        Ok(number) => {
            dbg!(number);
        }
        Err(err) => {
            dbg!(err);
        }
    };
}

(暂时忽略 unwrap 😉)

$ cargo r --example input -q
10
[examples\input.rs:7] number = 10.0

$ cargo r --example input -q
100
[examples\input.rs:7] number = 100.0

$ cargo r --example input -q
bad
[examples\input.rs:10] err = ParseFloatError {
    kind: Invalid,
}

在这里我们可以看到,当我们输入一个数字时,我们会得到一个带有数字的 Ok 变体,否则我们会得到一个 ParseFloatError

文件、网络和数据库

当您与外界或 Rust 运行时之外的事物交互时,所有错误都会发生。可能发生大量错误的地方之一是与文件系统的交互。 File::open 函数尝试打开文件。这可能会因多种原因而失败。文件名无效、文件不存在或者您根本没有读取该文件的权限。请注意,这些错误是明确定义的并且是事先已知的。您甚至可以使用 kind 函数访问错误变体,以便实现您的程序逻辑或向用户返回指导性错误消息。

混淆结果和错误

当您从事项目时,您经常会发现自己在函数签名中的返回类型方面重复自己:

fn foo() -> Result<SomeType, MyError> {
...
}

举一个具体的例子,所有操作文件系统的函数都会出现相同的错误(文件不存在、权限无效)。 io::Result 是result的别名,但意味着每个函数不必指定错误类型:

pub type Result<T> = Result<T, io::Error>;

If you have an API which has a common error type, you may want to consider this pattern.
如果您有一个具有常见错误类型的 API,您可能需要考虑此模式。

? 运算符

Results 最好的事情之一是 ? 运算符,? 运算符可以短路 Result 错误值。让我们看一个从文件上传文本的简单函数。这可能会以多种不同的方式出错:

fn upload_file() -> Result<(), &'static str> {
    let text = match std::fs::read_to_string("file.txt").map_err(|_| "read file error") {
        Ok(value) => value,
        Err(err) => {
            return err;
        }
    };
    if let Err(err) = upload_text(text) {
        return err
    }
    Ok(())
}

等等,我们正在写 Rust 而不是 Go!

如果将 ? 后缀到添加到result(或任何实现 try 的东西,也实现 Option ),我们可以获得功能上等效的结果,并且具有更易读和更清晰的结果。简洁的语法。

fn upload_file() -> Result<(), &'static str> {
    let text = std::fs::read_to_string("file.txt").map_err(|_| "read file error")?;
    upload_text(text)?;
    Ok(())
}

只要调用函数也返回具有相同 Error 类型的 Result? 就可以节省大量的显式代码编写。此外,问号在错误值上隐式运行 Into::into (它是为 From 实现者自动实现的)。所以我们在使用运算符之前不必担心转换错误:

// This derive an into implementation for `std::io::Error -> MyError`
#[derive(derive_enum_from_into::EnumFrom)]
enum MyError {
    IoError(std::io::Error)
    // ...
}

fn do_stuff() -> Result<(), MyError> {
    let file = File::open("data.csv")?;
    // ...
}

稍后我们将研究更多组合错误类型的模式!

Error Trait 介绍

Error Trait在标准库中定义。它基本上代表了错误值的期望 - Result<T,E>E 类型的值。 Error Trait针对许多错误实现,并为错误信息提供统一的 API。 Error Trait有点需要,要求错误同时实现 Debug 和 Display。虽然实现起来可能很麻烦,但我们稍后会看到一些工具库来实现这一点。

在标准库中,VarError(用于读取环境变量)和 ParseIntError(用于将字符串切片解析为整数)是不同的错误。当我们与它们交互时,我们需要区分类型,因为它们具有不同的属性和不同的堆栈大小。为了构建它们的组合,我们可以使用枚举构建一个总和类型。或者,我们可以使用动态分派的特征来处理不同的堆栈大小的项目和其他类型信息。

使用上面提到的 try 语法 ( ? ),我们可以将上面的错误转换为动态派发。这使得处理不同的错误变得容易,而无需构建枚举来组合错误。

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let key = std::env::var("NUMBER_IN_ENV")?;
    let number = key.parse::<i32>()?;
    println!("\"NUMBER_IN_ENV\" is {}", number);
    Ok(())
}

虽然这是处理错误的简单方法,但区分类型并不容易,并且可能会使库中的错误处理变得困难。稍后会详细介绍这一点。

错误特征与结果枚举

使用枚举时的一件事是我们可以使用 match 在枚举错误变体上进行分支。另一方面,对于 dyn 特征,除非您沿着向下转换路径,否则很难获得有关错误的具体信息:

match my_enum_error {
    FsError(err) => {
        report_fs_error(err)
    },
    DbError(DbError { err, database }) => {
        report_db_error(database, err)
    },
}

对于可重用的库,最好使用枚举来组合错误,以便库的用户可以自己处理具体细节。但对于 CLI 和其他应用程序来说,使用该特征可能要简单得多。

Methods on Result

结果和选项包含许多有用的功能。以下是我常用的一些功能:

Result::map()

Result::map 映射或转换 Ok 值(如果存在)。这比使用 ? 运算符更简洁。

fn string_to_plus_one(s: &str) -> Result<i32, std::num::ParseIntError> {
    s.parse::<i32>().map(|num| num + 1)
}

Result::ok()

Result::ok 对于将结果转换为选项很有用

assert_eq!(Ok(2).ok(), Some(2));
assert_eq!(Err("err!").ok(), None);

Option::ok_or_else()

Option::ok_or_else 对于从选项转换为结果的另一种方式很有用

fn get_first(vec: &Vec<i32>) -> Result<&i32, NotInVec> {
    vec.first().ok_or_else(|| NotInVec)
}

迭代中的错误处理

在迭代器链中使用Result可能会有点令人困惑。幸运的是 Result 实现了collect。如果发生错误,我们可以使用它来提前结束迭代器。在下面,如果所有 parse 都成功,那么我们将获得收集的数字结果向量。如果失败,那么它会返回一个带有失败错误的结果。

fn main() {
    let a = ["1", "2", "not a number"]
        .into_iter()
        .map(|a| a.parse::<f64>())
        .collect::<Result<Vec<_>, _>>();
    dbg!(a);
}
[examples\iteration.rs:6] a = Err( ParseFloatError { kind: Invalid, }, )

删除 "not a number" 条目后

[examples\iteration.rs:3] a = Ok( [ 1.0, 2.0, ], )

因为 Rust 迭代器是分段且惰性的,所以迭代器可能会短路,而无需对任何后续项进行解析。

更多Panic

特殊panics

todo!()unimplemented!()unreachable!() 都是 panic! () 的包装器,但专门针对其情况。 Panics 有一个特殊的 ! 类型,称为“never type”,它表示永远不会完成的计算结果(也意味着它可以传递到任何地方):

fn func_i_havent_written_yet() -> u32 {
    todo!()
}

有时,编译器无法正确推断某些 Rust 代码是否有效。对于这种情况,可以使用 unreachable! 恐慌:

fn get_from_vec_else_zero(a: Vec<i32>) -> i32 {
    if let Some(value) = a.get(2) {
        if let Some(prev_value) = a.get(1) {
            prev_value
        } else {
            unreachable!()
        }
    } else {
        0
    }
}

Unwrapping

unwrapResultOption 上的方法。他们返回 OkSome 变体,否则会出现恐慌…

// result.unwrap()

let value = if let Ok(value) = result {
    value
} else {
    panic!("Unwrapped!")
};

其用例是开发人员错误和编译器无法完全弄清楚的情况。如果您只是尝试某些操作并且不想设置完整的错误处理系统,那么它们可以用于忽略编译器警告。

即使情况需要 unwrap ,您最好使用 expect ,它附带一条消息 - 当出现 expect 错误消息时,您会感谢过去的自己帮助您在两周后找到问题的根本原因

标准库中的恐慌 Panics

值得注意的是,标准库中的某些 API 可能会出现恐慌。您应该在文档中查找这些注释。其中之一是 Vec::remove。如果您使用它,您应该确保参数位于其可索引范围内。

fn remove_at_idx(a: usize, vec: &mut Vec<i32>) -> Option<i32> {
    if a < idx.len() {
        Some(vec.remove(a))
    } else {
        None
    }
}

处理多个错误和 工具 Crates

处理来自多个库和 API 的错误可能会变得具有挑战性,因为您必须处理一堆不同类型的错误。它们的大小不同,包含不同的信息。为了统一类型,我们必须使用枚举构建一个总和类型,以确保它们在编译时具有相同的大小。

enum Errors {
    FileSystemError(..),
    StringParseError(..),
    NetworkError(..),
}

一些使创建这些统一枚举更容易的包:

thiserror crate

thiserror 提供了一个派生实现,为我们添加了 Error 特征。如前所述,要实现错误,我们必须实现显示,并且 thiserrors 的 #[error] 属性为显示的错误提供模板。

use thiserror::Error;

#[derive(Error, Debug)]
pub enum DataStoreError {
    #[error("data store disconnected")]
    Disconnect(#[from] io::Error),
    #[error("the data for key `{0}` is not available")]
    Redaction(String),
    #[error("invalid header (expected {expected:?}, found {found:?})")]
    InvalidHeader {
        expected: String,
        found: String,
    },
    #[error("unknown data store error")]
    Unknown,
}

anyhow crate

anyhow 提供了一种符合人体工程学且惯用的方法来显式处理错误。它与前面提到的错误特征类似,但具有附加功能,例如为抛出的错误添加上下文。

当您想以上下文感知的方式向应用程序的用户传达错误时,这确实非常有用:

use anyhow::{bail, Result, Context};

fn main() -> Result<()> {
    println!("Hello World!");
    func1().context("while calling func1")?;
    Ok(())
}

fn func1() -> Result<()> {
    func2().context("while calling func2")
}

fn func2() -> Result<()> {
    bail!("Hmm something went wrong ")
}
Error: while calling func1

Caused by:
    0: while calling func2
    1: Hmm something went wrong

Error 特征类似, anyhow 也会遇到无法匹配 anyhow 的结果错误变体的问题。这就是为什么 anyhow 的文档建议对应用程序使用 anyhow ,对库使用 thiserror

eyre crate

最后, eyreanyhow 的分支,并添加了更多回溯信息。它是高度可定制的,并且使用 color-eyre 我们可以在恐慌消息中获得颜色 - 一点颜色总是可以照亮开发体验。

The application panicked (crashed).
Message:  test
Location: examples\color_eyre.rs:6

  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ BACKTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
                                ⋮ 13 frames hidden ⋮
  14: core::ops::function::FnOnce::call_once<enum$<core::result::Result<tuple$<>,eyre::Report>, 1, 18446744073709551615, Err> (*)(),tuple$<> ><unknown>
      at /rustc/7737e0b5c4103216d6fd8cf941b7ab9bdbaace7c\library\core\src\ops\function.rs:227
                                ⋮ 17 frames hidden ⋮

尾声

感谢您阅读这篇文章!错误处理可能很困难,但通过本指南,希望您能更好地了解如何确保能够可靠地跟踪错误并使调试 Rust 应用程序变得更加容易。


原文地址:More than you've ever wanted to know about errors in Rust

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/468721.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C++第七弹---类与对象(四)

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】【C详解】 目录 1、拷贝构造函数 1.1、概念 1.2、特征 2、运算符重载 2.1、等号运算符重载 总结 1、拷贝构造函数 1.1、概念 在现实生活中&#xff0c;可能…

学习Python,需要知道的经典案例

文章目录 一、Python简介二、Python经典案例1. 猜数字游戏2. 文本文件处理3. 网络爬虫4. 数据可视化5. 电子邮件发送6. 实现一个简单的Web服务器。 三、Python处理IP相关知识点1. 处理IP地址2. 网络编程&#xff08;TCP/IP&#xff09;3. 使用第三方库处理IP信息 四、相关链接 …

Java安全 反序列化(1) URLDNS链原理分析

Java安全 反序列化(1) URLDNS链原理分析 文章目录 Java安全 反序列化(1) URLDNS链原理分析前置知识应用分析payload1.新建HashMap类2.新建URL类3.获取URL 的 Class对象4.通过反射访问URL内部变量5.通过反射为URL中类赋值6.调用HashMap#put方法传入key和value7.再次通过反射为UR…

基于单片机的智能台灯设计1.42

摘 要 社会在发展&#xff0c;时代在进步&#xff0c;人们对生活质量需求更加膨胀&#xff0c;是否拥有高科技技术也最终决定着产品是否可以满足人们的欲望&#xff0c;只有性价比更高&#xff0c;才可以得到更好的青睐。现在的电子产品愈来愈多&#xff0c;龙蛇混杂&#xff…

vue使用element-ui 实现自定义分页

element-ui文档截图&#xff0c;plus大同小异。 可以通过插槽实现自定义的分页。在layout里面进行配置。 全部代码 //page.js export default {name:Cuspage,props:{total:Number,},data(){return {currentPage:1,pageSize:10,}}methods: {setslot (h) {return(<div cla…

tinyrenderer-Bresenham绘制直线算法

如何画线段 第一种尝试 求x&#xff0c;y起始点的差值&#xff0c;按平均间隔插入固定点数 起始点平均插入100个点&#xff1a; void line(int x0, int y0, int x1, int y1, TGAImage& image, TGAColor color) {for (float t 0.; t < 1.; t .01) {int x x0 (x1 -…

力扣每日一题 2024/3/19 好子数组的最大分数

题目描述 用例说明 思路讲解 好子数组的下标在i<k<j,即nums[k]必须在好子数组当中&#xff0c;将数组以k为分界点分为左右两半&#xff0c;左半left从k-1向左移动&#xff0c;右半right从k1开始出发向右移动。 当left大于等于分界点的值时左移一位&#xff0c;当right大…

tp8 mpdf 导出pdf

1. 安装mpdf composer require mpdf/mpdf 2. 然后 使用 use mpdf\Mpdf; 或者 require_once __DIR__ . /vendor/autoload.php; 官方文档 mPDF – mPDF 手册 文档里有很多东西 可以自己去研究 3. 编写代码 下载 (支持中文) $mpdf new Mpdf([mode > utf-8,"autoS…

CMeet系列技术生态沙龙---《探索未来:生成式AI赋能千行百业·杭州》

当前数字化浪潮下&#xff0c;生成式AI技术正成为推动产业升级、提升竞争力的关键力量。为深入探索未来AI技术的赋能作用&#xff0c;促进技术生态的繁荣与发展&#xff0c;CSDN-CMeet系列沙龙活动旨在搭建一个交流与探索的平台&#xff0c;通过分享前沿研究成果和应用案例&…

服务器数据恢复—光纤环境互斥不当导致存储VMFS卷损坏的数据恢复案例

服务器数据恢复环境&故障&#xff1a; 某公司的信息管理平台&#xff0c;通过3台虚拟机共享了一台存储设备供企业内部使用&#xff0c;存储设备中存放了公司内部重要的数据文件。 由于业务增长的需要&#xff0c;管理员又在这个存储网络上连接了一台Windows server服务器&a…

媒体邀约:推广方法

1.媒体邀约推广媒体邀约推广是一种通过与商务合作以增强曝光度和名气的营销手段。积极与商务合作&#xff0c;公司能将产品和服务推广给更大范围受众人群&#xff0c;以达到增加销量和市场占有率目地。 1.1 选择适合自己的新闻媒体选择适合自己的新闻媒体是媒体邀约推广的关键…

sqllab第35-45关通关笔记

35关知识点&#xff1a; 宽字节注入数值型注入错误注入 payload:id1andextractvalue(1,concat(0x7e,database(),0x7e))0--联合注入 payload:id0unionselect1,database(),version()-- 36关知识点&#xff1a; 字符型注入宽字节注入错误注入 payload:id1%df%27andextractvalue(…

Qt实现简单的五子棋程序

Qt五子棋小程序 Qt五子棋演示及源码链接登陆界面单机模式联机模式联网模式参考 Qt五子棋 参考大佬中国象棋程序&#xff0c;使用Qt实现了一个简单的五子棋小程序&#xff0c;包含了单机、联机以及联网三种模式&#xff1b;单机模式下实现了简易的AI&#xff1b;联机模式为PtoP…

由于找不到kvpvbsext64.dll,无法继续执行代码。解决办法,

kvpvbsext64.dll 是一个动态链接库文件&#xff0c;通常作为某个软件的一部分存在。具体来说&#xff0c;它可能为某个程序的特定功能提供支持&#xff0c;在软件运行时被调用和使用。因此&#xff0c;当出现与该文件相关的错误时&#xff0c;可能会影响到相应软件的正常运行。…

ModbusTCP转Profinet网关高低字节交换切换

背景&#xff1a;在现场设备与设备通迅之间通常涉及到从一种字节序&#xff08;大端或小端&#xff09;转换到另一种字节序。大端字节序是指高位字节存储在高地址处&#xff0c;而小端字节序是指低位字节存储在低地址处。在不动原有程序而又不想或不能添加程序下可选用ModbusTC…

算法沉淀——贪心算法二(leetcode真题剖析)

算法沉淀——贪心算法二 01.最长递增子序列02.递增的三元子序列03.最长连续递增序列04.买卖股票的最佳时机 01.最长递增子序列 题目链接&#xff1a;https://leetcode.cn/problems/longest-increasing-subsequence/ 给你一个整数数组 nums &#xff0c;找到其中最长严格递增子…

Matplotlib数据可视化实战-1数据可视化Matplotlib基础

1.1绘图的一般过程&#xff1a; 1.导入相关库 2.生成、读入或计算得到数据&#xff1b; 3.根据需要绘制折线图、散点图、柱状图、饼状图、雷达图、箱线图、三维曲线/曲面以及极坐标系图形&#xff1b; 4.根据需要设置图形属性&#xff1b; 5.显示或保存绘图结果。 例如&…

缺失的第一个正数-面试热题 100?-Lua 中文代码解题第5题

缺失的第一个正数-面试热题 100&#xff1f;-Lua 中文代码解题第5题 解题思路&#xff1a; 遍历数组并尝试将元素放入正确的位置&#xff1a; 遍历输入数组 nums&#xff0c;对于每个元素 nums[i]&#xff1a; 如果 nums[i] 是一个正整数&#xff0c;并且它的值小于或等于数组…

Windows Server 2012 R2在安装软件的时候显示乱码

1、打开控制面板——时钟、语言和区域——语言&#xff0c;添加为汉语 2、接着选择区域为中国 3、完美解决

Covalent Network(CQT)借助最大规模的历史与实时 Web3 数据集,推动人工智能的发展

人工智能在众多领域中增强了区块链的实用性&#xff0c;反之亦然&#xff0c;区块链确保了 AI 模型所使用的数据的来源和质量。人工智能带来的生产力提升&#xff0c;将与区块链系统固有的安全性和透明度融合。 Covalent Network&#xff08;CQT&#xff09;正位于这两项互补技…