Master_Rust(译):错误处理(第五章)

在本章中,我们将了解Rust中如何处理意外情况。 Rust的错误处理基于泛型类型,例如Option和Result,我们在前一章中看到过。 还有一种称为恐慌的机制,它类似于异常,但与其他语言中的异常不同,恐慌并不用于可恢复的错误条件。

本章涉及的主题包括:

  • Option 和Result类型
  • 与Option 和Result类型匹配
  • 辅助方法处理错误
  • try!宏
  • ?操作符
  • 恐慌
  • 自定义错误和错误特征

Option 和Result类型

  • 使运行无错误的代码非常便宜,但错误情况非常昂贵
  • 使错误案例非常便宜,但非错误案例很昂贵
    这些都不适用于Rust的零运行时成本的核心理念。

其次,异常样式的错误处理(通常是实现的)允许通过catch-all异常处理程序忽略错误。 这会造成致命的运行时错误,这违背了Rust的安全原则。

我们将在实践中看到这一切是如何运作的。 这是我们已经看过几次的结果类型:

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

结果包含两种类型,T和E.T是我们用于成功案例的类型,E是错误案例。 我们将尝试打开一个文件,将其内容读入一个String,然后打印这些内容。 让我们看看当我们天真地行动时会发生什么,好像我们可以忽略所有错误情况:

// result-1.rs
use std::io::Read;
use std::path::Path;
use std::fs::File;
fn main() {
let path = Path::new("data.txt");
let file = File::open(&path);
let mut s = String::new();
file.read_to_string(&mut s);
println!("Read the string: {}", s);
}

这是编译器响应的方式:

在这里插入图片描述

请参阅错误中的结果类型:忽略完整的命名空间,变量文件的类型实际上是Result <File,Error>。 我们需要该文件类型才能读取文件的内容。 另外,看一下这种方法的官方文档可能会引起一些混乱。 以下是它的记录方式:

fn open<P: AsRef<Path>>(path: P) -> Result<File>

结果看起来它缺少第二个泛型类型,但它只是被类型别名隐藏了。 这不是我们之前看到的Result类型,而是特定于IO操作的Result类型。 它在std :: io :: Result模块中定义:

type Result<T> = Result<T, std::io::Error>;

原因很简单:每个IO操作都使用相同的错误类型,因此这种类型的别名可以避免开发人员在任何地方重复它。

但回到我们正在做的事情,我们可以使用match语句从Result类型中获取File:

let mut file = match File::open(&path) {
Ok(file) => file,
Err(err) => {
println!("Error while opening file: {}", err);
panic!();
}
};

所以,我们做了两处改动。 首先,我们使文件变量可变。 为什么? 因为read_to_string的函数签名如下:

fn read_to_string(&mut self, buf: &mut String) -> Result<usize>

&mut self意味着我们调用此方法的变量需要是可变的,因为读取文件会更改文件句柄的内部指针。

其次,如果一切正常,我们通过返回实际的文件句柄来处理Ok case和Err case,并显示错误并且如果没有则挽救。

通过此更改,程序将编译并运行它:
在这里插入图片描述

恐慌总是有点难看但适用于你永远不会发生的事情。 让我们对这个警告做一些事情:警告始终是代码质量差的标志,我们也没有。 警告就在那里,因为File :: read_to_string(它是Read trait实现的一部分)返回一个类型为Result 的值。 该值表示读入String的字节数。

我们有两种处理此警告的方法:

  • 像以前一样处理Ok和Err案例
  • 将它分配给一个特殊的变量,基本上告诉编译器我们不关心返回值
    既然我们已经完成了第一个,并且因为它无论如何都更适合这个目的,让我们做第二个。
    read_to_string行变为如下:
let _ = file.read_to_string(&mut s);

通过该更改,代码可以在没有警告的情况下进行编译。

展开

快速反复编写相同的匹配语句会成为样板代码,这会使读取更加糟糕。 标准库包含一些Result和Option类型实现的辅助方法。 您可以使用它们来简化您确实不希望事情失败的错误情况。
方法如下:

  • unwrap(self):T期望self为Ok / Some并返回其中包含的值。 如果它是Err或None,它会引起显示错误内容的恐慌。
  • expect(self,msg:&str):T的行为类似于展开,除了它在恐慌之前输出一个自定义消息以及错误的内容。
  • unwrap_or(self,opt_b:T):T表现得像展开,但不是恐慌,而是返回opt_b。
  • unwrap_or_else(self,op:F):T表现得像
  • 展开,除了不是恐慌,它调用op,它需要是一个函数或一个闭包:更准确地说,是任何实现FnOnce特性的东西。
    以下是使用转换为使用展开的匹配语句的上一个代码示例:
// result-unwrapping.rs
use std::io::Read;
use std::path::Path;
use std::fs::File;
fn main() {
let path = Path::new("data.txt");
let mut file = File::open(&path)
.expect("Error while opening data.txt");
let mut s = String::new();
file.read_to_string(&mut s)
.expect("Error while reading file contents");
println!("Read the string: {}", s);
}

如您所见,错误处理代码变得非常好。 当然,只有当错误非常严重以至于恐慌是一个不错的选择时,才应该使用此方法。

Option/Result值的映射

map和map_err方法提供了一种简单地将映射函数应用于Ok / Some和Err值内的值内容的方法。 由于使用None值执行任何操作都没有意义,因此没有为Option定义map_err。 以下是这些方法的完整类型:

map<U, F>(self, f: F) -> Result<U, E>
where F: FnOnce(T) -> U
map<U, F>(self, f: F) -> Option<U>
where F: FnOnce(T) -> U
map_err<F, O>(self, f: O) -> Result<T, F>
where O: FnOnce(E) -> F

仔细阅读这些类型,我们看到Result和Option类型的map函数都采用了一个函数,它将类型T的值转换为U类型的值,即FnOnce声明。 返回类型告诉我们类型U的新值包含在返回的Result或Option中。 在Result的情况下,错误类型保持不变。 对于map_err方法,即反之亦然,当然,Ok类型保持不变,并且错误类型通过f函数映射。

那么,这些方法在哪里适合? 一个显而易见的地方是您自己的库方法,您可以在其中对Ok / Some值进行一些修改,但将任何可能的Err或None值向上传播给调用者。 一个例子应该使这更清楚。 我们将编写两个带字节串的函数,尝试将其转换为String,然后将字符串转换为大写。 您可能还记得,转换可能会失败:

// mapping.rs
use std::string::FromUtf8Error;
fn bytestring_to_string_with_match(str: Vec<u8>) -> Result<String, FromUtf8Error> {
	match String::from_utf8(str) {
		Ok(str) => Ok(str.to_uppercase()),
		Err(err) => Err(err)
	}
}
 fn bytestring_to_string(str: Vec<u8>) -> Result<String, FromUtf8Error> {
	String::from_utf8(str).map(|s| s.to_uppercase())
} 

fn main() {
	let faulty_bytestring = vec!(130, 131, 132, 133);
	let ok_bytestring = vec!(80, 82, 84, 85, 86);
	let s1_faulty = bytestring_to_string_with_match(faulty_bytestring.clone());
	let s1_ok = bytestring_to_string_with_match(ok_bytestring.clone());
	let s2_faulty = bytestring_to_string(faulty_bytestring.clone());
	let s2_ok = bytestring_to_string(ok_bytestring.clone());
	println!("Read the string: {:?}", s1_faulty);
	println!("Read the string: {:?}", s1_ok);
	println!("Read the string: {:?}", s2_faulty);
	println!("Read the string: {:?}", s2_ok);
}

这两个功能在功能上是相同的,因此它们的输出也是相同的。 这是输出:
在这里插入图片描述

提前returns和try! 宏

这是错误处理的另一种模式:在任何操作中发生错误时从函数返回。 我们将修改将bytestring to String转换为此模式的早期代码:

fn bytestring_to_string_with_match(str: Vec<u8>) -> Result<String, FromUtf8Error> {
let ret = match String::from_utf8(str) {
Ok(str) => str.to_uppercase(),
Err(err) => return Err(err)
};
println!("Conversion succeeded: {}", ret);
Ok(ret)
}

try!宏 macro抽象了这种模式,使得以更简洁的方式编写它成为可能:

fn bytestring_to_string_with_try(str: Vec<u8>) -> Result<String, FromUtf8Error> {
let ret = try!(String::from_utf8(str));
println!("Conversion succeeded: {}", ret);
Ok(ret)
}

try! 宏有一个警告可能是不明显的,如果你忘记它扩展到早期返回因为它可能返回一个Result或Option类型,它根本不能在main函数中使用。 这是因为main函数的签名就是这样:

fn main()

它不接受任何参数并且不返回任何参数,因此它不能返回Option或Result类型。 这是一个简单的程序,可以准确地向您展示编译器会想到的内容:

// try-main.rs
fn main() {
let empty_ok_value = Ok(());
try!(empty_ok_value);
}

尝试构建时,编译器输出以下错误:

在这里插入图片描述

来自宏的编译错误总是有点难以阅读,因为它们必须源于生成的代码,而这不是你,编码器所写的。

?号操作符

一个较短的写作方式try! 宏可用?操作 符。 这样,前面的功能可以变得更加整洁:

// try.rs
fn bytestring_to_string_with_qmark(str: Vec<u8>) -> Result<String, FromUtf8Error> {
	let ret = String::from_utf8(str)?;
	println!("Conversion succeeded: {}", ret);
	Ok(ret)
}

如果您有多个操作的组合语句,则此运算符会变得更好,其中每个运算符的失败应该意味着整体失败。 例如,我们可以通过打开文件,读取文件并将其转换为大写为单行来合并整个文件:

File::create("foo.txt")?.write_all(b"Hello world!")

该运营商在1.13版本中进入了稳定版本。 在目前的形式,它几乎可以作为尝试的替代品! 宏,但有一些计划,使其更通用,也可用于其他情况。 如果您对进度感兴趣,可以在 https://gith
ub.com/rust-lang/rfcs/issues/1718

恐慌(Panicking)

即使Rust没有为一般错误处理用法设计的异常机制,恐慌也离它不远。 恐慌是不可恢复的错误,会导致线程崩溃。 如果当前线程是主线程,则整个程序崩溃。

在更技术层面上,恐慌发生在与异常相同的过程中:展开调用堆栈。 这意味着在发生恐慌的代码中爬出并离开该位置直到击中顶部,此时,所讨论的线程中止。 这是一个我们有两个调用堆栈的例子:

  • f1生成一个新线程并调用f2,它调用f3,这会引起恐慌
  • main调用f2,调用f3,恐慌
    看一下下面的代码片段:
// panic.rs
use std::thread;
fn f1() -> thread::JoinHandle<()> {
thread::spawn(move || {
f2();
})
} f
n f2() {
f3();
} f
n f3() {
panic!("Panicking in f3!");
} f
n main() {
let child = f1();
child.join().ok();
f2();
println!("This is unreachable code");
}

以下是它在运行时的外观:

在这里插入图片描述

在这里,你可以看到即使孩子的线程恐慌,主线程也陷入了恐慌。 尽管通常建议通过它正确处理错误
选项/结果机制,您可以使用此方法来处理工作线程中的致命错误; 让工人死亡,重新启动它们。

如果您需要更多地控制恐慌的处理方式,可以使用std :: panic :: catch_unwind函数随时停止展开。 如前所述,panic / catch_unwind不是Rust程序的推荐常规错误处理方法,使用Option / Result返回值为。 catch_unwind函数接受一个闭包并处理其中发生的任何恐慌。 这是它的类型签名:

fn catch_unwind<F: FnOnce() -> R + UnwindSafe, R>(f: F) -> Result<R>

如您所见,catch_unwind的返回值还有一个附加约束UnwindSafe。 这意味着闭包中的变量是异常安全的; 大多数类型都是,但值得注意的例外是可变引用(&mut T)。 在未来的章节中,将会有更多关于这些及其限制的内容。

这是catch_unwind的一个例子:

// catch-unwind.rs
use std::panic;
fn main() {
panic::catch_unwind(|| {
panic!("Panicking!");
}).ok();
println!("Survived that panic.");
}

以下是它的运行方式:

在这里插入图片描述

正如您所看到的,catch_unwind并不能防止恐慌发生; 它只是停止展开,因此不会停止线程。 请再次注意,catch_unwind不是Rust中推荐的错误管理方式。 不能保证能够抓住所有的恐慌。 在某些情况下可能会发生双重恐慌,在这种情况下,整个程序将中止。 此外,还有一个编译器选项可以将所有恐慌变为中止,并且此方法无法捕获中止。

自定义错误和错误特征

通常,我们希望将我们的程序可能产生的错误与其他错误分开。 这通常通过创建一些基于异常的类的新子类来完成,并且可能覆盖一些父方法。

Rust的方法类似,但由于我们没有真正的类或对象,我们使用特征和实现。 这是标准库中的错误特征:

pub trait Error: Debug + Display + Reflect {
fn description(&self) -> &str;
fn cause(&self) -> Option<&Error> { None }
}

因此,我们即将编写的新错误类型需要这两种方法。 description方法返回一个字符串切片引用,它以自由形式告诉错误是什么。 cause方法返回另一个Error trait的可选引用,表示错误的可能的低级原因。 因此,最高级别的错误特征可以访问最低级别,从而可以精确记录错误
可能。

我们以HTTP查询作为原因链的一个例子。 我们调用get来执行实际查询的库。 由于许多不同的原因,查询可能会失败:

  • 由于网络故障或地址错误,DNS查询可能会失败
  • 实际的数据传输可能会失败
  • 可能会正确接收数据,但收到的数据可能有问题
  • HTTP标头,依此类推
    如果是第一种情况,我们可能会想到三个级别的错误,由原因字段链接在一起:
  • 由于网络关闭导致UDP连接失败(原因=无)
  • 由于UDP连接失败导致DNS查找失败(cause = UDPError)
  • 由于DNS查找失败,get查询失败(cause = DNSError)
    除了需要这两种方法之外,Error trait还取决于Debug和Display特性(在这种情况下可以忽略Reflect),这意味着任何新的错误类型也需要实现(或派生)这三种。

让我们通过简单地将货币和金额配对来以更加随意的方式模拟货币。 货币将是美元或欧元的枚举,我们将有新的方法将任意字符串转换为货币或货币。 这里的潜在错误是在转换阶段。 在添加自定义错误类型之前,让我们首先看一下样板,结构和虚拟实现:

// custom-error-1.rs
#[derive(Debug)]enum Currency { USD, EUR }
#[derive(Debug)]
struct CurrencyError;
impl Currency {
fn new(currency: &str) -> Result<Self, CurrencyError> {
match currency {
"USD" => Ok(Currency::USD),
"EUR" => Ok(Currency::EUR),
_ => Err(CurrencyError{})
}
}
} #
[derive(Debug)]
struct Money {
currency: Currency,
amount: u64
} #
[derive(Debug)]
struct MoneyError;
impl Money {
fn new(currency: &str, amount: u64) -> Result<Self, MoneyError> {
let currency = match Currency::new(currency) {
Ok(c) => c,
Err(_) => panic!("Unimplemented!")
};
Ok(Money {
currency: currency,
amount: amount
})
}
} f
n main() {
let money_1 = Money::new("EUR", 12345);
let money_2 = Money::new("FIM", 600000);
println!("Money_1 is {:?}", money_1);
println!("Money_2 is {:?}", money_2);
}

你会看到我们已经有了伪结构错误结构,但它们还没有实现错误特征。 让我们看看它是如何完成的。 除此之外,我们还需要一些额外的使用声明:

// custom-error-2.rs
use std::error::Error;
use std::fmt;
use std::fmt::Display;

现在,我们可以向CurrencyError添加一个描述字段,并为它实现Display和Error:

// custom-error-2.rs
#[derive(Debug)]
struct CurrencyError {
description: String
} i
mpl Display for CurrencyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "CurrencyError: {}", self.description)
}
} i
mpl Error for CurrencyError {fn description(&self) -> &str {
"CurrencyError"
}
}

由于CurrencyError没有任何低杠杆原因导致错误,因此返回None的默认实现很好。 描述方法也不是非常有趣,但错误的细节由Display实现给出。 我们只需要更改Currency :: new方法的最后一个模式匹配来支持:

_ => Err(CurrencyError{ description: format!("{} not a valid currency", currency)})

接下来,我们将增加MoneyError:

// custom-error-2.rs
#[derive(Debug)]
struct MoneyError {
cause: CurrencyError
} i
mpl Display for MoneyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "MoneyError due to {}", self.cause)
}
} i
mpl Error for MoneyError {
fn description(&self) -> &str {
"MoneyError"
}f
n cause(&self) -> Option<&Error> {
Some(&self.cause)
}
}

我们现在在MoneyError的cause字段中保存较低级别的错误。 由于在这种情况下唯一已知的低级错误是CurrencyError,因此它是具体类型。 如果有更多选项,这可能是一个枚举,包含所有不同的可能错误。 用于在错误情况下发生恐慌的Money类的新方法现在可以变为如下:

Err(e) => return Err(MoneyError { cause: e })

我们在那里! 现在我们可以通过在main函数中显示MoneyError来查看导致错误的原因:

let cause_for_money_2 = money_2.unwrap_err();
println!("{}", cause_for_money_2);

现在运行程序会在调试格式化版本和我们刚刚添加的代码中显示错误,这些错误通过Display trait输出:

在这里插入图片描述

这个旅程的道德:Rust有一个很好的框架来定义你的自定义错误类型。 特别是如果您正在编写自己的库,则应定义自己的错误类型以使调试更容易。

Exercise

让我们回到游戏项目。 我们将为实体添加操作以在地图上移动,可能会发生各种预期和意外的错误处理。 以下是我们在第3章单元测试和基准测试中得出的数据结构:

#[derive(PartialEq, Debug)]
enum TerrainGround {
Soil,
Stone
} #
[derive(PartialEq, Debug)]
enum TerrainBlock {
Tree,
Soil,
Stone
} #
[derive(PartialEq, Debug)]
enum Being {
Orc,
Human
} s
truct Square {
ground: TerrainGround,
block: Option<TerrainBlock>,
beings: Option<Being>
} s
truct Grid {
size: (usize, usize),
squares: Vec<Square>
}

因此,我们希望能够在以下情况下将Grid移动到Grid上的任何方向是错误:

  • 广场上没有存在
  • 试图从网格的边缘脱落
  • 试图进入已经存在的广场
  • 试图搬到地形,这是石头
    这是第一个作为示例,您可以填写其余部分。 我们将为Grid结构实现move_being方法,因为这是唯一具有此操作所需信息的方法。 上述结构被省略:
enum Direction {
West,
East,
North,
South
} #
[derive(Debug, PartialEq)]
enum MovementError {
NoBeingInSquare
}

impl Grid {
fn move_being_in_coord(&self, coord: (usize, usize), dir: Direction) -> Result<(usize, usize), MovementError
let square = self.squares.get(coord.0 * self.size.0 + coord.1).expect("Index out of bounds trying to get
match square.being {
Some(_) => Ok((0,0)), // XXX: fill in the implementations here
None => Err(MovementError::NoBeingInSquare)
}
} f
n generate_empty(size_x: usize, size_y: usize) -> Grid {
let number_of_squares = size_x * size_y;
let mut squares: Vec<Square> = Vec::with_capacity(number_of_squares);
for _ in 0..number_of_squares {
squares.push(Square{ground: TerrainGround::Soil, block: None, being: None});
} G
rid {
size: (size_x, size_y),
squares: squares
}
}
}

#
[cfg(test)]
mod tests {
#[test]
fn test_empty_grid() {
let grid = ::Grid::generate_empty(5, 13);
assert_eq!(grid.size, (5, 13));
let mut number_of_squares = 0;
for square in &grid.squares {
assert_eq!(square.ground, ::TerrainGround::Soil);
assert_eq!(square.block, None);
assert_eq!(square.being, None);
number_of_squares += 1;
} a
ssert_eq!(grid.squares.len(), 5*13);
assert_eq!(number_of_squares, 5*13);
} #
[test]
fn test_move_being_without_being_in_square() {
let grid = ::Grid::generate_empty(3, 3);
assert_eq!(grid.move_being_in_coord((0, 0), ::Direction::West), Err(::MovementError::NoBeingInSquare));
}
}

我们走了。 现在你的部分:

1.实现错误情况,其中Being试图从Grid的边缘脱落。

2.实现错误情况,其中Being试图进入已存在存在的Square。

3.实现错误情况,其中Being试图移动到Terrain,这是Stone。

4.实现无错误发生且成功移动到新Square的快乐案例。

5.使MovementError实现Error trait。

摘要

以下是本章应该记住的内容:

Rust中的错误处理是显式的:可能失败的操作通过Result或Option泛型类型具有两部分返回值

您必须以某种方式处理错误,方法是通过匹配语句解析Result / Option值,或者使用辅助方法或宏

您通常应该选择正确处理错误,即不要诉诸恐慌失败的操作

当错误编程错误或致命错误导致无法进行恢复时,请使用展开方法或恐慌

恐慌大多是不可恢复的,这意味着它们会使你的线程崩溃

对您自己的错误类型使用标准错误特征

下一章将介绍Rust有些独特的内存处理,包括引用,借用和生命周期。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值