Rust 错误处理(下)

news2024/11/28 12:35:32

目录

1、用 Result 处理可恢复的错误

1.1 传播错误的简写:? 运算符

1.2 哪里可以使用 ? 运算符

2、要不要 panic!

2.1 示例、代码原型和测试都非常适合 panic

2.2 当我们比编译器知道更多的情况

2.3 错误处理指导原则

2.4 创建自定义类型进行有效性验证

2.5 总结


1、用 Result 处理可恢复的错误

1.1 传播错误的简写:? 运算符

先看下如下示例:

fn main() {
    fn read_file() -> Result<String, io::Error> {
        let file_result = File::open("hello.txt");
        let mut v = String::new();
        file_result.unwrap().read_to_string(&mut v)?;
        Ok(v)
    }
    let res = read_file();
    print!("{:?}", res)
}

Result 值之后的 ? 被定义为之前的处理 Result 值的 match 表达式有着完全相同的工作方式。如果 Result 的值是 Ok,这个表达式将会返回 Ok 中的值而程序将继续执行。如果值是 ErrErr 将作为整个函数的返回值,就好像使用了 return 关键字一样,这样错误值就被传播给了调用者。

之前示例的 match 表达式与 ? 运算符所做的有一点不同:? 运算符所使用的错误值被传递给了 from 函数,它定义于标准库的 From trait 中,其用来将错误从一种类型转换为另一种类型。当 ? 运算符调用 from 函数时,收到的错误类型被转换为由当前函数返回类型所指定的错误类型。这在当函数返回单个错误类型来代表所有可能失败的方式时很有用,即使其可能会因很多种原因失败。

? 运算符消除了大量样板代码并使得函数的实现更简单。我们甚至可以在 ? 之后直接使用链式方法调用来进一步缩短代码,如下所示:

fn main() {
    fn read_file() -> Result<String, io::Error> {
        let mut v = String::new();
        let _ = File::open("hello.txt")?.read_to_string(&mut v)?;
        Ok(v)
    }
    let res = read_file();
    print!("{:?}", res)
}

以下代码展示了一个使用 fs::read_to_string 的更为简短的写法:

fn main() {
    fn read_file() -> Result<String, io::Error> {
        fs::read_to_string("hello1.txt")
    }
    let res = read_file();
    print!("{:?}", res)
}

将文件读取到一个字符串是相当常见的操作,所以 Rust 提供了名为 fs::read_to_string 的函数,它会打开文件、新建一个 String、读取文件的内容,并将内容放入 String,接着返回它。当然,这样做就没有展示所有这些错误处理的机会了.

1.2 哪里可以使用 ? 运算符

? 运算符只能被用于返回值与 ? 作用的值相兼容的函数。因为 ? 运算符被定义为从函数中提早返回一个值,这与之前示例的 match 表达式有着完全相同的工作方式。示例中 match 作用于一个 Result 值,提早返回的分支返回了一个 Err(e) 值。函数的返回值必须是 Result 才能与这个 return 相兼容。

让我们看看在返回值不兼容的 main 函数中使用 ? 运算符会得到什么错误:

fn main() {
    fs::read_to_string("hello1.txt")?;
}

这段代码打开一个文件,这可能会失败。? 运算符作用于 File::open 返回的 Result 值,不过 main 函数的返回类型是 () 而不是 Result。当编译这些代码,会得到如下错误信息: 

这个错误指出只能在返回 Result 或者其它实现了 FromResidual 的类型的函数中使用 ? 运算符。

为了修复这个错误,有两个选择。一个是,如果没有限制的话将函数的返回值改为 Result<T, E>。另一个是使用 match 或 Result<T, E> 的方法中合适的一个来处理 Result<T, E>

错误信息也提到 ? 也可用于 Option<T> 值。如同对 Result 使用 ? 一样,只能在返回 Option 的函数中对 Option 使用 ?。在 Option<T> 上调用 ? 运算符的行为与 Result<T, E> 类似:如果值是 None,此时 None 会从函数中提前返回。如果值是 SomeSome 中的值作为表达式的返回值同时函数继续。

文本中返回第一行最后一个字符的函数的例子:

    fn last_char_of_first_line(text: &str) -> Option<char> {
        text.lines().next()?.chars().last()
    }

这个函数返回 Option<char> 因为它可能会在这个位置找到一个字符,也可能没有字符。这段代码获取 text 字符串 slice 作为参数并调用其 lines 方法,这会返回一个字符串中每一行的迭代器。因为函数希望检查第一行,所以调用了迭代器 next 来获取迭代器中第一个值。如果 text 是空字符串,next 调用会返回 None,此时我们可以使用 ? 来停止并从 last_char_of_first_line 返回 None。如果 text 不是空字符串,next 会返回一个包含 text 中第一行的字符串 slice 的 Some 值。

? 会提取这个字符串 slice,然后可以在字符串 slice 上调用 chars 来获取字符的迭代器。我们感兴趣的是第一行的最后一个字符,所以可以调用 last 来返回迭代器的最后一项。这是一个 Option,因为有可能第一行是一个空字符串,例如 text 以一个空行开头而后面的行有文本,像是 "\nhi"。不过,如果第一行有最后一个字符,它会返回在一个 Some 成员中。? 运算符作用于其中给了我们一个简洁的表达这种逻辑的方式。如果我们不能在 Option 上使用 ? 运算符,则不得不使用更多的方法调用或者 match 表达式来实现这些逻辑。

注意你可以在返回 Result 的函数中对 Result 使用 ? 运算符,可以在返回 Option 的函数中对 Option 使用 ? 运算符,但是不可以混合搭配。? 运算符不会自动将 Result 转化为 Option,反之亦然;在这些情况下,可以使用类似 Result 的 ok 方法或者 Option 的 ok_or 方法来显式转换。

目前为止,我们所使用的所有 main 函数都返回 ()main 函数是特殊的因为它是可执行程序的入口点和退出点,为了使程序能正常工作,其可以返回的类型是有限制的。

幸运的是 main 函数也可以返回 Result<(), E>,以下示例 main 的返回值为 Result<(), Box<dyn Error>> 并在结尾增加了一个 Ok(()) 作为返回值。这段代码可以编译:

fn main() -> Result<(), Box<dyn Error>> {
    let greeting_file = File::open("hello1.txt")?;
    Ok(())
}

Box<dyn Error> 类型是一个 trait 对象trait object

目前可以将 Box<dyn Error> 理解为 “任何类型的错误”。在返回 Box<dyn Error> 错误类型 main 函数中对 Result 使用 ? 是允许的,因为它允许任何 Err 值提前返回。即便 main 函数体从来只会返回 std::io::Error 错误类型,通过指定 Box<dyn Error>,这个签名也仍是正确的,甚至当 main 函数体中增加更多返回其他错误类型的代码时也是如此。

当 main 函数返回 Result<(), E>,如果 main 返回 Ok(()) 可执行程序会以 0 值退出,而如果 main 返回 Err 值则会以非零值退出;成功退出的程序会返回整数 0,运行错误的程序会返回非 0 的整数。Rust 也会从二进制程序中返回与这个惯例相兼容的整数。

main 函数也可以返回任何实现了 std::process::Termination trait 的类型,它包含了一个返回 ExitCode 的 report 函数。

2、要不要 panic!

那么,该如何决定何时应该 panic! 以及何时应该返回 Result 呢?如果代码 panic,就没有恢复的可能。你可以选择对任何错误场景都调用 panic!,不管是否有可能恢复,不过这样就是你代替调用者决定了这是不可恢复的。选择返回 Result 值的话,就将选择权交给了调用者,而不是代替他们做出决定。调用者可能会选择以符合他们场景的方式尝试恢复,或者也可能干脆就认为 Err 是不可恢复的,所以他们也可能会调用 panic! 并将可恢复的错误变成了不可恢复的错误。因此返回 Result 是定义可能会失败的函数的一个好的默认选择。

在一些类似示例、原型代码(prototype code)和测试中,panic 比返回 Result 更为合适,不过它们并不常见。让我们讨论一下为何在示例、代码原型和测试中,以及那些人们认为不会失败而编译器不这么看的情况下,panic 是合适的。

2.1 示例、代码原型和测试都非常适合 panic

当你编写一个示例来展示一些概念时,在拥有健壮的错误处理代码的同时也会使得例子不那么明确。例如,调用一个类似 unwrap 这样可能 panic! 的方法可以被理解为一个你实际希望程序处理错误方式的占位符,它根据其余代码运行方式可能会各不相同。

类似地,在我们准备好决定如何处理错误之前,unwrapexpect方法在原型设计时非常方便。当我们准备好让程序更加健壮时,它们会在代码中留下清晰的标记。

如果方法调用在测试中失败了,我们希望这个测试都失败,即便这个方法并不是需要测试的功能。因为 panic! 会将测试标记为失败,此时调用 unwrap 或 expect 是恰当的。

2.2 当我们比编译器知道更多的情况

当你有一些其他的逻辑来确保 Result 会是 Ok 值时,调用 unwrap 或者 expect 也是合适的,虽然编译器无法理解这种逻辑。你仍然需要处理一个 Result 值:即使在你的特定情况下逻辑上是不可能的,你所调用的任何操作仍然有可能失败。如果通过人工检查代码来确保永远也不会出现 Err 值,那么调用 unwrap 也是完全可以接受的,这里是一个例子:

fn main() {
    let home: IpAddr = "127.0.0.1111".parse().expect("IP address error......");
    panic!("{:?}", home)
}

我们通过解析一个硬编码的字符来创建一个 IpAddr 实例。可以看出 127.0.0.1 是一个有效的 IP 地址,所以这里使用 expect 是可以接受的。然而,拥有一个硬编码的有效的字符串也不能改变 parse 方法的返回值类型:它仍然是一个 Result 值,而编译器仍然会要求我们处理这个 Result,好像还是有可能出现 Err 成员那样。这是因为编译器还没有智能到可以识别出这个字符串总是一个有效的 IP 地址。如果 IP 地址字符串来源于用户而不是硬编码进程序中的话,那么就 确实 有失败的可能性,这时就绝对需要我们以一种更健壮的方式处理 Result 了。提及这个 IP 地址是硬编码的假设会促使我们将来把 expect 替换为更好的错误处理,我们应该从其它代码获取 IP 地址。

2.3 错误处理指导原则

在当有可能会导致有害状态的情况下建议使用 panic! —— 在这里,有害状态是指当一些假设、保证、协议或不可变性被打破的状态,例如无效的值、自相矛盾的值或者被传递了不存在的值 —— 外加如下几种情况:

  • 有害状态是非预期的行为,与偶尔会发生的行为相对,比如用户输入了错误格式的数据。
  • 在此之后代码的运行依赖于不处于这种有害状态,而不是在每一步都检查是否有问题。
  • 没有可行的手段来将有害状态信息编码进所使用的类型中的情况。

如果别人调用你的代码并传递了一个没有意义的值,尽最大可能返回一个错误,如此库的用户就可以决定在这种情况下该如何处理。然而在继续执行代码是不安全或有害的情况下,最好的选择可能是调用 panic! 并警告库的用户他们的代码中有 bug,这样他们就会在开发时进行修复。类似的,如果你正在调用不受你控制的外部代码,并且它返回了一个你无法修复的无效状态,那么 panic! 往往是合适的。

然而当错误预期会出现时,返回 Result 仍要比调用 panic! 更为合适。这样的例子包括解析器接收到格式错误的数据,或者 HTTP 请求返回了一个表明触发了限流的状态。在这些例子中,应该通过返回 Result 来表明失败预期是可能的,这样将有害状态向上传播,调用者就可以决定该如何处理这个问题。使用 panic! 来处理这些情况就不是最好的选择。

当你的代码在进行一个使用无效值进行调用时可能将用户置于风险中的操作时,代码应该首先验证值是有效的,并在其无效时 panic!。这主要是出于安全的原因:尝试操作无效数据会暴露代码漏洞,这就是标准库在尝试越界访问数组时会 panic! 的主要原因:尝试访问不属于当前数据结构的内存是一个常见的安全隐患。函数通常都遵循 契约contracts):它们的行为只有在输入满足特定条件时才能得到保证。当违反契约时 panic 是有道理的,因为这通常代表调用方的 bug,而且这也不是那种你希望所调用的代码必须处理的错误。事实上所调用的代码也没有合理的方式来恢复,而是需要调用方的 程序员 修复其代码。函数的契约,尤其是当违反它会造成 panic 的契约,应该在函数的 API 文档中得到解释。

虽然在所有函数中都拥有许多错误检查是冗长而烦人的。幸运的是,可以利用 Rust 的类型系统(以及编译器的类型检查)为你进行很多检查。如果函数有一个特定类型的参数,可以在知晓编译器已经确保其拥有一个有效值的前提下进行你的代码逻辑。例如,如果你使用了一个并不是 Option 的类型,则程序期望它是 有值 的并且不是 空值。你的代码无需处理 Some 和 None 这两种情况,它只会有一种情况就是绝对会有一个值。尝试向函数传递空值的代码甚至根本不能编译,所以你的函数在运行时没有必要判空。另外一个例子是使用像 u32 这样的无符号整型,也会确保它永远不为负。

2.4 创建自定义类型进行有效性验证

让我们使用 Rust 类型系统的思想来进一步确保值的有效性,并尝试创建一个自定义类型以进行验证。回忆一下第二章的猜猜看游戏,我们的代码要求用户猜测一个 1 到 100 之间的数字,在将其与秘密数字做比较之前我们从未验证用户的猜测是位于这两个数字之间的,我们只验证它是否为正。在这种情况下,其影响并不是很严重:“Too high” 或 “Too low” 的输出仍然是正确的。但是这是一个很好的引导用户得出有效猜测的辅助,例如当用户猜测一个超出范围的数字或者输入字母时采取不同的行为。

一种实现方式是将猜测解析成 i32 而不仅仅是 u32,来默许输入负数,接着检查数字是否在范围内:

 loop {
        // --snip--

        let guess: i32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        if guess < 1 || guess > 100 {
            println!("The secret number will be between 1 and 100.");
            continue;
        }

        match guess.cmp(&secret_number) {
            // --snip--
    }

2.5 总结

Rust 的错误处理功能被设计为帮助你编写更加健壮的代码。panic! 宏代表一个程序无法处理的状态,并停止执行而不是使用无效或不正确的值继续处理。Rust 类型系统的 Result 枚举代表操作可能会在一种可以恢复的情况下失败。可以使用 Result 来告诉代码调用者他需要处理潜在的成功或失败。在适当的场景使用 panic! 和 Result 将会使你的代码在面对不可避免的错误时显得更加可靠。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

欧盟玩具CE认证标准EN71详细介绍

玩具EN71认证简介 EN71是欧盟市场玩具类产品的规范标准。许多国家都就这些产品建立了自己的安全规章&#xff0c;生产公司必须保证其产品在该地区销售前符合相关标准。制造商必须对因生产缺陷、不良设计或不适当材料的使用而导致的事故负责。由此在欧洲推出玩具EN71认证法令&am…

设计模式-创建者模式

1.单例模式 单例模式&#xff08;Singleton Pattern&#xff09;是 Java 中最简单的设计模式之一&#xff0c;此模式保证某个类在运行期间&#xff0c;只有一个实例对外提供服务&#xff0c;而这个类被称为单例类。 使用单例模式要做的两件事 1. 保证一个类只有一个实例 2.…

【UE5】交互式展厅数字博物馆交互是开发实战课程

长久以来&#xff0c;我们总是不断被初学者问到类似这样的问题&#xff1a;如何从头到尾做一个交互式程序开发项目&#xff1f;本套课程尝试对这个问题进行解答。 课程介绍视频如下 【UE5】数字展厅交互式开发全流程 【谁适合学习这门课】 本套课程面向初学者&#xff0c;满足…

关于C#中Monitor的wait/pulse的理解

wait&#xff1a;表示释放对象上的锁并阻止当前线程&#xff0c;直到它重新获取该锁。 pulse&#xff1a;表示通知等待队列中的线程锁定对象状态的更改。 当线程调用 Wait 时&#xff0c;它会释放对象上的锁并进入对象的等待队列。 对象的就绪队列中的下一个线程 (如果有一个…

【数据库】聊聊数据库中的 fetchsize 参数

聊聊数据库中的 fetchsize 参数 1.介绍2.案例3.MySQL 中的 fetchsize4.Oracle 中的 fetchsize 1.介绍 在使用查询语句的时候&#xff0c;经常需要根据条件来进行查询得到最终的总记录条数&#xff0c;然后得到结果之后需要来进行处理。 场景&#xff1a;Java 端从数据库读取 …

【数据结构与算法】之数组系列-20240117

这里写目录标题 一、167. 两数之和 II - 输入有序数组二、164. 最大间距三、128. 最长连续序列四、122. 买卖股票的最佳时机 II五、78. 子集六、75. 颜色分类 一、167. 两数之和 II - 输入有序数组 中等 给你一个下标从 1 开始的整数数组 numbers &#xff0c;该数组已按 非递…

【python】基础知识类的语法功能讲解

Python代码定义了一个名为Calculation的类&#xff0c;用于执行基础的数学运算&#xff08;加法、减法、乘法、除法和取模&#xff09;。下面我将详细解释各个部分的功能&#xff0c;并以列表形式总结&#xff1a; 类定义&#xff1a; class Calculation: 定义了一个名为Cal…

Simulink|双机并联自适应虚拟阻抗下垂控制仿真模型

目录 主要内容 模型研究 结果一览 下载链接 主要内容 风电高渗透率下&#xff0c;电力系统对风电场频率调节能力提出了技术要求。考虑风机惯性控制和变桨距控制的频率响应能力&#xff0c;提出将储能与风电自身调频手段相结合&#xff0c;参与系统频率调节。模型…

JSP简单学习

jsp是在html中嵌入java代码 jsp也是在j2ee服务端中的java组件 第一次运行 在第一次运行jsp代码时会经历以下步骤&#xff0c;将jsp转为java代码&#xff0c;将java代码转为class文件。 所以通常会比较慢&#xff0c;编译后就好多了。 四大作用域 requestsessionpageapplica…

25考研英语复习计划

Hello各位小伙伴大家好&#xff0c;今天要给大家分享的是英语备考计划&#xff0c;大家可以作为参考&#xff0c;制定适合自己的备考计划。 【英一/二】 英语分为英一、英二&#xff0c;一般学硕英一&#xff0c;专硕英二。 英一要比英二难度大。 【复习计划】 1-2月&#xf…

python - 无法正常显示xlabel / ylabel

python 无法正常显示xlabel / ylabel 如上述所示&#xff0c; 第一个子图是不带投影的&#xff0c;可以正常显示横纵轴标签和标题第二个子图带有投影&#xff0c;横纵轴通过手动设置范围&#xff0c;可以正常显示横纵轴标签和标题第三个子图带有投影&#xff0c;横纵轴的tick …

基于LDA的评论大数据的分析及主题建模

1.微博的关键词大数据采集&#xff1b; 已完成&#xff0c;待优化 2.LDA 错误1&#xff1a;使用了import pyLDAvis.sklearn&#xff0c;提示没有模块no module named pyldavis.sklearn。 默认安装 pyLDAvis3.4.1&#xff0c;最后降级处理&#xff0c;解决方式&#xff1a; …

Java精品项目源码springboot面向社区养老服务的应用系统(编号V71)

Java精品项目源码springboot面向社区养老服务的应用系统(编号V71) 大家好&#xff0c;小辰今天给大家介绍一个面向社区养老服务的应用系统&#xff0c;演示视频公众号&#xff08;小辰哥的Java&#xff09;对号查询观看即可 文章目录 Java精品项目源码springboot面向社区养老…

pve虚拟机的改名和修改ID

PVE的虚拟机名字在web界面是无法修改id和名字的。要注意id和名字不能重。 在使用备份时就发现虚拟机是以虚拟机id作为唯一标识&#xff0c;如果有多台pve节点&#xff0c;但共用同一个nfs目录备份或使用同一个pbs进行备份时就必须保障id的唯一性。这时可以使用这个方法来进行补…

maven无法识别本地maven仓库包解决方案

前言&#xff1a;由于本地maven仓库已经有了相关依赖包&#xff0c;idea还是去远程仓库下载(不知何原因&#xff0c;生产上到远程仓库的网络突然不通了)&#xff0c;故需要自己本地上传相关包到生产主机并修改setttings文件来强制读取本地仓库方案 settings文件修改如下方式即…

【Gradle】Maven-Publishing

使用Java开发完成一个模块或者一个基础框架需要提供给团队项目使用&#xff0c;这个时候有两种方式可提供&#xff0c;一是提供源码&#xff0c;二是提供编译构建好的jar包供使用&#xff0c;这个时候需要讲构建好的包发布到公司的私服&#xff08;公司maven仓库&#xff09;&a…

最新版git2.43安装、记住用户名和密码以及tortoisegit2.15使用

一、下载git 打开git官网地址&#xff1a;https://git-scm.com/进行下载 下载完安装&#xff0c;一直next就好&#xff0c;如果愿意就可以改下安装路径&#xff0c;改在d盘。 具体可以参考&#xff1a;git安装教程 二、安装完下载小乌龟以及中文语言包 下载地址&#xff1a;…

SQL性能分析

SQL性能分析 1、SQL执行频率 ​ MySQL 客户端连接成功后&#xff0c;通过 show [session|global] status 命令可以提供服务器状态信 息。通过如下指令&#xff0c;可以查看当前数据库的INSERT、UPDATE、DELETE、SELECT的访问频次&#xff1a; -- session 是查看当前会话 ; …

【Maven】009-Maven 简单父子工程搭建

【Maven】009-Maven 简单父子工程搭建 文章目录 【Maven】009-Maven 简单父子工程搭建一、需求说明1、结构2、第三方库 二、工程搭建1、父工程第一步&#xff1a;创建父工程第二步&#xff1a;引入公共依赖 lombok 和管理 hutool 依赖版本 2、公共子模块第一步&#xff1a;创建…

越来越多的人学习PMP,2024年考PMP还有价值吗?

转管理是大部分人30人的想法&#xff0c;尤其是 IT行业有个 "35大关”考PMP的人是最多的。 “经验式管理终将成为过去&#xff0c;专业式管理才是时代趋势”&#xff0c;要想做好一个项目经理&#xff0c;系统的项目管理知识和项目经验缺一不可。经验是需要积累的&#x…