From d73c4fd4e17e4c6d7a1a53df1766ce11242794a8 Mon Sep 17 00:00:00 2001 From: Liang Liu Date: Tue, 8 Feb 2022 22:57:52 +1100 Subject: [PATCH 01/14] Fixed typo --- book/contents/advance/crate-module/use.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book/contents/advance/crate-module/use.md b/book/contents/advance/crate-module/use.md index a63c4aea..5416dda4 100644 --- a/book/contents/advance/crate-module/use.md +++ b/book/contents/advance/crate-module/use.md @@ -185,7 +185,7 @@ use std::io::{self, Write}; - `use self::xxx`,表示加载当前模块中的 `xxx`。此时 `self` 可省略 - `use xxx::{self, yyy}`,表示,加载当前路径下模块 `xxx` 本身,以及模块 `xxx` 下的 `yyy` -## 使用*引入模块下的所有项* +## 使用`*`引入模块下的所有项 对于之前一行一行引入 `std::collections` 的方式,我们还可以使用 ```rust use std::collections::*; From 21304427ad0c4b8d42d26da2fbae22916b5c6ebb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=99=E5=B0=8F=E7=A3=8A?= Date: Tue, 8 Feb 2022 21:06:35 +0800 Subject: [PATCH 02/14] =?UTF-8?q?=E5=A6=82=E6=9E=9C=E9=9C=80=E8=A6=81?= =?UTF-8?q?=E6=BB=A1=E8=B6=B3=E8=BF=90=E8=A1=8C=E8=BE=93=E5=87=BA=E7=9A=84?= =?UTF-8?q?=E7=BB=93=E6=9E=9C=EF=BC=8C=E5=88=99=E9=9C=80=E8=A6=81=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=20println!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- book/contents/basic/trait/trait.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/book/contents/basic/trait/trait.md b/book/contents/basic/trait/trait.md index d27bb98b..8251c505 100644 --- a/book/contents/basic/trait/trait.md +++ b/book/contents/basic/trait/trait.md @@ -69,8 +69,8 @@ fn main() { let post = Post{title: "Rust语言简介".to_string(),author: "Sunface".to_string(), content: "Rust棒极了!".to_string()}; let weibo = Weibo{username: "sunface".to_string(),content: "好像微博没Tweet好用".to_string()}; - post.summarize(); - weibo.summarize(); + println!("{}",post.summarize()); + println!("{}",weibo.summarize()); } ``` From ae4b50c05922eaa90490aed66b525c4af169ff00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=99=E5=B0=8F=E7=A3=8A?= Date: Tue, 8 Feb 2022 22:00:35 +0800 Subject: [PATCH 03/14] =?UTF-8?q?=20=E8=BF=99=E9=87=8C=E6=98=AF=E5=90=A6?= =?UTF-8?q?=E5=BA=94=E8=AF=A5=E6=98=AF=20pair=20=E5=91=A2=EF=BC=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- book/contents/basic/trait/trait.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book/contents/basic/trait/trait.md b/book/contents/basic/trait/trait.md index d27bb98b..d4fb3d52 100644 --- a/book/contents/basic/trait/trait.md +++ b/book/contents/basic/trait/trait.md @@ -237,7 +237,7 @@ impl Pair { } ``` -`cmd_display` 方法,并不是所有的 `Pair` 结构体对象都可以拥有,只有 `T` 同时实现了 `Display + PartialOrd` 的 `Part` 才可以拥有此方法。 +`cmd_display` 方法,并不是所有的 `Pair` 结构体对象都可以拥有,只有 `T` 同时实现了 `Display + PartialOrd` 的 `Pair` 才可以拥有此方法。 该函数可读性会更好,因为泛型参数、参数、返回值都在一起,可以快速的阅读,同时每个泛型参数的特征也在新的代码行中通过**特征约束**进行了约束。 **也可以有条件的实现特征**, 例如,标准库为任何实现了 `Display` 特征的类型实现了 `ToString` 特征: From 4d02423b57ff11f6502f49bd26d27722f6195e4d Mon Sep 17 00:00:00 2001 From: zongzi531 Date: Wed, 9 Feb 2022 12:49:43 +0800 Subject: [PATCH 04/14] Fix typo in ownership/ownership.md --- book/contents/basic/ownership/ownership.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book/contents/basic/ownership/ownership.md b/book/contents/basic/ownership/ownership.md index dc338043..f3950052 100644 --- a/book/contents/basic/ownership/ownership.md +++ b/book/contents/basic/ownership/ownership.md @@ -157,7 +157,7 @@ let s2 = s1; 好吧,就假定一个值可以拥有两个所有者,会发生什么呢? -当变量离开作用域后,Rust 会自动调用 `drop` 函数并清理变量的堆内存。不过由于两个 `String` 变量指向了同一位置。这就有了一个问题:当 `s1` 和 `s2` 离开作用域,它们都会尝试释放相同的内存。这是一个叫做**二次释放(double free)**的错误,也是之前提到过的内存安全性 BUG 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。 +当变量离开作用域后,Rust 会自动调用 `drop` 函数并清理变量的堆内存。不过由于两个 `String` 变量指向了同一位置。这就有了一个问题:当 `s1` 和 `s2` 离开作用域,它们都会尝试释放相同的内存。这是一个叫做 **二次释放(double free)** 的错误,也是之前提到过的内存安全性 BUG 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。 因此,Rust 这样解决问题:**当 `s1` 赋予 `s2` 后,Rust 认为 `s1` 不再有效,因此也无需在 `s1` 离开作用域后 `drop` 任何东西,这就是把所有权从 `s1` 转移给了 `s2`,`s1` 在被赋予 `s2` 后就马上失效了**。 From fe901e5be838b5a5ebcc57100921b4f877cc793d Mon Sep 17 00:00:00 2001 From: zongzi531 Date: Wed, 9 Feb 2022 16:03:26 +0800 Subject: [PATCH 05/14] Fix typo in compound-type/string-slice.md --- book/contents/basic/compound-type/string-slice.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book/contents/basic/compound-type/string-slice.md b/book/contents/basic/compound-type/string-slice.md index fffe1d2e..49c935d2 100644 --- a/book/contents/basic/compound-type/string-slice.md +++ b/book/contents/basic/compound-type/string-slice.md @@ -87,7 +87,7 @@ let slice = &s[..]; > let a = &s[0..2]; > println!("{}",a); >``` ->因为我们只取 `s` 字符串的前两个字节,但是一个中文占用三个字节,因此没有落在边界处,也就是连 `中` 字都取不完整,此时程序会直接崩溃退出,如果改成 `&a[0..3]`,则可以正常通过编译。 +>因为我们只取 `s` 字符串的前两个字节,但是一个中文占用三个字节,因此没有落在边界处,也就是连 `中` 字都取不完整,此时程序会直接崩溃退出,如果改成 `&s[0..3]`,则可以正常通过编译。 > 因此,当你需要对字符串做切片索引操作时,需要格外小心这一点, 关于该如何操作 UTF8 字符串,参见[这里](#操作UTF8字符串) 字符串切片的类型标识是 `&str`,因此我们可以这样声明一个函数,输入 `String` 类型,返回它的切片: `fn first_word(s: &String) -> &str `。 From 620da64df24e0683c4f100ace5734511a98a86c1 Mon Sep 17 00:00:00 2001 From: PengJunhuan Date: Wed, 9 Feb 2022 23:55:12 +0800 Subject: [PATCH 06/14] Update method.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修改错误拼写 --- book/contents/basic/method.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book/contents/basic/method.md b/book/contents/basic/method.md index 8f89d1fd..df385cdc 100644 --- a/book/contents/basic/method.md +++ b/book/contents/basic/method.md @@ -104,7 +104,7 @@ fn main() { } ``` -当我们使用 `rect1.width()` 时, Rust 知道我们调用的是它的方法,如果使用 `rect1.witdh`,则是访问它的字段。 +当我们使用 `rect1.width()` 时, Rust 知道我们调用的是它的方法,如果使用 `rect1.width`,则是访问它的字段。 一般来说,方法跟字段同名,往往适用于实现 `getter` 访问器,例如: ```rust From 2f802ab94029c07e024f0ff5575e1f1523c11a18 Mon Sep 17 00:00:00 2001 From: AllenYu Date: Thu, 10 Feb 2022 09:09:14 +0800 Subject: [PATCH 07/14] delete redundant space --- book/contents/basic/trait/trait-object.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book/contents/basic/trait/trait-object.md b/book/contents/basic/trait/trait-object.md index 51caa131..5037db10 100644 --- a/book/contents/basic/trait/trait-object.md +++ b/book/contents/basic/trait/trait-object.md @@ -265,7 +265,7 @@ help: function arguments must have a statically known size, borrowed types alway 在 Rust 中,有两个`self`,一个指代当前的实例对象,一个指代特征或者方法类型的别名: ```rust trait Draw { - fn draw(&self) -> Self; + fn draw(&self) -> Self; } #[derive(Clone)] From ed60697bd867d9cb5b52dc089f2d8c3c6d32b9f8 Mon Sep 17 00:00:00 2001 From: sunface Date: Thu, 10 Feb 2022 09:33:54 +0800 Subject: [PATCH 08/14] add try!, update errors --- book/contents/SUMMARY.md | 6 +---- book/contents/advance/errors.md | 1 + book/contents/advance/errors/intro.md | 1 - book/contents/advance/errors/panic-codes.md | 11 -------- book/contents/advance/errors/pretty-format.md | 1 - book/contents/advance/errors/simplify.md | 1 - book/contents/advance/errors/user-define.md | 1 - book/contents/basic/result-error/result.md | 26 ++++++++++++++++++- 8 files changed, 27 insertions(+), 21 deletions(-) create mode 100644 book/contents/advance/errors.md delete mode 100644 book/contents/advance/errors/intro.md delete mode 100644 book/contents/advance/errors/panic-codes.md delete mode 100644 book/contents/advance/errors/pretty-format.md delete mode 100644 book/contents/advance/errors/simplify.md delete mode 100644 book/contents/advance/errors/user-define.md diff --git a/book/contents/SUMMARY.md b/book/contents/SUMMARY.md index fd5565a5..99d221cc 100644 --- a/book/contents/SUMMARY.md +++ b/book/contents/SUMMARY.md @@ -83,11 +83,7 @@ - [基于Send和Sync的线程安全](advance/concurrency-with-threads/send-sync.md) - [实践应用:多线程Web服务器 todo](advance/concurrency-with-threads/web-server.md) - [全局变量](advance/global-variable.md) - - [错误处理 doing](advance/errors/intro.md) - - [简化错误处理 todo](advance/errors/simplify.md) - - [自定义错误 todo](advance/errors/user-define.md) - - [让错误输出更优雅 todo](advance/errors/pretty-format.md) - - [会导致panic的代码 todo](advance/errors/panic-codes.md) + - [错误处理](advance/errors.md) ## 专题内容,每个专题都配套一个小型项目进行实践 diff --git a/book/contents/advance/errors.md b/book/contents/advance/errors.md new file mode 100644 index 00000000..eadde0d2 --- /dev/null +++ b/book/contents/advance/errors.md @@ -0,0 +1 @@ +# 错误处理 \ No newline at end of file diff --git a/book/contents/advance/errors/intro.md b/book/contents/advance/errors/intro.md deleted file mode 100644 index a27f18e2..00000000 --- a/book/contents/advance/errors/intro.md +++ /dev/null @@ -1 +0,0 @@ -# 错误处理 diff --git a/book/contents/advance/errors/panic-codes.md b/book/contents/advance/errors/panic-codes.md deleted file mode 100644 index 1e3e8c21..00000000 --- a/book/contents/advance/errors/panic-codes.md +++ /dev/null @@ -1,11 +0,0 @@ -# 会导致panic的代码 - - -String slice range indices must occur at valid UTF-8 character boundaries. If you attempt to create a string slice in the middle of a multibyte character, your program will exit with an error. For the purposes of introducing string slices, we are assuming ASCII only in this section; a more thorough discussion of UTF-8 handling is in the “Storing UTF-8 Encoded Text with Strings” section of Chapter 8. - - -> -> 比方说有一个 `u8` ,它可以存放从 0 到 255 的值。那么当你将其修改为范围之外的值,比如 256,则会发生**整型溢出**。关于这一行为 Rust 有一些有趣的规则。当在 debug 模式编译时,Rust 会检查整型溢出若存在这些问题则使程序在编译时 *panic*。Rust 使用这个术语来表明程序因错误而退出。 [该章节](../../errors/panic.md)会详细介绍 panic。 -> -> 在当使用 `--release` 参数进行 release 模式构建时,Rust **不**检测溢出。相反,当检测到整型溢出时,Rust 会按照补码循环溢出(*two’s complement wrapping*)的规则处理。简而言之,大于该类型最大值的数值会被补码转换成该类型能够支持的对应数字的最小值。比如在 `u8` 的情况下,256 变成 0,257 变成 1,依此类推。程序不会 panic,但是该变量的值可能不是你期望的值。依赖这种默认行为的代码都应该被认为是错误的代码。 -> \ No newline at end of file diff --git a/book/contents/advance/errors/pretty-format.md b/book/contents/advance/errors/pretty-format.md deleted file mode 100644 index 5020fa72..00000000 --- a/book/contents/advance/errors/pretty-format.md +++ /dev/null @@ -1 +0,0 @@ -# 让错误展示更优雅 diff --git a/book/contents/advance/errors/simplify.md b/book/contents/advance/errors/simplify.md deleted file mode 100644 index c0a95e2e..00000000 --- a/book/contents/advance/errors/simplify.md +++ /dev/null @@ -1 +0,0 @@ -# 简化错误处理 diff --git a/book/contents/advance/errors/user-define.md b/book/contents/advance/errors/user-define.md deleted file mode 100644 index b691927e..00000000 --- a/book/contents/advance/errors/user-define.md +++ /dev/null @@ -1 +0,0 @@ -# 自定义错误 diff --git a/book/contents/basic/result-error/result.md b/book/contents/basic/result-error/result.md index db23cb25..50215deb 100644 --- a/book/contents/basic/result-error/result.md +++ b/book/contents/basic/result-error/result.md @@ -290,7 +290,7 @@ fn first(arr: &[i32]) -> Option<&i32> { - `xxx()?.yyy()?;` #### 带返回值的main函数 -因为刚才讲的 `?` 使用限制,这段代码你很容易看出它无法编译: +在了解了 `?` 的使用限制后,这段代码你很容易看出它无法编译: ```rust use std::fs::File; @@ -316,6 +316,30 @@ fn main() -> Result<(), Box> { 至于 `main` 函数可以有多种返回值,那是因为实现了 [std::process::Termination](https://doc.rust-lang.org/std/process/trait.Termination.html) 特征,目前为止该特征还没进入稳定版 Rust 中,也许未来你可以为自己的类型实现该特征! +#### try! +在 `?` 横空出世之前( Rust 1.13 ),Rust 开发者还可以使用 `try!` 来处理错误,该宏的大致定义如下: +```rust +macro_rules! try { + ($e:expr) => (match $e { + Ok(val) => val, + Err(err) => return Err(::std::convert::From::from(err)), + }); +} +``` + +简单看一下与 `?` 的对比: +```rust +// `?` +let x = function_with_error()?; // 若返回 Err, 则立刻返回;若返回 Ok(255),则将 x 的值设置为 255 + +// `try!()` +let x = try!(function_with_error()); +``` + +可以看出 `?` 的优势非常明显,何况 `?` 还能做链式调用。 + +总之,`try!` 作为前浪已经死在了沙滩上,**在当前版本中,我们要尽量避免使用 try! ** + 至此,Rust 的基础内容学习已经全部完成,下面我们将学习 Rust 的高级进阶内容,正式开启你的高手之路。 From 966a98336cdbeafc51bcbae887f6b4f1d24517cf Mon Sep 17 00:00:00 2001 From: Sunface Date: Thu, 10 Feb 2022 09:42:21 +0800 Subject: [PATCH 09/14] Update book/contents/advance/crate-module/use.md --- book/contents/advance/crate-module/use.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book/contents/advance/crate-module/use.md b/book/contents/advance/crate-module/use.md index 5416dda4..05a2c713 100644 --- a/book/contents/advance/crate-module/use.md +++ b/book/contents/advance/crate-module/use.md @@ -185,7 +185,7 @@ use std::io::{self, Write}; - `use self::xxx`,表示加载当前模块中的 `xxx`。此时 `self` 可省略 - `use xxx::{self, yyy}`,表示,加载当前路径下模块 `xxx` 本身,以及模块 `xxx` 下的 `yyy` -## 使用`*`引入模块下的所有项 +## 使用 `*` 引入模块下的所有项 对于之前一行一行引入 `std::collections` 的方式,我们还可以使用 ```rust use std::collections::*; From 7f1e6f54e7d0686c3825a3b56b68bdbba7f66acf Mon Sep 17 00:00:00 2001 From: zongzi531 Date: Thu, 10 Feb 2022 12:56:19 +0800 Subject: [PATCH 10/14] Fix typo in compound-type/struct.md --- book/contents/basic/compound-type/struct.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book/contents/basic/compound-type/struct.md b/book/contents/basic/compound-type/struct.md index da500617..d501ffed 100644 --- a/book/contents/basic/compound-type/struct.md +++ b/book/contents/basic/compound-type/struct.md @@ -190,7 +190,7 @@ struct AlwaysEqual; let subject = AlwaysEqual; -// 我们不关心为 AlwaysEqual 的字段数据,只关心它的行为,因此将它声明为元结构体,然后再为它实现某个特征 +// 我们不关心 AlwaysEqual 的字段数据,只关心它的行为,因此将它声明为元结构体,然后再为它实现某个特征 impl SomeTrait for AlwaysEqual { } From a28374bf2b5402464a8ff8a43dc7b6963cfe1665 Mon Sep 17 00:00:00 2001 From: sunface Date: Thu, 10 Feb 2022 15:22:13 +0800 Subject: [PATCH 11/14] add image for dynamic dispatch --- book/contents/basic/trait/trait-object.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/book/contents/basic/trait/trait-object.md b/book/contents/basic/trait/trait-object.md index 5037db10..8ecfe6c7 100644 --- a/book/contents/basic/trait/trait-object.md +++ b/book/contents/basic/trait/trait-object.md @@ -261,6 +261,10 @@ help: function arguments must have a statically known size, borrowed types alway 当使用特征对象时,Rust 必须使用动态分发。编译器无法知晓所有可能用于特征对象代码的类型,所以它也不知道应该调用哪个类型的哪个方法实现。为此,Rust 在运行时使用特征对象中的指针来知晓需要调用哪个方法。动态分发也阻止编译器有选择的内联方法代码,这会相应的禁用一些优化。 +下面这张图很好的解释了静态分发 `Box` 和动态分发 `Box` 的区别: + + + ## Self与self 在 Rust 中,有两个`self`,一个指代当前的实例对象,一个指代特征或者方法类型的别名: ```rust From b27b5bd21a4035fad8a440a8e90b892b4f8d88cc Mon Sep 17 00:00:00 2001 From: sunface Date: Thu, 10 Feb 2022 15:22:45 +0800 Subject: [PATCH 12/14] add image for dynamic dispatch --- book/contents/img/trait-object-01.svg | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 book/contents/img/trait-object-01.svg diff --git a/book/contents/img/trait-object-01.svg b/book/contents/img/trait-object-01.svg new file mode 100644 index 00000000..9538ae1d --- /dev/null +++ b/book/contents/img/trait-object-01.svg @@ -0,0 +1,3 @@ + + +
ptr
ptr
Box<T>
Box<T>
T
T
ptr
ptr
Box<dyn Trait>
Box<dyn Trait>
T
T
vptr
vptr
destructor
destructor
size
size
align
align
methods...
methods...
ptr
ptr
size
size
fn
fn
allocation
allocation
T
T
pointer / usize
pointer / usize
fields
fields
function
function
heap allocation
heap allocation
user defined type
user defined type
https://doc.rust-lang.org/reference/type-layout.html
https://doc.rust-lang.org/reference/type-layout....
vtable
vtable
Viewer does not support full SVG 1.1
\ No newline at end of file From 99a85513777d7707507c4bbe5a5dd29b55e0d02a Mon Sep 17 00:00:00 2001 From: sunface Date: Thu, 10 Feb 2022 17:08:30 +0800 Subject: [PATCH 13/14] =?UTF-8?q?add=20=E8=BF=9B=E9=98=B6=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- book/contents/advance/errors.md | 642 +++++++++++++++++++++++++++++++- 1 file changed, 641 insertions(+), 1 deletion(-) diff --git a/book/contents/advance/errors.md b/book/contents/advance/errors.md index eadde0d2..3061f824 100644 --- a/book/contents/advance/errors.md +++ b/book/contents/advance/errors.md @@ -1 +1,641 @@ -# 错误处理 \ No newline at end of file +# 错误处理 +在之前的[返回值和错误章节](https://course.rs/basic/result-error/intro.html)中,我们学习了几个重要的概念,例如 `Result` 用于返回结果处理,`?` 用于错误的传播,若大家对此还较为模糊,强烈建议回头温习下。 + +在本章节中一起来看看如何对 `Result` ( `Option` ) 做进一步的处理,以及如何定义自己的错误类型。 + +## 组合器 +在设计模式中,有一个组合器模式,相信有 Java 背景的同学对此并不陌生。 + +> 将对象组合成树形结构以表示“部分整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。–GoF <<设计模式>> + +与组合起模式有所不同,在 Rust 中,组合器更多的是用于对返回结果的类型进行变换:例如使用 `ok_or` 将一个 `Option` 类型转换成 `Result` 类型。 + +下面我们来看看一些常见的组合器。 + +#### or() 和 and() +跟布尔关系的与/或很像,这两个方法会对两个表达式做逻辑组合,最终返回 `Option` / `Result`。 + +- `or()`,表达式按照顺序求值,若任何一个表达式的结果是 `Some` 或 `Ok`,则该值会立刻返回 +- `and()`,若两个表达式的结果都是 `Some` 或 `Ok`,则**第二个表达式中的值被返回**。若任何一个的结果是 `None` 或 `Err` ,则立刻返回。 + +实际上,只要将布尔表达式的 `true` / `false`,替换成 `Some` / `None` 或 `Ok` / `Err` 就很好理解了。 + +```rust +fn main() { + let s1 = Some("some1"); + let s2 = Some("some2"); + let n: Option<&str> = None; + + let o1: Result<&str, &str> = Ok("ok1"); + let o2: Result<&str, &str> = Ok("ok2"); + let e1: Result<&str, &str> = Err("error1"); + let e2: Result<&str, &str> = Err("error2"); + + assert_eq!(s1.or(s2), s1); // Some1 or Some2 = Some1 + assert_eq!(s1.or(n), s1); // Some or None = Some + assert_eq!(n.or(s1), s1); // None or Some = Some + assert_eq!(n.or(n), n); // None1 or None2 = None2 + + assert_eq!(o1.or(o2), o1); // Ok1 or Ok2 = Ok1 + assert_eq!(o1.or(e1), o1); // Ok or Err = Ok + assert_eq!(e1.or(o1), o1); // Err or Ok = Ok + assert_eq!(e1.or(e2), e2); // Err1 or Err2 = Err2 + + assert_eq!(s1.and(s2), s2); // Some1 and Some2 = Some2 + assert_eq!(s1.and(n), n); // Some and None = None + assert_eq!(n.and(s1), n); // None and Some = None + assert_eq!(n.and(n), n); // None1 and None2 = None1 + + assert_eq!(o1.and(o2), o2); // Ok1 and Ok2 = Ok2 + assert_eq!(o1.and(e1), e1); // Ok and Err = Err + assert_eq!(e1.and(o1), e1); // Err and Ok = Err + assert_eq!(e1.and(e2), e1); // Err1 and Err2 = Err1 +} +``` + +除了 `or` 和 `and` 之外,Rust 还为我们提供了 `xor` ,但是它只能应用在 `Option` 上,其实想想也是这个理,如果能应用在 `Result` 上,那你又该如何对一个值和错误进行异或操作? + +#### or_else() 和 and_then() +它们跟 `or()` 和 `and()` 类似,唯一的区别在于,它们的第二个表达式是一个闭包。 + +```rust +fn main() { + // or_else with Option + let s1 = Some("some1"); + let s2 = Some("some2"); + let fn_some = || Some("some2"); // 类似于: let fn_some = || -> Option<&str> { Some("some2") }; + + let n: Option<&str> = None; + let fn_none = || None; + + assert_eq!(s1.or_else(fn_some), s1); // Some1 or_else Some2 = Some1 + assert_eq!(s1.or_else(fn_none), s1); // Some or_else None = Some + assert_eq!(n.or_else(fn_some), s2); // None or_else Some = Some + assert_eq!(n.or_else(fn_none), None); // None1 or_else None2 = None2 + + // or_else with Result + let o1: Result<&str, &str> = Ok("ok1"); + let o2: Result<&str, &str> = Ok("ok2"); + let fn_ok = |_| Ok("ok2"); // 类似于: let fn_ok = |_| -> Result<&str, &str> { Ok("ok2") }; + + let e1: Result<&str, &str> = Err("error1"); + let e2: Result<&str, &str> = Err("error2"); + let fn_err = |_| Err("error2"); + + assert_eq!(o1.or_else(fn_ok), o1); // Ok1 or_else Ok2 = Ok1 + assert_eq!(o1.or_else(fn_err), o1); // Ok or_else Err = Ok + assert_eq!(e1.or_else(fn_ok), o2); // Err or_else Ok = Ok + assert_eq!(e1.or_else(fn_err), e2); // Err1 or_else Err2 = Err2 +} +``` + +```rust +fn main() { + // and_then with Option + let s1 = Some("some1"); + let s2 = Some("some2"); + let fn_some = |_| Some("some2"); // 类似于: let fn_some = |_| -> Option<&str> { Some("some2") }; + + let n: Option<&str> = None; + let fn_none = |_| None; + + assert_eq!(s1.and_then(fn_some), s2); // Some1 and_then Some2 = Some2 + assert_eq!(s1.and_then(fn_none), n); // Some and_then None = None + assert_eq!(n.and_then(fn_some), n); // None and_then Some = None + assert_eq!(n.and_then(fn_none), n); // None1 and_then None2 = None1 + + // and_then with Result + let o1: Result<&str, &str> = Ok("ok1"); + let o2: Result<&str, &str> = Ok("ok2"); + let fn_ok = |_| Ok("ok2"); // 类似于: let fn_ok = |_| -> Result<&str, &str> { Ok("ok2") }; + + let e1: Result<&str, &str> = Err("error1"); + let e2: Result<&str, &str> = Err("error2"); + let fn_err = |_| Err("error2"); + + assert_eq!(o1.and_then(fn_ok), o2); // Ok1 and_then Ok2 = Ok2 + assert_eq!(o1.and_then(fn_err), e2); // Ok and_then Err = Err + assert_eq!(e1.and_then(fn_ok), e1); // Err and_then Ok = Err + assert_eq!(e1.and_then(fn_err), e1); // Err1 and_then Err2 = Err1 +} +``` + +#### filter +`filter` 用于对 `Option` 进行过滤: +```rust +fn main() { + let s1 = Some(3); + let s2 = Some(6); + let n = None; + + let fn_is_even = |x: &i8| x % 2 == 0; + + assert_eq!(s1.filter(fn_is_even), n); // Some(3) -> 3 is not even -> None + assert_eq!(s2.filter(fn_is_even), s2); // Some(6) -> 6 is even -> Some(6) + assert_eq!(n.filter(fn_is_even), n); // None -> no value -> None +} +``` + +#### map() 和 map_err() +`map` 可以将 `Some` 或 `Ok` 中的值映射为另一个: +```rust +fn main() { + let s1 = Some("abcde"); + let s2 = Some(5); + + let n1: Option<&str> = None; + let n2: Option = None; + + let o1: Result<&str, &str> = Ok("abcde"); + let o2: Result = Ok(5); + + let e1: Result<&str, &str> = Err("abcde"); + let e2: Result = Err("abcde"); + + let fn_character_count = |s: &str| s.chars().count(); + + assert_eq!(s1.map(fn_character_count), s2); // Some1 map = Some2 + assert_eq!(n1.map(fn_character_count), n2); // None1 map = None2 + + assert_eq!(o1.map(fn_character_count), o2); // Ok1 map = Ok2 + assert_eq!(e1.map(fn_character_count), e2); // Err1 map = Err2 +} +``` + +但是如果你想要将 `Err` 中的值进行改变, `map` 就无能为力了,此时我们需要用 `map_err`: +```rust +fn main() { + let o1: Result<&str, &str> = Ok("abcde"); + let o2: Result<&str, isize> = Ok("abcde"); + + let e1: Result<&str, &str> = Err("404"); + let e2: Result<&str, isize> = Err(404); + + let fn_character_count = |s: &str| -> isize { s.parse().unwrap() }; // 该函数返回一个 isize + + assert_eq!(o1.map_err(fn_character_count), o2); // Ok1 map = Ok2 + assert_eq!(e1.map_err(fn_character_count), e2); // Err1 map = Err2 +} +``` + +通过对 `o1` 的操作可以看出,与 `map` 面对 `Err` 时的短小类似, `map_err` 面对 `Ok` 时也是相当无力的。 + +#### map_or() 和 map_or_else() +`map_or` 在 `map` 的基础上提供了一个默认值: +```rust +fn main() { + const V_DEFAULT: u32 = 1; + + let s: Result = Ok(10); + let n: Option = None; + let fn_closure = |v: u32| v + 2; + + assert_eq!(s.map_or(V_DEFAULT, fn_closure), 12); + assert_eq!(n.map_or(V_DEFAULT, fn_closure), V_DEFAULT); +} +``` + +如上所示,当处理 `None` 的时候,`V_DEFAULT` 作为默认值被直接返回。 + +`map_or_else` 与 `map_or` 类似,但是它是通过一个闭包来提供默认值: +```rust +fn main() { + let s = Some(10); + let n: Option = None; + + let fn_closure = |v: i8| v + 2; + let fn_default = || 1; + + assert_eq!(s.map_or_else(fn_default, fn_closure), 12); + assert_eq!(n.map_or_else(fn_default, fn_closure), 1); + + let o = Ok(10); + let e = Err(5); + let fn_default_for_result = |v: i8| v + 1; // 闭包可以对 Err 中的值进行处理,并返回一个新值 + + assert_eq!(o.map_or_else(fn_default_for_result, fn_closure), 12); + assert_eq!(e.map_or_else(fn_default_for_result, fn_closure), 6); +} +``` + +#### ok_or() and ok_or_else() +这两兄弟可以将 `Option` 类型转换为 `Result` 类型。其中 `ok_or` 接收一个默认的 `Err` 参数: +```rust +fn main() { + const ERR_DEFAULT: &str = "error message"; + + let s = Some("abcde"); + let n: Option<&str> = None; + + let o: Result<&str, &str> = Ok("abcde"); + let e: Result<&str, &str> = Err(ERR_DEFAULT); + + assert_eq!(s.ok_or(ERR_DEFAULT), o); // Some(T) -> Ok(T) + assert_eq!(n.ok_or(ERR_DEFAULT), e); // None -> Err(default) +} +``` + +而 `ok_or_else` 接收一个闭包作为 `Err` 参数: +```rust +fn main() { + let s = Some("abcde"); + let n: Option<&str> = None; + let fn_err_message = || "error message"; + + let o: Result<&str, &str> = Ok("abcde"); + let e: Result<&str, &str> = Err("error message"); + + assert_eq!(s.ok_or_else(fn_err_message), o); // Some(T) -> Ok(T) + assert_eq!(n.ok_or_else(fn_err_message), e); // None -> Err(default) +} +``` + +以上列出的只是常用的一部分,强烈建议大家看看标准库中有哪些可用的 API,在实际项目中,这些 API 将会非常有用: [Option](https://doc.rust-lang.org/stable/std/option/enum.Option.html) 和 [Result](https://doc.rust-lang.org/stable/std/result/enum.Result.html)。 + +## 自定义错误类型 +虽然标准库定义了大量的错误类型,但是一个严谨的项目,光使用这些错误类型往往是不够的,例如我们可能会为暴露给用户的错误定义相应的类型。 + +为了帮助我们更好的定义错误,Rust 在标准库中提供了一些可复用的特征,例如 `std::error::Erro` 特征: +```rust +use std::fmt::{Debug, Display}; + +pub trait Error: Debug + Display { + fn source(&self) -> Option<&(Error + 'static)> { ... } +} +``` + +当自定义类型实现该特征后,该类型就可以作为 `Err` 来使用,下面一起来看看。 + +> 实际上,自定义错误类型只需要实现 `Debug` 和 `Display` 特征即可,`source` 方法是可选的,而 `Debug` 特征往往也无需手动实现,可以直接通过 `derive` 来派生 + +#### 最简单的错误 +```rust +use std::fmt; + +// AppError 是自定义错误类型,它可以是当前包中定义的任何类型,在这里为了简化,我们使用了单元结构体作为例子。 +// 为 AppError 自动派生 Debug 特征 +#[derive(Debug)] +struct AppError; + +// 为 AppError 实现 std::fmt::Display 特征 +impl fmt::Display for AppError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "An Error Occurred, Please Try Again!") // user-facing output + } +} + +// 一个示例函数用于产生 AppError 错误 +fn produce_error() -> Result<(), AppError> { + Err(AppError) +} + +fn main(){ + match produce_error() { + Err(e) => eprintln!("{}", e), + _ => println!("No error"), + } + + eprintln!("{:?}", produce_error()); // Err({ file: src/main.rs, line: 17 }) +} +``` + +上面的例子很简单,我们定义了一个错误类型,当为它派生了 `Debug` 特征,同时手动实现了 `Display` 特征后,该错误类型就可以作为 `Err`来使用了。 + +事实上,实现 `Debug` 和 `Display` 特征并不是作为 `Err` 使用的必要条件,大家可以把这两个特征实现和相应使用去除,然后看看代码会否报错。既然如此,我们为何要为自定义类型实现这两个特征呢?原因有二: + +- 错误得打印输出后,才能有实际用处,而打印输出就需要实现这两个特征 +- 可以将自定义错误转换成 `Box` 特征对象,在后面的**归一化不同错误类型**部分,我们会详细介绍 + +#### 更详尽的错误 +上一个例子中定义的错误非常简单,我们无法从错误中得到更多的信息,现在再来定义一个具有错误码和信息的错误: +```rust +use std::fmt; + +struct AppError { + code: usize, + message: String, +} + +// 根据错误码显示不同的错误信息 +impl fmt::Display for AppError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let err_msg = match self.code { + 404 => "Sorry, Can not find the Page!", + _ => "Sorry, something is wrong! Please Try Again!", + }; + + write!(f, "{}", err_msg) + } +} + +impl fmt::Debug for AppError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "AppError {{ code: {}, message: {} }}", + self.code, self.message + ) + } +} + +fn produce_error() -> Result<(), AppError> { + Err(AppError { + code: 404, + message: String::from("Page not found"), + }) +} + +fn main() { + match produce_error() { + Err(e) => eprintln!("{}", e), // 抱歉,未找到指定的页面! + _ => println!("No error"), + } + + eprintln!("{:?}", produce_error()); // Err(AppError { code: 404, message: Page not found }) + + eprintln!("{:#?}", produce_error()); + // Err( + // AppError { code: 404, message: Page not found } + // ) +} +``` + +在本例中,我们除了增加了错误码和消息外,还手动实现了 `Debug` 特征,原因在于,我们希望能自定义 `Debug` 的输出内容,而不是使用派生后系统提供的默认输出形式。 + +#### 错误转换 `From` 特征 +标准库、三方库、本地库,各有各的精彩,各也有各的错误。那么问题就来了,我们该如何将其它的错误类型转换成自定义的错误类型?总不能神鬼牛魔,同台共舞吧。。 + +好在 Rust 为我们提供了 `std::convert::From` 特征: +```rust +pub trait From: Sized { + fn from(_: T) -> Self; +} +``` + +> 事实上,该特征在之前的 [`?` 操作符](https://course.rs/basic/result-error/result.html#传播界的大明星-)章节中就有所介绍。 +> +> 大家都使用过 `String::from` 函数吧?它可以通过 `&str` 来创建一个 `String`,其实该函数就是 `From` 特征提供的 + +下面一起来看看如何为自定义类型实现 `From` 特征: +```rust +use std::fs::File; +use std::io; + +#[derive(Debug)] +struct AppError { + kind: String, // 错误类型 + message: String, // 错误信息 +} + +// 为 AppError 实现 std::convert::From 特征,由于 From 包含在 std::prelude 中,因此可以直接简化引入。 +// 实现 From 意味着我们可以将 io::Error 错误转换成自定义的 AppError 错误 +impl From for AppError { + fn from(error: io::Error) -> Self { + AppError { + kind: String::from("io"), + message: error.to_string(), + } + } +} + +fn main() -> Result<(), AppError> { + let _file = File::open("nonexistent_file.txt")?; + + Ok(()) +} + +// --------------- 上述代码运行后输出 --------------- +Error: AppError { kind: "io", message: "No such file or directory (os error 2)" } +``` + +上面的代码中除了实现 `From` 外,还有一点特别重要,那就是 `?` 可以将错误进行隐式的强制转换:`File::open` 返回的是 `std::io::Error`, 我们并没有进行任何显式的转换,它就能自动变成 `AppError` ,这就是 `?` 的强大之处! + +上面的例子只有一个标准库错误,再来看看多个不同的错误转换成 `AppError` 的实现: +```rust +use std::fs::File; +use std::io::{self, Read}; +use std::num; + +#[derive(Debug)] +struct AppError { + kind: String, + message: String, +} + +impl From for AppError { + fn from(error: io::Error) -> Self { + AppError { + kind: String::from("io"), + message: error.to_string(), + } + } +} + +impl From for AppError { + fn from(error: num::ParseIntError) -> Self { + AppError { + kind: String::from("parse"), + message: error.to_string(), + } + } +} + +fn main() -> Result<(), AppError> { + let mut file = File::open("hello_world.txt")?; + + let mut content = String::new(); + file.read_to_string(&mut content)?; + + let _number: usize; + _number = content.parse()?; + + Ok(()) +} + + +// --------------- 上述代码运行后的可能输出 --------------- + +// 01. 若 hello_world.txt 文件不存在 +Error: AppError { kind: "io", message: "No such file or directory (os error 2)" } + +// 02. 若用户没有相关的权限访问 hello_world.txt +Error: AppError { kind: "io", message: "Permission denied (os error 13)" } + +// 03. 若 hello_world.txt 包含有非数字的内容,例如 Hello, world! +Error: AppError { kind: "parse", message: "invalid digit found in string" } +``` + +## 归一化不同的错误类型 +至此,关于 Rust 的错误处理大家已经了若指掌了,下面再来看看一些实战中的问题。 + +在实际项目中,我们往往会为不同的错误定义不同的类型,这样做非常好,但是如果你要在一个函数中返回不同的错误呢?例如: + +```rust +use std::fs::read_to_string; + +fn main() -> Result<(), std::io::Error> { + let html = render()?; + println!("{}", html); + Ok(()) +} + +fn render() -> Result { + let file = std::env::var("MARKDOWN")?; + let source = read_to_string(file)?; + Ok(source) +} +``` + +上面的代码会报错,原因在于 `render` 函数中的两个 `?` 返回的实际上是不同的错误:`env::var()` 返回的是 `std::env::VarError`,而 `read_to_string` 返回的是 `std::io::Error`。 + +为了满足 `render` 函数的签名,我们就需要将 `env::VarError` 和 `io::Error` 归一化为同一种错误类型。要实现这个目的有两种方式: + +- 使用特征对象 `Box` +- 自定义错误类型 +- 使用 `thiserror` + +下面依次来看看相关的解决方式。 + +#### Box\ +大家还记得我们之前提到的 `std::error::Error` 特征吧,当时有说:自定义类型实现 `Debug + Display` 特征的主要原因就是为了能转换成 `Error` 的特征对象,而特征对象恰恰是在同一个地方使用不同类型的关键: + +```rust +use std::fs::read_to_string; +use std::error::Error; +fn main() -> Result<(), Box> { + let html = render()?; + println!("{}", html); + Ok(()) +} + +fn render() -> Result> { + let file = std::env::var("MARKDOWN")?; + let source = read_to_string(file)?; + Ok(source) +} +``` + +这个方法很简单,在绝大多数场景中,性能也非常够用,但是有一个问题:`Result` 实际上不会限制错误的类型,也就是一个类型就算不实现 `Error` 特征,它依然可以在 `Result` 中作为 `E` 来使用,此时这种特征对象的解决方案就无能为力了。 + +#### 自定义错误类型 +与特征对象相比,自定义错误类型麻烦归麻烦,但是它非常灵活,因此也不具有上面的类似限制: +```rust +use std::fs::read_to_string; + +fn main() -> Result<(), MyError> { + let html = render()?; + println!("{}", html); + Ok(()) +} + +fn render() -> Result { + let file = std::env::var("MARKDOWN")?; + let source = read_to_string(file)?; + Ok(source) +} + +#[derive(Debug)] +enum MyError { + EnvironmentVariableNotFound, + IOError(std::io::Error), +} + +impl From for MyError { + fn from(_: std::env::VarError) -> Self { + Self::EnvironmentVariableNotFound + } +} + +impl From for MyError { + fn from(value: std::io::Error) -> Self { + Self::IOError(value) + } +} + +impl std::error::Error for MyError {} + +impl std::fmt::Display for MyError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MyError::EnvironmentVariableNotFound => write!(f, "Environment variable not found"), + MyError::IOError(err) => write!(f, "IO Error: {}", err.to_string()), + } + } +} +``` + +上面代码中有一行值得注意:`impl std::error::Error for MyError {}` ,只有为自定义错误类型实现 `Error` 特征后,才能转换成相应的特征对象。 + +不得不说,真是啰嗦啊。因此在能用特征对象的时候,建议大家还是使用特征对象,无论如何,代码可读性还是很重要的! + +上面的第二种方式灵活归灵活,啰嗦也是真啰嗦,好在 Rust 的社区为我们提供了 `thiserror` 解决方案,下面一起来看看该如何简化 Rust 中的错误处理。 + +## 简化错误处理 +对于开发者而言,错误处理是代码中打交道最多的部分之一,因此选择一把趁手的武器也很重要,它可以帮助我们节省大量的时间和精力,好钢应该用在代码逻辑而不是冗长的错误处理上。 + +#### thiserror +[`thiserror`](https://github.com/dtolnay/thiserror)可以帮助我们简化上面的第二种解决方案: +```rust +use std::fs::read_to_string; + +fn main() -> Result<(), MyError> { + let html = render()?; + println!("{}", html); + Ok(()) +} + +fn render() -> Result { + let file = std::env::var("MARKDOWN")?; + let source = read_to_string(file)?; + Ok(source) +} + +#[derive(thiserror::Error, Debug)] +enum MyError { + #[error("Environment variable not found")] + EnvironmentVariableNotFound(#[from] std::env::VarError), + #[error(transparent)] + IOError(#[from] std::io::Error), +} +``` + +如上所示,只要简单谢谢注释,就可以实现错误处理了,惊不惊喜? + +#### error-chain +[`error-chain`](https://github.com/rust-lang-deprecated/error-chain) 也是简单好用的库,可惜不再维护了,但是我觉得它依然可以在合适的地方大放光彩,值得大家去了解下。 + +```rust +use std::fs::read_to_string; + +error_chain::error_chain! { + foreign_links { + EnvironmentVariableNotFound(::std::env::VarError); + IOError(::std::io::Error); + } +} + +fn main() -> Result<()> { + let html = render()?; + println!("{}", html); + Ok(()) +} + +fn render() -> Result { + let file = std::env::var("MARKDOWN")?; + let source = read_to_string(file)?; + Ok(source) +} +``` + +喏,简单吧?使用 `error-chain` 的宏你可以获得:`Error` 结构体,错误类型 `ErrorKind` 枚举 以及一个自定义的 `Result` 类型。 + + +#### anyhow +[`anyhow`](https://github.com/dtolnay/anyhow) 和 `thiserror` 是同一个作者开发的,这里是作者关于 `anyhow` 和 `thiserror` 的原话: + +> 如果你想要设计自己的错误类型,同时给调用者提供具体的信息时,就使用 `thiserror`,例如当你在开发一个三方库代码时。如果你只想要简单,就使用 `anyhow`,例如在自己的应用服务中。 + +本章的篇幅已经过长,因此就不具体介绍 `anyhow` 该如何使用,官方提供的例子已经足够详尽,这里就留给大家自己探索了 :) + +## 总结 +Rust 一个为人津津乐道的点就是强大、易用的错误处理,对于新手来说,这个机制可能会有些复杂,但是一旦体会到了其中的好处,你将跟我一样沉醉其中不能自拔。 From ea57fee39448e2d91871b67b731207feafe4383e Mon Sep 17 00:00:00 2001 From: sunface Date: Thu, 10 Feb 2022 17:15:42 +0800 Subject: [PATCH 14/14] update readme.md --- README.md | 2 +- book/contents/advance/errors.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bd5943e9..b7f509d6 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ - 国内镜像: https://book.rust.team - 知乎: [支持章节内目录跳转,很好用!](https://www.zhihu.com/column/c_1452781034895446017) -- 最近修订: 新增章节 [Tokio使用指南 - 同步桥接](https://zhuanlan.zhihu.com/p/463813904) +- 最近修订: 新增章节 [进阶错误处理](https://zhuanlan.zhihu.com/p/465842938) - Rust版本: Rust edition 2021 - QQ交流群:1009730433 diff --git a/book/contents/advance/errors.md b/book/contents/advance/errors.md index 3061f824..1fedd462 100644 --- a/book/contents/advance/errors.md +++ b/book/contents/advance/errors.md @@ -496,7 +496,7 @@ fn render() -> Result { 下面依次来看看相关的解决方式。 -#### Box\ +#### Box 大家还记得我们之前提到的 `std::error::Error` 特征吧,当时有说:自定义类型实现 `Debug + Display` 特征的主要原因就是为了能转换成 `Error` 的特征对象,而特征对象恰恰是在同一个地方使用不同类型的关键: ```rust