From e6e70c20eb0f2d5026f9115da323f0933fd94625 Mon Sep 17 00:00:00 2001 From: sunface Date: Sat, 12 Feb 2022 17:29:03 +0800 Subject: [PATCH] update markdown format --- book/contents/about-book.md | 62 ++++++++++++------- .../advance/functional-programing/closure.md | 4 ++ book/contents/advance/lifetime/advance.md | 2 +- book/contents/advance/lifetime/basic.md | 8 +++ book/contents/basic/converse.md | 3 + book/contents/basic/flow-control.md | 3 + book/contents/basic/ownership/ownership.md | 3 + book/contents/basic/result-error/panic.md | 3 + book/contents/basic/result-error/result.md | 4 ++ 9 files changed, 68 insertions(+), 24 deletions(-) diff --git a/book/contents/about-book.md b/book/contents/about-book.md index ab0f75b0..2f61290d 100644 --- a/book/contents/about-book.md +++ b/book/contents/about-book.md @@ -1,38 +1,54 @@ -# Rust 语言圣经 (Rust course) +# Rust语言圣经 (The Course) -- 官方网址: https://course.rs -- 修订时间: **尚未发行** -- Rust 版本: Rust edition 2021 -- QQ 交流群:1009730433 +- 在线阅读 + - 官方: https://course.rs + - 国内镜像: https://book.rust.team + - 知乎: [支持章节内目录跳转,很好用!](https://www.zhihu.com/column/c_1452781034895446017) +- Rust版本: Rust edition 2021 +- QQ交流群:1009730433 ### 教程简介 -`Rust 语言圣经`涵盖从**入门到精通**所需的全部 Rust 知识,目录及内容都经过深思熟虑的设计,同时语言生动幽默,行文流畅自如,摆脱技术书籍常有的机器味和晦涩感。 -在 Rust 基础教学的同时,我们还提供了: -- **深入度**,在基础教学的同时,提供了深入剖析,浅尝辄止并不能让我们站上紫禁之巅 +**`Rust语言圣经`**涵盖从**入门到精通**所需的 Rust 知识,目录及内容都经过深思熟虑的设计,同时语言生动幽默,行文流畅自如,摆脱技术书籍常有的机器味和晦涩感。 + +在 Rust 基础教学的同时,我们还提供了(部分): + +- **深入度**,在基础教学的同时,提供了深入剖析。浅尝辄止并不能让我们站上紫禁之巅 - **性能优化**,选择 Rust,意味着就要追求性能,因此你需要体系化的了解性能优化 - **专题**,将 Rust 高级内容通过专题的方式一一呈现,内容内聚性极强 -- **难点索引**,作为一本工具书,优秀的索引能力非常重要,遗忘不可怕,找不到才可怕 -- **场景化模版**,程序员上网查询如何操作是常事,没有人能记住所有代码,场景化模版可解君忧 -- **开源库推荐**, 根据场景推荐高质量的开源库,降低 Rust 上手门槛 +- **难点和错误索引**,作为一本工具书,优秀的索引能力非常重要,遗忘不可怕,找不到才可怕 +- **场景化模版**,程序员上网查询如何操作文件是常事,没有人能记住所有代码,场景化模版可解君忧 -总之在写作过程中我们始终铭记初心:为中国用户打造一本**全面的、深入的、持续更新的** Rust 教程。 新手用来入门,老手用来提高,高手用来提升生产力。 +总之在写作过程中我们始终铭记初心:为中国用户打造一门**全面的、深入的、持续更新的** Rust 教程。 新手用来入门,老手用来提高,高手用来提升生产力。 -### 开源说明 +### 借鉴的书籍 + +站在巨人的肩膀上,能帮我们看的更远,特此感谢以下巨人: -Rust 语言圣经是**完全开源**的电子书,每个章节都至少用时 4-6 个小时才能初步完稿,牺牲了大量休闲娱乐,陪伴家人的时间。而且还没有任何钱赚,**如果大家觉得这本书的作者真的用心了,希望你能帮我们点一个 🌟[star](https://github.com/sunface/rust-course)**,感激不尽!:) +- [Rust Book](https://doc.rust-lang.org/book) +- [Rust nomicon](https://doc.rust-lang.org/nomicon/dot-operator.html) +- [Async Rust](https://rust-lang.github.io/async-book/01_getting_started/01_chapter.html) +- 详细清单参见 [这里](./book/writing-material/books.md) -在开源版权上,我们选择了 [No License](https://www.google.com.hk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&ved=2ahUKEwigkv-KtMT0AhXFdXAKHdI4BCcQFnoECAQQAw&url=https%3A%2F%2Fchoosealicense.com%2Fno-permission%2F&usg=AOvVaw3M2Q4IbdhnpJ2K71TF7SPB),这意味着**读者可以随意的 fork 和阅读,但是不能私下修改后再包装分发。** +因为它们绝大部分是支持 APACHE + MIT 双协议的,因此我们选择了遵循其中的 MIT 协议,并在这里统一对借鉴的书籍进行说明。 -如果有这方面的需求,请联系我们。我们不会收钱,只是希望知道谁通过什么方式分发了这本书的部分内容,望理解。 +### 贡献者 -### Rust 社区 +非常感谢本教程的所有贡献者们,正是有了你们,才有了现在的高质量 Rust 教程! + +- [@JesseAtSZ](https://github.com/JesseAtSZ) +- [@mg-chao](https://github.com/mg-chao) +- [@1132719438](https://github.com/1132719438) +- [@codemystery](https://github.com/codemystery) +- [@AllanDowney](https://github.com/AllanDowney) +- [@Mintnoii](https://github.com/Mintnoii) + +尤其感谢这些主要贡献者,谢谢你们花费大量时间贡献了多处`fix`和高质量的内容优化。非常感动,再次感谢~~ + +### 开源说明 -与国外的 Rust 发展如火如荼相比,国内的近况不是特别理想。导致目前这种状况的原因,我个人认为有以下几点: +在开源版权上,我们选择了 [No License](https://www.google.com.hk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&ved=2ahUKEwigkv-KtMT0AhXFdXAKHdI4BCcQFnoECAQQAw&url=https%3A%2F%2Fchoosealicense.com%2Fno-permission%2F&usg=AOvVaw3M2Q4IbdhnpJ2K71TF7SPB),这意味着读者可以随意的 fork 和阅读,但是**不能私下修改后再包装分发**,如果有这方面的需求,请联系我们,望理解。 -1. 上手难度大,学习曲线陡峭 -2. 英文资料难学(阅读较难的技术内容,需要精准阅读,因此对外语能力要求较高),中文资料也不太好学(内容全面度、实时性,晦涩难懂等) -3. 没有体系化的学习路线,新人往往扫完一遍入门书籍,就不知道何去何从 +Rust语言圣经是**完全开源**的电子书,每个章节都至少用时 4-6 个小时才能初步完稿,牺牲了大量休闲娱乐、陪伴家人的时间,还没有任何钱赚。 -为此,我整了一本书和一个社区,欢迎大家的加入: -- QQ 群:1009730433 +**如果大家觉得这本书作者真的用心了,希望你能帮我们点一个 🌟 `star`。感激不尽!:)** diff --git a/book/contents/advance/functional-programing/closure.md b/book/contents/advance/functional-programing/closure.md index 16739109..618a04c4 100644 --- a/book/contents/advance/functional-programing/closure.md +++ b/book/contents/advance/functional-programing/closure.md @@ -160,6 +160,7 @@ Rust 闭包在形式上借鉴了 `Smalltalk` 和 `Ruby` 语言,与函数最大 ``` 上例中还有两点值得注意: + - **闭包中最后一行表达式返回的值,就是闭包执行后的返回值**,因此 `action()` 调用返回了 `intensity` 的值 `10` - `let action = ||...` 只是把闭包赋值给变量 `action`,并不是把闭包执行后的结果赋值给 `action`,因此这里 `action` 就相当于闭包函数,可以跟函数一样进行调用:`action()` @@ -215,6 +216,7 @@ error[E0308]: mismatched types ## 结构体中的闭包 假设我们要实现一个简易缓存,功能是获取一个值,然后将其缓存起来,那么可以这样设计: + - 一个闭包用于获取值 - 一个变量,用于存储该值 @@ -320,6 +322,7 @@ error[E0434]: can't capture dynamic environment in a fn item // 在函数中无 #### 三种 Fn 特征 闭包捕获变量有三种途径,恰好对应函数参数的三种传入方式:转移所有权、可变借用、不可变借用,因此相应的 `Fn` 特征也有三种: + 1. `FnOnce`,该类型的闭包会拿走被捕获变量的所有权。`Once` 顾名思义,说明该闭包只能运行一次: ```rust @@ -529,6 +532,7 @@ fn exec(f: F) { ##### 三种 Fn 的关系 实际上,一个闭包并不仅仅实现某一种 `Fn` 特征,规则如下: + - 所有的闭包都自动实现了 `FnOnce` 特征,因此任何一个闭包都至少可以被调用一次 - 没有移出所捕获变量的所有权的闭包自动实现了 `FnMut` 特征 - 不需要对捕获变量进行改变的闭包自动实现了 `Fn` 特征 diff --git a/book/contents/advance/lifetime/advance.md b/book/contents/advance/lifetime/advance.md index 3cb3074f..ef1e96d5 100644 --- a/book/contents/advance/lifetime/advance.md +++ b/book/contents/advance/lifetime/advance.md @@ -513,4 +513,4 @@ fn use_list(list: &List) { } ``` -至此,生命周期终于完结,两章超级长的内容,可以满足几乎所有对生命周期的学习目标。学完生命周期,意味着你正式入门了 Rust,只要再掌握几个常用概念,就可以上手写项目了,下面让我们看看在实际项目中极其常见的功能 - 迭代器。 +至此,生命周期终于完结,两章超级长的内容,可以满足几乎所有对生命周期的学习目标。学完生命周期,意味着你正式入门了 Rust,只要再掌握几个常用概念,就可以上手写项目了。 diff --git a/book/contents/advance/lifetime/basic.md b/book/contents/advance/lifetime/basic.md index ded7f063..7413f678 100644 --- a/book/contents/advance/lifetime/basic.md +++ b/book/contents/advance/lifetime/basic.md @@ -1,6 +1,7 @@ # 认识生命周期 生命周期,简而言之就是引用的有效作用域。在大多数时候,我们无需手动的声明生命周期,因为编译器可以自动进行推导,用类型来类比下: + - 就像编译器大部分时候可以自动推导类型 <-> 一样,编译器大多数时候也可以自动推导生命周期 - 在多种类型存在时,编译器往往要求我们手动标明类型 <-> 当多个生命周期存在,且编译器无法推导出某个引用的生命周期时,就需要我们手动标明生命周期 @@ -22,6 +23,7 @@ Rust 生命周期之所以难,是因为这个概念对于我们来说是全新 ``` 这段代码有几点值得注意: + - `let r;` 的声明方式貌似存在使用 `null` 的风险,实际上,当我们不初始化它就使用时,编译器会给予报错 - `r` 引用了内部花括号中的 `x` 变量,但是 `x` 会在内部花括号 `}` 处被释放,因此回到外部花括号后,`r` 会引用一个无效的 `x` @@ -157,6 +159,7 @@ fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { ``` 需要注意的点如下: + - 和泛型一样,使用生命周期参数,需要先声明 `<'a>` - `x`、`y` 和返回值至少活得和 `'a` 一样久(因为返回值要么是 `x`,要么是 `y`) @@ -347,6 +350,7 @@ fn first_word(s: &str) -> &str { 该函数的参数和返回值都是引用类型,尽管我们没有显式的为其标注生命周期,编译依然可以通过。其实原因不复杂,**编译器为了简化用户的使用,运用了生命周期消除大法**。 对于 `first_word` 函数,它的返回值是一个引用类型,那么该引用只有两种情况: + - 从参数获取 - 从函数体内部新创建的变量获取 @@ -362,6 +366,7 @@ fn first_word<'a>(s: &'a str) -> &'a str { 生命周期消除的规则不是一蹴而就,而是伴随着 `总结-改善` 流程的周而复始,一步一步走到今天,这也意味着,该规则以后可能也会进一步增加,我们需要手动标注生命周期的时候也会越来越少,hooray! 在开始之前有几点需要注意: + - 消除规则不是万能的,若编译器不能确定某件事是正确时,会直接判为不正确,那么你还是需要手动标注生命周期 - **函数或者方法中,参数的生命周期被称为 `输入生命周期`,返回值的生命周期被称为 `输出生命周期`** @@ -464,6 +469,7 @@ impl<'a> ImportantExcerpt<'a> { ``` 其中有几点需要注意的: + - `impl` 中必须使用结构体的完整名称,包括 `<'a>`,因为*生命周期标注也是结构体类型的一部分*! - 方法签名中,往往不需要标注生命周期,得益于生命周期消除的第一和第三规则 @@ -526,6 +532,7 @@ impl<'a: 'b, 'b> ImportantExcerpt<'a> { Bang,一个复杂的玩意儿被甩到了你面前,就问怕不怕? 就关键点稍微解释下: + - `'a: 'b`,是生命周期约束语法,跟泛型约束非常相似,用于说明`'a`必须比`'b`活得久 - 为了实现这一点,必须把`'a`和`'b`都在同一个地方声明,你不能把`'a`在`impl`后面声明,而把`'b`在方法中声明 @@ -548,6 +555,7 @@ let s: &'static str = "我没啥优点,就是活得久,嘿嘿"; 但是,话说回来,存在即合理,有时候,`'static` 确实可以帮助我们解决非常复杂的生命周期问题甚至是无法被手动解决的生命周期问题,那么此时就应该放心大胆的用,只要你确定:**你的所有引用的生命周期都是正确的,只是编译器太笨不懂罢了**。 总结下: + - 生命周期 `'static` 意味着能和程序活得一样久,例如字符串字面量和特征对象 - 实在遇到解决不了的生命周期标注问题,可以尝试 `T: 'static`,有时候它会给你奇迹 diff --git a/book/contents/basic/converse.md b/book/contents/basic/converse.md index 4ef4a04d..9e4117d9 100644 --- a/book/contents/basic/converse.md +++ b/book/contents/basic/converse.md @@ -162,6 +162,7 @@ error[E0277]: the trait bound `&mut i32: Trait` is not satisfied 假设有一个方法 `foo`,它有一个接收器(接收器就是 `self`、`&self`、`&mut self` 参数)。如果调用 `value.foo()`,编译器在调用 `foo` 之前,需要决定到底使用哪个 `Self` 类型来调用。现在假设 `value` 拥有类型 `T`。 再进一步,我们使用[完全限定语法](https://course.rs/basic/trait/advance-trait.html#完全限定语法)来进行准确的函数调用: + 1. 首先,编译器检查它是否可以直接调用 `T::foo(value)`,称之为**值方法调用** 2. 如果上一步调用无法完成(例如方法类型错误或者特征没有针对 `Self` 进行实现,上文提到过特征不能进行强制转换),那么编译器会尝试增加自动引用,例如会尝试以下调用: `<&T>::foo(value)` 和 `<&mut T>::foo(value)`,称之为**引用方法调用** 3. 若上面两个方法依然不工作,编译器会试着解引用 `T` ,然后再进行尝试。这里使用了 `Deref` 特征 —— 若 `T: Deref` (`T` 可以被解引用为 `U`),那么编译器会使用 `U` 类型进行尝试,称之为**解引用方法调用** @@ -175,6 +176,7 @@ let first_entry = array[0]; ``` `array` 数组的底层数据隐藏在了重重封锁之后,那么编译器如何使用 `array[0]` 这种数组原生访问语法通过重重封锁,准确的访问到数组中的第一个元素? + 1. 首先, `array[0]` 只是[`Index`](https://doc.rust-lang.org/std/ops/trait.Index.html)特征的语法糖:编译器会将 `array[0]` 转换为 `array.index(0)` 调用,当然在调用之前,编译器会先检查 `array` 是否实现了 `Index` 特征。 2. 接着,编译器检查 `Rc>` 是否有否实现 `Index` 特征,结果是否,不仅如此,`&Rc>` 与 `&mut Rc>` 也没有实现。 3. 上面的都不能工作,编译器开始对 `Rc>` 进行解引用,把它转变成 `Box<[T; 3]>` @@ -254,6 +256,7 @@ impl Clone for Container { 类型系统,你让开!我要自己转换这些类型,不成功便成仁!虽然本书大多是关于安全的内容,我还是希望你能仔细考虑避免使用本章讲到的内容。这是你在 Rust 中所能做到的真真正正、彻彻底底、最最可怕的非安全行为,在这里,所有的保护机制都形同虚设。 先让你看看深渊长什么样,开开眼,然后你再决定是否深入: `mem::transmute` 将类型 `T` 直接转成类型 `U`,唯一的要求就是,这两个类型占用同样大小的字节数!我的天,这也算限制?这简直就是无底线的转换好吧?看看会导致什么问题: + 1. 首先也是最重要的,转换后创建一个任意类型的实例会造成无法想象的混乱,而且根本无法预测。不要把 `3` 转换成 `bool` 类型,就算你根本不会去使用该 `bool` 类型,也不要去这样转换 2. 变形后会有一个重载的返回类型,即使你没有指定返回类型,为了满足类型推导的需求,依然会产生千奇百怪的类型 3. 将 `&` 变形为 `&mut` 是未定义的行为 diff --git a/book/contents/basic/flow-control.md b/book/contents/basic/flow-control.md index 4f89b613..f997876c 100644 --- a/book/contents/basic/flow-control.md +++ b/book/contents/basic/flow-control.md @@ -33,6 +33,7 @@ fn main() { ``` 以上代码有以下几点要注意: + - **`if` 语句块是表达式**,这里我们使用 `if` 表达式的返回值来给 `number` 进行赋值:`number` 的值是 `5` - 用 `if` 来赋值时,要保证每个分支返回的类型一样(事实上,这种说法不完全准确,见[这里](../appendix/expressions.md#if表达式)),此处返回的 `5` 和 `6` 就是同一个类型,如果返回类型不一致就会报错 @@ -160,6 +161,7 @@ for item in collection { ``` 第一种方式是循环索引,然后通过索引下标去访问集合,第二种方式是直接循环集合中的元素,优劣如下: + - **性能**:第一种使用方式中 `collection[index]` 的索引访问,会因为边界检查(Bounds Checking)导致运行时的性能损耗 —— Rust会检查并确认 `index` 是否落在集合内,但是第二种直接迭代的方式就不会触发这种检查,因为编译器会在编译时就完成分析并证明这种访问是合法的 - **安全**:第一种方式里对 `collection` 的索引访问是非连续的,存在一定可能性在两次访问之间,`collection` 发生了变化,导致脏数据产生。而第二种直接迭代的方式是连续访问,因此不存在这种风险(这里是因为所有权吗?是的话可能要强调一下) @@ -311,6 +313,7 @@ fn main() { 以上代码当 `counter` 递增到 `10` 时,就会通过 `break` 返回一个 `counter * 2` 的值,最后赋给 `result` 并打印出来。 这里有几点值得注意: + - **break 可以单独使用,也可以带一个返回值**,有些类似 `return` - **loop 是一个表达式**,因此可以返回一个值 diff --git a/book/contents/basic/ownership/ownership.md b/book/contents/basic/ownership/ownership.md index 0131ea9f..8c6ec16f 100644 --- a/book/contents/basic/ownership/ownership.md +++ b/book/contents/basic/ownership/ownership.md @@ -1,6 +1,7 @@ # 所有权 所有的程序都必须和计算机内存打交道,如何从内存中申请空间来存放程序的运行内容,如何在不需要的时候释放这些空间,成了重中之重,也是所有编程语言设计的难点之一。在计算机语言不断演变过程中,出现了三种流派: + - **垃圾回收机制(GC)**,在程序运行时不断寻找不再使用的内存,典型代表:Java、Go - **手动管理内存的分配和释放**, 在程序中,通过函数调用的方式来申请和释放内存,典型代表:C++ - **通过所有权来管理内存**,编译器在编译时会根据一系列规则进行检查 @@ -103,6 +104,7 @@ let s = "hello" 之前提到过,本章会用 `String` 作为例子,因此这里会进行一下简单的介绍,具体的 `String` 学习请参见 [String类型](../compound-type/string-slice.md)。 我们已经见过字符串字面值 `let s ="hello"`,`s` 是被硬编码进程序里的字符串值(类型为 `&str` )。字符串字面值是很方便的,但是它并不适用于所有场景。原因有二: + - **字符串字面值是不可变的**,因为被硬编码到程序代码中 - 并非所有字符串的值都能在编写代码时得知 @@ -185,6 +187,7 @@ error[E0382]: use of moved value: `s1` ``` 现在再回头看看之前的规则,相信大家已经有了更深刻的理解: + > 1. Rust 中每一个值都 `有且只有` 一个所有者(变量) > 2. 当所有者(变量)离开作用域范围时,这个值将被丢弃(free) diff --git a/book/contents/basic/result-error/panic.md b/book/contents/basic/result-error/panic.md index 4dd6ca22..b4d4911d 100644 --- a/book/contents/basic/result-error/panic.md +++ b/book/contents/basic/result-error/panic.md @@ -24,6 +24,7 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace ``` 以上信息包含了两条重要信息: + - `main` 函数所在的线程崩溃了,发生的代码位置是 `src/main.rs` 中的第2行第5个字符(去除该行前面的空字符) - 在使用时加上一个环境变量可以获取更详细的栈展开信息:`RUST_BACKTRACE=1 cargo run` @@ -132,6 +133,7 @@ let home: IpAddr = "127.0.0.1".parse().unwrap(); #### 可能导致全局有害状态时 有害状态大概分为几类: + - 非预期的错误 - 后续代码的运行会受到显著影响 - 内存安全的问题 @@ -148,6 +150,7 @@ let home: IpAddr = "127.0.0.1".parse().unwrap(); 本来不想写这块儿内容,因为真的难写,但是转念一想,既然号称圣经,那么本书就得与众不同,避重就轻显然不是该有的态度。 当调用 `panic!` 宏时,它会 + 1. 格式化 `panic` 信息,然后使用该信息作为参数,调用 `std::panic::panic_any()` 函数 2. `panic_any` 会检查应用是否使用了 `panic hook`,如果使用了,该 `hook` 函数就会被调用(`hook` 是一个钩子函数,是外部代码设置的,用于在 `panic` 触发时,执行外部代码所需的功能) 3. 当 `hook` 函数返回后,当前的线程就开始进行栈展开:从 `panic_any` 开始,如果寄存器或者栈因为某些原因信息错乱了,那很可能该展开会发生异常,最终线程会直接停止,展开也无法继续进行 diff --git a/book/contents/basic/result-error/result.md b/book/contents/basic/result-error/result.md index 50215deb..2fe386cd 100644 --- a/book/contents/basic/result-error/result.md +++ b/book/contents/basic/result-error/result.md @@ -23,6 +23,7 @@ fn main() { > #### 如何获知变量类型或者函数的返回类型 > > 有几种常用的方式,此处更推荐第二种方法: +> > - 第一种是查询标准库或者三方库文档,搜索 `File`,然后找到它的 `open` 方法 > - 在 [Rust IDE](../../first-try/editor.md) 章节,我们推荐了 `VSCode` IDE 和 `rust-analyzer` 插件,如果你成功安装的话,那么就可以在 `VSCode` 中很方便的通过代码跳转的方式查看代码,同时 `rust-analyzer` 插件还会对代码中的类型进行标注,非常方便好用! > - 你还可以尝试故意标记一个错误的类型,然后让编译器告诉你: @@ -90,6 +91,7 @@ fn main() { ``` 上面代码在匹配出 `error` 后,又对 `error` 进行了详细的匹配解析,最终结果: + - 如果是文件不存在错误 `ErrorKind::NotFound`,就创建文件,这里创建文件`File::create` 也是返回 `Result`,因此继续用 `match` 对其结果进行处理:创建成功,将新的文件句柄赋值给 `f`,如果失败,则 `panic` - 剩下的错误,一律 `panic` @@ -165,6 +167,7 @@ fn read_username_from_file() -> Result { ``` 有几点值得注意: + - 该函数返回一个 `Result` 类型,当读取用户名成功时,返回 `Ok(String)`,失败时,返回 `Err(io:Error)` - `File::open` 和 `f.read_to_string` 返回的 `Result` 中的 `E` 就是 `io::Error` @@ -286,6 +289,7 @@ fn first(arr: &[i32]) -> Option<&i32> { } ``` 这段代码无法通过编译,切记:`?` 操作符需要一个变量来承载正确的值,这个函数只会返回 `Some(&i32)` 或者 `None`,只有错误值能直接返回,正确的值不行,所以如果数组中存在 0 号元素,那么函数第二行使用 `?` 后的返回类型为 `&i32` 而不是 `Some(&i32)`。因此 `?` 只能用于以下形式: + - `let v = xxx()?;` - `xxx()?.yyy()?;`