首页 > 其他分享 >Rust 错误处理

Rust 错误处理

时间:2024-01-19 16:45:02浏览次数:34  
标签:std use File Result file 错误处理 panic Rust

目录

本文略有删减,原文请访问错误处理

panic! 宏代表一个程序无法处理的状态,并停止执行而不是使用无效或不正确的值继续处理。
Rust 类型系统的 Result 枚举代表操作可能会在一种可以恢复的情况下失败,可以使用 Result 来告诉代码调用者他需要处理潜在的成功或失败。

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

在实践中有两种方法造成 panic:执行会造成代码 panic 的操作(比如访问超过数组结尾的内容)或者显式调用 panic! 宏。

对应 panic 时的栈展开或终止

当出现 panic 时,程序默认会开始 展开(unwinding),这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。另一种选择是直接 终止(abort),这会不清理数据就退出程序,程序所使用的内存需要由操作系统来清理。

如果需要项目的最终二进制文件越小越好,panic 时通过在 Cargo.toml 的 [profile] 部分增加 panic = 'abort',可以由展开切换为终止,如在 release 模式中 panic 时直接终止:

[profile.release]
panic = 'abort'

在一个简单的程序中调用 panic!:

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

使用 panic! 的 backtrace

让我们来看看另一个因为我们代码中的 bug 引起的别的库中 panic! 的例子,而不是直接的宏调用。
尝试访问超越 vector 结尾的元素,这会造成 panic!:

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

    v[99];
}

可以设置 RUST_BACKTRACE 环境变量来得到一个 backtrace,backtrace 是一个执行到目前位置所有被调用的函数的列表。Rust 的 backtrace 跟其他语言中的一样:阅读 backtrace 的关键是从头开始读直到发现你编写的文件。

将 RUST_BACKTRACE 环境变量设置为任何不是 0 的值来获取 backtrace 看看:

$ RUST_BACKTRACE=1 cargo run
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5
stack backtrace:
   0: rust_begin_unwind
             at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/std/src/panicking.rs:584:5
   1: core::panicking::panic_fmt
             at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:142:14
   2: core::panicking::panic_bounds_check
             at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:84:5
   3: <usize as core::slice::index::SliceIndex<[T]>>::index
             at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:242:10
   4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
             at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:18:9
   5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
             at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/alloc/src/vec/mod.rs:2591:9
   6: panic::main
             at ./src/main.rs:4:5
   7: core::ops::function::FnOnce::call_once
             at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/ops/function.rs:248:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

为了获取带有这些信息的 backtrace,必须启用 debug 标识。当不使用 --release 参数运行 cargo build 或 cargo run 时 debug 标识会默认启用,就像这里一样。

Windows设置 RUST_BACKTRACE 环境变量的两种方式

在cmd中执行

set RUST_BACKTRACE=1

在powershell中执行:

$env:RUST_BACKTRACE=1 ; cargo run

用 Result 处理可恢复的错误

使用 Result 类型来处理潜在的错误中的那个 Result 枚举,它定义有Ok 和 Err两个成员:

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

T 和 E 是泛型类型参数:

  • T 代表成功时返回的 Ok 成员中的数据的类型
  • E 代表失败时返回的 Err 成员中的错误的类型

调用一个返回 Result 的函数,它打开一个文件,可能会失败:

use std::fs::File;

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

File::open 的返回值是 Result<T, E>:

  • 泛型参数 T 会被 File::open 的实现放入成功返回值的类型 std::fs::File,这是一个文件句柄。
  • 错误返回值使用的 E 的类型是 std::io::Error

`File::open`` 调用可能成功并返回一个可以读写的文件句柄,也可能会失败,如文件不存在或无访问权限。

需要在代码中增加根据 File::open 返回值进行不同处理的逻辑:

use std::fs::File;

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

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => panic!("Problem opening the file: {:?}", error),
    };
}

如果当前目录没有一个叫做 hello.txt 的文件,当运行这段代码时 panic! 宏的输出能告诉了我们出错的地方。

匹配不同的错误

使用不同的方式处理不同类型的错误:

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

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

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => match error.kind() {
            //尝试打开的文件并不存在,通过 File::create 创建文件
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("Problem creating the file: {:?}", e),
            },
            other_error => {
                panic!("Problem opening the file: {:?}", other_error);
            }
        },
    };
}
  • File::open 返回的 Err 成员中的值类型 io::Error,它是一个标准库中提供的结构体。
  • io::Error 结构体有一个返回 io::ErrorKind 值的 kind 方法可供调用。
  • io::ErrorKind 是一个标准库提供的枚举,它的成员对应 io 操作可能导致的不同错误类型。

不同于使用 match 和 Result<T, E>

在处理代码中的 Result<T, E> 值时,相比于使用 match ,使用闭包和 unwrap_or_else 方法会更加简洁:

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

fn main() {
    let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create("hello.txt").unwrap_or_else(|error| {
                panic!("Problem creating the file: {:?}", error);
            })
        } else {
            panic!("Problem opening the file: {:?}", error);
        }
    });
}

失败时 panic 的简写:unwrap 和 expect

Result<T, E> 类型定义了很多辅助方法来处理各种情况,其中之一叫做 unwrap:

  • 如果 Result 值是成员 Ok,unwrap 会返回 Ok 中的值。
  • 如果 Result 是成员 Err,unwrap 会为我们调用 panic!。

一个实践 unwrap 的例子:

use std::fs::File;

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

另一个类似于 unwrap 的方法它还允许选择 panic! 的错误信息:expect,语法如下:

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")
        .expect("hello.txt should be included in this project");
}

expect 与 unwrap 的使用方式一样:返回文件句柄或调用 panic! 宏。

在生产级别的代码中,大部分人选择 expect 而不是 unwrap 并提供更多关于为何操作期望是一直成功的上下文。

传播错误

一个从文件中读取用户名的函数,如果文件不存在或不能读取,这个函数会将这些错误返回给调用它的代码:

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

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

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

    let mut username = String::new();

    match username_file.read_to_string(&mut username) {
        Ok(_) => Ok(username),
        Err(e) => Err(e),  //最后一个表达式,无需显式调用 return 语句
    }
}

函数的返回值类型为 Result<String, io::Error>,说明函数返回一个 Result<T, E> 类型的值:泛型参数 T 的具体类型是 String,而 E 的具体类型是 io::Error。

这里选择 io::Error 作为函数的返回值是因为它正好是函数体中那两个可能会失败的操作的错误返回值:File::open 函数和 read_to_string 方法。

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

一个使用 ? 运算符向调用者返回错误的函数:

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

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

Result 值之后的 ? 被定义为与前面示例中定义的处理 Result 值的 match 表达式有着完全相同的工作方式。不同的是,? 运算符所使用的错误值被传递给了 from 函数,它定义于标准库的 From trait 中,其用来将错误从一种类型转换为另一种类型。

问号运算符之后的链式方法调用:

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

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

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

    Ok(username)
}

使用 fs::read_to_string 而不是打开后读取文件

use std::fs;
use std::io;

fn read_username_from_file() -> Result<String, io::Error> {
    fs::read_to_string("hello.txt")
}

哪里可以使用 ? 运算符

? 运算符只能被用于返回值与 ? 作用的值相兼容的函数,因为 ? 运算符被定义为从函数中提早返回一个值,函数的返回值必须是 Result 才能与这个 return 相兼容。

尝试在返回 () 的 main 函数中使用 ? 的代码不能编译:

use std::fs::File;

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

当编译这些代码时会出错,错误指出只能在返回 Result 或者其它实现了 FromResidual 的类型的函数中使用 ? 运算符

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

在 Option<T> 上调用 ? 运算符的行为与 Result<T, E> 类似:如果值是 None,此时 None 会从函数中提前返回。如果值是 Some,Some 中的值作为表达式的返回值同时函数继续

在 Option 值上使用 ? 运算符:

//从给定文本中返回第一行最后一个字符
fn last_char_of_first_line(text: &str) -> Option<char> {
    text.lines().next()?.chars().last()
}

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

修改 main 返回 Result<(), E> 允许对 Result 值使用 ? 运算符:

use std::error::Error;
use std::fs::File;

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

    Ok(())
}

目前可以将 Box<dyn Error> 理解为 “任何类型的错误”,在返回 Box<dyn Error> 错误类型 main 函数中对 Result 使用 ? 是允许的,因为它允许任何 Err 值提前返回。

要不要 panic!

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

调用一个类似 unwrap 这样可能 panic! 的方法可以被理解为一个你实际希望程序处理错误方式的占位符,它根据其余代码运行方式可能会各不相同。
在我们准备好决定如何处理错误之前,unwrap和expect方法在原型设计时非常方便。当我们准备好让程序更加健壮时,它们会在代码中留下清晰的标记。

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

当你有一些其他的逻辑来确保 Result 会是 Ok 值时,调用 unwrap 或者 expect 也是合适的,虽然编译器无法理解这种逻辑:

use std::net::IpAddr;

let home: IpAddr = "127.0.0.1"
    .parse()
    .expect("Hardcoded IP address should be valid");

可以看出 127.0.0.1 是一个有效的 IP 地址,所以这里使用 expect 是可以接受的。

错误处理指导原则

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

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

当接收到无效输入时,优先返回错误信息以通知用户。若继续执行可能引发安全问题或严重后果,则调用 panic! 停止程序并指出bug。同样,在遇到无法修复的外部代码无效状态时,适宜使用 panic!。

当错误属于预期范围(如解析错误或HTTP限流响应),应返回 Result,表明可能出现失败,并将问题传递给调用者处理。此时不宜使用 panic! 来应对这些情况。

当代码执行操作可能因无效值而危及安全时,应先验证其有效性,并在无效时 panic!。这是因为尝试处理此类数据易暴露漏洞,如数组越界访问会导致 panic! 以防止潜在的安全风险。函数遵循输入条件的契约,若违反契约则通过 panic! 指出调用方 bug,因为这种错误通常无法在函数内部妥善处理,需要程序员修复源代码。函数契约及其可能导致 panic! 的情况应在 API 文档中明确说明。

虽然大量错误检查可能冗长繁琐,但 Rust 的类型系统和编译器能帮你自动进行许多检查。若函数参数为非 Option 类型,编译器确保其非空且有有效值,因此无需在代码中处理 Some/None 情况。传递空值的代码无法通过编译,故函数运行时无须判空。同样,使用如 u32 的无符号整型可确保值永不会为负数。

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

使用 if 表达式检查值是否超出范围(代码冗余、可能影响性能):

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--
}

可以创建一个新类型来将验证放入创建其实例的函数中,而不是到处重复这些检查。这样就可以安全地在函数签名中使用新类型并相信它们接收到的值。

一个 Guess 类型,它只在值位于 1 和 100 之间时才继续:

pub struct Guess {
    //私有的字段,确保了不会存在没有通过 Guess::new 函数的条件检查的 value 
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 || value > 100 {
            panic!("Guess value must be between 1 and 100, got {}.", value);
        }

        Guess { value }
    }

    //有时被称为 getter,目的就是返回对应字段的数据
    pub fn value(&self) -> i32 {
        self.value
    }
}

标签:std,use,File,Result,file,错误处理,panic,Rust
From: https://www.cnblogs.com/timefiles/p/17975021

相关文章

  • Rust 常见集合
    目录使用Vector储存列表新建vectorVec::new函数(无初值)vec!宏(有初值)更新vector读取vector的元素注意可变和不可变引用遍历vector中的元素使用枚举来储存多种类型丢弃vector时也会丢弃其所有元素使用字符串储存UTF-8编码的文本什么是字符串?新建字符串更新字符串使......
  • Rust采集天气预报信息并实时更新数据
    最近天气温度时高时低,虽说这是大自然的力量人无法抗拒,不能改变那么我们就做预防工作。今天我将用Rust写一个爬虫程序实现电脑桌面实时更新天气情况,这个是一个底层逻辑,需要多方面配合,不仅要有完善的代码还有爬虫IP试试更新才能保证数据最完整最新。这是一个简单的示例,它使用Rust的网......
  • 42 干货系列从零用Rust编写负载均衡及代理,wmproxy中配置tcp转websocket
    wmproxywmproxy已用Rust实现http/https代理,socks5代理,反向代理,静态文件服务器,四层TCP/UDP转发,七层负载均衡,内网穿透,后续将实现websocket代理等,会将实现过程分享出来,感兴趣的可以一起造个轮子项目地址国内:https://gitee.com/tickbh/wmproxygithub:https://github.com/......
  • 22. 从零用Rust编写正反向代理,一个数据包的神奇HTTP历险记!
    wmproxywmproxy已用Rust实现http/https代理,socks5代理,反向代理,静态文件服务器,四层TCP/UDP转发,内网穿透,后续将实现websocket代理等,会将实现过程分享出来,感兴趣的可以一起造个轮子项目地址国内:https://gitee.com/tickbh/wmproxygithub:https://github.com/tickbh/wmproxy数......
  • 23. 从零用Rust编写正反向代理,流控小姐姐的温柔一刀!
    wmproxywmproxy已用Rust实现http/https代理,socks5代理,反向代理,静态文件服务器,四层TCP/UDP转发,内网穿透,后续将实现websocket代理等,会将实现过程分享出来,感兴趣的可以一起造个轮子项目地址国内:https://gitee.com/tickbh/wmproxygithub:https://github.com/tickbh/wmproxy温......
  • 41. 干货系列从零用Rust编写负载均衡及代理,websocket与tcp的映射,WS与TCP互转
    wmproxywmproxy已用Rust实现http/https代理,socks5代理,反向代理,静态文件服务器,四层TCP/UDP转发,七层负载均衡,内网穿透,后续将实现websocket代理等,会将实现过程分享出来,感兴趣的可以一起造个轮子项目地址国内:https://gitee.com/tickbh/wmproxygithub:https://github.com/......
  • 初探: 通过pyo3用rust为python写扩展加速
    众所周知,python性能比较差,尤其在计算密集型的任务当中,所以机器学习领域的算法开发,大多是将python做胶水来用,他们会在项目中写大量的C/C++代码然后编译为so动态文件供python加载使用。那么时至今日,对于不想学习c/c++的朋友们,rust可以是一个不错的替代品,它有着现代化语言的设计和并......
  • [源码分析] - flex 标准文档导读与 一个rust实现解析
    本文是w3中css-flexbox[标准文档](CSSFlexibleBoxLayoutModuleLevel1(w3.org)解读.(2023.1),并对一些开源实现进行调研分析.文档导读csslayoutmodecsslayout模式用于确定在盒模型中的元素如何排布(大小与位置),在css2.1中有如下几种方式.blocklayout,块级别......
  • [Rust] Rust开发Wasm运行到支付宝小程序
    Rust开发Wasm运行到支付宝小程序最近参加了支付宝小程序开发者大赛,把我之前的RustNES项目(任天堂红白机游戏机模拟器,可以玩小霸王上的马里奥,冒险岛等)迁移到了支付宝小程序上,发现相关内容网上的资料比较匮乏,在此分享一下开发经历.本项目两大核心点Rust编译成Wasm运......
  • Slint 文件编辑不能在 Rust 中及时索引
    这个现象在编写VSCode中编写SlintDSL代码时非常常见.表现为修改Slint代码,如:导出新的component/global;为component增加/修改方法,属性,回调;在global中修改结构体属性,修改回调;随后前往Rust的nativecode中试图调用这些方法时,Rust的代码提示无......