首页 > 编程语言 >Rust 程序设计语言(8)

Rust 程序设计语言(8)

时间:2023-01-05 21:13:10浏览次数:36  
标签:返回 语言 错误 Result File 程序设计 main panic Rust

title: Rust程序设计语言(8)              
date: 2023-01-03            
updated: 2023-01-05         
comments: true              
toc: true                   
excerpt: Rust错误处理
tags:                       
- Rust
categories:                 
- 编程

前言

本章介绍 rust 的错误处理

错误处理分类

只要是人写的代码就有可能出现 bug, rust 会尽可能的在编译时就将错误抛出, 目的是让你的代码更加健壮, 避免在运行时出现问题.
rust 将错误分为两种, 可恢复的(recoverable)不可恢复的(unrecoverable) 错误, 如果是可恢复的错误, 例如文件未找到, rust只会向用户报告错误. 而针对不可恢复的错误, 例如数组下标超限等, 程序会立即停止.

使用 panic! 处理不可恢复错误

panic!

rust自带了宏panic!, 当执行这个宏时, rust会打印出一个错误信息, 展开并清理栈数据, 然后程序会退出.
默认情况下, 当出现 panic 时, 程序会默认展开(unwinding), Rust 会回溯栈并清理数据, 这个过程可能需要很多工作, 你也可以设置关闭 rust 的展开功能, 在程序结束后由操作系统进行清理, 这通常会减小最后生成的二进制文件, 你可以通过在 Cargo.toml 添加配置来设置在 release 模式直接终止程序

[profile.release]
panic = 'abort'

简单的调用一下 panic!

fn main() {
    panic!("panic")
}

运行可以看到错误输出

➜  try git:(master) ✗ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/try`
thread 'main' panicked at 'panic', src/main.rs:3:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

可以看到已经输出了错误信息, 提示我们错误在 src/main.rs 的第三行第5个字符
而在真正的项目中, 如果我们调用其他包出现错误, 很可能这里的错误会指向我们调用的包内的代码, 此时我们可以调用 backtrace 来获取错误上下文信息

panic! 的 backtrace

我们修改代码, 使 panic! 由其他代码触发而不是自己调用

fn main() {
    let v = vec![1, 2, 3];
    v[99];  // 下标 99
}

在这里, 我们建立v只有三个元素, 但是在下方获取下标为99的元素, 在rust 中, 如果访问了无效索引, 会导致 panic, 如果是在 C 语言中, 会获取到对应数据结构中这个元素内存中的位置的值, 也可能会访问到不属于这个数据结构的数据, 这被称之为内存泄露, 可能会导致安全漏洞问题, 因此Rust 将这个操作进行了捕捉和错误处理. 例如:

➜  try git:(master) ✗ cargo run
   Compiling try v0.1.0 (/Work/Code/Rust/student/try)
    Finished dev [unoptimized + debuginfo] target(s) in 0.34s
     Running `target/debug/try`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:3:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

错误输出提示我们, 错误在 src/main.rs:3:5, 并告诉我们错误时尝试获取索引99的数据但是总长度为3.
另外也告诉我们, 可以设置 RUST_BACKTRACE=1 来获取backtrace 信息, 我们在运行命令中指定RUST_BACKTRACE=1 运行, 命令变成了 RUST_BACKTRACE=1 cargo run

➜  try git:(master) ✗ RUST_BACKTRACE=1 cargo run
   Compiling try v0.1.0 (/Work/Code/Rust/student/try)
    Finished dev [unoptimized + debuginfo] target(s) in 1.38s
     Running `target/debug/try`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:3:5
stack backtrace:
   0: rust_begin_unwind
             at /rustc/69f9c33d71c871fc16ac445211281c6e7a340943/library/std/src/panicking.rs:575:5
   1: core::panicking::panic_fmt
             at /rustc/69f9c33d71c871fc16ac445211281c6e7a340943/library/core/src/panicking.rs:65:14
   2: core::panicking::panic_bounds_check
             at /rustc/69f9c33d71c871fc16ac445211281c6e7a340943/library/core/src/panicking.rs:151:5
   3: <usize as core::slice::index::SliceIndex<[T]>>::index
             at /rustc/69f9c33d71c871fc16ac445211281c6e7a340943/library/core/src/slice/index.rs:259:10
   4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
             at /rustc/69f9c33d71c871fc16ac445211281c6e7a340943/library/core/src/slice/index.rs:18:9
   5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
             at /rustc/69f9c33d71c871fc16ac445211281c6e7a340943/library/alloc/src/vec/mod.rs:2736:9
   6: try::main
             at ./src/main.rs:3:5
   7: core::ops::function::FnOnce::call_once
             at /rustc/69f9c33d71c871fc16ac445211281c6e7a340943/library/core/src/ops/function.rs:251:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

此时就会有 backtrace 信息打印出来, 可以看到错误上下文, 当程序开启 debug 模式时, 默认在 panic 时会打印 backtrace 信息, 而当不使用 --release 参数运行cargo buildcargo run 时 debug 会默认启用.
backtrace 信息中输出了错误的上下文信息, 可以看到, 错误是在 ./src/main.rs:3:5 开始触发的, 如果你想要对错误进行排查, 需要从你的调用代码开始查看和排查问题.

使用 Result 处理可以恢复的错误

举个例子, 当我们在代码中希望打开某一个文件, 当文件不存在时, 我们应该进行其他处理, 例如重试或者更换文件, 而不是直接将程序停止, 因为这是很可能出现的错误.
在之前的章节中, 我们提到过, Result 枚举成员有两个

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

其中, TE 是泛型类型参数, 之后会学习泛型的知识, 现在, 我们可以认为, T 是成功时Ok 成员中的数据类型, E 代表错误时Err成员中的数据类型, 因此我们可以通过枚举来判断调用是否成功
下面的代码我们尝试打开一个文件

use std::fs::File;

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

我们如何知道File::open 的返回值是什么类型呢? 如果你使用 Vscode 并且安装了相关模块, 他会自己提示, 或者你可以查看文档, 同时, 如果你定义了错误的类型作为返回值接收, 在编译和运行时也会有错误信息, 例如 let f:u32 =File::open("a.txt");
则会报错

➜  try git:(master) ✗ cargo run             
   Compiling try v0.1.0 (/Work/Code/Rust/student/try)
error[E0308]: mismatched types
 --> src/main.rs:4:16
  |
4 |     let f:u32 =File::open("a.txt");
  |           ---  ^^^^^^^^^^^^^^^^^^^ expected `u32`, found enum `Result`
  |           |
  |           expected due to this
  |
  = note: expected type `u32`
             found enum `Result<File, std::io::Error>`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `try` due to previous error

错误会告诉你, 返回值应该是 Result<File, std::io::Error>
这就是典型的返回结构, Result<File, std::io::Error>, File则是调用成功后返回的文件句柄, Error 则是错误信息, 我们可以通过枚举来进行分支处理(match 表达式)

use std::fs::File;
   
fn main() {
   let f =File::open("a.txt");
   let _f = match f {
	   Ok(file) => {file},
	   Err(error) => panic!("open file error: {:?}", error),
   };
   print!("OK");
}

需要注意的是Result枚举和成员也是默认导入到 prelude 中的, 所以无需通过 Result:: 来进行手动导入
这里, 我们对 f 进行枚举, 当 open 调用成功时, 进行Ok 中逻辑, 将 file 返回给 f, 当错误时, 调用Err 进行 panic 抛出.当我们本地没有 a.txt 时. 运行会报错

➜  try git:(master) ✗ cargo run
      Compiling try v0.1.0 (/Work/Code/Rust/student/try)
       Finished dev [unoptimized + debuginfo] target(s) in 0.30s
        Running `target/debug/try`
   thread 'main' panicked at 'open file error: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:7:23
   note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

错误很好的告诉了我们没有这个文件

匹配不同的错误

我们还是希望对错误进行分别处理, 当没有文件时, 我们希望能自己创建这个文件, 返回句柄, 而因为其他原因失败, 触发panic, 我们就需要借用 ErrorKind 来判断具体的错误并进行处理

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

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

    let f = match f {
        Ok(file) => file,  // success
        Err(error) => match error.kind() {  // kind 返回错误
            // 是 NotFound 错误, 文件不存在
            ErrorKind::NotFound => match File::create("hello.txt") {  // 创建文件, 枚举创建是否成功
                Ok(fc) => fc,  // 返回文件句柄
                Err(e) => panic!("Problem creating the file: {:?}", e),  // 创建文件失败, panic
            },
            other_error => {
                // 其他错误
                panic!("Problem opening the file: {:?}", other_error)
            }
        },
    };
}

io::ErrorKind是标准库中的枚举, 包含了io 操作各种可能错误, 例如文件找不到, 就对应了 ErrorKind::NotFound
我们这里使用了3次 match, 可以看到, 代码嵌套比较难懂, 之后我们会学习闭包, 可以讲这种代码使用闭包进行简化, 例如

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

fn main() {
    let f = File::open("a.txt").unwrap_or_else(|error|{
        if error.kind()== ErrorKind::NotFound{
            File::create("a.txt").unwrap_or_else(|error|{
                panic!("{:?}", error);
            })
        }else{
            panic!("{:?}", error)
        }
    });
}

这样代码就变得简单了, 我们之后会学习闭包和unwrap_or_else 的用法

失败时 panic 的快捷方式

如果我们想要在返回错误时直接进行panic 抛出, 并打印错误信息, 有两个简写unwrapexpect
对于 unwrap, 如果调用成功, 则会返回Ok中的值, 如果错误则会为我们调用panic!

use std::fs::File;

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

在错误时

    Finished dev [unoptimized + debuginfo] target(s) in 0.28s
     Running `target/debug/try`
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:33
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

expect 的区别是, 当错误时, 开发者可以指定携带一些自定义的错误信息, 方便我们定位错误

use std::fs::File;

fn main() {
    let f = File::open("a.txt").expect("open file error");
}

错误时

    Finished dev [unoptimized + debuginfo] target(s) in 0.21s
     Running `target/debug/try`
thread 'main' panicked at 'open file error: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:33
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

可以看到, 其中的 open file error 是我们自定义的错误信息, 这样可以帮助我们更快的定位问题

传播错误

在我们自己编写函数时, 往往也需要把可能出现的错误返回给调用者, 让调用者知道函数内部发生了问题, 并进行处理, 这被称为 传播错误
作为被调用者, 我们很难明白调用者调用我们的意图, 所以, 将错误传播出去, 而不是我们内部触发 panic 或者其他操作是正确的处理方式

use std::{fs::File, io::{Read, self}};

fn read_file() -> Result<String, io::Error> {  // 返回字符串和io::Error
    let f = File::open("a.txt");  // 打开文件
    let mut f = match f {
        Ok(file) => file,  // 成功返回给 f
        Err(e) => return Err(e),  // 错误直接将函数退出并返回错误
    };
    let mut s= String::new();  // 新建字符串 s
    match f.read_to_string(&mut s) {  // read_to_string 将文件内容读取到 s 中
        Ok(_) => Ok(s),  // 成功返回 s, 因为代码块到最后了同时无变量接收, 所以这里直接感受结束了, 正确响应需要包一个 Ok, 符合枚举
        Err(e) => Err(e),  // 错误返回, Err 包含
    }
}

需要注意的是, 最后函数的返回需要使用 Ok 或者 Err 包含, 使其符合Result 结构

传播错误简写方式

传播错误是我们经常使用的开发方式, 所以 Rust 内置了 ? 运算符帮助我们简化传播错误的代码, 例如:

use std::{fs::File, io::{Read, self}};

fn read_file() -> Result<String, io::Error> {  // 返回字符串和io::Error
    let mut f = File::open("a.txt")?;  // 如果Ok 赋值给 f, 如果Err 直接 return Err(e)
    let mut s = String::new();
    f.read_to_string(& mut s)?;  // 如果 Ok 往下走, 如果Err 直接 return Err(e)
    Ok(s)  // 返回 Ok(s), 因为是最后一行代码所以无需写 return
}

? 将错误值传递给了标准库From, 其将错误值包装为指定的错误类型, 在这里是 io::Error 下面的错误类型, ?在我们只需要将错误返回而不是加以处理时非常的有用. 前提是错误类型实现了from 函数, 内置的宏都已经实现了这个函数, 因此可以直接调用.
?同样可以支持链式调用, 帮助我们进一步简化代码

use std::{fs::File, io::{Read, self}};

fn read_file() -> Result<String, io::Error> {  // 返回字符串和io::Error
    let mut s = String::new();
    File::open("a.txt")?.read_to_string(& mut s)?;  // 链式调用, 正确时才会进行链的下一节调用, 错误时直接返回, 不执行后的节
    Ok(s)  // 返回 Ok(s), 因为是最后一行代码所以无需写 return
}

fn main() {
    read_file().expect("read error");
}

链式调用可以帮助我们更进一步简化代码
同时, 针对这个简单的函数, 我们可以直接调用 fs::read_to_string 宏, rust 内置了一些方便的宏帮助我们进行简单的操作, 比如 fs::read_to_string 就是将文件内容读取并返回, 当出现错误时同样的是Result, 可以直接将错误传播, 当然, 大部分情况, 函数内部的逻辑不会这么简单

标签:返回,语言,错误,Result,File,程序设计,main,panic,Rust
From: https://www.cnblogs.com/chnmig/p/17028859.html

相关文章

  • day54 - 外键,DML语言
    外键创建表的时候添加外键太多了不想写了注意删除有外键关系的表时,必须要先删除引用别人的表,才能删除被引入的表修改表时添加外键关系 ALTERTABLE`student`......
  • C语言指针常见问题
    我们在学C语言时,指针是我们最头疼的问题之一,针对C语言指针,博主根据自己的实际学到的知识以及开发经验,总结了以下使用C语言指针时常见问题。指针指针做函数参数学习......
  • Go语言的复合数据类型struct,array,slice,map
    1、结构体struct定义格式:type结构体名称struct{属性1类型属性2类型...}结构体的初始化可以使用new关键词和var关键词,不同的是如果使用new,则返......
  • R语言用贝叶斯层次模型进行空间数据分析|附代码数据
    阅读全文:http://tecdat.cn/?p=10932最近我们被客户要求撰写关于贝叶斯层次模型的研究报告,包括一些图形和统计输出。在本文中,我将重点介绍使用集成嵌套拉普拉斯近似方法......
  • R语言多元(多变量)GARCH :GO-GARCH、BEKK、DCC-GARCH和CCC-GARCH模型和可视化|附代码数据
    全文链接:http://tecdat.cn/?p=30647最近我们被客户要求撰写关于GARCH的研究报告,包括一些图形和统计输出。从Engle在1982发表自回归条件异方差(ARCH)模型的论文以来,金融......
  • R语言随机波动模型SV:马尔可夫蒙特卡罗法MCMC、正则化广义矩估计和准最大似然估计上证
    全文链接:http://tecdat.cn/?p=31162最近我们被客户要求撰写关于SV模型的研究报告,包括一些图形和统计输出。本文做SV模型,选取马尔可夫蒙特卡罗法(MCMC)、正则化广义矩估计......
  • go语言之初识gin
    1.首先搭环境1.1安装go1.2创建目录1.3在当前目录下执行 gomodinitxxx_name1.4在当前目录下执行  gomodinitxxx_name1.5最简单的......
  • Qt多国语言
    ​​Qt多国语言的实现与切换(国际化)​​Qt多国语言的实现​​【大话QT之十四】QT实现多语言切换-​​用Qt5中实现多国语言支持-简书​​​​VS2017QT实现多语言国际......
  • ThinkPHP多语言模块文件包含RCE复现详细教程
    漏洞描述:ThinkPHP在开启多语言功能的情况下存在文件包含漏洞,攻击者可以通过get、header、cookie等位置传入参数,实现目录穿越+文件包含,通过pearcmd文件包含这个trick即可实......
  • Go语言(golang)开源项目大全
    ​​http://www.open-open.com/lib/view/open1396063913278.html#Compression​​内容目录​​Astronomy​​​​构建工具​​​​缓存​​​​云计算​​​​命令行选项解......