From ab22f5febc0c994909ed1eb47732d63dd289e3bb Mon Sep 17 00:00:00 2001 From: Allan Downey Date: Sun, 30 Jan 2022 15:28:38 +0800 Subject: [PATCH] Update: unified format --- book/contents/basic/result-error/panic.md | 26 +++++++++++----------- book/contents/basic/result-error/result.md | 26 +++++++++++----------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/book/contents/basic/result-error/panic.md b/book/contents/basic/result-error/panic.md index 7bd47e92..4dd6ca22 100644 --- a/book/contents/basic/result-error/panic.md +++ b/book/contents/basic/result-error/panic.md @@ -38,13 +38,13 @@ fn main() { v[99]; } ``` -上面的代码很简单,数组只有`3`个元素,我们却尝试去访问它的第`100`号元素(数组索引从`0`开始),那自然会崩溃。 +上面的代码很简单,数组只有 `3` 个元素,我们却尝试去访问它的第 `100` 号元素(数组索引从 `0` 开始),那自然会崩溃。 我们的读者里不乏正义之士,此时肯定要质疑,一个简单的数组越界访问,为何要直接让程序崩溃?是不是有些小题大作了? -如果有过C语言的经验,即使你越界了,问题不大,我依然尝试去访问,至于这个值是不是你想要的(`100`号内存地址也有可能有值,只不过是其它变量或者程序的!),抱歉,不归我管,我只负责取,你要负责管理好自己的索引访问范围。上面这种情况被称为**缓冲区溢出**,并可能会导致安全漏洞,例如攻击者可以通过索引来访问到数组后面不被允许的数据。 +如果有过C语言的经验,即使你越界了,问题不大,我依然尝试去访问,至于这个值是不是你想要的(`100` 号内存地址也有可能有值,只不过是其它变量或者程序的!),抱歉,不归我管,我只负责取,你要负责管理好自己的索引访问范围。上面这种情况被称为**缓冲区溢出**,并可能会导致安全漏洞,例如攻击者可以通过索引来访问到数组后面不被允许的数据。 -说实话,我宁愿程序崩溃,为什么?当你取到了一个不属于你的值,这在很多时候会导致程序上的逻辑bug! 有编程经验的人都知道这种逻辑上的bug是多么难被发现和修复!因此程序直接崩溃,然后告诉我们问题发生的位置,最后我们对此进行修复,这才是最合理的软件开发流程,而不是把问题藏着掖着: +说实话,我宁愿程序崩溃,为什么?当你取到了一个不属于你的值,这在很多时候会导致程序上的逻辑bug! 有编程经验的人都知道这种逻辑上的 BUG 是多么难被发现和修复!因此程序直接崩溃,然后告诉我们问题发生的位置,最后我们对此进行修复,这才是最合理的软件开发流程,而不是把问题藏着掖着: ```console 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 @@ -75,29 +75,29 @@ note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose bac 上面的代码就是一次栈展开(也称栈回溯),它包含了函数调用的顺序,当然按照逆序排列:最近调用的函数排在列表的最上方。因为咱们的 `main` 函数基本是最先调用的函数了,所以排在了倒数第二位,还有一个关注点,排在最顶部最后一个调用的函数是 `rust_begin_unwind`,该函数的目的就是进行栈展开,呈现这些列表信息给我们。 -要获取到栈回溯信息,你还需要开启 `debug` 标志,该标志在使用 `cargo run` 或者 `cargo build` 时自动开启(这两个操作默认是 `Debug` 运行方式). 同时,栈展开信息在不同操作系统或者 Rust 版本上也所有不同。 +要获取到栈回溯信息,你还需要开启 `debug` 标志,该标志在使用 `cargo run` 或者 `cargo build` 时自动开启(这两个操作默认是 `Debug` 运行方式)。同时,栈展开信息在不同操作系统或者 Rust 版本上也所有不同。 ## panic时的两种终止方式 当出现 `panic!` 时,程序提供了两种方式来处理终止流程: **栈展开** 和 **直接终止**。 其中,默认的方式就是 `栈展开`,这意味着 Rust 会回溯栈上数据和函数调用,因此也意味着更多的善后工作,好处是可以给出充分的报错信息和栈调用信息,便于事后的问题复盘。`直接终止`,顾名思义,不清理数据就直接退出程序,善后工作交与操作系统来负责。 -对于绝大多数用户,使用默认选择是最好的,但是当你关心最终编译出的二进制可执行文件大小时,那么可以尝试去使用直接终止的方式,例如下面的配置修改 `Cargo.toml` 文件,实现在[`release`](../first-try/cargo.md#手动编译和运行项目)模式下遇到 `panic` 直接终止: +对于绝大多数用户,使用默认选择是最好的,但是当你关心最终编译出的二进制可执行文件大小时,那么可以尝试去使用直接终止的方式,例如下面的配置修改 `Cargo.toml` 文件,实现在 [`release`](../first-try/cargo.md#手动编译和运行项目) 模式下遇到 `panic` 直接终止: ```rust [profile.release] panic = 'abort' ``` -## 线程`panic`后,程序是否会终止? +## 线程 `panic` 后,程序是否会终止? 长话短说,如果是 `main` 线程,则程序会终止,如果是其它子线程,该线程会终止,但是不会影响 `main` 线程。因此,尽量不要在 `main` 线程中做太多任务,将这些任务交由子线程去做,就算子线程 `panic` 也不会导致整个程序的结束。 -具体解析见[panic原理剖析](#panic原理剖析) +具体解析见 [panic原理剖析](#panic原理剖析)。 ## 何时该使用panic! 下面让我们大概罗列下何时适合使用 `panic`,也许经过之前的学习,你已经能够对 `panic` 的使用有了自己的看法,但是我们还是会罗列一些常见的用法来加深你的理解。 -先来一点背景知识,在前面章节我们粗略讲过 `Result` 这个枚举类型,它是用来表示函数的返回结果: +先来一点背景知识,在前面章节我们粗略讲过 `Result` 这个枚举类型,它是用来表示函数的返回结果: ```rust enum Result { Ok(T), @@ -110,17 +110,17 @@ use std::net::IpAddr; let home: IpAddr = "127.0.0.1".parse().unwrap(); ``` -上面的 `parse` 方法试图将字符串 `"127.0.0.1" `解析为一个IP地址类型 `IpAddr`,它返回一个 `Result` 类型,如果解析成功,则把 `Ok(IpAddr)` 中的值赋给 `home`,如果失败,则不处理 `Err(E)`,而是直接 `panic`。 +上面的 `parse` 方法试图将字符串 `"127.0.0.1" `解析为一个IP地址类型 `IpAddr`,它返回一个 `Result` 类型,如果解析成功,则把 `Ok(IpAddr)` 中的值赋给 `home`,如果失败,则不处理 `Err(E)`,而是直接 `panic`。 因此 `unwrap` 简而言之:成功则返回值,失败则 `panic`,总之不进行任何错误处理。 #### 示例、原型、测试 -这几个场景下,需要快速地搭建代码,错误处理会拖慢编码的速度,也不是特别有必要,因此通过`unwrap`、`expect`等方法来处理是最快的。 +这几个场景下,需要快速地搭建代码,错误处理会拖慢编码的速度,也不是特别有必要,因此通过 `unwrap`、`expect` 等方法来处理是最快的。 同时,当我们回头准备做错误处理时,可以全局搜索这些方法,不遗漏地进行替换。 #### 你确切的知道你的程序是正确时,可以使用panic -因为 `panic` 的触发方式比错误处理要简单,因此可以让代码更清晰,可读性也更加好,当我们的代码注定是正确时,你可以用 `unrawp` 等方法直接进行处理,反正也不可能`panic`: +因为 `panic` 的触发方式比错误处理要简单,因此可以让代码更清晰,可读性也更加好,当我们的代码注定是正确时,你可以用 `unrawp` 等方法直接进行处理,反正也不可能 `panic` : ```rust use std::net::IpAddr; let home: IpAddr = "127.0.0.1".parse().unwrap(); @@ -149,9 +149,9 @@ let home: IpAddr = "127.0.0.1".parse().unwrap(); 当调用 `panic!` 宏时,它会 1. 格式化 `panic` 信息,然后使用该信息作为参数,调用 `std::panic::panic_any()` 函数 -2. `panic_any` 会检查应用是否使用了 `panic hook`,如果使用了,该 `hook` 函数就会被调用(hook是一个钩子函数,是外部代码设置的,用于在panic触发时,执行外部代码所需的功能) +2. `panic_any` 会检查应用是否使用了 `panic hook`,如果使用了,该 `hook` 函数就会被调用(`hook` 是一个钩子函数,是外部代码设置的,用于在 `panic` 触发时,执行外部代码所需的功能) 3. 当 `hook` 函数返回后,当前的线程就开始进行栈展开:从 `panic_any` 开始,如果寄存器或者栈因为某些原因信息错乱了,那很可能该展开会发生异常,最终线程会直接停止,展开也无法继续进行 -4. 展开的过程是一帧一帧的去回溯整个栈,每个帧的数据都会随之被丢弃,但是在展开过程中,你可能会遇到被用户标记为 `catching` 的帧(通过 `std::panic::catch_unwind()` 函数标记),此时用户提供的 `catch` 函数会被调用,展开也随之停止:当然,如果 `catch` 选择在内部调用 `std::panic::resume_unwind()` 函数,则展开还会继续。 +4. 展开的过程是一帧一帧的去回溯整个栈,每个帧的数据都会随之被丢弃,但是在展开过程中,你可能会遇到被用户标记为 `catching` 的帧(通过 `std::panic::catch_unwind()` 函数标记),此时用户提供的 `catch` 函数会被调用,展开也随之停止:当然,如果 `catch` 选择在内部调用 `std::panic::resume_unwind()` 函数,则展开还会继续。 还有一种情况,在展开过程中,如果展开本身 `panic` 了,那展开线程会终止,展开也随之停止。 diff --git a/book/contents/basic/result-error/result.md b/book/contents/basic/result-error/result.md index 8e573f3a..db23cb25 100644 --- a/book/contents/basic/result-error/result.md +++ b/book/contents/basic/result-error/result.md @@ -1,9 +1,9 @@ # 可恢复的错误Result 还记得上一节中,提到的关于文件读取的思考题吧?当时我们解决了读取文件时遇到不可恢复错误该怎么处理的问题,现在来看看,读取过程中,正常返回和遇到可以恢复的错误时该如何处理。 -假设,我们有一台消息服务器,每个用户都通过 websocket 连接到该服务器来接收和发送消息,该过程就涉及到 socket 文件的读写,那么此时,如果一个用户的读写发生了错误,显然不能直接panic,否则服务器会直接崩溃,所有用户都会断开连接,因此我们需要一种更温和的错误处理方式:`Result`。 +假设,我们有一台消息服务器,每个用户都通过 websocket 连接到该服务器来接收和发送消息,该过程就涉及到 socket 文件的读写,那么此时,如果一个用户的读写发生了错误,显然不能直接 `panic`,否则服务器会直接崩溃,所有用户都会断开连接,因此我们需要一种更温和的错误处理方式:`Result`。 -之前章节有提到过,`Result` 是一个枚举类型,定义如下: +之前章节有提到过,`Result` 是一个枚举类型,定义如下: ```rust enum Result { Ok(T), @@ -24,7 +24,7 @@ fn main() { > > 有几种常用的方式,此处更推荐第二种方法: > - 第一种是查询标准库或者三方库文档,搜索 `File`,然后找到它的 `open` 方法 -> - 在[Rust IDE](../../first-try/editor.md)章节,我们推荐了 `VSCode` IED和 `rust-analyzer` 插件,如果你成功安装的话,那么就可以在 `VScode` 中很方便的通过代码跳转的方式查看代码,同时 `rust-analyzer` 插件还会对代码中的类型进行标注,非常方便好用! +> - 在 [Rust IDE](../../first-try/editor.md) 章节,我们推荐了 `VSCode` IDE 和 `rust-analyzer` 插件,如果你成功安装的话,那么就可以在 `VSCode` 中很方便的通过代码跳转的方式查看代码,同时 `rust-analyzer` 插件还会对代码中的类型进行标注,非常方便好用! > - 你还可以尝试故意标记一个错误的类型,然后让编译器告诉你: ```rust let f: u32 = File::open("hello.txt"); @@ -44,7 +44,7 @@ error[E0308]: mismatched types 上面代码,故意将 `f` 类型标记成整形,编译器立刻不乐意了,你是在忽悠我吗?打开文件操作返回一个整形?来,大哥来告诉你返回什么:`std::result::Result`,我的天呐,怎么这么长的类型! -别慌,其实很简单,首先 `Result` 本身是定义在 `std::result` 中的,但是因为 `Result` 很常用,所以就被包含在了[`prelude`](../../appendix/prelude.md)中(将常用的东东提前引入到当前作用域内),因此无需手动引入 `std::result::Result`,那么返回类型可以简化为 `Result`,你看看是不是很像标准的 `Result` 枚举定义?只不过 `T` 被替换成了具体的类型 `std::fs::File`,是一个文件句柄类型,`E` 被替换成 `std::io::Error`,是一个 IO 错误类型. +别慌,其实很简单,首先 `Result` 本身是定义在 `std::result` 中的,但是因为 `Result` 很常用,所以就被包含在了 [`prelude`](../../appendix/prelude.md) 中(将常用的东东提前引入到当前作用域内),因此无需手动引入 `std::result::Result`,那么返回类型可以简化为 `Result`,你看看是不是很像标准的 `Result` 枚举定义?只不过 `T` 被替换成了具体的类型 `std::fs::File`,是一个文件句柄类型,`E` 被替换成 `std::io::Error`,是一个 IO 错误类型. 这个返回值类型说明 `File::open` 调用如果成功则返回一个可以进行读写的文件句柄,如果失败,则返回一个 IO 错误:文件不存在或者没有访问文件的权限等。总之 `File::open` 需要一个方式告知调用者是成功还是失败,并同时返回具体的文件句柄(成功)或错误信息(失败),万幸的是,这些信息可以通过 `Result` 枚举提供: ```rust @@ -62,7 +62,7 @@ fn main() { } ``` -代码很清晰,对打开文件后的 `Result` 类型进行匹配取值,如果是成功,则将 `Ok(file)` 中存放的的文件句柄 `file` 赋值给 `f`,如果失败,则将 `Err(error)` 中存放的错误信息 `error` 使用 `panic` 抛出来,进而结束程序,这非常符合上文提到过的 `panic` 使用场景。 +代码很清晰,对打开文件后的 `Result` 类型进行匹配取值,如果是成功,则将 `Ok(file)` 中存放的的文件句柄 `file` 赋值给 `f`,如果失败,则将 `Err(error)` 中存放的错误信息 `error` 使用 `panic` 抛出来,进而结束程序,这非常符合上文提到过的 `panic` 使用场景。 好吧,也没有那么合理 :) @@ -93,12 +93,12 @@ fn main() { - 如果是文件不存在错误 `ErrorKind::NotFound`,就创建文件,这里创建文件`File::create` 也是返回 `Result`,因此继续用 `match` 对其结果进行处理:创建成功,将新的文件句柄赋值给 `f`,如果失败,则 `panic` - 剩下的错误,一律 `panic` -虽然很清晰,但是代码还是有些啰嗦,我们会在[简化错误处理](../../errors/simplify.md)一章重点讲述如何写出更优雅的错误。 +虽然很清晰,但是代码还是有些啰嗦,我们会在[简化错误处理](../../advance/errors/simplify.md)一章重点讲述如何写出更优雅的错误。 ## 失败就 panic: unwrap 和 expect 上一节中,已经看到过这两兄弟的简单介绍,这里再来回顾下。 -在不需要处理错误的场景,例如写原型、示例时,我们不想使用 `match` 去匹配 `Result `以获取其中的 `T` 值,因为 `match` 的穷尽匹配特性,你总要去处理下 `Err` 分支。那么有没有办法简化这个过程?有,答案就是 `unwrap` 和 `expect`。 +在不需要处理错误的场景,例如写原型、示例时,我们不想使用 `match` 去匹配 `Result ` 以获取其中的 `T` 值,因为 `match` 的穷尽匹配特性,你总要去处理下 `Err` 分支。那么有没有办法简化这个过程?有,答案就是 `unwrap` 和 `expect`。 它们的作用就是,如果返回成功,就将 `Ok(T)` 中的值取出来,如果失败,就直接 `panic`,真的勇士绝不多BB,直接崩溃。 @@ -166,7 +166,7 @@ fn read_username_from_file() -> Result { 有几点值得注意: - 该函数返回一个 `Result` 类型,当读取用户名成功时,返回 `Ok(String)`,失败时,返回 `Err(io:Error)` -- `File::open` 和 `f.read_to_string` 返回的 `Result` 中的 `E` 就是 `io::Error` +- `File::open` 和 `f.read_to_string` 返回的 `Result` 中的 `E` 就是 `io::Error` 由此可见,该函数将 `io::Error` 的错误往上进行传播,该函数的调用者最终会对 `Result` 进行再处理,至于怎么处理就是调用者的事,如果是错误,它可以选择继续向上传播错误,也可以直接 `panic`,亦或将具体的错误原因包装后写入 socket 中呈现给终端用户。 @@ -202,7 +202,7 @@ let mut f = match f { 虽然 `?` 和 `match` 功能一致,但是事实上 `?` 会更胜一筹。何解? -想象一下,一个设计良好的系统中,肯定有自定义的错误特征,错误之间很可能会存在上下级关系,例如标准库中的 `std::io::Error `和 `std::error::Error`,前者是io相关的错误结构体,后者是一个最最通用的标准错误特征,同时前者实现了后者,因此 `std::io::Error` 可以转换为 `std:error::Error`。 +想象一下,一个设计良好的系统中,肯定有自定义的错误特征,错误之间很可能会存在上下级关系,例如标准库中的 `std::io::Error `和 `std::error::Error`,前者是 IO 相关的错误结构体,后者是一个最最通用的标准错误特征,同时前者实现了后者,因此 `std::io::Error` 可以转换为 `std:error::Error`。 明白了以上的错误转换,`?` 的更胜一筹就很好理解了,它可以自动进行类型提升(转换): ```rust @@ -231,7 +231,7 @@ fn read_username_from_file() -> Result { Ok(s) } ``` -瞧见没? `?` 还能实现链式调用,`File::open` 遇到错误就返回,没有错误就将 `Ok` 中的值取出来用于下一个方法调用,简直太精妙了,从 Go 语言过来的我,内心狂喜(其实学 Rust 的苦和痛我才不会告诉你们)。 +瞧见没? `?` 还能实现链式调用,`File::open` 遇到错误就返回,没有错误就将 `Ok` 中的值取出来用于下一个方法调用,简直太精妙了,从 Go 语言过来的我,内心狂喜(其实学 Rust 的苦和痛我才不会告诉你们)。 不仅有更强,还要有最强,我不信还有人比我更短(不要误解): ```rust @@ -270,7 +270,7 @@ fn first(arr: &[i32]) -> Option<&i32> { arr.get(0) } ``` -有一句话怎么说?没有需求,制造需求也要上。。。大家别跟我学习,这是软件开发大忌。只能用代码洗洗眼了: +有一句话怎么说?没有需求,制造需求也要上……大家别跟我学习,这是软件开发大忌。只能用代码洗洗眼了: ```rust fn last_char_of_first_line(text: &str) -> Option { text.lines().next()?.chars().last() @@ -298,7 +298,7 @@ fn main() { let f = File::open("hello.txt")?; } ``` -因为 `?` 要求 `Result` 形式的返回值,而 `main` 函数的返回是 `()`,因此无法满足,那是不是就无解了呢? +因为 `?` 要求 `Result` 形式的返回值,而 `main` 函数的返回是 `()`,因此无法满足,那是不是就无解了呢? 实际上 Rust 还支持另外一种形式的 `main` 函数: ```rust @@ -314,7 +314,7 @@ fn main() -> Result<(), Box> { 这样就能使用 `?` 提前返回了,同时我们又一次看到了`Box` 特征对象,因为 `std::error:Error` 是 Rust 中抽象层次最高的错误,其它标准库中的错误都实现了该特征,因此我们可以用该特征对象代表一切错误,就算 `main` 函数中调用任何标准库函数发生错误,都可以通过 `Box` 这个特征对象进行返回. -至于 `main` 函数可以有多种返回值,那是因为实现了[std::process::Termination](https://doc.rust-lang.org/std/process/trait.Termination.html)特征,目前为止该特征还没进入稳定版Rust中,也许未来你可以为自己的类型实现该特征! +至于 `main` 函数可以有多种返回值,那是因为实现了 [std::process::Termination](https://doc.rust-lang.org/std/process/trait.Termination.html) 特征,目前为止该特征还没进入稳定版 Rust 中,也许未来你可以为自己的类型实现该特征! 至此,Rust 的基础内容学习已经全部完成,下面我们将学习 Rust 的高级进阶内容,正式开启你的高手之路。