13.错误处理

Rust中的错误处理

可恢复错误

  • 向用户报告错误和重试操作是合理操作,例如找不到文件

不可恢复错误

  • bug,例如访问数组超过数组结尾位置

panic! 和不可恢复的错误

  • 在执行panic!宏的时候,程序打印出错误信息,展开并且清理栈数据,然后退出。一般是出现一些类型的bug,并却不知道如何处理。
  • 使用panic会回溯到栈并且清理遇到的每一个函数的数据,另外一种方法是采用终止(abort),这种方法会直接退出程序,而交给操作系统处理,这种方法是在cargo.toml中的profile处添加:
[profile.release]
panic = 'abort'
  • 下面这段代码是使用panic! 的backtrace,可以使用panic!被别人调用的函数backtrace寻找出代码中出现的问题的地方。
fn main() {
    panic!("crash and burn");
}


   Compiling playground v0.0.1 (/playground)
    Finished dev [unoptimized + debuginfo] target(s) in 0.57s
     Running `target/debug/playground`
thread 'main' panicked at 'crash and burn', src/main.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
  • 下面是一段官方文档
    在这里插入图片描述
  • 下面是一段示例代码:
fn main() {
    let v = vec![1, 2, 3];

    v[99];
}


Compiling playground v0.0.1 (/playground)
Finished dev [unoptimized + debuginfo] target(s) in 0.85s
     Running `target/debug/playground`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
  • 最后第二行显示出了错误原因是超出数组最大索引范围,并且指向了一个源文件libcore/slice/mod.rs,这是真正出现panic!的地方
  • 这里相对于C语言,C语言会输出一个不正确的值,并有可能造成漏洞,而Rust不会。
  • 使用Rust中的Rust_BACKTRACE=1 cargo run代码,对代码添加backtrace(回溯)来对代码进行回溯。
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/panic`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', libcore/slice/mod.rs:2448:10
stack backtrace:
   0: std::sys::unix::backtrace::tracing::imp::unwind_backtrace
             at libstd/sys/unix/backtrace/tracing/gcc_s.rs:49
   1: std::sys_common::backtrace::print
             at libstd/sys_common/backtrace.rs:71
             at libstd/sys_common/backtrace.rs:59
   2: std::panicking::default_hook::{{closure}}
             at libstd/panicking.rs:211
   3: std::panicking::default_hook
             at libstd/panicking.rs:227
   4: <std::panicking::begin_panic::PanicPayload<A> as core::panic::BoxMeUp>::get
             at libstd/panicking.rs:476
   5: std::panicking::continue_panic_fmt
             at libstd/panicking.rs:390
   6: std::panicking::try::do_call
             at libstd/panicking.rs:325
   7: core::ptr::drop_in_place
             at libcore/panicking.rs:77
   8: core::ptr::drop_in_place
             at libcore/panicking.rs:59
   9: <usize as core::slice::SliceIndex<[T]>>::index
             at libcore/slice/mod.rs:2448
  10: core::slice::<impl core::ops::index::Index<I> for [T]>::index
             at libcore/slice/mod.rs:2316
  11: <alloc::vec::Vec<T> as core::ops::index::Index<I>>::index
             at liballoc/vec.rs:1653
  12: panic::main
             at src/main.rs:4
  13: std::rt::lang_start::{{closure}}
             at libstd/rt.rs:74
  14: std::panicking::try::do_call
             at libstd/rt.rs:59
             at libstd/panicking.rs:310
  15: macho_symbol_search
             at libpanic_unwind/lib.rs:102
  16: std::alloc::default_alloc_error_hook
             at libstd/panicking.rs:289
             at libstd/panic.rs:392
             at libstd/rt.rs:58
  17: std::rt::lang_start
             at libstd/rt.rs:74
  18: panic::main
  • 这段代码中第一次提到panic!的地方是主程序的src/main.rs:4 位置,所以这段代码有问题,检查后即为代码中有v[100],所以需要修改索引。

启用debug标识

  • 在rust中默认在使用carog runcargo build可以默认启用debug,而在添加 --release会出现失败。

Result和可恢复的错误

  • 在错误未严重到需要停止的时候,如:
    • 一个函数因为一个容易理解并做出反应的原因退出
    • 打开一个不存在的文件失败,需要创建这个文件,而不是abort
enum Result<T,E>
{
	Ok(T),
	Err(E),
}
  • 这里只需要了解T、E是泛型类型参数,T代表Ok返回成员中数据类型,E代表失败返回Err中错误类型,之后会提到。
  • 调用一个返回Result的函数,因为可能失败:
use std::fs::File;

fn main()
{
	let f = File::open("hello.txt");
}
  • 可以通过标准库API文档,或者编译器来知道File::open返回Result。或者你可以改变File::open的类型,使用u32后会报错。
let f: u32 = File::open("hello.txt");

error[E0308]: mismatched types
 --> src/main.rs:4:18
  |
4 |     let f: u32 = File::open("hello.txt");
  |                  ^^^^^^^^^^^^^^^^^^^^^^^ expected u32, found enum
`std::result::Result`
  |
  = note: expected type `u32`
             found type `std::result::Result<std::fs::File, std::io::Error>`
  • 下面是一段官方文档
    在这里插入图片描述
  • 这其实相当于告诉我们File::open函数的返回值类型是Result<T,E>,这里泛型参数T放入成功值类型std::fs::File,他是一个文件句柄,E对应的则是std::io::Error
  • 为了避免出现错误,需要使用下面代码
use std::fs::File;

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

	let f = match f{
	Ok(file) => file,
	Err(error)=>
	{
	panic!("Problem opening the file {:?}",error)
},
};
}
  • Result枚举和它的成员被导入到了prelude之中,不需要在match分支中Ok和Err再进行头文件的引用。
  • 此时,如果成功,可以使用f进行读写,如果错误,调用panic!,也可很清晰的看出有那些错误。
thread 'main' panicked at 'Problem opening the file: Error { repr:
Os { code: 2, message: "No such file or directory" } }', src/main.rs:9:12

匹配不同的错误

  • 此时有Errkind方法可以调用。
  • 目标是实现不同错误下的不同panic!。
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::creat("hello.txt"){
			Ok(fc) => fc,
			Err(e) => panic!("Problem creating the file {:?}",e),
			},
			other_error => panic!("Problem opening the file: {:?}",other_error),
	},
};
}
  • 这段代码使用了多个match语句,具体来说是以下几个:
    首先寻找f文件,是否能够找到:
    如果找不到,使用kind类型判断,这时使用ErrorKind::NotFound判断是否为找不到文件的错误,如果是,创建文件,如果无法创建,则输出panic!,其次是其他错误。

失败时panic的简写: unwrap和expect

  • unwrapexpect是Result中定义的辅助方法,用于返回其中的结果,
  • unwrap再Result结果为Ok时,返回Ok中的值,如果成员结果为Err,会返回panic!
use std::fs::File;

fn main()
{
	let f = File::open("hello.txt").unwrap();
}
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Error {
repr: Os { code: 2, message: "No such file or directory" } }',
src/libcore/result.rs:906:4

  • 另外一种方法是采用expect,这种方法调用panic!宏,expect有助于更好的判断代码错误来自何处,因为unwrap的话术差不多一致,你需要自己去找到他的位置。
use std::fs:;File;

fn main()
{
	let f = File::open("hello.txt").expect("Failed to open the hello.txt");
}
thread 'main' panicked at 'Failed to open hello.txt: Error { repr: Os { code:
2, message: "No such file or directory" } }', src/libcore/result.rs:906:4

传播错误

  • 在出现错误的时候同时可以选择让调用者决定如何处理错误。这种机制叫做传播错误。Rust中通过这样的方法,将错误扼杀在摇篮中,这是它安全性的一大表现。
    下文是一个例子
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),
	}
}
  • 下面是官方文档的描述
    在这里插入图片描述

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

  • 使用?运算符来实现Resultmatch表达式
use std::io;
use std::io::Read;
use std::fs::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)
}
  • ?运算符的作用是在函数中将result中的值返回,如果是Ok,继续执行,如果是Err,就将Err作为整个函数的返回值。
  • Form trait是定义在标准库之下的函数,用来将错误从一种类型转换为另一种类型,此处的?就使用的这种方法,这种方法在函数返回单个错误类型来代表所有可能失败的方式时很有用,即便它可能由多种原因导致错误。只要每一个错误类型都实现了 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)
}
}

  • 先定义s,再直接一步到位
  • 同样,还可以简化,使用fs::read_to_string函数,打开文件,新建String,读取文件内容,并存入String,接着返回值。但是这样做就不会有机会展示错误处理的机会。
fn main() {
use std::io;
use std::fs;

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

?运算符可被用于返回Result的函数

  • matchreturn Err(e)部分要求返回值类型是Result,所以函数返回值必须是Result才能相兼容。
  • 这就是说在main函数中和在其他函数中不同,在main函数中返回值为(),而Result必须要和result相兼容才可以。
  • 这就需要将函数返回值类型修改为Result<T,E>,或者使用其他类似match或者Resut方法进行处理。
use std::fs::File;

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

error[E0277]: the `?` operator can only be used in a function that returns
`Result` or `Option` (or another type that implements `std::ops::Try`)
 --> src/main.rs:4:13
  |
4 |     let f = File::open("hello.txt")?;
  |             ^^^^^^^^^^^^^^^^^^^^^^^^ cannot use the `?` operator in a
  function that returns `()`
  |
  = help: the trait `std::ops::Try` is not implemented for `()`
  = note: required by `std::ops::Try::from_error`
  • main函数的返回值是有限制的,它的返回值有一个是(),出于方便,另一个为Result<T,E>.
use std::error::Error;
use std::fs::File;

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

    Ok(())
}
  • Box<dyn Error> 被称为 “trait 对象”(trait object), “为使用不同类型的值而设计的 trait 对象” 部分会做介绍。目前可以理解 Box 为使用 ? 时 main 允许返回的 “任何类型的错误”。

使用panic!还是不使用panic!

  • 在代码进行panic之后,就不能够再进行恢复了。选择Result会将选择权交给调用者,而不是由Rust不信任用户。
  • 所以说Result是一个比较好的默认选择,因为不仅可以使用panic,也可以使用Ok,Err等方式

在示例、代码原型、测试中非常适合panic

  • 使用unwrap()和expect()两种方法在代码原型中设计比较方便,根据自己的需求选择合理的方法。

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

  • 这个时候使用unwrap较为合理。
fn main() {
use std::net::IpAddr;

let home: IpAddr = "127.0.0.1".parse().unwrap();
}

  • 这里可以知道的是parse()返回值类型是一种Result类型,parse是转换格式类型,这个在之前的章节有提到过。链接: link
  • 但是在这种方式下仍然有可能Err成员此时仍然有失败的可能性,这就需要用更加健壮的方式处理Result

错误处理指导原则

  • 这里由于太多,直接使用官方文档

在这里插入图片描述

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

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

  • 使用这种方法进行函数检查比较冗余,可以采用新的方法,构建一个impl程序块。
fn main() {
pub struct Guess {
    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
        }
    }

    pub fn value(&self) -> i32 {
        self.value
    }
}
}

  • 对代码进行修改,接着使用new创建新的Guess实例,这里要注意的是实例和函数是不同的。
  • 这里创建的Guess::new遵守的契约是不超过value的值。
  • 接着创建一个Guess中的value值,value值被设置为value,接着返回Guess。
  • 接着使用了借用self方法的value,返回一个i32,这类方法有时候称作getter,目的是返回对应字段的数据。
  • 因为Guess中的value字段是私有字段,使用Guess中的Guess::new创建一个新的Guess实例,确保不会存在一个value没有通过Guess::new函数条件检查的Guess
  • 这样就构建了一个Guess示例,而不是一个i32类型,同时无需进行任何额外检查。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值