首页 > 其他分享 >08.错误处理

08.错误处理

时间:2022-11-28 14:57:16浏览次数:64  
标签:std use 错误 08 Result File 错误处理 panic

Rust将错误分为两大类:可恢复错误与不可恢复错误。其他大部分变成语言都没有可以区分这两种错误,而是通过异常之类的机制来统一处理它们。虽然Rust没有类似的异常处理机制,但它提供了用于可恢复错误的类型Result<T,F>,以及在程序出现不可恢复错误时中止运行的panic!宏。

一、不可恢复错误与panic!

程序会在panic!宏执行时打印出一段错误提示信息,展开并清理当前的调用栈,然后退出程序。

panic中的栈展开与终止
1)当panic发生时,程序会默认开始栈展开。这意味着Rust会沿着调用栈的反向顺序遍历所有调用函数,并依次清理这些函数中的数据。
2)当panic发生时我们还可以立即终止程序,它会直接结束程序且不进行任何清理工作,程序所使用过的内存只能由操作系统进行回收。

调用panic!

fn main() {
    panic!("crash and burn");
}


由于调用了panic!,因此输出了最后两行错误提示信息。第一行显示了我们向panic所提供的信息,并指出了源代码中panic所发生的位置:src\main.rs:2:5表明panic发生在文件src\main.rs中的第二行的第五个字符处。

1、使用panic!产生回溯信息

下面代码,它没有直接在代码中调用panic!宏,但会因为其中代码的bug而导致标注库中产生panic!

fn main() {
    let v = vec![1, 2, 3];

    v[99];
}

这段代码中,动态函数只有持有3个元素,但我们却在尝试访问它的第100个元素。在这种情况下,Rust会触发panic。
在类似于C语言中,程序在这种情况下依然会尝试访问你所请求的值,即便这可能会于你所期望的并不相符;你会得到动态数组中对应这个索引位置的内存,而这个内存可能存储了其他数据,甚至都不属于动态数组本身,这种情形也被称为缓冲区溢出,并可能导致严重的安全性问题。

我们可通过RUST_BACKTRACE=1得到回溯信息,进而确定触发错误的原因。回溯(backtrace)中包含了到达错误点的所有调用函数列表。

二、可恢复错误与Result

Result类型来处理可能失败的情况,该枚举定义了两个变体——Ok和Err,它们都是泛型参数。

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

T代表了Ok变体中包含的值类型,该变体中的值会在执行成功时返回;而E则代表了Err变体中包含的错误类型,该变体中的值会在执行失败时返回
调用一个运行失败的函数,它会返回Result值作为运行结果。

use std::fs::File;

fn main() {
    let f = File::open("hello.txt");
}

如何知道相关函数是否会返回Result?

  1. 翻阅标准库API文档;std - Rust (rust-lang.org)
  2. 直接向编译器索要答案;
use std::fs::File;

fn main() {

    let f: u32 = File::open("hello.txt");

}


上述输出表明,File::open函数的返回类型是Result<T, E>。这里的泛型参数T被替换为了成功值得类型File,也就是文件的句柄,而错误值所对应的类型E则被替换为了std::io::Error
File::open函数运行成功时,变量f中的值将会是一个包含文件句柄的Ok实例。当它运行失败时,变量f中的值则会使一个包含了用于描述错误种类信息的Err实例。

use std::fs::File;

fn main() {
    let f = File::open("hello.txt");

    let f = match f {
        Ok(file) => file,
        Err(error) => {
            panic!("There was a problem opening the file {:?}", error)
        },
    };
}

注意,与Option枚举一样,Result枚举及其变体已经通过与导入模块被自动地引入当前作用域中,所以我们不需要在使用Ok变体与Err变体之前在match分支中显示声明Result::

  • 当Result的结果是Ok的时候,将Ok变体内部的file值移出,并将这个文件句柄重新绑定至变量f
  • 另一个分支则处理了File::open返回Err值得情况。我们选择通过调用panic!宏来处理该情形。

1、匹配不同的错误

当文件不存在时,创建该文件;其他错误继续返回。

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let f = File::open("hello.txt");

    let f = match f {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("Tried to create file but there was a problem: {:?}", e)
            },
            other_error => panic!("There was a problem opening the file: {:?}", other_error),
        },
    };
}


File::open返回的Err变体中的错误值类型,是定义在某个标准库中的结构体类型:io::Error。这个结构体拥有一个被称作kind的方法,可通过它来获得io::ErrorKind值,这个io::ErrorKind枚举是由标注库提供的,它的变体被用于描述io操作所可能导致的不同错误。其中ErrorKind::NotFound,它用于说明我们尝试打开的文件不存在。
在这个匹配分支中,我们检查error.kind()返回的值是不是ErrorKind枚举的NotFound变体;如果是,我们就接着使用函数File:create来创建这个文件。
然而,由于File::create本身也可能运行事变,所以我们也需要对它的返回值条件一个match表达式。外部match的最后一个分支保持不变,用于在出现其余错误时让程序除法panic。
如下代码与上述代码拥有完全一致的性,但没有相对复杂的match表达式,更为清晰易读:

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let f = File::open("hello.txt").map_err(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create ("hello.txt").unwrap_or_else(|error| {
                panic!("Tried to create file but there was a problem: {:?}", error);
            })
        } else {
            panic!("There was a problem opening the file: {:?}", error);
        }
    });
}

2、失败时除法panic的快捷方式:unwrap和expect

类型Result<T, E>自身定义了许多辅助方法来应对各种任务。其中一个被称为unwrap的方法实现了我们在上述实例中match表达式的效果。当Result的返回值是Ok变体时,unwrap则会替我们调用panic!宏。

fn main() {
    let f = File::open("hello.txt").unwrap();
}


还有另外一个被称作expect的方法,它允许我们在unwrap的基础上指定panic!所附带的错误提示信息。使用expect并附带上一段清晰的错误提示信息可以阐明的意图,并使你更容易追踪到panic的起源。

use std::fs::File;

fn main() {
    let f = File::open("hello.txt").expect("Failed to open hello.txt");
}

3、传播错误

当你编写的函数中包含了一些可能会执行失败的调用时,除了可以在函数中处理这个错误,还可以将这个错误返回给调用者,让他们决定应该如何做进一步处理,这个过程被称作传播错误(progagating)。
如下代码9-6,展示了一个从文件中读取用户名的函数。当文件不存在或无法读取时,这个函数会将错误作为作为结果返回给自己的调用者:

fn main() {
    use std::io;
    use std::io::Read;
    use std::fs::File;

    fn read_username_from_file() -> Result<String, io::Error> {
        let f = File::open("hello.txt");

        let mut f = match f {
            Ok(file) => file,
            Err(e) => return Err(e),
        };

        let mut s = String::new();

        match f.read_to_string(&mut s) {
            Ok(_) => Ok(s),
            Err(e) => Err(e),
        }
    }
}

传播错误模式在Rust编程中非常常见,所以Rust抓们提供了一个问号运算符(?)来简化它的语法。

1.传播错误的快捷方式:?运算符

通过将?放置于Result值之后,我们将实现与上述实例使用match表达式来处理Result时一样的功能。

fn main() {
    use std::io;
    use std::io::Read;
    use std::fo::File;

    fn read_username_from_file() -> Result<String, io::Error> {
        let mut f = File::open("hello.txt")?;
        let mut s = String::new();
        f.read_to_string(&mut s)?;
        Ok(s)
    }
}

不过,在使用match表达式与?还是在一个区别:被*?运算符所接受的错误值会隐式被from函数处理*,这个函数定义于标准库的From trait中,用于在错误类型之间进行转换。?运算符调用from函数时,它就开始尝试将传入的错误类型转换为当前函数的返回错误类型。当一个函数拥有不同的失败原因,却使用了统一的错误返回类型来同时进行表达式,这个功能会十分有用。只要每个错误类型都实现了转换为返回错误类型的from函数,?运算符就会自动帮我们处理所有的转换过程。
?运算符帮忙我们消除了大量模板代码,使函数更为简单。还可通过链式方法调用来进一步简化代码:

fn main() {
    use std::io;
    use std::io::Read;
    use std::fs::File;

    fn read_username_from_file() -> Result<String, io::Error> {
        let mut s = String::new();

        File::open("hello.txt")?.read_to_string(&mut s)?;

        Ok(s)
    }
}

创建新String并赋值给s的语句移动到了函数开始的地方,这一部分没有任何改变。接下来,我们并没有创建变量f,而是直接将read_to_string链接至File::open("hello.txt")?所产生的结果处并进行调用。我们依然依然在read_to_string调用的尾部保留了?,并依然会在File::openread_to_string都运行成功时,返回一个包含了用户名s的Ok值。这种写法跟符合项目实践的写法。

2.?运算符只能被用于返回Result的函数

使用了?运算符的函数必须返回Result、Option或任何实现了std::ops::Try的类型。在那些没有返回上述类型的函数里,一旦调用的其他函数返回了Result<T, E>,就需要使用matchResult<T, E>自身的方法来对Result<T, E>进行恰当的处理。

三、要不要使用panic!

只要你认为自己可以代替调用者决定某种情形是不可恢复的,那么就可以使用panic!;当你选择返回一个Result值时,你就将这种选择权交给了调用者。

标签:std,use,错误,08,Result,File,错误处理,panic
From: https://www.cnblogs.com/ColoFly/p/16932192.html

相关文章

  • 【JS】121-重温基础:流程控制和错误处理
    本文是 重温基础 系列文章的第二篇,需要让自己静下心来,学习,养成好习惯。本章节复习的是JS中的控制流语句,让我们能实现更多的交互功能。注意一点:在ES6之前,JS是没有块作用域......
  • SQL SERVER 2008 Hierarchyid数据类型
    以往我们在关系数据库中建立树状结构的时候,通常使用ID+ParentID来实现两条纪录间的父子关系。但这种方式只能标示其相对位置。解决这类问题在SqlServer20......
  • 使用SQL Server Management Studio 2008 将数据库里的数据导成脚本
    之前很羡慕MySQL有这样的工具可以把数据库里的数据导成脚本,SQLServer2005的时候大牛PinalDave写了个​​DatabasePublishingWizard​​​,具体用法参考他写的文章​​......
  • 【05.08】大疆 影像部相机方向嵌入式岗位内推
    朱老师推荐语:此岗位为大讲堂海思视频编解码项目学员何工,提供的自己本人部门(大疆影像部)内推,共有十个内推码,想从事相机软件研发的工程师们好好把握机会。​你如果不找工作那......
  • 0108-Go-定时器
    环境Time2022-08-24Go1.19前言说明参考:https://gobyexample.com/tickers目标使用Go语言的定时器。示例packagemainimport("fmt""time")fu......
  • vs2008 调试js
    面对一大段的JavaScript脚本,以前总是会很头疼,找不到调试这些代码的方法。如果出现什么错误或异常,总是要从头分析,然后插入很多Alert(),调试起来很麻烦。VisualStudio2008中J......
  • jquery008-表单验证
    <!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><title>form</title><style>.error{color:red;}.hide{......
  • 『题解』Codeforces 808D Array Division
    题目传送门解题思路首先统计\(n\)个数字和为\(sum\),那么一半就是\(sum=sum\div2\)从\(1\)到\(n\)枚举,累计进前缀和\(ans\)中,如果发现第\(i\)个数字累......
  • D108设计模式_2抽象工厂模式20221120
    抽象工厂模式(AbstractFactoryPattern):属于创建型模式,它提供了一种创建对象的最佳方式,以一个超级工厂创建其他工厂。在抽象工厂模式中,接口是负责创建一个相关对象的工厂,不......
  • 29.1308
    #include<stdio.h>#include<math.h>intmain(){intn=0,m=0,i,j,a[10][10];for(i=0;i<3;i++){for(j=0;j<3;j++){scanf("%d",&a[i][j]);}}for(i=0;i<3;i++)n+=a[i]......