diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 0413159..7db3292 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -32,7 +32,15 @@ - [BTreeMap](./chapter_3/chapter_3_15_6.md) - [BTreeSet](./chapter_3/chapter_3_15_7.md) - [智能指针](./chapter_3/chapter_3_16.md) - - [包、crate、模块-todo](./chapter_3/chapter_3_17.md) + - [智能指针介绍](./chapter_3/chapter_3_16_1.md) + - [Box智能指针](./chapter_3/chapter_3_16_2.md) + - [Deref trait](./chapter_3/chapter_3_16_3.md) + - [Drop trait](./chapter_3/chapter_3_16_4.md) + - [Rc智能指针](./chapter_3/chapter_3_16_5.md) + - [RefCell智能指针](./chapter_3/chapter_3_16_6.md) + - [引用循环、内存泄露、Weak智能指针](./chapter_3/chapter_3_16_7.md) + - [包、crate、模块](./chapter_3/chapter_3_17.md) + - [包、crate和模块介绍](./chapter_3/chapter_3_17_1.md) - [测试-todo](./chapter_3/chapter_3_18.md) - [再谈注释-todo](./chapter_3/chapter_3_19.md) - [Rust并发编程](./chapter_3/chapter_3_20.md) diff --git a/src/chapter_1.md b/src/chapter_1.md index 029bd43..c40aac5 100644 --- a/src/chapter_1.md +++ b/src/chapter_1.md @@ -1,7 +1,9 @@ # 1 前言:为什么写这本书 + 和Rust结缘是2019年,当时为了能看懂libra的源码,开始了Rust的学习。起初的目标只是能看懂Rust的代码就好,谁知一看竟然发现这门编程语言很对我的胃口,于是开启了真正的Rust学习之路。彼时的工作中并没有使用Rust,但是周围的小伙伴有在研究谷歌操作系统fuchsia,跟他们的交流更加坚定了我要学习这门编程语言的想法。 所谓“拳不离手,曲不离口”,要能熟练的掌握一门编程语言必须要有大量的练习,但是因为在工作中并不会使用Rust,这无疑会让Rust学习的效果打折扣。为了促使自己不断的学习输入,我想到了边学习边录制视频,然后将视频放到B站上。虽然当时对Rust的理解不够深入,甚至是有些视频中还有错误,但还是让我有了一批一起学习Rust的粉丝。 + 2021年换工作后算是真正开始使用Rust了,在真正使用Rust的过程中也曾产生过撰写一本Rust教程的想法。但是一直感觉写书是一件严肃的事情,更何况自己的Rust水平也只能算是个熟练的使用者而已。直到最近和DaviRain聊天,说起如果能有办法降低一点点Rust入门的难度,也算是给Rust做了一点贡献,于是决定和DaviRain写这本书。 所以本书定位就是为了方便入门Rust,在本书中: @@ -12,7 +14,7 @@ 本书撰写过程中,我们参考了其它的Rust书籍,参考的书籍主要有《The Rust Programming Language》、《Rust语言圣经》、《Rust入门秘籍》、《Rust第一课》等。本书第2.1节(为什么选择Rust)是由chatGPT4进行生成;第3.8节复合数据类型、第3.15节常见的Collections、第4章Rust使用技巧由DaviRain完成;第3.9节Trait部分由DaviRain和令狐壹冲一起完成;其它章节由令狐壹冲完成。但是由于我们自身水平的问题,本书必然存在一些错误和描述不清楚的地方,希望各位读者能够不吝指正! 愿本书能让您的Rust入门之路能够变得不再崎岖! - - - - 令狐壹冲 + + + + 令狐壹冲 diff --git a/src/chapter_2.md b/src/chapter_2.md index 6d46ce7..27907b7 100644 --- a/src/chapter_2.md +++ b/src/chapter_2.md @@ -1,13 +1,16 @@ # 2 欢迎来到Rust的世界 + ## 2.1 为什么选择Rust + ### 2.1.1 Rust的历史与背景 + Rust是一种现代的系统编程语言,它注重性能、安全性和并发。Rust最初由Mozilla研究院的Graydon Hoare于2006年开始设计,最早的目标是为了解决C++在系统编程领域的一些痛点。在2010年,Mozilla正式开始支持这个项目,从那时起,Rust开始迅速发展并逐渐成为一个强大的编程语言。 -1. 设计理念 +#### 1. 设计理念 Rust的设计理念是将系统编程的性能与安全性相结合。它的核心创新是引入了一套所有权系统,这套系统能在编译时检测许多常见的内存错误,如空指针解引用、数据竞争等。这种设计使得Rust在保持C和C++级别性能的同时,提供了更高的内存安全性。 -2. 发展历程 +#### 2. 发展历程 - 2006年:Graydon Hoare开始设计Rust。 - 2009年:Mozilla开始关注Rust,希望它能成为一种更安全的系统编程语言。 @@ -18,36 +21,37 @@ Rust的设计理念是将系统编程的性能与安全性相结合。它的核 - 2016年:Rust开始获得广泛关注,社区逐渐壮大。 - 2018年:WebAssembly的推广使得Rust成为一个受欢迎的前端编程语言。 -3. 社区与生态系统 +#### 3. 社区与生态系统 随着Rust的发展,其社区和生态系统也在不断壮大。Rust编程语言被广泛应用于各种领域,如网络编程、游戏开发、操作系统、嵌入式系统、区块链等。此外,Rust已经连续多年被Stack Overflow开发者调查评为最受欢迎的编程语言。 Rust的成功和流行归功于其活跃的社区和丰富的第三方库。社区不断努力改进和扩展Rust的功能,使其成为一个更加实用和强大的编程语言。总的来说,Rust的历史与背景显示了一个充满创新、活力和潜力的编程语言,它将继续为现代软件开发带来更多的机会。 ### 2.1.2 Rust的主要优势 + Rust的设计目标是为程序员提供一种高性能、安全且具有现代化特性的系统编程语言。以下是Rust的一些主要优势: -1. 内存安全性 +#### 1. 内存安全性 Rust通过其独特的所有权系统、生命周期和借用检查器确保了内存安全。这些功能让Rust能够在编译时检测许多常见的内存错误,如悬垂指针、空指针解引用、数据竞争等。这种设计减少了内存泄漏、悬垂指针等问题的出现,从而使得编写安全的代码变得更加容易。 -2. 高性能 +#### 2. 高性能 Rust注重零开销抽象(zero-cost abstractions),这意味着Rust提供的高级抽象不会对程序性能产生负面影响。Rust的性能与C和C++相当,这使得它成为一个理想的选择,尤其是对于对性能要求较高的系统编程任务。 -3. 并发友好 +#### 3. 并发友好 Rust的内存模型和类型系统让并发变得更加简单和安全。通过提供原子操作和线程安全的数据结构,Rust在编译时就可以预防数据竞争等多线程问题。这使得Rust在多核处理器和分布式系统领域具有优势。 -4. 易于集成 +#### 4. 易于集成 Rust的C兼容的FFI(Foreign Function Interface)允许轻松地与其他编程语言集成。这使得Rust可以逐步替换现有的C和C++代码,提高系统的安全性和性能,而无需重写整个项目。 -5. 生产力与现代化特性 +#### 5. 生产力与现代化特性 Rust提供了许多现代编程语言的特性,如模式匹配、类型推断、闭包等。这些特性可以提高程序员的生产力,使得编写代码更加愉快。Rust的丰富的标准库和第三方库也使得开发者能够轻松地找到所需的功能。 -6. 活跃的社区与生态系统 +#### 6. 活跃的社区与生态系统 Rust拥有一个友好、活跃且不断壮大的社区。开发者们分享知识、讨论问题、改进编译器和标准库,这使得Rust不断进化和完善。Rust生态系统的成熟也使得开发者能够更容易地找到和使用高质量的第三方库。 @@ -67,51 +71,53 @@ Rust拥有一个友好、活跃且不断壮大的社区。开发者们分享知 Rust的高性能、内存安全和现代化特性使其在许多领域具有广泛的应用。以下是一些Rust在不同领域中的典型应用: -1. 网络编程 +#### 1. 网络编程 在网络编程领域,Rust的性能和安全性特性使其成为构建可靠、高性能服务器和网络应用的理想选择。例如,Tokio是一个使用Rust编写的高性能异步运行时,它可以让开发者轻松地构建高吞吐量、低延迟的服务。同时,Hyper是一个快速的HTTP库,可以用于构建网络客户端和服务器。 -2. WebAssembly +#### 2. WebAssembly Rust在WebAssembly开发中具有很大的优势。由于其性能和安全性,Rust成为了一个受欢迎的WebAssembly目标语言。通过将Rust编译为WebAssembly,开发者可以构建高性能、安全的前端应用程序,提高网页的加载速度和运行效率。 -3. 游戏开发 +#### 3. 游戏开发 Rust在游戏开发领域具有很大的潜力。其高性能和内存安全性特性使得Rust成为游戏开发的理想选择。Amethyst和Bevy是两个使用Rust编写的游戏引擎,它们为开发者提供了丰富的功能和性能优势。同时,Rust也可以与现有的游戏引擎(如Unity和Unreal Engine)进行集成,提供更安全的原生插件和后端服务。 -4. 嵌入式系统和物联网 +#### 4. 嵌入式系统和物联网 Rust在嵌入式系统和物联网领域具有优势。由于其高性能、低内存占用和安全性特性,Rust成为了嵌入式设备开发的理想选择。使用Rust可以更容易地构建可靠、安全的嵌入式系统。此外,Rust在实时操作系统(RTOS)如Tock OS等项目中也得到了应用。 -5. 操作系统开发 +#### 5. 操作系统开发 Rust在操作系统开发中具有很大的潜力。其内存安全和高性能特性使得Rust成为开发创新操作系统的理想选择。例如,Redox OS是一个使用Rust编写的现代化、安全的微内核操作系统。 -6. 区块链技术 +#### 6. 区块链技术 在区块链领域,Rust的高性能和安全性特性使其成为构建区块链系统的理想选择。一些知名的区块链项目,如Parity和Solana,都采用了Rust来实现其核心组件。这些项目展示了Rust在处理高并发、安全性要求高的场景中的能力。 -7. 机器学习和数据科学 +#### 7. 机器学习和数据科学 虽然Python在机器学习和数据科学领域占据主导地位,但Rust也在逐渐成为一个有吸引力的选择。Rust的高性能特性使其在计算密集型任务中具有优势。ArrayFire-rust和ndarray等库提供了高性能的数组计算和线性代数功能,而tch-rs等库则提供了Rust的Torch绑定,允许在Rust中使用深度学习功能。 -8. 跨平台开发 +#### 8. 跨平台开发 Rust具有良好的跨平台支持,可以轻松地在不同的操作系统和硬件架构上运行。这使得Rust成为构建跨平台应用的理想选择。例如,使用Rust编写的GUI库(如druid和iced)可以帮助开发者轻松地构建跨平台的桌面应用。 -9. 系统工具与实用程序 +#### 9. 系统工具与实用程序 Rust在系统工具和实用程序开发中也表现出色。Rust编写的工具可以在性能、安全性和可维护性方面取得优异的表现。例如,ripgrep是一个使用Rust编写的高性能grep工具,它在速度和功能上超越了许多现有的grep实现。 -10. Web开发 +#### 10. Web开发 虽然Rust主要用于系统编程,但它也在Web开发领域取得了一定的成功。使用Rocket、Actix Web和Tide等Web框架,开发者可以用Rust构建高性能、安全的Web应用程序和API服务。 综上所述,Rust在不同领域的应用表明了它作为一种通用编程语言的潜力。随着Rust社区和生态系统的不断壮大,Rust将继续在各个领域发挥越来越重要的作用。 ## 2.2 安装和配置Rust + ### 2.2.1 安装Rust -1. 对于macOS和Linux用户 + +#### 1. 对于macOS和Linux用户 通过如下命令进行安装: @@ -119,11 +125,11 @@ Rust在系统工具和实用程序开发中也表现出色。Rust编写的工具 curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` -2. 对于Windows用户 +#### 2. 对于Windows用户 -- 访问https://rustup.rs/ ,点击"Get started"。 -- 下载rustup-init.exe安装程序。 -- 双击rustup-init.exe运行安装程序,按照提示操作。 +- 访问[https://rustup.rs/](https://rustup.rs/) ,点击"Get started"。 +- 下载`rustup-init.exe`安装程序。 +- 双击`rustup-init.exe`运行安装程序,按照提示操作。 - 在安装过程中,选择默认选项(按回车键)即可。 - 安装完成后,重启计算机。 @@ -138,9 +144,11 @@ rustc --version ``` ## 2.3 第一个Rust程序 -本节将编写和运行本书的第一个Rust程序:Hello, World! + +本节将编写和运行本书的第一个Rust程序:`Hello, World!` ### 2.3.1 创建一个新的Rust项目 + Rust使用Cargo作为官方的构建工具和包管理器,下面通过Cargo创建一个新的Rust项目: ```bash @@ -154,33 +162,38 @@ cargo new hello_world 其中Cargo.toml是项目的配置文件,src为源代码目录,main.rs为主程序文件。 ### 2.3.2 编写程序 -默认情况下,main.rs文件中已经包含了一个简单的Hello, World程序,代码如下: + +默认情况下,main.rs文件中已经包含了一个简单的`Hello, World`程序,代码如下: ```Rust fn main() { println!("Hello, world!"); } ``` -其中,main函数是Rust程序的入口;而println!则是一个Rust宏(而非函数),用于在标准输出上打印一行文本。 + +其中,main函数是Rust程序的入口;而`println!`则是一个Rust宏(而非函数),用于在标准输出上打印一行文本。 ### 2.3.3 编译并运行Hello, World程序 -运行程序需要进去到项目目录中,然后运行cargo run命令即可,步骤如下: + +运行程序需要进去到项目目录中,然后运行`cargo run`命令即可,步骤如下: ```bash cd hello_world cargo run ``` + 运行结果如下: ![运行结果](../assets/63.png) ### 2.3.4 理解Rust源码的基本结构 + 下面简单介绍Rust源码的结构。 -1. main函数 +#### 1. main函数 main函数是程序的入口。当运行程序时,main函数将首先被调用。Rust中函数的定义以关键字fn开始,后跟函数名和参数列表,函数体则由一对花括号包围。main函数没有参数,也没有返回值。 -2. println!宏 +#### 2. println!宏 -println!是一个用于向控制台输出一行文本的Rust宏。在例子中,我们向println!宏传递了一个字符串字面量"Hello, World!"作为参数。println!宏会将这个字符串作为控制台输出。 +`println!`是一个用于向控制台输出一行文本的Rust宏。在例子中,我们向`println!`宏传递了一个字符串字面量`"Hello, World!"`作为参数。`println!`宏会将这个字符串作为控制台输出。 diff --git a/src/chapter_3/chapter_3_1.md b/src/chapter_3/chapter_3_1.md index b2a0246..ca63ac2 100644 --- a/src/chapter_3/chapter_3_1.md +++ b/src/chapter_3/chapter_3_1.md @@ -38,8 +38,8 @@ Rust中变量分为不可变变量和可变变量。不可变变量不能对其 b = 3; ``` -设计思考: -从编译器的角度,如果一个值定义为不可变变量,那它就不会改变,更易于推导。想想一下如果代码非常多,如果变量不会变化,但是允许它可变,其实会更容易滋生bug。 +> 设计思考: +> 从编译器的角度,如果一个值定义为不可变变量,那它就不会改变,更易于推导。想想一下如果代码非常多,如果变量不会变化,但是允许它可变,其实会更容易滋生bug。 ## 3.1.3常量 diff --git a/src/chapter_3/chapter_3_10_1.md b/src/chapter_3/chapter_3_10_1.md index 4d789df..a156d77 100644 --- a/src/chapter_3/chapter_3_10_1.md +++ b/src/chapter_3/chapter_3_10_1.md @@ -19,267 +19,270 @@ pub trait GetInformation { ## 2. 为类型实现trait -- 为类型实现trait +### 2.1 为类型实现trait - 代码示例如下: - ```rust - // 定义trait - pub trait GetInformation { - fn get_name(&self) -> &String; - fn get_age(&self) -> u32; - } +代码示例如下: +```rust +// 定义trait +pub trait GetInformation { + fn get_name(&self) -> &String; + fn get_age(&self) -> u32; +} - pub struct Student { - pub name: String, - pub age: u32, +pub struct Student { + pub name: String, + pub age: u32, +} +// 为Student类型实现GetInformation trait +impl GetInformation for Student { + fn get_name(&self) -> &String { + &self.name } - // 为Student类型实现GetInformation trait - impl GetInformation for Student { - fn get_name(&self) -> &String { - &self.name - } - fn get_age(&self) -> u32 { - self.age - } + fn get_age(&self) -> u32 { + self.age } +} - pub struct Teacher { - pub name: String, - pub age: u32, +pub struct Teacher { + pub name: String, + pub age: u32, +} +// 为Teacher类型实现GetInformation trait +impl GetInformation for Teacher { + fn get_name(&self) -> &String { + &self.name } - // 为Teacher类型实现GetInformation trait - impl GetInformation for Teacher { - fn get_name(&self) -> &String { - &self.name - } - fn get_age(&self) -> u32 { - self.age - } + fn get_age(&self) -> u32 { + self.age } +} - fn main() { - let s = Student { - name: "alice".to_string(), - age: 18, - }; - // 可以在类型Student上使用GetInfomation trait中定义的方法 - println!("s.name = {:?}, s.age = {:?}", s.get_name(), s.get_age()); +fn main() { + let s = Student { + name: "alice".to_string(), + age: 18, + }; + // 可以在类型Student上使用GetInfomation trait中定义的方法 + println!("s.name = {:?}, s.age = {:?}", s.get_name(), s.get_age()); - let t = Teacher { - name: "bob".to_string(), - age: 25, - }; - // 可以类型Teacher使用GetInfomation trait中定义的方法 - println!("t.name = {:?}, t.age = {:?}", t.get_name(), t.get_age()); - } - ``` + let t = Teacher { + name: "bob".to_string(), + age: 25, + }; + // 可以类型Teacher使用GetInfomation trait中定义的方法 + println!("t.name = {:?}, t.age = {:?}", t.get_name(), t.get_age()); +} +``` -- 可以在trait定义时提供默认实现 +### 2.2 可以在trait定义时提供默认实现 - 可以在定义trait的时候提供默认的行为,trait的类型可以使用默认的行为,示例如下: - ```rust - // 定义trait - pub trait GetInformation { - fn get_name(&self) -> &String; - fn get_age(&self) -> u32 { // 在定义trait时就提供默认实现 - 25u32 - } +可以在定义trait的时候提供默认的行为,trait的类型可以使用默认的行为,示例如下: +```rust +// 定义trait +pub trait GetInformation { + fn get_name(&self) -> &String; + fn get_age(&self) -> u32 { // 在定义trait时就提供默认实现 + 25u32 } +} - pub struct Student { - pub name: String, - pub age: u32, +pub struct Student { + pub name: String, + pub age: u32, +} +// 为Student类型实现GetInformation trait +impl GetInformation for Student { + fn get_name(&self) -> &String { + &self.name } - // 为Student类型实现GetInformation trait - impl GetInformation for Student { - fn get_name(&self) -> &String { - &self.name - } - // 实现get_age方法,Student不会使用trait定义时的默认实现 - fn get_age(&self) -> u32 { - self.age - } + // 实现get_age方法,Student不会使用trait定义时的默认实现 + fn get_age(&self) -> u32 { + self.age } +} - pub struct Teacher { - pub name: String, - pub age: u32, - } - // 为Teacher类型实现GetInformation trait - impl GetInformation for Teacher { - fn get_name(&self) -> &String { - &self.name - } - // 不实现get_age方法,将使用trait的默认实现 +pub struct Teacher { + pub name: String, + pub age: u32, +} +// 为Teacher类型实现GetInformation trait +impl GetInformation for Teacher { + fn get_name(&self) -> &String { + &self.name } + // 不实现get_age方法,将使用trait的默认实现 +} - fn main() { - let s = Student { - name: "alice".to_string(), - age: 18, - }; - // 可以在类型Student上使用GetInfomation trait中定义的方法 - // 输出t.name = "bob", t.age = 25 - println!("s.name = {:?}, s.age = {:?}", s.get_name(), s.get_age()); +fn main() { + let s = Student { + name: "alice".to_string(), + age: 18, + }; + // 可以在类型Student上使用GetInfomation trait中定义的方法 + // 输出t.name = "bob", t.age = 25 + println!("s.name = {:?}, s.age = {:?}", s.get_name(), s.get_age()); - let t = Teacher { - name: "bob".to_string(), - age: 25, - }; - // 可以类型Teacher使用GetInfomation trait中定义的方法(t.get_age将使用定义trait时的默认方法) - // 输出t.name = "bob", t.age = 25 - println!("t.name = {:?}, t.age = {:?}", t.get_name(), t.get_age()); - } - ``` + let t = Teacher { + name: "bob".to_string(), + age: 25, + }; + // 可以类型Teacher使用GetInfomation trait中定义的方法(t.get_age将使用定义trait时的默认方法) + // 输出t.name = "bob", t.age = 25 + println!("t.name = {:?}, t.age = {:?}", t.get_name(), t.get_age()); +} +``` 如果定义trait时提供了某个方法的默认实现,则: - - 如果为类型实现该trait时,为该类型实现了此方法,则使用自己实现的方法(如上面示例中的Student类型); - - 如果为类型实现该trait时,没有为该类型实现此方法,则该类型使用trait提供的默认实现(如上面的Teacher类型)。 +- 如果为类型实现该trait时,为该类型实现了此方法,则使用自己实现的方法(如上面示例中的Student类型); +- 如果为类型实现该trait时,没有为该类型实现此方法,则该类型使用trait提供的默认实现(如上面的Teacher类型)。 ## 3. trait作为参数 -- trait作为参数 - trait可以用来参数,示例如下: +### 3.1 trait作为参数 - ```rust - // 定义trait - pub trait GetInformation { - fn get_name(&self) -> &String; - fn get_age(&self) -> u32 { - 25u32 - } +trait可以用来参数,示例如下: + +```rust +// 定义trait +pub trait GetInformation { + fn get_name(&self) -> &String; + fn get_age(&self) -> u32 { + 25u32 } +} - pub struct Student { - pub name: String, - pub age: u32, +pub struct Student { + pub name: String, + pub age: u32, +} +// 为Student类型实现GetInformation trait +impl GetInformation for Student { + fn get_name(&self) -> &String { + &self.name } - // 为Student类型实现GetInformation trait - impl GetInformation for Student { - fn get_name(&self) -> &String { - &self.name - } - fn get_age(&self) -> u32 { - self.age - } + fn get_age(&self) -> u32 { + self.age } +} - pub struct Teacher { - pub name: String, - pub age: u32, +pub struct Teacher { + pub name: String, + pub age: u32, +} +// 为Teacher类型实现GetInformation trait +impl GetInformation for Teacher { + fn get_name(&self) -> &String { + &self.name } - // 为Teacher类型实现GetInformation trait - impl GetInformation for Teacher { - fn get_name(&self) -> &String { - &self.name - } +} + +// 参数类型必须是实现了GetInfomation trait的类型 +pub fn print_information(item: impl GetInformation) { + println!("name = {}", item.get_name()); + println!("age = {}", item.get_age()); +} + +fn main() { + let s = Student { + name: "alice".to_string(), + age: 18, + }; + print_information(s); + + let t = Teacher { + name: "bob".to_string(), + age: 25, + }; + print_information(t); +} +``` + +在上面的例子中,函数`pub fn print_information(item: impl GetInformation)`的要求参数item必须实现`GetInformation trait`,否则无法调用该参数。 + +### 3.2 使用`trait bound`语法 + + +上面中的print_information函数还可以写成如下: +```rust +// 使用trait bound的写法一 +pub fn print_information(item: T) { + println!("name = {}", item.get_name()); + println!("age = {}", item.get_age()); +} +``` + +这种写法叫做Trait bound语法,它是Rust中用于指定泛型类型参数所需的trait的一种方式,它还可以使用where关键字写成如下: +```rust +// 使用trait bound的写法二 +pub fn print_information(item: T) +where + T: GetInformation, +{ + println!("name = {}", item.get_name()); + println!("age = {}", item.get_age()); +} +``` + +### 3.3 通过“`+`”指定多个trait bound + +可以要求类型实现多个trait,示例如下: +```rust +pub trait GetName { + fn get_name(&self) -> &String; +} +pub trait GetAge { + fn get_age(&self) -> u32; +} + +//使用trait bound写法一,类型T必须实现GetName和GetAge trait +pub fn print_information1(item: T) { + println!("name = {}", item.get_name()); + println!("age = {}", item.get_age()); +} + +//使用trait bound写法二,类型T必须实现GetName和GetAge trait +pub fn print_information2(item: T) +where + T: GetName + GetAge, +{ + println!("name = {}", item.get_name()); + println!("age = {}", item.get_age()); +} + +#[derive(Clone)] +struct Student { + name: String, + age: u32, +} + +impl GetName for Student { + fn get_name(&self) -> &String { + &self.name } +} - // 参数类型必须是实现了GetInfomation trait的类型 - pub fn print_information(item: impl GetInformation) { - println!("name = {}", item.get_name()); - println!("age = {}", item.get_age()); +impl GetAge for Student { + fn get_age(&self) -> u32 { + self.age } +} - fn main() { - let s = Student { - name: "alice".to_string(), - age: 18, - }; - print_information(s); +fn main() { + let s = Student { + name: "alice".to_string(), + age: 18u32, + }; + print_information1(s.clone()); + print_information1(s); +} +``` - let t = Teacher { - name: "bob".to_string(), - age: 25, - }; - print_information(t); - } - ``` - - 在上面的例子中,函数`pub fn print_information(item: impl GetInformation)`的要求参数item必须实现`GetInformation trait`,否则无法调用该参数。 - -- 使用`trait bound`语法 - - 上面中的print_information函数还可以写成如下: - ```rust - // 使用trait bound的写法一 - pub fn print_information(item: T) { - println!("name = {}", item.get_name()); - println!("age = {}", item.get_age()); - } - ``` - - 这种写法叫做Trait bound语法,它是Rust中用于指定泛型类型参数所需的trait的一种方式,它还可以使用where关键字写成如下: - ```rust - // 使用trait bound的写法二 - pub fn print_information(item: T) - where - T: GetInformation, - { - println!("name = {}", item.get_name()); - println!("age = {}", item.get_age()); - } - ``` - -- 通过“`+`”指定多个trait bound - - 可以要求类型实现多个trait,示例如下: - ```rust - pub trait GetName { - fn get_name(&self) -> &String; - } - pub trait GetAge { - fn get_age(&self) -> u32; - } - - //使用trait bound写法一,类型T必须实现GetName和GetAge trait - pub fn print_information1(item: T) { - println!("name = {}", item.get_name()); - println!("age = {}", item.get_age()); - } - - //使用trait bound写法二,类型T必须实现GetName和GetAge trait - pub fn print_information2(item: T) - where - T: GetName + GetAge, - { - println!("name = {}", item.get_name()); - println!("age = {}", item.get_age()); - } - - #[derive(Clone)] - struct Student { - name: String, - age: u32, - } - - impl GetName for Student { - fn get_name(&self) -> &String { - &self.name - } - } - - impl GetAge for Student { - fn get_age(&self) -> u32 { - self.age - } - } - - fn main() { - let s = Student { - name: "alice".to_string(), - age: 18u32, - }; - print_information1(s.clone()); - print_information1(s); - } - ``` - - 在上面的代码中,`print_information1`和`print_information2`函数要求其参数类型T必须实现`GetName`和`GetAge`两个trait,通过`+`来进行多个约束的连接。 +在上面的代码中,`print_information1`和`print_information2`函数要求其参数类型T必须实现`GetName`和`GetAge`两个trait,通过`+`来进行多个约束的连接。 ## 4. 返回trait的类型 trait类型可以作为函数的返回类型,示例如下: + ```rust pub trait GetName { fn get_name(&self) -> &String; @@ -310,6 +313,7 @@ fn main() { ``` 上面代码中,`produce_item_with_name`函数返回了一个实现了`GetName trait`的类型。不过需要注意的是,这种方式返回的是单一类型,例如如下的代码就是错误的,无法编译通过: + ```rust pub trait GetName { fn get_name(&self) -> &String; @@ -357,6 +361,7 @@ fn main() { 错误原因分析(非常重要): 上面的代码中的`produce_item_with_name`函数的定义实际上等价于如下: + ```rust pub fn produce_item_with_name(is_teacher: bool) -> T { ... @@ -506,4 +511,5 @@ fn main() { s.print_name(); //student实现了GetName trait,因此可是直接使用PrintName trait中的函数print_name } ``` + 上面的例子中,就是为实现了`GetName trait`的类型实现`PrintName trait`。 diff --git a/src/chapter_3/chapter_3_10_2.md b/src/chapter_3/chapter_3_10_2.md index 79100ef..3d36473 100644 --- a/src/chapter_3/chapter_3_10_2.md +++ b/src/chapter_3/chapter_3_10_2.md @@ -1,10 +1,10 @@ # 3.10.2 trait对象 -在上一节中第4点返回trait对象时,提到了produce_item_with_name函数返回的是单一的类型,即在编译时就确定了具体的类型,因此produce_item_with_name无法正确编译。为解决这种问题,Rust中引入了trait对象。 +在上一节中第4点返回trait对象时,提到了`produce_item_with_name`函数返回的是单一的类型,即在编译时就确定了具体的类型,因此`produce_item_with_name`无法正确编译。为解决这种问题,Rust中引入了trait对象。 ## 1. 使用trait对象 -在Rust中,trait自身不能当作数据类型来使用,但trait 对象可以当作数据类型使用。因此,可以将实现了Trait A的类型B、C、D当作trait A的trait对象来使用。使用trait对象时,基本都是以引用的方式使用,所以使用时通常是引用符号加dyn关键字(即&dyn)。 +在Rust中,trait自身不能当作数据类型来使用,但trait 对象可以当作数据类型使用。因此,可以将实现了`Trait A`的类型`B`、`C`、`D`当作`trait A`的trait对象来使用。使用trait对象时,基本都是以引用的方式使用,所以使用时通常是引用符号加`dyn`关键字(即`&dyn`)。 示例如下: ```Rust @@ -61,20 +61,22 @@ fn main() { chalie.get_name(); } ``` -上面代码中,Student和Teacher都实现了GetName trait,因此这两种类型可以当做GetName的trait对象使用。 + +上面代码中,`Student`和`Teacher`都实现了`GetName trait`,因此这两种类型可以当做`GetName`的trait对象使用。 ## 2. trait对象动态分发的原理 对于trait对象,需要说明如下几点: -- trait 对象大小不固定:这是因为,对于trait A,类型B可以实现trait A,类型C也可以实现trait A,因此A trait对象的大小是无法确定的(因为可能是B类型也可能是C类型)。 +- trait 对象大小不固定:这是因为,对于`trait A`,类型`B`可以实现`trait A`,类型`C`也可以实现`trait A`,因此A trait对象的大小是无法确定的(因为可能是B类型也可能是C类型)。 - 使用trait对象时,总是使用它们的引用的方式: - 虽然trait对象没有固定大小,但它的引用类型的大小固定,它由两个指针组成,因此占两个指针大小。 - 一个指针指向具体类型的实例。 - 另一个指针指向一个虚表vtable,vtable中保存了实例对于可以调用的实现于trait的方法。当调用方法时,直接从vtable中找到方法并调用。 - - trait对象的引用方式有多种。例如对于trait A,其trait对象类型的引用可以是&dyn A、Box、Rc等。 + - trait对象的引用方式有多种。例如对于`trait A`,其trait对象类型的引用可以是`&dyn A`、`Box`、`Rc`等。 下面通过一段代码来分析一下使用trait对象时内存的布局。代码如下: + ```Rust trait Vehicle { fn run(&self); @@ -110,28 +112,31 @@ fn main() { vehicle1.run(); vehicle2.run(); } -``` -在上面的代码中,vehicle1和vehicle1都是Vehicle trait对象的引用;对于vehicle1来说,trait对象的具体类型是Car;对于vehicle2来说,trait对象的具体类型是Truck。上面代码对应的内存布局如下: +``` + +在上面的代码中,vehicle1和vehicle1都是Vehicle trait对象的引用;对于vehicle1来说,trait对象的具体类型是`Car`;对于vehicle2来说,trait对象的具体类型是`Truck`。上面代码对应的内存布局如下: ![注释](../../assets/14.png) - + 变量car和变量truck分别是Car类型和Truck类型,存储在栈上;vehicle1和vehicle2是Vehicle trait对象的引用,具有两个指针,其中指针ptr指向具体类型的实例,vptr指向虚函数表;虚函数表存储在只读数据区。 - -更进一步的理解,虚函数表存储在程序的可执行文件中的只读数据段(.rodata)中,这个只读数据段在程序运行时被加载到内存中,因此虚函数表也是只读的。实现trait对象的时候,编译器会在对象的内存布局中添加一个指向虚函数表的指针,这个指针被称为虚函数表指针。在程序运行到vehicle1.run()和vehicle2.run()时,程序通过虚函数表找到对应的函数指针,然后来执行。 + +更进一步的理解,虚函数表存储在程序的可执行文件中的只读数据段(.rodata)中,这个只读数据段在程序运行时被加载到内存中,因此虚函数表也是只读的。实现trait对象的时候,编译器会在对象的内存布局中添加一个指向虚函数表的指针,这个指针被称为虚函数表指针。在程序运行到`vehicle1.run()`和`vehicle2.run()`时,程序通过虚函数表找到对应的函数指针,然后来执行。 ## 3. trait对象要求对象安全 - + 只有对象安全(object safe)的 trait 才可以组成 trait 对象。trait的方法满足以下两条要求才是对象安全的: -- 返回值类型不为 Self; +- 返回值类型不为 `Self`; - 方法没有任何泛型类型参数。 -分析: - ***不允许返回Self,是因为trait对象在产生时,原来的具体的类型会被抹去,Self具体是什么类型不知道,所以编译会报错; - 不允许携带泛型参数,是因为Rust用带泛型的类型在编译时会做单态化,而trait对象是运行时才确定,即一个运行时才能确定的东西里又包含一个需要在编译时确定的东西,相互冲突,必然是不行的***。 +> 分析: +> +>***不允许返回`Self`,是因为trait对象在产生时,原来的具体的类型会被抹去,Self具体是什么类型不知道,所以编译会报错; +> 不允许携带泛型参数,是因为Rust用带泛型的类型在编译时会做单态化,而trait对象是运行时才确定,即一个运行时才能确定的东西里又包含一个需要在编译时确定的东西,相互冲突,必然是不行的***。 + +如下代码编译会报错,因为`Clone`返回的是`Self`: -如下代码编译会报错,因为Clone返回的是Self: ```Rust pub struct Screen { pub components: Vec>, -} +} ``` diff --git a/src/chapter_3/chapter_3_10_3.md b/src/chapter_3/chapter_3_10_3.md index 268ef16..3c2cbce 100644 --- a/src/chapter_3/chapter_3_10_3.md +++ b/src/chapter_3/chapter_3_10_3.md @@ -93,6 +93,7 @@ impl Add for Point { ``` ## 7. std::iter::Iterator: + ```rust struct Counter { count: u32, diff --git a/src/chapter_3/chapter_3_11.md b/src/chapter_3/chapter_3_11.md index ad16742..91f2079 100644 --- a/src/chapter_3/chapter_3_11.md +++ b/src/chapter_3/chapter_3_11.md @@ -1,26 +1,32 @@ # 3.11 生命周期 + Rust中的生命周期是用来管理内存的一种机制。在Rust中,内存的所有权和使用必须是确定的,生命周期就是用来确定内存的使用范围的(说的更具体点,就是确定引用的有效范围): - 编译器大多数时间能够自动推导生命周期(3.11.1和3.11.2中的例子编译器都能自动推导); - 在多种类型存在,且编译器无法推导某个引用的生命周期时,需要在代码中显式的标明生命周期。 ## 3.11.1 悬垂指针和生命周期 + 生命周期的主要目的就是为了避免悬垂引用。考虑如下代码: + ```Rust fn main() { let r; { - let x = 5; + let x = 5; r = &x; } println!("r = {}", r); //r为悬垂引用 } ``` + 上面代码中,x的有效作用域是从第4行到第6行花括号结束前,即从第6行的花括号后开始,x变为无效,r为x的应用,此时将指向的是一块无效的内存。其内存示意图如下: ![注释](../../assets/15.png) ## 3.11.2 借用检查 + Rust编译器有一个借用检查器,用它来检查所有的应用的都是有效的,具体的方式为比较变量及其引用的作用域。 + ### 1. 示例1如下: ```Rust fn main() { @@ -33,9 +39,11 @@ fn main() { println!("r = {}", r); // | //r为悬垂引用 } //---------------------------------------+ ``` -对于上面的代码,借用检查器将r的生命周期标注为'a,将x的生命周期标注为'b,然后比较'a和'b的范围,发现'b < 'a,被引用的对象比它的引用者存在的时间还短,然后编译报错。 +对于上面的代码,借用检查器将`r`的生命周期标注为`'a`,将x的生命周期标注为`'b`,然后比较`'a`和`'b`的范围,发现`'b < 'a`,被引用的对象比它的引用者存在的时间还短,然后编译报错。 + ### 2. 示例2如下: + ```Rust fn main() { let x = 5; // ----------+-- 'b @@ -46,7 +54,7 @@ fn main() { // --+ | } // ----------+ ``` -对于上面的代码,借用检查器将x的生命周期标注为'b,将r的生命周期标注为'a,比较两者范围,发现'b > 'a,被引用对象比它的应用者存在的时间长,编译检查通过。 +对于上面的代码,借用检查器将x的生命周期标注为`'b`,将`r`的生命周期标注为`'a`,比较两者范围,发现`'b > 'a`,被引用对象比它的应用者存在的时间长,编译检查通过。 ## 3.11.3 编译器有时无法自动推导生命周期 @@ -66,15 +74,19 @@ fn main() { let r = longest(s1.as_str(), s2.as_str()); } ``` + 原因为:***在存在多个引用时,编译器有时会无法自动推导生命周期,此时就需要程序员在代码中手动去标注,通过为参数标注合适的生命周期来帮助编译器进行借用检查的分析***。 ## 3.11.4 标注生命周期 + ### 1. 生命周期标注的语法 在开始说明生命周期标注语法前,需要特别明确的是:生命周期标注并不会改变任何引用的实际作用域,标记的生命周期只是为了取悦编译器,让编译器不要难为代码。 -生命周期标注的语法为:生命周期参数名称必须以撇号'开头,其名称通常全是小写,类似于泛型,其名称非常短。比较常见的是使用'a作为第一个生命周期标注。生命周期参数标注位于引用符号&之后,并有一个空格来将引用类型与生命周期注解分隔开。 + +生命周期标注的语法为:生命周期参数名称必须以撇号`'`开头,其名称通常全是小写,类似于泛型,其名称非常短。比较常见的是使用`'a`作为第一个生命周期标注。生命周期参数标注位于引用符号`&`之后,并有一个空格来将引用类型与生命周期注解分隔开。 下面为生命周期标注的例子: + ```Rust &i32 // 引用 &'a i32 // 带有显式生命周期的引用 @@ -82,7 +94,9 @@ fn main() { ``` ### 2. 函数签名中的生命周期 + 对于3.11.3例子中的函数,显式标注生命周期后的代码如下: + ```Rust fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { @@ -92,10 +106,13 @@ fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { } } ``` -上面代码中,对x和y标注生命周期为'a,返回的引用的生命周期也为'a。当调用这个函数时,就要求传入的x和y的生命周期必须是大于等于'a的。当不遵守这个规则的参数传入时,借用检查器就会报错。 + +上面代码中,对x和y标注生命周期为`'a`,返回的引用的生命周期也为`'a`。当调用这个函数时,就要求传入的`x`和`y`的生命周期必须是大于等于`'a`的。当不遵守这个规则的参数传入时,借用检查器就会报错。 ### 3. 深入思考生命周期标注 -(1)指定生命周期参数的正确方式依赖函数实现的具体功能。如下代码中将不用标注y的生命周期,因为返回值不依赖于y的生命周期。 + +(1)指定生命周期参数的正确方式依赖函数实现的具体功能。如下代码中将不用标注`y`的生命周期,因为返回值不依赖于y的生命周期。 + ```Rust fn longest<'a>(x: &'a str, y: &str) -> &'a str { x @@ -103,6 +120,7 @@ fn longest<'a>(x: &'a str, y: &str) -> &'a str { ``` (2)当从函数返回一个引用,返回值的生命周期参数需要与一个参数的生命周期参数相匹配。如果返回的引用没有指向任何一个参数,那么唯一的可能就是它指向一个函数内部创建的值,它将会是一个悬垂引用。如下代码将编译错误: + ```Rust fn longest<'a>(x: &str, y: &str) -> &'a str { let result = String::from("really long string"); @@ -111,7 +129,9 @@ fn longest<'a>(x: &str, y: &str) -> &'a str { ``` ### 4. 结构体中的生命周期 + 结构体中的生命周期标注示例如下: + ```Rust #[derive(Debug)] struct A<'a> { @@ -126,12 +146,15 @@ fn main() { ``` ### 5. 生命周期省略 + 在大多数情况下,程序员不用在代码中显式标注生命周期,因为编译器能自动推导。不标注生命周期,我们称之为生命周期省略。例如下面的代码可以正确编译: + ```Rust fn get_s(s: &str) -> &str { s } ``` + 关于生命周期省略有如下说明: (1)遵守生命周期省略规则的情况下能明确变量的生命周期,则无需明确指定生命周期。函数或者方法的参数的生命周期称为输入生命周期,而返回值的生命周期称为输出生命周期。 @@ -142,20 +165,21 @@ fn get_s(s: &str) -> &str { 三条判断规则如下: - a、每个引用的参数都有它自己的生命周期参数。例如如下: - 一个引用参数的函数,其中有一个生命周期: ```fn foo<'a>(x: &'a i32)``` - 两个引用参数的函数,则有两个生命周期 :```fn foo<'a, 'b>(x: &'a i32, y: &'b i32)``` - - 以此类推。 - - b、如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数: - ```Rust - fn foo(x: &i32) -> &i32 等价于 fn foo<'a>(x: &'a i32) -> &'a i32 - ``` - c、如果方法有多个输入生命周期参数,不过其中之一因为方法的缘故为```&self```或者```&mut self```,那么```self```的生命周期被赋予所有输出生命周期参数。 +a、每个引用的参数都有它自己的生命周期参数。例如如下: + 一个引用参数的函数,其中有一个生命周期: `fn foo<'a>(x: &'a i32)` + 两个引用参数的函数,则有两个生命周期 :`fn foo<'a, 'b>(x: &'a i32, y: &'b i32)`以此类推。 + +b、如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数: +```Rust +fn foo(x: &i32) -> &i32 等价于 fn foo<'a>(x: &'a i32) -> &'a i32 +``` + +c、如果方法有多个输入生命周期参数,不过其中之一因为方法的缘故为`&self`或者`&mut self`,那么`self`的生命周期被赋予所有输出生命周期参数。 ### 6. 方法中的生命周期 + 结构体字段的生命周期必须总是在impl关键字之后声明并在结构体名称之后使用,这些声明周期是结构体类型的一部分,示例如下: + ```Rust struct StuA<'a> { name: &'a str, @@ -166,7 +190,9 @@ impl<'a> StuA<'a> { } } ``` + 下面的例子中,其方法没有显式标注生命周期,因为它符合生命周期省略规则中的第三条规则,代码如下: + ```Rust struct StuA<'a> { name: &'a str, @@ -179,13 +205,17 @@ impl<'a> StuA<'a> { ``` ### 7. 静态生命周期 -静态生命周期定义方式为:```'static```,其生命周期存活于整个程序期间。所有的字符字面值都拥有```'static```生命周期,代码中可以如下来标注: + +静态生命周期定义方式为:`'static`,其生命周期存活于整个程序期间。所有的字符字面值都拥有`'static`生命周期,代码中可以如下来标注: + ```Rust let s: &'static str = "I have a static filetime"; ``` ### 8. 结合泛型参数、trait bounds和生命周期的例子 + 下面示例为在同一函数中指定泛型类型参数、trait bounds 和生命周期: + ```Rust use std::fmt::Display; fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str diff --git a/src/chapter_3/chapter_3_12.md b/src/chapter_3/chapter_3_12.md index f5e5c20..b3e6106 100644 --- a/src/chapter_3/chapter_3_12.md +++ b/src/chapter_3/chapter_3_12.md @@ -1,15 +1,18 @@ # 3.12 错误处理 + Rust将错误分为两大类:可恢复的和不可恢复的错误。 - 可恢复错误通常代表向用户报告错误和重试操作是合理的情况,例如未找到文件。rust中使用```Result```来处理可恢复错误。 - 不可恢复错误是bug的同义词,如尝试访问超过数组结尾的位置。rust中通过```panic!```来实现。 ## 3.12.1 用panic!处理不可恢复错误 -panic的使用方式如下: + +`panic!`的使用方式如下: ```Rust fn main() { panic!("crash and burn"); } ``` + 运行该程序会打印如下错误: ![注释](../../assets/16.png) @@ -19,18 +22,24 @@ fn main() { ![注释](../../assets/17.png) ## 3.12.2 用Result处理可恢复错误 + ### 1. Result的定义 + Rust中使用```Result```类型处理可恢复错误,其定义如下: + ```Rust enum Result { Ok(T), Err(E), } ``` + ```T```和```E```是泛型类型参数,```T```代表成功是返回的```Ok```成员中的数据类型,```E```代表失败是返回的```Err```成员中的错误的类型。 ### 2. 使用Result + 使用示例如下: + ```Rust use std::fs::File; fn main() { @@ -45,7 +54,9 @@ fn main() { }; } ``` + 第3行返回的结果就是一个```Result```类型,可以使用```match```匹配```Result```的具体类型。下面为使用```str```作为```Result```中的错误```E```的例子: + ```Rust // 该函数返回结果为Result,其中T为(),E为具有静态生命周期的&str类型 fn produce_error(switch: bool) -> Result<(), &'static str> { @@ -70,9 +81,11 @@ fn main() { ``` ### 3. 失败时直接panic的简写 + 对于返回```Result```类型的函数,不用总是使用```match```去匹配结果,还可以使用简写获取到```Result```中的```T```类型,不过使用简写时,当```Result```是```Err```时程序会panic: -- 使用unwrap简写: +#### 3.1 使用unwrap简写: + ```Rust use std::fs::File; fn main() { @@ -81,7 +94,8 @@ fn main() { } ``` -- 使用except简写: +#### 3.2 使用except简写: + ```Rust use std::fs::File; @@ -92,8 +106,11 @@ fn main() { .expect("hello.txt should be included in this project"); } ``` + ### 4. 传播错误 + 除了函数中处理错误外,还可以选择让调用者知道这个错误并决定如何处理,这叫做传播错误。示例如下: + ```Rust fn produce_error(switch: bool) -> Result<(), &'static str> { if switch { @@ -123,9 +140,11 @@ fn main() { } } ``` + 在上面的代码中,```transmit_error```中就没有自己处理错误,而是选择将错误传递给外层。 传播错误可以用```?```进行简写,上面的```transmit_error```函数代码用简写方式示例如下: + ```Rust fn produce_error(switch: bool) -> Result<(), &'static str> { if switch { @@ -144,16 +163,18 @@ fn transmit_error(flag: bool) -> Result { fn main() { // 下面的调用将不会打印"如果produce_error函数返回的是错误将不会执行到这里" let result1 = transmit_error(true); - + println!("+++++++++++++++++++++++++++++++++++++++++++++++"); // 下面的调用将会打印"如果produce_error函数返回的是错误将不会执行到这里" - let result2 = transmit_error(false); - + let result2 = transmit_error(false); + ... } ``` + 下面是更复杂的简写: + ```Rust use std::io; use std::io::Read; @@ -161,13 +182,15 @@ use std::fs::File; fn read_username_from_file() -> Result { let mut s = String::new(); //如果open()失败将直接把“打开失败错误”返回,如果read_to_string失败也将把“读取失败错误”返回 - File::open("hello.txt")?.read_to_string(&mut s)?; + File::open("hello.txt")?.read_to_string(&mut s)?; Ok(s) } ``` + ```?```运算符被用于返回```Result```的函数,```Result```返回的是```Err```类型,则直接结束将错误传播到上一级。 ## 3.12.3 什么时候使用panic + 关于什么时候使用panic,什么使用Result,总结如下: - 示例、代码原型和测试适合使用panic,使用Result可以使用unwrap、expect的方式; diff --git a/src/chapter_3/chapter_3_13.md b/src/chapter_3/chapter_3_13.md index 66bd098..7f8c08c 100644 --- a/src/chapter_3/chapter_3_13.md +++ b/src/chapter_3/chapter_3_13.md @@ -34,6 +34,7 @@ fn main() { ## 3.13.2 闭包捕获环境 下面的示例展示了闭包捕获环境中的变量: + ```rust fn main() { let x = 4; diff --git a/src/chapter_3/chapter_3_14.md b/src/chapter_3/chapter_3_14.md index c13f084..2b6b65b 100644 --- a/src/chapter_3/chapter_3_14.md +++ b/src/chapter_3/chapter_3_14.md @@ -64,7 +64,8 @@ fn main() { - `iter_mut()`方法,创建一个在`&mut T`上进行迭代的迭代器,即集合自身可变引用上迭代的迭代器; - `into_iter()`方法,创建一个在`T`上迭代的迭代器,即移动了自身所有权的迭代器。 -Vec就实现了IntoIterator trait,所以它可以转换为迭代器,使用示例如下: +Vec就实现了`IntoIterator trait`,所以它可以转换为迭代器,使用示例如下: + ```rust fn main() { // 1、----使用iter()示例------ @@ -105,7 +106,8 @@ fn main() { ## 3.14.3 迭代器消费器 -迭代器通过next方法来消费一个项。下面为直接使用next方法的示例: +迭代器通过`next`方法来消费一个项。下面为直接使用`next`方法的示例: + ```rust fn main() { let v1 = vec![1, 2, 3]; @@ -116,8 +118,10 @@ fn main() { } ``` -`Iterator trait`有一系列由标准库提供的默认实现的方法,有些方法调用了next方法,这些调用next方法的方法被称为消费适配器。 -下面是一个使用消费是适配器sum方法的例子: +`Iterator trait`有一系列由标准库提供的默认实现的方法,有些方法调用了`next`方法,这些调用`next`方法的方法被称为消费适配器。 + +下面是一个使用消费是适配器`sum`方法的例子: + ```rust fn main() { let v1 = vec![1, 2, 3]; @@ -130,6 +134,7 @@ fn main() { ## 3.14.4 迭代器适配器 `Iterator trait`中定义了一类方法,可以把将当前迭代器变为不同类型的迭代器,这类方法就是迭代器适配器。 + ```rust fn main() { let v1: Vec = vec![1, 2, 3]; @@ -139,7 +144,9 @@ fn main() { println!("total = {:?}", v2); } ``` + 下面的代码也是使用迭代器适配器: + ```rust fn main() { let v1: Vec = vec![1, 11, 5, 34, 2, 10]; @@ -152,6 +159,7 @@ fn main() { ## 3.14.5 自定义迭代器 自定义迭代器示例如下: + ```rust struct Counter { count: u32, diff --git a/src/chapter_3/chapter_3_15_1.md b/src/chapter_3/chapter_3_15_1.md index 87ad5c3..c470848 100644 --- a/src/chapter_3/chapter_3_15_1.md +++ b/src/chapter_3/chapter_3_15_1.md @@ -4,13 +4,13 @@ Rust 中的 Vector(向量)是一个动态的、可增长的数组,它可 ## 1. 创建 Vector: -可以使用 Vec 类型创建一个向量,其中 T 是存储在向量中的元素的类型。要创建一个新的空向量,可以使用 Vec::new() 方法。 +可以使用 `Vec` 类型创建一个向量,其中 T 是存储在向量中的元素的类型。要创建一个新的空向量,可以使用 `Vec::new()` 方法。 ```rust let mut vec = Vec::new(); ``` -或者,可以使用宏 vec![] 初始化一个包含初始值的向量: +或者,可以使用宏 `vec![]` 初始化一个包含初始值的向量: ```rust let vec = vec![1, 2, 3, 4, 5]; @@ -18,7 +18,7 @@ let vec = vec![1, 2, 3, 4, 5]; ## 2. 添加元素: -可以使用 push() 方法向向量的末尾添加一个元素。 +可以使用 `push()` 方法向向量的末尾添加一个元素。 ```rust vec.push(6); ``` @@ -31,7 +31,7 @@ vec.push(6); let first = vec[0]; // 访问向量中的第一个元素 ``` -或者,可以使用 get() 方法安全地访问元素,它返回一个 Option 类型。如果指定的索引有效,则返回 Some(element),否则返回 None。 +或者,可以使用 `get()` 方法安全地访问元素,它返回一个 `Option` 类型。如果指定的索引有效,则返回 `Some(element)`,否则返回 `None`。 ```rust let first = vec.get(0); // 返回 Option<&T> @@ -39,7 +39,7 @@ let first = vec.get(0); // 返回 Option<&T> ## 4. 遍历元素: -可以使用 for 循环遍历向量中的所有元素。 +可以使用 `for` 循环遍历向量中的所有元素。 ```rust for element in &vec { @@ -49,7 +49,7 @@ for element in &vec { ## 5. 删除元素: -可以使用 remove() 方法删除向量中指定索引处的元素。此操作将删除元素并将后续元素向前移动。 +可以使用 `remove()` 方法删除向量中指定索引处的元素。此操作将删除元素并将后续元素向前移动。 ```rust vec.remove(0); // 删除向量中的第一个元素 @@ -57,7 +57,7 @@ vec.remove(0); // 删除向量中的第一个元素 ## 6. Vector 的容量和长度: -向量的长度是其中的元素数量,而容量是为这些元素分配的内存空间。向量会根据需要自动增长,但当长度超过容量时,它需要重新分配内存并复制元素到新内存区域。可以使用 len() 获取向量的长度,使用 capacity() 获取容量,还可以使用 shrink_to_fit() 方法减小容量以匹配当前长度。 +向量的长度是其中的元素数量,而容量是为这些元素分配的内存空间。向量会根据需要自动增长,但当长度超过容量时,它需要重新分配内存并复制元素到新内存区域。可以使用 `len()` 获取向量的长度,使用 `capacity()` 获取容量,还可以使用 `shrink_to_fit()` 方法减小容量以匹配当前长度。 ## 7. 切片: diff --git a/src/chapter_3/chapter_3_15_2.md b/src/chapter_3/chapter_3_15_2.md index a5f6861..820331e 100644 --- a/src/chapter_3/chapter_3_15_2.md +++ b/src/chapter_3/chapter_3_15_2.md @@ -1,53 +1,63 @@ # 3.14.2 String(字符串) - Rust 中的 String(字符串)是一个可增长的、UTF-8 编码的字符串类型。它可以存储和处理文本数据。这里是关于 Rust 字符串的一些详细信息: +Rust 中的 String(字符串)是一个可增长的、UTF-8 编码的字符串类型。它可以存储和处理文本数据。这里是关于 Rust 字符串的一些详细信息: ## 1. 创建 String: -可以使用 String::new() 创建一个新的空字符串,或者使用 String::from() 从字符串字面量创建字符串。 +可以使用 `String::new()` 创建一个新的空字符串,或者使用 `String::from()` 从字符串字面量创建字符串。 + ```rust let mut s = String::new(); let s = String::from("hello"); ``` -还可以使用 to_string() 方法将基本类型转换为字符串。 +还可以使用 `to_string()` 方法将基本类型转换为字符串。 + ```rust let num = 42; let s = num.to_string(); ``` + ## 2. 字符串长度: -字符串的长度是其 UTF-8 编码的字节的数量,而不是 Unicode 字符的数量。可以使用 len() 方法获取字符串的长度。 +字符串的长度是其 UTF-8 编码的字节的数量,而不是 Unicode 字符的数量。可以使用 `len()` 方法获取字符串的长度。 + ```rust let len = s.len(); ``` ## 3. 字符串拼接: -有多种方法可以将字符串拼接在一起。例如,可以使用 + 运算符或 format! 宏。 +有多种方法可以将字符串拼接在一起。例如,可以使用 `+` 运算符或 `format!` 宏。 + ```rust let s1 = String::from("hello"); let s2 = String::from("world"); let s3 = s1 + " " + &s2; ``` -或者使用 push_str() 方法将字符串附加到现有字符串。 + +或者使用 `push_str()` 方法将字符串附加到现有字符串。 + ```rust let mut s = String::from("hello"); s.push_str(" world"); ``` + ## 4. 访问字符: 由于 Rust 字符串是 UTF-8 编码的,不能直接使用索引访问单个字符。但是,可以使用迭代器遍历字符串中的字符。 + ```rust for c in s.chars() { println!("{}", c); } ``` -还可以使用 bytes() 方法遍历字节,或者使用 char_indices() 方法获取字符及其对应的字节索引。 +还可以使用 `bytes()` 方法遍历字节,或者使用 `char_indices()` 方法获取字符及其对应的字节索引。 ## 5.字符串切片: 可以使用范围语法创建字符串的切片,它表示原始字符串中一部分的引用。需要确保范围边界位于有效的 UTF-8 字符边界上,否则将导致运行时错误。 + ```rust let s = String::from("hello"); let slice = &s[0..4]; // 获取字符串前 4 个字节的切片 @@ -55,7 +65,7 @@ let slice = &s[0..4]; // 获取字符串前 4 个字节的切片 ## 6. 修改字符串: -可以使用 push() 方法将一个字符添加到字符串的末尾,或者使用 insert() +可以使用 `push()` 方法将一个字符添加到字符串的末尾,或者使用 `insert()` 方法将字符插入指定的字节位置。 ```rust @@ -64,11 +74,12 @@ s.push('!'); s.insert(5, ','); ``` -注意,字符串插入操作可能需要 O(n) 时间,其中 n 是插入位置之后的字节数。 +注意,字符串插入操作可能需要 `O(n)` 时间,其中 n 是插入位置之后的字节数。 ## 7. 删除字符串中的字符: -可以使用 pop() 方法删除并返回字符串末尾的字符,或者使用 remove() 方法删除并返回指定字节位置处的字符。 +可以使用 `pop()` 方法删除并返回字符串末尾的字符,或者使用 `remove()` 方法删除并返回指定字节位置处的字符。 + ```rust let mut s = String::from("hello"); s.pop(); diff --git a/src/chapter_3/chapter_3_15_3.md b/src/chapter_3/chapter_3_15_3.md index 0f42035..b316835 100644 --- a/src/chapter_3/chapter_3_15_3.md +++ b/src/chapter_3/chapter_3_15_3.md @@ -5,7 +5,8 @@ Rust 中的 HashMap(哈希映射)是一个基于键值对的无序集合, ## 1. 创建 HashMap: -要创建一个新的空 HashMap,可以使用 HashMap::new() 方法。需要导入 std::collections::HashMap 模块以使用 HashMap。 +要创建一个新的空 HashMap,可以使用 `HashMap::new()` 方法。需要导入 `std::collections::HashMap` 模块以使用 HashMap。 + ```rust use std::collections::HashMap; @@ -15,7 +16,7 @@ let mut map = HashMap::new(); ## 2. 插入键值对: -可以使用 insert() 方法向 HashMap 中添加键值对。如果使用相同的键插入新值,旧值将被替换。 +可以使用 `insert()` 方法向 HashMap 中添加键值对。如果使用相同的键插入新值,旧值将被替换。 ```rust map.insert("one", 1); map.insert("two", 2); @@ -23,16 +24,18 @@ map.insert("two", 2); ## 3. 访问值: -可以使用 get() 方法根据键查找值。此方法返回一个 Option<&V> 类型,如果找到键,则返回 Some(&value),否则返回 None。 +可以使用 `get()` 方法根据键查找值。此方法返回一个 `Option<&V>` 类型,如果找到键,则返回 `Some(&value)`,否则返回 `None`。 + ```rust let value = map.get("one"); // 返回 Option<&V> ``` -还可以使用 get_mut() 方法获取可变引用。 +还可以使用 `get_mut()` 方法获取可变引用。 ## 4. 遍历键值对: -可以使用 for 循环遍历 HashMap 中的所有键值对。 +可以使用 `for` 循环遍历 HashMap 中的所有键值对。 + ```rust for (key, value) in &map { println!("{}: {}", key, value); @@ -40,28 +43,28 @@ for (key, value) in &map { ``` ## 5. 删除键值对: -可以使用 remove() 方法根据键删除键值对。此方法返回一个 Option 类型,如果找到并删除了键值对,则返回 Some(value),否则返回 None。 +可以使用 `remove()` 方法根据键删除键值对。此方法返回一个 `Option` 类型,如果找到并删除了键值对,则返回 `Some(value)`,否则返回 `None`。 ```rust map.remove("one"); // 删除键为 "one" 的键值对 ``` ## 6. 检查键是否存在: -可以使用 contains_key() 方法检查 HashMap 中是否存在指定的键。 +可以使用 `contains_key()` 方法检查 HashMap 中是否存在指定的键。 ```rust let has_key = map.contains_key("one"); // 返回布尔值 ``` ## 7. 更新值: -可以使用 entry() 方法与 or_insert() 方法结合,更新 HashMap 中的值或插入新值。 +可以使用 `entry()` 方法与 `or_insert()` 方法结合,更新 HashMap 中的值或插入新值。 ```rust *map.entry("three").or_insert(3) += 1; ``` ## 8. HashMap 的容量和长度: -可以使用 len() 方法获取 HashMap 中的键值对数量,使用 capacity() 方法获取容量。还可以使用 shrink_to_fit() 方法减小容量以匹配当前长度。 +可以使用 `len()` 方法获取 HashMap 中的键值对数量,使用 `capacity()` 方法获取容量。还可以使用 `shrink_to_fit()` 方法减小容量以匹配当前长度。 ## 9. 默认哈希器: @@ -79,11 +82,11 @@ map.insert("one", 1); map.insert("two", 2); ``` -在这个例子中,我们使用了 twox_hash 库中的 XxHash64 哈希函数,它通常比默认的 SipHash 更快。请注意,使用自定义哈希函数可能会降低安全性,因此要确保在明确了解潜在风险的情况下进行更改。 +在这个例子中,我们使用了 `twox_hash` 库中的 `XxHash64` 哈希函数,它通常比默认的 `SipHash` 更快。请注意,使用自定义哈希函数可能会降低安全性,因此要确保在明确了解潜在风险的情况下进行更改。 ## 10. 合并两个 HashMap: -可以使用 extend() 方法将另一个 HashMap 的键值对添加到当前 HashMap 中。如果存在重复的键,目标 HashMap 中的值将被源 HashMap 中的值覆盖。 +可以使用 `extend()` 方法将另一个 HashMap 的键值对添加到当前 HashMap 中。如果存在重复的键,目标 HashMap 中的值将被源 HashMap 中的值覆盖。 ```rust let mut map1 = HashMap::new(); @@ -97,6 +100,6 @@ map2.insert("three", 3); map1.extend(map2); // 将 map2 中的键值对添加到 map1 ``` -这样,map1 将包含键值对 "one" -> 1, "two" -> 22 和 "three" -> 3。 +这样,map1 将包含键值对 `"one" -> 1`, `"two" -> 22` 和 `"three" -> 3`。 总之,Rust 中的 HashMap 是一个功能丰富且性能优越的键值对集合,非常适合在需要快速查找和修改操作的场景中使用。 diff --git a/src/chapter_3/chapter_3_15_4.md b/src/chapter_3/chapter_3_15_4.md index 7ba6cc7..2ed5b97 100644 --- a/src/chapter_3/chapter_3_15_4.md +++ b/src/chapter_3/chapter_3_15_4.md @@ -4,7 +4,8 @@ Rust 中的 HashSet(哈希集合)是一种无序的、不含重复元素的 ## 1. 创建 HashSet: -要创建一个新的空 HashSet,可以使用 HashSet::new() 方法。需要导入 std::collections::HashSet 模块以使用 HashSet。 +要创建一个新的空 HashSet,可以使用 `HashSet::new()` 方法。需要导入 `std::collections::HashSet` 模块以使用 HashSet。 + ```rust use std::collections::HashSet; @@ -13,7 +14,7 @@ let mut set = HashSet::new(); ## 2. 添加元素: -可以使用 insert() 方法向 HashSet 中添加元素。如果元素已存在,则此方法将返回 false,否则返回 true。 +可以使用 `insert()` 方法向 HashSet 中添加元素。如果元素已存在,则此方法将返回 `false`,否则返回 `true`。 ```rust set.insert(1); @@ -23,14 +24,16 @@ set.insert(3); ## 3. 检查元素是否存在: -可以使用 contains() 方法检查 HashSet 中是否存在指定的元素。 +可以使用 `contains()` 方法检查 HashSet 中是否存在指定的元素。 + ```rust let contains = set.contains(&1); // 返回布尔值 ``` ## 4. 删除元素: -可以使用 remove() 方法删除 HashSet 中的元素。此方法返回一个 bool 类型,如果找到并删除了元素,则返回 true,否则返回 false。 +可以使用 `remove()` 方法删除 HashSet 中的元素。此方法返回一个 bool 类型,如果找到并删除了元素,则返回 `true`,否则返回 `false`。 + ```rust set.remove(&1); // 删除元素 1 ``` @@ -46,7 +49,7 @@ for element in &set { ## 6. HashSet 的长度: -可以使用 len() 方法获取 HashSet 中的元素数量。还可以使用 is_empty() 方法检查 HashSet 是否为空。 +可以使用 `len()` 方法获取 HashSet 中的元素数量。还可以使用 `is_empty()` 方法检查 HashSet 是否为空。 ## 7. 集合操作: @@ -67,26 +70,29 @@ HashSet 支持一些基本的集合操作,如并集、交集、差集和对称 ``` - 差集(difference):返回一个新的 HashSet,包含第一个集合中存在但第二个集合中不存在的元素。 + ```rust let difference: HashSet<_> = set1.difference(&set2).cloned().collect(); ``` + - 对称差集(symmetric_difference):返回一个新的 HashSet,包含两个集合中唯一的元素(也就是只存在于一个集合中的元素)。 + ```rust let symmetric_difference: HashSet<_> = set1.symmetric_difference(&set2).cloned().collect(); ``` ## 8. 清空 HashSet: -可以使用 clear() 方法删除 HashSet 中的所有元素。 +可以使用 `clear()` 方法删除 HashSet 中的所有元素。 ```rust set.clear(); // 清空 HashSet ``` - 总的来说,Rust 中的 HashSet提供了一种高效且易于使用的无序集合实现,适用于需要快速查找、添加和删除操作的场景。由于 HashSet 的底层实现基于哈希表,它能够在大部分情况下为这些操作提供 O(1) 的平均时间复杂度。 +总的来说,Rust 中的 HashSet提供了一种高效且易于使用的无序集合实现,适用于需要快速查找、添加和删除操作的场景。由于 HashSet 的底层实现基于哈希表,它能够在大部分情况下为这些操作提供 O(1) 的平均时间复杂度。 - 与 HashMap 类似,Rust 的 HashSet 默认使用一个加密安全的哈希函数(SipHash),以防止哈希碰撞攻击。如果需要更高的性能,可以考虑使用自定义哈希器,但要确保在明确了解潜在风险的情况下进行更改。这是一个使用自定义哈希器的例子 - : +与 HashMap 类似,Rust 的 HashSet 默认使用一个加密安全的哈希函数(SipHash),以防止哈希碰撞攻击。如果需要更高的性能,可以考虑使用自定义哈希器,但要确保在明确了解潜在风险的情况下进行更改。这是一个使用自定义哈希器的例子 +: ```rust use std::collections::HashSet; @@ -100,4 +106,4 @@ set.insert(1); set.insert(2); ``` -在这个例子中,我们使用了 twox_hash 库中的 XxHash64 哈希函数,它通常比默认的 SipHash 更快。 +在这个例子中,我们使用了 `twox_hash` 库中的 `XxHash64` 哈希函数,它通常比默认的 SipHash 更快。 diff --git a/src/chapter_3/chapter_3_15_5.md b/src/chapter_3/chapter_3_15_5.md index 492ac50..04c146b 100644 --- a/src/chapter_3/chapter_3_15_5.md +++ b/src/chapter_3/chapter_3_15_5.md @@ -5,7 +5,7 @@ Rust 中的 LinkedList(链表)是一种线性数据结构,它由一系列 ## 1. 创建 LinkedList: -要创建一个新的空 LinkedList,可以使用 LinkedList::new() 方法。需要导入 std::collections::LinkedList 模块以使用 LinkedList。 +要创建一个新的空 LinkedList,可以使用 `LinkedList::new()` 方法。需要导入 `std::collections::LinkedList` 模块以使用 LinkedList。 ```rust use std::collections::LinkedList; @@ -14,7 +14,7 @@ let mut list = LinkedList::new(); ## 2. 添加元素: -可以使用 push_front() 和 push_back() 方法将元素添加到链表的开头和结尾。 +可以使用 `push_front()` 和 `push_back()` 方法将元素添加到链表的开头和结尾。 ```rust list.push_front(1); list.push_back(2); @@ -22,7 +22,7 @@ list.push_back(2); ## 3. 访问元素: -可以使用 front() 和 back() 方法分别访问链表的第一个和最后一个元素。这些方法返回一个 Option<&T> 类型,如果链表不为空,则返回 Some(&element),否则返回 None。 +可以使用 `front()` 和 `back()` 方法分别访问链表的第一个和最后一个元素。这些方法返回一个 `Option<&T>` 类型,如果链表不为空,则返回 `Some(&element)`,否则返回 `None`。 ```rust let first_element = list.front(); // 返回 Option<&T> @@ -33,7 +33,8 @@ let last_element = list.back(); // 返回 Option<&T> ## 4. 删除元素: -可以使用 pop_front() 和 pop_back() 方法分别删除并返回链表的第一个和最后一个元素。这些方法返回一个 Option 类型,如果链表不为空且成功删除元素,则返回 Some(element),否则返回 None。 +可以使用 `pop_front()` 和 `pop_back()` 方法分别删除并返回链表的第一个和最后一个元素。这些方法返回一个 `Option` 类型,如果链表不为空且成功删除元素,则返回 `Some(element)`,否则返回 `None`。 + ```rust list.pop_front(); // 删除并返回第一个元素 list.pop_back(); // 删除并返回最后一个元素 @@ -41,7 +42,8 @@ list.pop_back(); // 删除并返回最后一个元素 ## 5. 遍历元素: -可以使用 iter() 方法遍历链表中的所有元素。iter_mut() 方法可用于遍历可变引用。 +可以使用 `iter()` 方法遍历链表中的所有元素。`iter_mut()` 方法可用于遍历可变引用。 + ```rust for element in list.iter() { println!("{}", element); @@ -50,11 +52,11 @@ for element in list.iter() { ## 6. 链表长度: -可以使用 len() 方法获取链表中的元素数量。还可以使用 is_empty() 方法检查链表是否为空。 +可以使用 `len()` 方法获取链表中的元素数量。还可以使用 `is_empty()` 方法检查链表是否为空。 ## 7. 清空链表: -可以使用 clear() 方法删除链表中的所有元素。 +可以使用 `clear()` 方法删除链表中的所有元素。 ```rust list.clear(); // 清空链表 @@ -62,7 +64,7 @@ list.clear(); // 清空链表 ## 8. 分割链表: -可以使用 split_off() 方法在指定索引处分割链表。此操作会将链表分成两个链表,前一个链表包含指定索引之前的元素,后一个链表包含指定索引及之后的元素。 +可以使用 `split_off()` 方法在指定索引处分割链表。此操作会将链表分成两个链表,前一个链表包含指定索引之前的元素,后一个链表包含指定索引及之后的元素。 ```rust let mut list1 = LinkedList::new(); diff --git a/src/chapter_3/chapter_3_15_6.md b/src/chapter_3/chapter_3_15_6.md index cb99901..f670ed6 100644 --- a/src/chapter_3/chapter_3_15_6.md +++ b/src/chapter_3/chapter_3_15_6.md @@ -4,7 +4,7 @@ Rust 中的 BTreeMap(B 树映射)是一种自平衡的有序映射数据结 ## 1. 创建 BTreeMap: -要创建一个新的空 BTreeMap,可以使用 BTreeMap::new() 方法。需要导入 std::collections::BTreeMap 模块以使用 BTreeMap。 +要创建一个新的空 BTreeMap,可以使用 `BTreeMap::new()` 方法。需要导入 `std::collections::BTreeMap` 模块以使用 BTreeMap。 ```rust use std::collections::BTreeMap; @@ -14,7 +14,8 @@ let mut map = BTreeMap::new(); ## 2. 添加元素: -可以使用 insert() 方法向 BTreeMap 中添加键值对。如果键已存在,则此方法将返回 Some(old_value),否则返回 None。 +可以使用 `insert()` 方法向 BTreeMap 中添加键值对。如果键已存在,则此方法将返回 `Some(old_value)`,否则返回 `None`。 + ```rust map.insert("one", 1); map.insert("two", 2); @@ -23,17 +24,17 @@ map.insert("three", 3); ## 3. 访问元素: -可以使用 get() 方法根据键查找对应的值。此方法返回一个 Option<&V> 类型,如果找到键,则返回 Some(&value),否则返回 None。 +可以使用 `get()` 方法根据键查找对应的值。此方法返回一个 `Option<&V>` 类型,如果找到键,则返回 `Some(&value)`,否则返回 `None`。 ```rust let value = map.get("one"); // 返回 Option<&V> ``` -还可以使用 get_mut() 方法获取可变引用。 +还可以使用 `get_mut()` 方法获取可变引用。 ## 4. 删除元素: -可以使用 remove() 方法删除 BTreeMap 中的键值对。此方法返回一个 Option 类型,如果找到并删除了键值对,则返回 Some(value),否则返回 None。 +可以使用`remove()` 方法删除 BTreeMap 中的键值对。此方法返回一个 `Option` 类型,如果找到并删除了键值对,则返回 `Some(value)`,否则返回 `None`。 ```rust map.remove("one"); // 删除键为 "one" 的键值对 @@ -41,7 +42,8 @@ map.remove("one"); // 删除键为 "one" 的键值对 ## 5. 遍历元素: -可以使用 iter() 方法遍历 BTreeMap 中的所有键值对。遍历顺序按键的顺序进行。iter_mut() 方法可用于遍历可变引用。 +可以使用 `iter()` 方法遍历 BTreeMap 中的所有键值对。遍历顺序按键的顺序进行。`iter_mut()` 方法可用于遍历可变引用。 + ```rust for (key, value) in map.iter() { println!("{}: {}", key, value); @@ -50,11 +52,11 @@ for (key, value) in map.iter() { ## 6. BTreeMap 的长度: -可以使用 len() 方法获取 BTreeMap 中的键值对数量。还可以使用 is_empty() 方法检查 BTreeMap 是否为空。 +可以使用 `len()` 方法获取 BTreeMap 中的键值对数量。还可以使用 `is_empty()` 方法检查 BTreeMap 是否为空。 ## 7. 最小和最大键: -可以使用 first_key_value() 和 last_key_value() 方法分别获取 BTreeMap 中具有最小和最大键的键值对。这些方法返回一个 Option<(&K, &V)> 类型,如果找到键值对,则返回 Some((&key, &value)),否则返回 None。 +可以使用 `first_key_value()` 和 `last_key_value()` 方法分别获取 BTreeMap 中具有最小和最大键的键值对。这些方法返回一个 `Option<(&K, &V)>` 类型,如果找到键值对,则返回 `Some((&key, &value))`,否则返回 None。 ```rust let min_key_value = map.first_key_value(); // 返回 Option<(&K, &V)> @@ -63,7 +65,8 @@ let max_key_value = map.last_key_value(); // 返回 Option<(&K, &V)> ## 8. 范围查询: -可以使用 range() 方法查询 BTreeMap 中某个范围内的键值对。例如,可以查询所有键大于等于 "one" 且小于等于 "three" 的键值对: +可以使用 `range()` 方法查询 BTreeMap 中某个范围内的键值对。例如,可以查询所有键大于等于 "one" 且小于等于 "three" 的键值对: + ```rust for (key, value) in map.range("one".."three") { println!("{}: {}", key, value); diff --git a/src/chapter_3/chapter_3_15_7.md b/src/chapter_3/chapter_3_15_7.md index 88f180f..7d2058c 100644 --- a/src/chapter_3/chapter_3_15_7.md +++ b/src/chapter_3/chapter_3_15_7.md @@ -4,7 +4,8 @@ Rust 中的 BTreeSet(B 树集合)是一种自平衡的有序集合数据结 ## 1. 创建 BTreeSet: -要创建一个新的空 BTreeSet,可以使用 BTreeSet::new() 方法。需要导入 std::collections::BTreeSet 模块以使用 BTreeSet。 +要创建一个新的空 BTreeSet,可以使用 `BTreeSet::new()` 方法。需要导入 `std::collections::BTreeSet` 模块以使用 BTreeSet。 + ```rust use std::collections::BTreeSet; @@ -13,7 +14,8 @@ let mut set = BTreeSet::new(); ## 2. 添加元素: -可以使用 insert() 方法向 BTreeSet 中添加元素。如果元素已存在,则此方法将返回 false,否则返回 true。 +可以使用 `insert()` 方法向 BTreeSet 中添加元素。如果元素已存在,则此方法将返回 `false`,否则返回 `true`。 + ```rust set.insert(1); set.insert(2); @@ -22,21 +24,24 @@ set.insert(3); ## 3. 检查元素是否存在: -可以使用 contains() 方法检查 BTreeSet 中是否存在指定的元素。 +可以使用 `contains()` 方法检查 BTreeSet 中是否存在指定的元素。 + ```rust let contains = set.contains(&1); // 返回布尔值 ``` ## 4. 删除元素: -可以使用 remove() 方法删除 BTreeSet 中的元素。此方法返回一个 bool 类型,如果找到并删除了元素,则返回 true,否则返回 false。 +可以使用 `remove()` 方法删除 BTreeSet 中的元素。此方法返回一个 bool 类型,如果找到并删除了元素,则返回 `true`,否则返回 `false`。 + ```rust set.remove(&1); // 删除元素 1 ``` ## 5. 遍历元素: -可以使用 for 循环遍历 BTreeSet 中的所有元素。遍历顺序按元素的顺序进行。 +可以使用 `for` 循环遍历 BTreeSet 中的所有元素。遍历顺序按元素的顺序进行。 + ```rust for element in &set { println!("{}", element); @@ -45,33 +50,42 @@ for element in &set { ## 6. BTreeSet 的长度: -可以使用 len() 方法获取 BTreeSet 中的元素数量。还可以使用 is_empty() 方法检查 BTreeSet 是否为空。 +可以使用 `len()` 方法获取 BTreeSet 中的元素数量。还可以使用 `is_empty()` 方法检查 BTreeSet 是否为空。 ## 7. 集合操作: BTreeSet 支持一些基本的集合操作,如并集、交集、差集和对称差集。 - 并集(union):返回一个新的 BTreeSet,包含两个集合中的所有元素。 -```rust -let set1: BTreeSet<_> = [1, 2, 3].iter().cloned().collect(); -let set2: BTreeSet<_> = [3, 4, 5].iter().cloned().collect(); -let union: BTreeSet<_> = set1.union(&set2).cloned().collect(); -``` + + ```rust + let set1: BTreeSet<_> = [1, 2, 3].iter().cloned().collect(); + let set2: BTreeSet<_> = [3, 4, 5].iter().cloned().collect(); + let union: BTreeSet<_> = set1.union(&set2).cloned().collect(); + ``` + - 交集(intersection):返回一个新的 BTreeSet,包含两个集合中共有的元素。 -```rust -let intersection: BTreeSet<_> = set1.intersection(&set2).cloned().collect(); -``` + + ```rust + let intersection: BTreeSet<_> = set1.intersection(&set2).cloned().collect(); + ``` + - 差集(difference):返回一个新的 BTreeSet,包含第一个集合中存在但第二个集合中不存在的元素。 -```rust -let difference: BTreeSet<_> = set1.difference(&set2).cloned().collect(); -``` + + ```rust + let difference: BTreeSet<_> = set1.difference(&set2).cloned().collect(); + ``` + - 对称差集(symmetric_difference):返回一个新的 BTreeSet,包含两个集合中唯一的元素(也就是只存在于一个集合中的元素) -```rust -let symmetric_difference: BTreeSet<_> = set1.symmetric_difference(&set2).cloned().collect(); -``` + + ```rust + let symmetric_difference: BTreeSet<_> = set1.symmetric_difference(&set2).cloned().collect(); + ``` + ## 8. 最小和最大元素: -可以使用 first() 和 last() 方法分别获取 BTreeSet 中的最小和最大元素。这些方法返回一个 Option<&T> 类型,如果找到元素,则返回 Some(&element),否则返回 None。 +可以使用 `first()` 和 `last()` 方法分别获取 BTreeSet 中的最小和最大元素。这些方法返回一个 `Option<&T>` 类型,如果找到元素,则返回 `Some(&element)`,否则返回 `None`。 + ```rust let min_element = set.first(); // 返回 Option<&T> let max_element = set.last(); // 返回 Option<&T> @@ -79,7 +93,8 @@ let max_element = set.last(); // 返回 Option<&T> ## 9. 范围查询: -可以使用 range() 方法查询 BTreeSet 中某个范围内的元素。例如,可以查询所有大于等于 1 且小于等于 3 的元素: +可以使用 `range()` 方法查询 BTreeSet 中某个范围内的元素。例如,可以查询所有大于等于 1 且小于等于 3 的元素: + ```rust for element in set.range(1..=3) { println!("{}", element); @@ -88,7 +103,7 @@ for element in set.range(1..=3) { ## 10. 清空 BTreeSet: -可以使用 clear() 方法删除 BTreeSet 中的所有元素。 +可以使用 `clear()` 方法删除 BTreeSet 中的所有元素。 ```rust set.clear(); // 清空 BTreeSet diff --git a/src/chapter_3/chapter_3_16_1.md b/src/chapter_3/chapter_3_16_1.md index 14284e2..23ccfd0 100644 --- a/src/chapter_3/chapter_3_16_1.md +++ b/src/chapter_3/chapter_3_16_1.md @@ -1,4 +1,5 @@ # 3.16.1 智能指针介绍 + 指针是一个包含了内存地址的变量,该内存地址引用或者指向了另外的数据,其在内存中的示意图如下: ![注释](../../assets/18.png) diff --git a/src/chapter_3/chapter_3_16_2.md b/src/chapter_3/chapter_3_16_2.md index c661936..508a208 100644 --- a/src/chapter_3/chapter_3_16_2.md +++ b/src/chapter_3/chapter_3_16_2.md @@ -1,4 +1,5 @@ # 3.16.2 Box智能指针 + ```Box```智能指针是Rust中最基本的在堆上分配内存的方式。定义```Box```变量,将值存放在堆上,栈上则保留指向堆数据的指针。除了数据被存储在堆上外,```Box```没有任何性能损失。 ## 1. Box的基本使用方式 @@ -12,11 +13,13 @@ fn main() { ``` ## 2. 使用Box的内存布局 + 前面示例中使用```Box```定义了变量```b```,其内存布局方式如下: ![注释](../../assets/20.png) ## 3. Box适合使用的场景 + ```Box```适合用于如下场景: - 当有一个在编译时未知大小的类型,而又需要在确切大小的上下文中使用这个类型值的时候; - 当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候; @@ -32,7 +35,7 @@ enum List { //struct List{ // int data; // struct List next;//编译报错,因为编译器并不知道next有多大,next又是一个List - //} + //} Nil, } diff --git a/src/chapter_3/chapter_3_16_3.md b/src/chapter_3/chapter_3_16_3.md index effb60e..10f2a50 100644 --- a/src/chapter_3/chapter_3_16_3.md +++ b/src/chapter_3/chapter_3_16_3.md @@ -1,7 +1,9 @@ -# 3.16.3 Deref trait +# 3.16.3 Deref trait + ## 1. 通过*使用引用背后的值 -常规引用是一个指针类型,包含了目标数据存储的内存地址。对常规引用使用 * ,可以通过解引用的方式获取到内存地址对应的数据值,示例如下: +常规引用是一个指针类型,包含了目标数据存储的内存地址。对常规引用使用 `*` ,可以通过解引用的方式获取到内存地址对应的数据值,示例如下: + ```Rust fn main() { let x = 5; @@ -14,7 +16,8 @@ fn main() { ``` ## 2. 通过*使用智能指针背后的值 -对于智能指针,也可以通过*使用其背后的值,示例如下: + +对于智能指针,也可以通过`*`使用其背后的值,示例如下: ```Rust fn main() { let x = 5; @@ -26,14 +29,16 @@ fn main() { ## 3. 使用Deref -实现Deref trait允许我们重载解引用运算符*。通过为类型实现Deref trait,类型可以被当做常规引用来对待。简单来说,如果类型A实现了Deref trait,那么就可以写如下代码: +实现`Deref trait`允许我们重载解引用运算符`*`。通过为类型实现`Deref trait`,类型可以被当做常规引用来对待。简单来说,如果类型A实现了`Deref trait`,那么就可以写如下代码: + ```Rust let a: A = A::new(); let b = &a; let c = *b; //对A实现了Deref trait,所以可以对A类型解引用 ``` -下面的代码定义一个MyBox类型,并为其实现Deref trait: +下面的代码定义一个`MyBox`类型,并为其实现`Deref trait`: + ```Rust use std::ops::Deref; struct MyBox(T); @@ -59,13 +64,15 @@ fn main() { ``` ## 4. 函数和方法的隐式Deref强制转换 + 对于函数和方法的传参,Rust 提供了隐式转换:Deref 转换。若一个类型实现了 Deref 特征,那它的引用在传给函数或方法时,会根据参数签名来决定是否进行隐式的 Deref 转换,例如: + ```Rust use std::ops::Deref; struct MyString(String); impl Deref for MyString { // MyString类型实现了Deref trait type Target = str; - fn deref(&self) -> &str { + fn deref(&self) -> &str { self.0.as_str() } } @@ -76,17 +83,18 @@ fn print(s: &str) { fn main() { let y = MyString("Hello".to_string()); print(&y); //此处发生隐式转换,&y从&MyString自动转换为&str类型,转换由&触发,过程如下: - // &y隐式转换过程: &MyString -> &str + // &y隐式转换过程: &MyString -> &str } ``` 上面代码的关键点为: -- MyString实现了Deref特征,可以在需要时自动被转换为&str类型; -- &y是一个&MyString类型,当它被传给print函数时,自动通过Deref转换成了&str; -- 必须使用&y的方式来触发Deref(仅引用类型的实参才会触发自动解引用)。 +- `MyString`实现了Deref特征,可以在需要时自动被转换为`&str`类型; +- `&y`是一个`&MyString`类型,当它被传给`print`函数时,自动通过`Deref`转换成了`&str`; +- 必须使用`&y`的方式来触发Deref(仅引用类型的实参才会触发自动解引用)。 下面为调用方法时发生的隐式自动转换的例子: + ```Rust use std::ops::Deref; struct MyType(u32); @@ -116,6 +124,7 @@ fn main() { ``` Deref还支持连续的隐式转换,示例如下: + ```Rust fn print(s: &str) { println!("{}", s); @@ -125,13 +134,15 @@ fn main() { print(&s) // &s隐式转换过程:&Box -> &String -> &str } ``` -上面的代码中,Box、String都实现了Deref,当把&s传入到print函数时,发送连续隐式转换(&s先从&Box转换到&String,再转换到&str)。 + +上面的代码中,`Box`、`String`都实现了Deref,当把`&s`传入到`print`函数时,发送连续隐式转换(`&s`先从`&Box`转换到`&String`,再转换到`&str`)。 ## 5. Deref强制转换与可变性交互 -类似于如何使用Dereftrait 重载不可变引用的*运算符,Rust提供了DerefMut trait用于重载可变引用的*运算符。 + +类似于如何使用`Deref trait` 重载不可变引用的*运算符,Rust提供了DerefMut trait用于重载可变引用的*运算符。 Rust在发现类型和 trait 实现满足三种情况时会进行 Deref 强制转换: -- 当T: Deref时从&T到&U。 -- 当T: DerefMut时从&mut T到&mut U。 -- 当 T: Deref时从&mut T到&U。 +- 当`T: Deref`时从`&T`到`&U`。 +- 当`T: DerefMut`时从`&mut T`到`&mut U`。 +- 当 `T: Deref`时从`&mut T`到`&U`。 diff --git a/src/chapter_3/chapter_3_16_4.md b/src/chapter_3/chapter_3_16_4.md index 4a7de75..9b650c7 100644 --- a/src/chapter_3/chapter_3_16_4.md +++ b/src/chapter_3/chapter_3_16_4.md @@ -1,7 +1,11 @@ # 3.16.4 Drop trait + ## 1. Drop trait -Drop trait类似于其它语言中的析构函数,当值离开作用域时执行此函数的代码。可以为任何类型提供Drop trait的实现。 -为一个类型实现Drop trait的示例如下: + +`Drop trait`类似于其它语言中的析构函数,当值离开作用域时执行此函数的代码。可以为任何类型提供Drop trait的实现。 + +为一个类型实现`Drop trait`的示例如下: + ```Rust struct Dog(String); @@ -25,7 +29,9 @@ fn main() { 在上面示例代码中并没有打印语句,但是在drop方法中实现了打印,可以看到,当_a和_b离开作用域时,自动调用了drop方法。 ## 2. 通过std::mem::drop提早丢弃值 -当要显示的清理值时,不能直接调用Drop trait里面的drop方法,而要使用std::mem::drop方法,示例如下: + +当要显示的清理值时,不能直接调用Drop trait里面的drop方法,而要使用`std::mem::drop`方法,示例如下: + ```Rust struct Dog(String); @@ -44,6 +50,7 @@ fn main() { println!("At the end of main"); } ``` + 代码运行结果如下: ![注释](../../assets/25.png) diff --git a/src/chapter_3/chapter_3_16_5.md b/src/chapter_3/chapter_3_16_5.md index 14b630f..073504f 100644 --- a/src/chapter_3/chapter_3_16_5.md +++ b/src/chapter_3/chapter_3_16_5.md @@ -1,10 +1,13 @@ # 3.16.5 Rc智能指针 + ## 1. 使用场景分析 + 假定有这样一个需求,希望创建两个共享第三个列表所有权的列表,其概念类似于如下图: ![注释](../../assets/26.png) 根据前面的知识,可能写出来的代码如下: + ```Rust enum List { Cons(i32, Box), @@ -21,17 +24,20 @@ fn main() { 但是上面的代码报错,因为**Rust 所有权机制要求一个值只能有一个所有者**。为了解决此类需求,Rust提供了Rc智能指针。 ## 2. 使用Rc共享数据 + Rc智能指针通过引用计数解决数据共享的问题,下面是Rc使用的简单代码: + ```Rust use std::rc::Rc; fn main() { let a = Rc::new(5u32); let b = Rc::clone(&a); - let c = a.clone(); + let c = a.clone(); } ``` + 上面代码中,a、b、c就共享数据5。当创建b时,不会获取a的所有权,会克隆a所包含的Rc(5u32),这会使引用计数从1增加到2并允许a和b共享Rc(5u32)的所有权。 -创建c时也会克隆 a,引用计数从2增加为3。每次调用Rc::clone,Rc(5u32)的引用计数都会增加,直到有零个引用之前其数据都不会被清理。其在内存中的表示如下: +创建c时也会克隆 a,引用计数从2增加为3。每次调用`Rc::clone`,Rc(5u32)的引用计数都会增加,直到有零个引用之前其数据都不会被清理。其在内存中的表示如下: ![注释](../../assets/27.png) @@ -57,7 +63,9 @@ fn main() { ## 3. 打印Rc的引用计数 + 下面的示例打印了Rc的引用计数: + ```Rust enum List { Cons(i32, Rc), @@ -82,4 +90,3 @@ fn main() { - 通过Rc是允许程序的多个部分之间**只读的共享数据**,因为相同位置的多个可变引用可能会造成数据竞争和不一致。如果涉及到修改,需要使用RefCell或者Mutex。 - Rc只能是同一个线程内部共享数据,它是非线程安全的。如果要在多线程中共享,需要使用Arc。 - diff --git a/src/chapter_3/chapter_3_16_6.md b/src/chapter_3/chapter_3_16_6.md index f67b0aa..eeaa38b 100644 --- a/src/chapter_3/chapter_3_16_6.md +++ b/src/chapter_3/chapter_3_16_6.md @@ -1,20 +1,25 @@ # 3.16.6 RefCell智能指针 + ## 1. 使用RefCell + **内部可变性(Interior mutability)** 是Rust中的一个设计模式,它允许在有不可变引用时改变数据,这通常是借用规则所不允许的。RefCell正是为Rust提供内部可变性的智能指针。 在Rust中,当使用mut或者&mut显示的声明一个变量或者引用时,才能修改它们的值。编译器会对此严格检查。 + ```Rust -let mut a = 1u32; +let mut a = 1u32; a = 2u32; // 可以修改a的值 let b = 3u32; b = 4u32; // 报错,不允许修改 ``` + 但是当使用RefCell时,可以对其内部包含的内容进行修改,如下: + ```Rust use std::cell::RefCell; fn main() { let data = RefCell::new(1); // data本身是不可变变量 { - let mut v = data.borrow_mut(); + let mut v = data.borrow_mut(); *v = 2; // 但是却可以对RefCell内部的值进行修改 } println!("data: {:?}", data.borrow()); // 将输出为2 @@ -22,8 +27,11 @@ fn main() { ``` ## 2. 使用RefCell,编译器在运行时检查可变性 + 在上面的代码中,data本身是一个不可变变量,但是在代码中却可以改变它内部的值(第6行,将值从1改成了2),这就是内部可变性。**当使用RefCell定义变量后,编译器会认为:在编译时这个变量是不可变的,但是在运行时可以得到其可变借用,从而改变其内部的值**。换句话说,**使用RefCell是运行时检查借用规则**。 + 下面是另一个使用RefCell的例子: + ```Rust #[derive(Debug)] enum List { @@ -52,6 +60,7 @@ fn main() { ``` 下面是一个使用RefCell时容易犯错的例子: + ```Rust use std::cell::RefCell; fn main() { @@ -62,7 +71,10 @@ fn main() { // 前面使用了可变引用,第7行这里又使用不可变引用,违背所有权规则 } ``` -***分析:此处需要注意的是,对于RefCell的可变引用、不可变引用的作用域范围(Rust 1.68.2中),其定义方式还是从定义开始,到花括号前结束。这和普通引用是不一样的,因为在新版编译器中(1.31以后),普通引用的作用域范围变成了从定义开始,到不再使用结束。因此,下面的代码是可以正确的***: + +> ***分析:此处需要注意的是,对于RefCell的可变引用、不可变引用的作用域范围(Rust 1.68.2中),其定义方式还是从定义开始,到花括号前结束。这和普通引用是不一样的,因为在新版编译器中(1.31以后),普通引用的作用域范围变成了从定义开始,到不再使用结束。因此,下面的代码是可以正确的***: + + ```Rust fn main() { let mut a = 5u32; @@ -76,6 +88,7 @@ fn main() { ``` ## 3. 关于Box、Rc或RefCell的选择总结 -- Rc允许相同数据有多个所有者;Box和RefCell有单一所有者。 + +- Rc允许相同数据有多个所有者;`Box`和`RefCell`有单一所有者。 - Box允许在编译时执行不可变或可变借用检查;Rc仅允许在编译时执行不可变借用检查;RefCell 允许在运行时执行不可变或可变借用检查。 - 因为RefCell允许在运行时执行可变借用检查,所以可以在即便RefCell自身是不可变的情况下修改其内部的值。 diff --git a/src/chapter_3/chapter_3_16_7.md b/src/chapter_3/chapter_3_16_7.md index 0c15937..b7f69c4 100644 --- a/src/chapter_3/chapter_3_16_7.md +++ b/src/chapter_3/chapter_3_16_7.md @@ -1,9 +1,12 @@ # 3.16.7 引用循环、内存泄露、Weak智能指针 + ## 1. 引用循环和内存泄露 + 下图是一个循环链表: ![注释](../../assets/29.png) 下面的代码试图用之前学到的智能指针相关知识实现上面的链表: + ```Rust use crate::List::{Cons, Nil}; use std::cell::RefCell; @@ -49,41 +52,45 @@ fn main() { ``` 下面分析整个过程中的内存布局: + - 当执行完第26行后,内存布局如下: - -![注释](../../assets/30.png) + +![注释](../../assets/30.png) - 在执行完第33行后,b对应的Rc引用计数变成2,内存布局如下: -![注释](../../assets/31.png) +![注释](../../assets/31.png) 此时如果第40行代码执行将会panic,因为已经成了一个循环链表,Rust无法匹配到a的tail,最终会造成栈溢出。 - 在最后离开作用域时,Rust将会对b和a调用drop方法,对b调用drop方法后,内存布局如下: -![注释](../../assets/32.png) +![注释](../../assets/32.png) 此时b的Rc实例引用计数减去1,但是仍然不为0(因为第33行让a也引用了b的Rc实例),所以b所指向的内存并不会被释放。 - 然后Rust尝试drop a,其对应的Rc示例引用计数减去1,但仍然不为0,所以调用a的drop后,内存布局为: -![注释](../../assets/33.png) +![注释](../../assets/33.png) 至此,造成内存泄露。 ## 2. 使用弱引用Weak + Weak类似于Rc,但它不持有所有权,它仅仅保存一份指向数据的弱引用。弱引用,就是不保证引用的对象存在,如果不存在,就返回一个 None。 + 下面为Weak和Rc的对比: -| Weak | Rc | +| Weak | Rc | | ------------------------ | ------------------------ | | 引用不计数 | 引用计数 | | 不拥有所有权 | 拥有所有权 | | 不阻止值被释放 | 阻止值被释放 | |引用的值存在返回Some,不存在返回None | 引用的值必定存在 | |通过upgrade取到Option>,然后取值|通过Deref自动解引用,取值无需任何操作| - + 对于上一节中循环链表的例子,使用Weak实现为如下: + ```Rust use crate::List::{Cons, Nil}; use std::cell::RefCell; @@ -152,13 +159,14 @@ fn main() { println!("a next item = {:?}", a.tail()); } ``` + 下图为上面代码的内存布局示意图: - -![注释](../../assets/34.png) + +![注释](../../assets/34.png) 下面再总结一下Weak的特点: - 可访问,但没有所有权,不增加引用计数,因此不会影响被引用值的释放回收; -- 可由Rc调用downgrade方法转换成Weak; -- Weak可使用upgrade方法转换成Option>,如果资源已经被释放,则Option的值是None; +- 可由`Rc`调用`downgrade`方法转换成`Weak`; +- `Weak`可使用`upgrade`方法转换成`Option>`,如果资源已经被释放,则`Option`的值是`None`; - 常用于解决循环引用的问题。 diff --git a/src/chapter_3/chapter_3_17_1.md b/src/chapter_3/chapter_3_17_1.md index 743e6aa..dd56c23 100644 --- a/src/chapter_3/chapter_3_17_1.md +++ b/src/chapter_3/chapter_3_17_1.md @@ -1,15 +1,18 @@ # 3.17.1 包、crate和模块介绍 + 关于三者的描述如下: - 包(Package)是一个 Cargo 项目,它包含了一个 Cargo.toml 配置文件和一些 Rust 代码文件。一个包可以包含多个 crate,并且可以有多种构建方式和配置。 -- crate(Crate)是一个可以编译成库或二进制文件的 Rust 代码单元。每个 crate 都有一个唯一的名称,它可以被其他 crate 引用,也可以被其它包中的代码引用。每个crate都有一个crate root ,它是一个源文件,Rust 编译器以它为起始点来构成crate根模块。对于crate来说,crate root要么是src/main.rs(对于二进制crate来说),要么是src/lib.rs(对于库crate来说)。 +- crate(Crate)是一个可以编译成库或二进制文件的 Rust 代码单元。每个 crate 都有一个唯一的名称,它可以被其他 crate 引用,也可以被其它包中的代码引用。每个crate都有一个crate root ,它是一个源文件,Rust 编译器以它为起始点来构成crate根模块。对于crate来说,crate root要么是`src/main.rs`(对于二进制crate来说),要么是`src/lib.rs`(对于库crate来说)。 - 模块(Module)是一种组织 Rust 代码的方式,它可以将相关的代码放在一起,形成一个代码块。模块可以嵌套,可以有多个不同的访问级别(public、private),并且可以从其他模块中引用和访问。 三者的范围为:模块< crate < 包,即一个包包含一个或多个crate,一个crate可以包含多个模块。 ## 1. 包含一个crate的包 + 下面的命令会创建一个Rust工程,这个工程同时是一个crate,也是一个包(只有一个crate的包): -``` + +```bash cargo new main ``` @@ -24,8 +27,10 @@ cargo new main 可以看到这个crate的名字为main。 ## 2. 包含多个crate的包 + 下面再创建一个稍微复杂一点的包。执行如下几条命令: -``` + +```bash mkdir my-pack # 创建包文件夹 cd my-pack cargo new my-lib --lib # 创建一个库crate @@ -33,6 +38,7 @@ cargo new main # 创建一个可执行的二进制crate ``` 然后在my-pack文件夹中添加文件Cargo.toml,其内容如下: + ```TOML [workspace] @@ -41,6 +47,7 @@ members = [ "my-lib", ] ``` + 最后的目录层级关系如下: ![注释](../../assets/37.png) diff --git a/src/chapter_3/chapter_3_2.md b/src/chapter_3/chapter_3_2.md index 38451d8..ea247a7 100644 --- a/src/chapter_3/chapter_3_2.md +++ b/src/chapter_3/chapter_3_2.md @@ -7,7 +7,7 @@ Rust有两种数据类型子集,分别是:标量(scalar)类型和复合 标量类型包括:整型、浮点型、布尔类型和字符类型。 -- 整型和浮点型 +### 3.2.1.1 整型和浮点型 Rust中的整型和浮点型如下: @@ -25,6 +25,7 @@ Rust中的整型和浮点型如下: 上面的表格中,`f32`和`f64`为浮点型,其它为整型。浮点型和整型一起构成数值型。 (1)可以在数值字面量后面加上类型表示该类型的数值,如下: + ```Rust fn main(){ let _a = 33u32; // 直接加类型后缀 @@ -33,7 +34,9 @@ fn main(){ let _d = 33_f32; } ``` + (2)可以数值的任意位置使用下划线分割来增强可读性,如下: + ```Rust fn main(){ let _a = 10_000_000u32; @@ -41,14 +44,18 @@ fn main(){ let _c = 1_33_333f32; } ``` -(3)当不明确指定变量的类型,也不明确指定数值字面量的类型后缀时,Rust默认将整数当做i32类型,将浮点数当做f64类型,如下: + +(3)当不明确指定变量的类型,也不明确指定数值字面量的类型后缀时,Rust默认将整数当做`i32`类型,将浮点数当做`f64`类型,如下: + ```Rust fn main(){ let _a = 33; // 等价于 let _a: i32 = 33; 等价于 let _a = 33i32; let _b = 64.123; // 等价于let _b: f64 = 64.123; 等价于 let _b = 64.123f64; } ``` -(4)Rust使用0b表示二进制、0o表示八进制、0x表示十六进制,如下: + +(4)Rust使用`0b`表示二进制、`0o`表示八进制、`0x`表示十六进制,如下: + ```Rust fn main(){ let a: u32 = 0b101; // 二进制整数 @@ -58,7 +65,9 @@ fn main(){ println!("{}, {}, {}, {}", a, b, c, d); // 5, 15, 1, 172 } ``` + (5)Rust中所有的数值类型都支持基本数学运算:加、减、乘、除、取余,如下: + ```Rust fn main() { let sum = 5 + 10; @@ -70,117 +79,129 @@ fn main() { } ``` -- 布尔型 +### 3.2.1.2 布尔型 - Rust中的布尔型用bool表示,有两个可能的值,为true和false。布尔类型使用的场景主要是条件表达式(控制流的内容),使用如下: +Rust中的布尔型用`bool`表示,有两个可能的值,为`true`和`false`。布尔类型使用的场景主要是条件表达式(控制流的内容),使用如下: - ```Rust - fn main() { - // 定义方式 - let a: bool = true; - let b: bool = false; +```Rust +fn main() { + // 定义方式 + let a: bool = true; + let b: bool = false; - // 使用场景 - if a { - println!("a is true"); - } else { - println!("a is false"); - } - - if b { - println!("b is true"); - } else { - println!("b is false"); - } + // 使用场景 + if a { + println!("a is true"); + } else { + println!("a is false"); } - ``` -- 字符类型 - - char类型用于存放单个unicode字符,占用4个字节空间。当存储char类型数据时,Rust会将其转换为utf-8编码的数据存储。char字面量是单引号包含的任意单个字符,字符类型使用示例如下: - - ```Rust - fn main() { - let c: char = 'z'; - let x: char = 'x'; - let heart_eyed_cat: char = '😻'; + if b { + println!("b is true"); + } else { + println!("b is false"); } - ``` +} +``` + +### 3.2.1.3 字符类型 + +char类型用于存放单个unicode字符,占用4个字节空间。当存储char类型数据时,Rust会将其转换为utf-8编码的数据存储。`char`字面量是单引号包含的任意单个字符,字符类型使用示例如下: + +```Rust +fn main() { + let c: char = 'z'; + let x: char = 'x'; + let heart_eyed_cat: char = '😻'; +} +``` ## 3.2.2. 原生复合类型 + 复合类型是将多个值组合成一个类型。Rust有两个原生复合类型:元组和数组。 -- 元组 +### 3.2.2.1 元组 - 圆括号以及其中逗号分割的值列表组成元组,定义一个元组方式如下: - ```Rust - fn main() { - let tup: (i32, f64, u8) = (500, 6.4, 1); - } - ``` - 可以将元组重新结构到变量上,如下: - ```Rust - fn main() { - let tup = (500, 6.4, 1); - let (x, y, z) = tup; - //接下来你可以使用x、y、z - } - ``` - 也可以直接使用元组的元素,如下: - ```Rust - fn main() { - let x: (i32, f64, u8) = (500, 6.4, 1); - let first = x.0; - let second = x.1; - let third = x.2; - } - ``` +圆括号以及其中逗号分割的值列表组成元组,定义一个元组方式如下: - 不带任何值的元组,称为unit类型(单元元组),可以代表空值或者空的返回类型,如下: - ```Rust - fn main() { - let x: () = (); // 将值()保存到类型为()的变量x中 - } - ``` +```Rust +fn main() { + let tup: (i32, f64, u8) = (500, 6.4, 1); +} +``` -- 数组 +可以将元组重新结构到变量上,如下: - 数组中的每个元素的类型必须相同,数组的长度是固定的,数组的定义方式如下: +```Rust +fn main() { + let tup = (500, 6.4, 1); + let (x, y, z) = tup; + //接下来你可以使用x、y、z +} +``` - ```Rust - fn main() { - let a = [1, 2, 3, 4, 5]; //直接写数组的值 - let b: [i32; 5] = [1, 2, 3, 4, 5]; //显示指定数组的类型和长度 - let c: [i32; 5] = [3; 5]; //数组每个元素为同样的值,等价于let a = [5, 5, 5, 5, 5]; - } - ``` - 数组通过索引来访问元素,索引从0开始计数,如下: - ```Rust - fn main() { - let a = [1, 2, 3, 4, 5]; +也可以直接使用元组的元素,如下: - let first = a[0]; // first = 1 - let second = a[1]; // second = 2 - } - ``` - Rust中,访问无效的索引元素会报错,如下: - ```Rust - fn main() { - let a = [1, 2, 3, 4, 5]; - let b = a[5]; // 错误,只能放为0-4,所以这个代码将无法编译 - } - ``` +```Rust +fn main() { + let x: (i32, f64, u8) = (500, 6.4, 1); + let first = x.0; + let second = x.1; + let third = x.2; +} +``` + +不带任何值的元组,称为unit类型(单元元组),可以代表空值或者空的返回类型,如下: + +```Rust +fn main() { + let x: () = (); // 将值()保存到类型为()的变量x中 +} +``` + +### 3.2.2.2 数组 + +数组中的每个元素的类型必须相同,数组的长度是固定的,数组的定义方式如下: + +```Rust +fn main() { + let a = [1, 2, 3, 4, 5]; //直接写数组的值 + let b: [i32; 5] = [1, 2, 3, 4, 5]; //显示指定数组的类型和长度 + let c: [i32; 5] = [3; 5]; //数组每个元素为同样的值,等价于let a = [5, 5, 5, 5, 5]; +} +``` + +数组通过索引来访问元素,索引从0开始计数,如下: + +```Rust +fn main() { + let a = [1, 2, 3, 4, 5]; + + let first = a[0]; // first = 1 + let second = a[1]; // second = 2 +} +``` + +Rust中,访问无效的索引元素会报错,如下: + +```Rust +fn main() { + let a = [1, 2, 3, 4, 5]; + let b = a[5]; // 错误,只能放为0-4,所以这个代码将无法编译 +} +``` ## 3.2.3 类型转换 (少一个From, Into, TryFrom, TryInto) -Rust中可以使用as进行类型转换。 +Rust中可以使用`as`进行类型转换。 + +- 数值类型之间默认不会隐式转换,如果要转换,则需手动使用`as`进行转换。 +- bool类型可以转换为各种数值类型,`false`对应`0`,`true`对应`1`。 +- 可以通过`as`将`char`类型转换为各种整型,目标类型小于4字节时,会从高位截断。 +- 可以通过`as`将`u8`转换为`char`类型。 +- 可以使用`std::char::from_u32`将`u32`转换为`char`类型。 +- 可以使用`std::char::from_digit`将十进制整型转换为`char`类型。 -- 数值类型之间默认不会隐式转换,如果要转换,则需手动使用as进行转换。 -- bool类型可以转换为各种数值类型,false对应0,true对应1。 -- 可以通过as将char类型转换为各种整型,目标类型小于4字节时,会从高位截断。 -- 可以通过as将u8转换为char类型。 -- 可以使用std::char::from_u32将u32转换为char类型。 -- 可以使用std::char::from_digit将十进制整型转换为char类型。 ```Rust fn main() { // 数值类型的转换 diff --git a/src/chapter_3/chapter_3_20.md b/src/chapter_3/chapter_3_20.md index 4a21020..0acfe52 100644 --- a/src/chapter_3/chapter_3_20.md +++ b/src/chapter_3/chapter_3_20.md @@ -2,375 +2,377 @@ ## 3.20.1 使用线程 -1. 相关概念 +### 3.20.1.1 相关概念 - - 进程是资源分配的最小单位,线程是CPU调度的最小单位。 - - 在使用多线程时,经常会遇到如下一些问题: - - 竞争状态:多个线程以不一致的顺序访问数据或资源; - - 死锁:两个线程相互等待对方停止使用其所拥有的资源,造成两者都永久等待; - - 只会发生在特定情况下且难以稳定重现和修复的bug。 - - 编程语言提供的线程叫做绿色线程,如go语言,在底层实现了M:N的模型,即M个绿色线程对应N个OS线程。但是,Rust标准库只提供1:1的线程模型的实现,即一个Rust线程对应一个Os线程。 +- 进程是资源分配的最小单位,线程是CPU调度的最小单位。 +- 在使用多线程时,经常会遇到如下一些问题: + - 竞争状态:多个线程以不一致的顺序访问数据或资源; + - 死锁:两个线程相互等待对方停止使用其所拥有的资源,造成两者都永久等待; + - 只会发生在特定情况下且难以稳定重现和修复的bug。 +- 编程语言提供的线程叫做绿色线程,如go语言,在底层实现了M:N的模型,即M个绿色线程对应N个OS线程。但是,Rust标准库只提供1:1的线程模型的实现,即一个Rust线程对应一个Os线程。 -2. 创建线程 +### 3.20.1.2. 创建线程 - 创建一个新线程需要调用thread::spawn函数并传递一个闭包,示例如下: +创建一个新线程需要调用thread::spawn函数并传递一个闭包,示例如下: - ```rust - use std::thread; - use std::time::Duration; +```rust +use std::thread; +use std::time::Duration; - fn main() { - thread::spawn(|| { // 创建一个线程 - for i in 1..10 { - println!("hi number {} from the spawned thread!", i); - thread::sleep(Duration::from_millis(1)); - } - }); - - for i in 1..5 { - println!("hi number {} from the main thread!", i); +fn main() { + thread::spawn(|| { // 创建一个线程 + for i in 1..10 { + println!("hi number {} from the spawned thread!", i); thread::sleep(Duration::from_millis(1)); } + }); + + for i in 1..5 { + println!("hi number {} from the main thread!", i); + thread::sleep(Duration::from_millis(1)); } - ``` +} +``` -3. 等待线程结束 - 前面的例子并不能保证子线程执行完所有的打印,因为主线程有可能在子线程执行完成前结束从而退出程序,所以需要在主线程中等待子线程结束。等待子线程结束需要调用join方法,示例如下: - ```rust - use std::thread; - use std::time::Duration; +### 3.20.1.3. 等待线程结束 - fn main() { - let handle = thread::spawn(|| { - for i in 1..10 { - println!("hi number {} from the spawned thread!", i); - thread::sleep(Duration::from_millis(1)); - } - }); +前面的例子并不能保证子线程执行完所有的打印,因为主线程有可能在子线程执行完成前结束从而退出程序,所以需要在主线程中等待子线程结束。等待子线程结束需要调用join方法,示例如下: - for i in 1..5 { - println!("hi number {} from the main thread!", i); +```rust +use std::thread; +use std::time::Duration; + +fn main() { + let handle = thread::spawn(|| { + for i in 1..10 { + println!("hi number {} from the spawned thread!", i); thread::sleep(Duration::from_millis(1)); } + }); - handle.join().unwrap(); // 等待子线程结束 + for i in 1..5 { + println!("hi number {} from the main thread!", i); + thread::sleep(Duration::from_millis(1)); } - ``` -4. 线程与move闭包 + handle.join().unwrap(); // 等待子线程结束 +} +``` - move关键字可用于传递给thread::spawn的闭包,获取环境中的值的所有权,从而达到将值的所有权从一个线程传送到另一个线程的目的。示例如下: - ```rust - use std::thread; - fn main() { - let v = vec![1, 2, 3]; - let handle = thread::spawn(move || { // 将v移动进了闭包,完成了值从主线程到子线程的过程 - println!("Here's a vector: {:?}", v); - }); - handle.join().unwrap(); - } - ``` +### 3.20.1.4. 线程与move闭包 + +move关键字可用于传递给thread::spawn的闭包,获取环境中的值的所有权,从而达到将值的所有权从一个线程传送到另一个线程的目的。示例如下: +```rust +use std::thread; +fn main() { + let v = vec![1, 2, 3]; + let handle = thread::spawn(move || { // 将v移动进了闭包,完成了值从主线程到子线程的过程 + println!("Here's a vector: {:?}", v); + }); + handle.join().unwrap(); +} +``` ## 3.20.2 传递消息 -1. 通道简单介绍 +### 3.20.2.1. 通道简单介绍 - Rust中实现消息传递并发的主要工具是通道。通道由发送者和接受者两部分组成: - - 发送者用来发送消息; - - 接收者用来接收消息; - - 发送者或者接收者任一被丢弃时就认为通道被关闭了。 - Rust标准库中提供的通道叫做mpsc,是多个生产者,单个消费者的通道,其使用示例如下: - ```rust - use std::sync::mpsc; - use std::thread; +Rust中实现消息传递并发的主要工具是通道。通道由发送者和接受者两部分组成: +- 发送者用来发送消息; +- 接收者用来接收消息; +- 发送者或者接收者任一被丢弃时就认为通道被关闭了。 +Rust标准库中提供的通道叫做mpsc,是多个生产者,单个消费者的通道,其使用示例如下: +```rust +use std::sync::mpsc; +use std::thread; - fn main() { - let (tx, rx) = mpsc::channel(); //创建channel,返回发送者、接收者 +fn main() { + let (tx, rx) = mpsc::channel(); //创建channel,返回发送者、接收者 - thread::spawn(move || { - let val = String::from("hi"); - tx.send(val).unwrap(); //使用发送者通过channel发送 - }); + thread::spawn(move || { + let val = String::from("hi"); + tx.send(val).unwrap(); //使用发送者通过channel发送 + }); - let received = rx.recv().unwrap(); //使用接收者通过channel接收 - println!("Got: {}", received); - } - ``` + let received = rx.recv().unwrap(); //使用接收者通过channel接收 + println!("Got: {}", received); +} +``` - 关于mpsc通道的使用,有以下几点说明: - - 发送者的send方法返回一个Result类型,如果接收端已经被丢弃了,将没有发送值的目标,所以发送操作将返回错误; - - 接收者的recv方法也返回Result类型,当通道发送端关闭时,将返回一个错误值表明不会再由新的值到来了; - - 接收还可以使用try_recv方法,recv方法会阻塞到一直等待到消息到来,而try_recv不会阻塞,它会立即返回,Ok值标识包含可用信息,而Err则代表此时没有任何信息。 +关于mpsc通道的使用,有以下几点说明: +- 发送者的send方法返回一个Result类型,如果接收端已经被丢弃了,将没有发送值的目标,所以发送操作将返回错误; +- 接收者的recv方法也返回Result类型,当通道发送端关闭时,将返回一个错误值表明不会再由新的值到来了; +- 接收还可以使用try_recv方法,recv方法会阻塞到一直等待到消息到来,而try_recv不会阻塞,它会立即返回,Ok值标识包含可用信息,而Err则代表此时没有任何信息。 -2. 通道和所有权 +### 3.20.2.2. 通道和所有权 - 在使用通道时,send 函数会获取参数的所有权并移动这个值归接收者所有。例如下面的代码将会编译错误: - ```rust - use std::sync::mpsc; - use std::thread; +在使用通道时,send 函数会获取参数的所有权并移动这个值归接收者所有。例如下面的代码将会编译错误: +```rust +use std::sync::mpsc; +use std::thread; - fn main() { - let (tx, rx) = mpsc::channel(); +fn main() { + let (tx, rx) = mpsc::channel(); - thread::spawn(move || { - let val = String::from("hi"); + thread::spawn(move || { + let val = String::from("hi"); + tx.send(val).unwrap(); + println!("val is {}", val); //错误,此处不能使用val,因为val的所有权已经move到通道里面去了 + }); + + let received = rx.recv().unwrap(); + println!("Got: {}", received); +} +``` + +### 3.20.2.3. 发送多个值示例 + +利用通道发送多个值的示例如下: +```rust +use std::sync::mpsc; +use std::thread; +use std::time::Duration; + +fn main() { + let (tx, rx) = mpsc::channel(); + + thread::spawn(move || { + let vals = vec![ + String::from("hi"), + String::from("from"), + String::from("the"), + String::from("thread"), + ]; + + for val in vals { tx.send(val).unwrap(); - println!("val is {}", val); //错误,此处不能使用val,因为val的所有权已经move到通道里面去了 - }); + thread::sleep(Duration::from_secs(1)); + } + }); - let received = rx.recv().unwrap(); + for received in rx { println!("Got: {}", received); } - ``` +} +``` -3. 发送多个值示例 +### 3.20.2.4. 多个生产者示例 - 利用通道发送多个值的示例如下: - ```rust - use std::sync::mpsc; - use std::thread; - use std::time::Duration; +mpsc是multiple producer, single consumer的缩写,此通道可以有多个生产者。相对应的,spmc通道则可以有单个生产者,多个消费者。下面的示例演示多个生产者、单个消费者一起工作: - fn main() { - let (tx, rx) = mpsc::channel(); +```rust +use std::sync::mpsc; +use std::thread; +use std::time::Duration; - thread::spawn(move || { - let vals = vec![ - String::from("hi"), - String::from("from"), - String::from("the"), - String::from("thread"), - ]; +fn main() { + let (tx, rx) = mpsc::channel(); + let tx1 = mpsc::Sender::clone(&tx); //通过clone来使用 + thread::spawn(move || { + //第一个发送者线程 + let vals = vec![ + String::from("hi"), + String::from("from"), + String::from("the"), + String::from("thread"), + ]; - for val in vals { - tx.send(val).unwrap(); - thread::sleep(Duration::from_secs(1)); - } - }); - - for received in rx { - println!("Got: {}", received); + for val in vals { + tx1.send(val).unwrap(); + thread::sleep(Duration::from_secs(1)); } - } - ``` + }); -4. 多个生产者示例 + thread::spawn(move || { + //第二个发送者线程 + let vals = vec![ + String::from("hi"), + String::from("from"), + String::from("the"), + String::from("thread"), + ]; - mpsc是multiple producer, single consumer的缩写,此通道可以有多个生产者。相对应的,spmc通道则可以有单个生产者,多个消费者。下面的示例演示多个生产者、单个消费者一起工作: - - ```rust - use std::sync::mpsc; - use std::thread; - use std::time::Duration; - - fn main() { - let (tx, rx) = mpsc::channel(); - let tx1 = mpsc::Sender::clone(&tx); //通过clone来使用 - thread::spawn(move || { - //第一个发送者线程 - let vals = vec![ - String::from("hi"), - String::from("from"), - String::from("the"), - String::from("thread"), - ]; - - for val in vals { - tx1.send(val).unwrap(); - thread::sleep(Duration::from_secs(1)); - } - }); - - thread::spawn(move || { - //第二个发送者线程 - let vals = vec![ - String::from("hi"), - String::from("from"), - String::from("the"), - String::from("thread"), - ]; - - for val in vals { - tx.send(val).unwrap(); - thread::sleep(Duration::from_secs(1)); - } - }); - - for received in rx { - println!("Got: {}", received); + for val in vals { + tx.send(val).unwrap(); + thread::sleep(Duration::from_secs(1)); } - } - ``` + }); - 上面代码中,有两个发送者发送数据,一个接收者结束数据。 + for received in rx { + println!("Got: {}", received); + } +} +``` + +上面代码中,有两个发送者发送数据,一个接收者结束数据。 ## 3.20.3 共享内存 某些情况下,多个线程之间需要共享内存,即多个线程都访问某个变量。如何在多个线程间安全的共享内存,就需要用到锁、原子操作等机制,下面主要介绍Rust中锁的使用。 -1. 互斥锁(Mutex) +### 3.20.3.1. 互斥锁(Mutex) - 在任意时刻,互斥锁只允许一个线程访问数据;在使用数据前需获取锁,使用完成后需要释放锁。关于Mutex的用法示例如下: - ```rust - use std::sync::Mutex; - fn main() { - let m = Mutex::new(5); - { - let mut num = m.lock().unwrap(); - *num = 6; // 可以对Mutex内部的值进行修改,可见Mutex和RefCell类似,提供内部可变性 - } // 离开作用域,Mutex的锁会自动释放 - println!("m = {:?}", m); +在任意时刻,互斥锁只允许一个线程访问数据;在使用数据前需获取锁,使用完成后需要释放锁。关于Mutex的用法示例如下: +```rust +use std::sync::Mutex; +fn main() { + let m = Mutex::new(5); + { + let mut num = m.lock().unwrap(); + *num = 6; // 可以对Mutex内部的值进行修改,可见Mutex和RefCell类似,提供内部可变性 + } // 离开作用域,Mutex的锁会自动释放 + println!("m = {:?}", m); +} +``` +Mutex本质上是一个智能指针,lock()返回一个叫做MutexGuard的智能指针,其内部提供了drop方法,所以当MutexGuard离开作用域时会自动释放锁。Mutex获取到锁后,可以对其内部的值进行修改,可见它和RefCell类似,也提供了内部可变性。 + +### 3.20.3.2. 多线程与多所有权 + +有了Mutex之后,能对共享数据进行保护;但是由于Rust的所有权机制,每个值都有且仅有一个所有者。下面的代码将无法通过编译: + +```rust +use std::sync::Mutex; +use std::thread; +fn main() { + let counter = Mutex::new(0); + let mut handles = vec![]; + for _ in 0..10 { + let handle = thread::spawn(move || { // 不能将counter移到多个线程中 + let mut num = counter.lock().unwrap(); + *num += 1; + }); + handles.push(handle); } - ``` - Mutex本质上是一个智能指针,lock()返回一个叫做MutexGuard的智能指针,其内部提供了drop方法,所以当MutexGuard离开作用域时会自动释放锁。Mutex获取到锁后,可以对其内部的值进行修改,可见它和RefCell类似,也提供了内部可变性。 - -2. 多线程与多所有权 - - 有了Mutex之后,能对共享数据进行保护;但是由于Rust的所有权机制,每个值都有且仅有一个所有者。下面的代码将无法通过编译: - - ```rust - use std::sync::Mutex; - use std::thread; - fn main() { - let counter = Mutex::new(0); - let mut handles = vec![]; - for _ in 0..10 { - let handle = thread::spawn(move || { // 不能将counter移到多个线程中 - let mut num = counter.lock().unwrap(); - *num += 1; - }); - handles.push(handle); - } - for handle in handles { - handle.join().unwrap(); - } - println!("Result: {}", *counter.lock().unwrap()); + for handle in handles { + handle.join().unwrap(); } - ``` + println!("Result: {}", *counter.lock().unwrap()); +} +``` - 因此要在多个线程间共享内存,还需要有一种机制能让多个线程都能访问Mutex保护的数据。根据本书目前学到的知识,可以想到Rc智能指针。上面的代码使用Rc智能指针后的示例如下: - ```rust - use std::rc::Rc; - use std::sync::Mutex; - use std::thread; - fn main() { - let counter = Rc::new(Mutex::new(0)); - let mut handles = vec![]; - for _ in 0..10 { - let counter = Rc::clone(&counter); - let handle = thread::spawn(move || { - let mut num = counter.lock().unwrap(); - *num += 1; - }); - handles.push(handle); - } - for handle in handles { - handle.join().unwrap(); - } - println!("Result: {}", *counter.lock().unwrap()); +因此要在多个线程间共享内存,还需要有一种机制能让多个线程都能访问Mutex保护的数据。根据本书目前学到的知识,可以想到Rc智能指针。上面的代码使用Rc智能指针后的示例如下: +```rust +use std::rc::Rc; +use std::sync::Mutex; +use std::thread; +fn main() { + let counter = Rc::new(Mutex::new(0)); + let mut handles = vec![]; + for _ in 0..10 { + let counter = Rc::clone(&counter); + let handle = thread::spawn(move || { + let mut num = counter.lock().unwrap(); + *num += 1; + }); + handles.push(handle); } - ``` - - 上面的代码仍将编译错误,因为Rc是非线程安全的,此时需要使用Arc类型。Arc是一个类似于Rc并可以安全的用于并发环境的类型,使用Arc的示例如下: - - ```rust - use std::sync::{Arc, Mutex}; - use std::thread; - - fn main() { - let counter = Arc::new(Mutex::new(0)); // 使用Arc共享 - let mut handles = vec![]; - - for _ in 0..10 { - let counter = Arc::clone(&counter); - let handle = thread::spawn(move || { - let mut num = counter.lock().unwrap(); - - *num += 1; - }); - handles.push(handle); - } - - for handle in handles { - handle.join().unwrap(); - } - - println!("Result: {}", *counter.lock().unwrap()); + for handle in handles { + handle.join().unwrap(); } - ``` + println!("Result: {}", *counter.lock().unwrap()); +} +``` -3. 读写锁 +上面的代码仍将编译错误,因为Rc是非线程安全的,此时需要使用Arc类型。Arc是一个类似于Rc并可以安全的用于并发环境的类型,使用Arc的示例如下: - 读变量并不会改变变量的状态,互斥锁Mutex每次读写都会加锁,当在有大量读、少量写的场景时使用Mutex就会效率不高,此时可以使用读写锁RwLock,示例如下: +```rust +use std::sync::{Arc, Mutex}; +use std::thread; - ```rust - use std::sync::{Arc, RwLock}; - use std::thread; +fn main() { + let counter = Arc::new(Mutex::new(0)); // 使用Arc共享 + let mut handles = vec![]; - fn main() { - let shared_data = Arc::new(RwLock::new(vec![1, 2, 3])); // 创建一个共享的 vector 数据 + for _ in 0..10 { + let counter = Arc::clone(&counter); + let handle = thread::spawn(move || { + let mut num = counter.lock().unwrap(); - let mut threads = vec![]; - - // 创建 5 个读线程 - for i in 0..5 { - let shared_data = shared_data.clone(); - threads.push(thread::spawn(move || { - // 获取读锁 - let shared_data = shared_data.read().unwrap(); - println!("Thread {} read data: {:?}", i, *shared_data); - })); - } - - // 创建 2 个写线程 - for i in 0..2 { - let shared_data = shared_data.clone(); - threads.push(thread::spawn(move || { - // 获取写锁 - let mut shared_data = shared_data.write().unwrap(); - println!("Thread {} write data", i); - shared_data.push(i); // 向 vector 中添加数据 - })); - } - - // 等待所有线程完成 - for thread in threads { - thread.join().unwrap(); - } + *num += 1; + }); + handles.push(handle); } - ``` -4. 小结 + for handle in handles { + handle.join().unwrap(); + } - 关于Mutex和RwLock的选择: + println!("Result: {}", *counter.lock().unwrap()); +} +``` - - 追求高并发读取时,使用RwLock,因为Mutex一次只允许一个线程读取; - - 如果要保证写操作的成功性,使用Mutex; - - 不知道哪个合适,统一使用Mutex。 +### 3.20.3.3. 读写锁 - 关于Mutex和Arc类型: +读变量并不会改变变量的状态,互斥锁Mutex每次读写都会加锁,当在有大量读、少量写的场景时使用Mutex就会效率不高,此时可以使用读写锁RwLock,示例如下: - - Mutex和RefCell一样具有内部可变性,不过Mutex是线程安全的,而RefCell不是线程安全的; - - Arc和Rc功能类似,但是Arc是线程安全的,而Rc不是线程安全的。 +```rust +use std::sync::{Arc, RwLock}; +use std::thread; + +fn main() { + let shared_data = Arc::new(RwLock::new(vec![1, 2, 3])); // 创建一个共享的 vector 数据 + + let mut threads = vec![]; + + // 创建 5 个读线程 + for i in 0..5 { + let shared_data = shared_data.clone(); + threads.push(thread::spawn(move || { + // 获取读锁 + let shared_data = shared_data.read().unwrap(); + println!("Thread {} read data: {:?}", i, *shared_data); + })); + } + + // 创建 2 个写线程 + for i in 0..2 { + let shared_data = shared_data.clone(); + threads.push(thread::spawn(move || { + // 获取写锁 + let mut shared_data = shared_data.write().unwrap(); + println!("Thread {} write data", i); + shared_data.push(i); // 向 vector 中添加数据 + })); + } + + // 等待所有线程完成 + for thread in threads { + thread.join().unwrap(); + } +} +``` + +### 3.20.3.4. 小结 + +关于Mutex和RwLock的选择: + +- 追求高并发读取时,使用RwLock,因为Mutex一次只允许一个线程读取; +- 如果要保证写操作的成功性,使用Mutex; +- 不知道哪个合适,统一使用Mutex。 + +关于Mutex和Arc类型: + +- Mutex和RefCell一样具有内部可变性,不过Mutex是线程安全的,而RefCell不是线程安全的; +- Arc和Rc功能类似,但是Arc是线程安全的,而Rc不是线程安全的。 ## 3.20.4 Send trait和Sync trait -1. Send和Sync +### 3.20.4.1. Send和Sync - Send trait和Sync trait是Rust语言中的两个标记trait(即未定义任何行为,但是可以标记一个类型),其作用如下: - - 实现了Send的类型可以在线程间安全的传递其所有权; - - 实现了Sync的类型可以在线程间通过引用安全的共享。 +Send trait和Sync trait是Rust语言中的两个标记trait(即未定义任何行为,但是可以标记一个类型),其作用如下: +- 实现了Send的类型可以在线程间安全的传递其所有权; +- 实现了Sync的类型可以在线程间通过引用安全的共享。 - 实现Sync的类型是通过引用在线程间共享的,因此一个类型要在线程间安全的共享,那么它的应用必须能安全的在线程间进行传递。所以可以有结论:若&T满足Send,那么T满足Sync。 +实现Sync的类型是通过引用在线程间共享的,因此一个类型要在线程间安全的共享,那么它的应用必须能安全的在线程间进行传递。所以可以有结论:若&T满足Send,那么T满足Sync。 -2. 实现了Send和Sync的类型 +### 3.20.4.2. 实现了Send和Sync的类型 - Rust中几乎所有类型都默认实现了Send和Sync。Send和Sync也是可自动派生的trait,因此一个复合类型,如果其内部成员都实现了Send或Sync,那么它就自动实现了Send或Sync。 +Rust中几乎所有类型都默认实现了Send和Sync。Send和Sync也是可自动派生的trait,因此一个复合类型,如果其内部成员都实现了Send或Sync,那么它就自动实现了Send或Sync。 - Rust中绝大多数类型都实现了Send和Sync,但是下面几个是没有实现Send或者Sync的: +Rust中绝大多数类型都实现了Send和Sync,但是下面几个是没有实现Send或者Sync的: - - 裸指针既没有实现Send也没有实现Sync,因为它本身就没有任何安全保证的; - - UnsafeCell没有实现Sync,Cell和RefCell也没有实现Sync(但是它们实现了Send); - - Rc既没有实现Send也没有实现Sync。 +- 裸指针既没有实现Send也没有实现Sync,因为它本身就没有任何安全保证的; +- UnsafeCell没有实现Sync,Cell和RefCell也没有实现Sync(但是它们实现了Send); +- Rc既没有实现Send也没有实现Sync。 通常情况下不需要为某个类型手动实现Send和Sync trait,手动实现这些标记trait 涉及到编写不安全的Rust代码,本书在后面unsafe编程部分介绍。 diff --git a/src/chapter_3/chapter_3_21.md b/src/chapter_3/chapter_3_21.md index 2ba08e3..bb2fb58 100644 --- a/src/chapter_3/chapter_3_21.md +++ b/src/chapter_3/chapter_3_21.md @@ -4,136 +4,136 @@ 到此节之前本书介绍的基本都是安全的Rust,即Rust编译时会强制执行内存安全保证的检查,只要编译通过,基本就能保证代码的安全性。既然有安全的Rust,那么必然也有不安全的Rust。 -1. 不安全的Rust存在的原因 +### 3.21.1.1 不安全的Rust存在的原因 - - 代码静态分析相对保守,这就意味着某些代码可能是合法的,但是编译器检查也会拒绝通过。在此情况下,可以使用不安全的代码。 - - 底层计算机硬件固有的不安全性。如果Rust不允许进行不安全的操作,有些任务根本就完成不了。 +- 代码静态分析相对保守,这就意味着某些代码可能是合法的,但是编译器检查也会拒绝通过。在此情况下,可以使用不安全的代码。 +- 底层计算机硬件固有的不安全性。如果Rust不允许进行不安全的操作,有些任务根本就完成不了。 -2. 不安全的Rust使用的场景 +### 3.21.1.2. 不安全的Rust使用的场景 - Rust通过unsafe关键字切换到不安全的Rust,不安全的Rust使用的场景如下: - - 解引用裸指针; - - 调用不安全的函数或者方法; - - 访问或修改可变静态变量; - - 实现不安全的trait。 +Rust通过unsafe关键字切换到不安全的Rust,不安全的Rust使用的场景如下: +- 解引用裸指针; +- 调用不安全的函数或者方法; +- 访问或修改可变静态变量; +- 实现不安全的trait。 注意:unsafe并不会关闭借用检查器或禁用任何其它的Rust安全检查规则,它只提供上述几个不被编译器检查内存安全的功能。unsafe也不意味着块中的代码一定是有问题的,它只是表示由程序员来确保安全。 ## 3.21.2 使用unsafe编程 -1. 解引用裸指针 +### 3.21.2.1. 解引用裸指针 - 裸指针又称为原生指针,在功能上和引用类似,但是引用的安全由Rust编译器保证,裸指针则不是。裸指针需要显式标明可变性,不可变的裸指针分别写作 ``*const T`` ,可变的裸指针写作 ``*mut T``。 +裸指针又称为原生指针,在功能上和引用类似,但是引用的安全由Rust编译器保证,裸指针则不是。裸指针需要显式标明可变性,不可变的裸指针分别写作 ``*const T`` ,可变的裸指针写作 ``*mut T``。 - 裸指针与引用和只能指针的区别如下: - - 可以忽略借用规则,在代码中同时拥有不可变和可变的裸指针,或多个指向相同位置的可变裸指针; - - 不保证裸指针指向有效的内存; - - 允许裸指针为空; - - 裸指针不能实现任何自动清理功能。 +裸指针与引用和只能指针的区别如下: +- 可以忽略借用规则,在代码中同时拥有不可变和可变的裸指针,或多个指向相同位置的可变裸指针; +- 不保证裸指针指向有效的内存; +- 允许裸指针为空; +- 裸指针不能实现任何自动清理功能。 - 可以在安全代码中创建可变或不可变的裸指针,但是只能在不安全块中解引用裸指针,示例如下: +可以在安全代码中创建可变或不可变的裸指针,但是只能在不安全块中解引用裸指针,示例如下: - ```rust - fn main() { - let mut num = 5; - let r1 = &num as *const i32; - let r2 = &mut num as *mut i32; - let address = 0x012345usize; - let _r = address as *const i32; - unsafe { - println!("r1 is: {}", *r1); - println!("r2 is: {}", *r2); - } +```rust +fn main() { + let mut num = 5; + let r1 = &num as *const i32; + let r2 = &mut num as *mut i32; + let address = 0x012345usize; + let _r = address as *const i32; + unsafe { + println!("r1 is: {}", *r1); + println!("r2 is: {}", *r2); } - ``` +} +``` - 说明:创建一个裸指针不会造成任何危险,只有当访问其指向的值时(并且这个值已经无效)才可能造成危险,所以可以在安全的代码块中创建裸指针,但是使用则必须在unsafe的块中。 +说明:创建一个裸指针不会造成任何危险,只有当访问其指向的值时(并且这个值已经无效)才可能造成危险,所以可以在安全的代码块中创建裸指针,但是使用则必须在unsafe的块中。 -2. 调用不安全的函数或者方法 +### 3.21.2.2. 调用不安全的函数或者方法 - 示例1,代码如下: +示例1,代码如下: - ```rust - unsafe fn dangerous() { - println!("Do some dangerous thing"); +```rust +unsafe fn dangerous() { + println!("Do some dangerous thing"); +} +fn main() { + unsafe { + dangerous(); } - fn main() { - unsafe { - dangerous(); - } - println!("Hello, world!"); + println!("Hello, world!"); +} +``` + +示例2,代码如下: +```rust +fn foo() { + let mut num = 5; + let r1 = &num as *const i32; + let r2 = &mut num as *mut i32; + unsafe { + println!("r1 is: {}", *r1); + println!("r2 is: {}", *r2); } - ``` +} +fn main() { + foo(); +} +``` - 示例2,代码如下: - ```rust - fn foo() { - let mut num = 5; - let r1 = &num as *const i32; - let r2 = &mut num as *mut i32; - unsafe { - println!("r1 is: {}", *r1); - println!("r2 is: {}", *r2); - } +### 3.21.2.3. 访问或者修改可变静态变量 + +全局变量在Rust中被称为静态变量。静态变量的名称采用SCREAMING_SNAKE_CASE写法,它只能存储拥有 'static生命周期的引用。下面的示例使用了不可变的静态变量: + +```rust +static HELLO_WORLD: &str = "Hello, world!"; // 不可变的静态变量 +fn main() { + println!("name is: {}", HELLO_WORLD); +} +``` + +这里对比一下常量和静态变量: + +- 静态变量中的值有一个固定的内存地址(即使用这个值总会访问相同的地址),常量则允许在任何被用到的时候复制其数据。 +- 静态变量可以是可变的,虽然这可能是不安全的。 + +下面的示例使用可变的静态变量: + +```rust +static mut COUNTER: u32 = 0; // 可变的静态变量,同样需要mut关键字 +fn add_to_count(inc: u32) { + unsafe { + COUNTER += inc; } - fn main() { - foo(); +} +fn main() { + add_to_count(3); + unsafe { + println!("COUNTER: {}", COUNTER); } - ``` +} +``` -3. 访问或者修改可变静态变量 +### 3.21.2.4. 实现不安全的trait - 全局变量在Rust中被称为静态变量。静态变量的名称采用SCREAMING_SNAKE_CASE写法,它只能存储拥有 'static生命周期的引用。下面的示例使用了不可变的静态变量: +当至少有一个方法中包含编译器不能验证的invariant时,该trait就是不安全的。在trait之前增加unsafe关键字将trait声明为unsafe,同时trait的实现也必须标记为unsafe。下面为使用unsafe的trait的示例: - ```rust - static HELLO_WORLD: &str = "Hello, world!"; // 不可变的静态变量 - fn main() { - println!("name is: {}", HELLO_WORLD); +```rust +struct Bar; + +unsafe trait Foo { + fn foo(&self); +} + +unsafe impl Foo for Bar { + fn foo(&self) { + println!("foo"); } - ``` +} - 这里对比一下常量和静态变量: - - - 静态变量中的值有一个固定的内存地址(即使用这个值总会访问相同的地址),常量则允许在任何被用到的时候复制其数据。 - - 静态变量可以是可变的,虽然这可能是不安全的。 - - 下面的示例使用可变的静态变量: - - ```rust - static mut COUNTER: u32 = 0; // 可变的静态变量,同样需要mut关键字 - fn add_to_count(inc: u32) { - unsafe { - COUNTER += inc; - } - } - fn main() { - add_to_count(3); - unsafe { - println!("COUNTER: {}", COUNTER); - } - } - ``` - -4. 实现不安全的trait - - 当至少有一个方法中包含编译器不能验证的invariant时,该trait就是不安全的。在trait之前增加unsafe关键字将trait声明为unsafe,同时trait的实现也必须标记为unsafe。下面为使用unsafe的trait的示例: - - ```rust - struct Bar; - - unsafe trait Foo { - fn foo(&self); - } - - unsafe impl Foo for Bar { - fn foo(&self) { - println!("foo"); - } - } - - fn main() { - let a = Bar; - a.foo(); - } - ``` +fn main() { + let a = Bar; + a.foo(); +} +``` diff --git a/src/chapter_3/chapter_3_3.md b/src/chapter_3/chapter_3_3.md index c07b9d9..fb42c7d 100644 --- a/src/chapter_3/chapter_3_3.md +++ b/src/chapter_3/chapter_3_3.md @@ -3,6 +3,7 @@ ## 3.3.1. 函数定义 fn关键字、函数名、函数参数名及其类型(如果有的话)、返回值类型(如果有的话)组成函数签名, 加上由一对花括号包含的函数体组成函数。例子如下: + ```Rust // 一个没有参数,也没有返回值的函数 fn print_line() { @@ -30,6 +31,7 @@ fn main() { ``` Rust中,函数也可以定义在函数内部,如下: + ```Rust fn calculate(a: u32, b: u32) { println!("a is {:?}", a); @@ -54,6 +56,7 @@ fn main() { ## 3.3.2. 语句和表达式 Rust中,语句是执行一个写操作但不返回值的指令,表达式则计算并产生一个值。 + ```Rust fn main() { let a = 1u32; // "1u32"就是一个表达式, “let a = 1u32;”则是一个语句 @@ -70,41 +73,43 @@ fn main() { ## 3.3.3. 函数返回值 - 使用return指定返回值,如下: -```Rust -fn sum(a: u32, b: u32) -> u32 { - let r = a + b; - return r //可以加分号,也可以不加分号, 所以这行等价于“return r;” -} -fn main() { - let a = 1u32; - let b = 2u32; - let c = sum(a, b); - println!("c = {:?}", c); -} -``` + ```Rust + fn sum(a: u32, b: u32) -> u32 { + let r = a + b; + return r //可以加分号,也可以不加分号, 所以这行等价于“return r;” + } -特别的return关键字不指定值时,表示返回的是(),如下: -```Rust -fn my_function() -> () { - println!("some thing"); - return; //等价于 “return ()” -} -``` + fn main() { + let a = 1u32; + let b = 2u32; + let c = sum(a, b); + println!("c = {:?}", c); + } + ``` + + 特别的return关键字不指定值时,表示返回的是(),如下: + ```Rust + fn my_function() -> () { + println!("some thing"); + return; //等价于 “return ()” + } + ``` - 不使用return关键字,将返回最后一条执行的表达式的计算结果,如下: -```Rust -fn sum(a: u32, b: u32) -> u32 { - println!("a is {:?}", a); - println!("b is {:?}", b); - a + b //注意,是没有加分号的 -} + ```Rust + fn sum(a: u32, b: u32) -> u32 { + println!("a is {:?}", a); + println!("b is {:?}", b); -fn main() { - let a = 1u32; - let b = 2u32; - let c = sum(a, b); - println!("c = {:?}", c); -} -``` + a + b //注意,是没有加分号的 + } + + fn main() { + let a = 1u32; + let b = 2u32; + let c = sum(a, b); + println!("c = {:?}", c); + } + ``` diff --git a/src/chapter_3/chapter_3_4.md b/src/chapter_3/chapter_3_4.md index c108f50..1a6a3bd 100644 --- a/src/chapter_3/chapter_3_4.md +++ b/src/chapter_3/chapter_3_4.md @@ -17,6 +17,7 @@ (2)块注释,使用“/*... */”。 示例如下: + ```Rust /* * 块注释: @@ -46,6 +47,7 @@ Rust提供了cargo doc命令可以把文档注释转换成html网页,最终展 (2)文档块注释,使用“/**...*/”。 示例如下: + ```Rust // 下面是文档行注释 @@ -90,8 +92,11 @@ fn main() { ``` 运行如下命令: -``` + +```bash cargo doc --open ``` + 将打开上面代码里面文档注释生成的文档,如下图: + ![注释](../assets/1.png) diff --git a/src/chapter_3/chapter_3_5.md b/src/chapter_3/chapter_3_5.md index 7b7dae3..29324c0 100644 --- a/src/chapter_3/chapter_3_5.md +++ b/src/chapter_3/chapter_3_5.md @@ -2,163 +2,173 @@ Rust中的控制流结构主要包括: -- if条件判断; -- loop循环; -- while循环; -- for .. in 循环。 +- `if`条件判断; +- `loop`循环; +- `while`循环; +- `for .. in` 循环。 ## 3.5.1. if条件判断 -- if执行条件判断,示例如下: -```Rust -fn main() { - let a = 2u32; +- `if`执行条件判断,示例如下: - if a > 5u32 { - println!("a > 5"); - } else { - println!("a <= 5"); + ```Rust + fn main() { + let a = 2u32; + + if a > 5u32 { + println!("a > 5"); + } else { + println!("a <= 5"); + } } -} -``` + ``` -- if - else if处理多重条件,示例如下: -```Rust -fn main() { - let a = 3u32; +- `if - else if`处理多重条件,示例如下: - if a > 5u32 { - println!("a > 5"); - } else if a > 4u32 { - println!("a > 4"); - } else if a > 3u32 { - println!("a > 3"); - } else if a > 2u32 { - println!("a > 2"); - } else if a > 1u32 { - println!("a > 1"); - } else { - println!("a = 1"); + ```Rust + fn main() { + let a = 3u32; + + if a > 5u32 { + println!("a > 5"); + } else if a > 4u32 { + println!("a > 4"); + } else if a > 3u32 { + println!("a > 3"); + } else if a > 2u32 { + println!("a > 2"); + } else if a > 1u32 { + println!("a > 1"); + } else { + println!("a = 1"); + } } -} -``` + ``` -- 在let语句中使用if +- 在`let`语句中使用`if` -if是一个表达式,所以可以在let右侧使用,如下: -```Rust -fn main() { - let a = 3u32; + `if`是一个表达式,所以可以在`let`右侧使用,如下: - let a_bigger_than_two: bool = if a > 2u32 { - true - } else { - false - }; + ```Rust + fn main() { + let a = 3u32; - if a_bigger_than_two { - println!("a > 2"); - } else { - println!("a <= 2"); + let a_bigger_than_two: bool = if a > 2u32 { + true + } else { + false + }; + + if a_bigger_than_two { + println!("a > 2"); + } else { + println!("a <= 2"); + } } -} -``` + ``` ## 3.5.2. loop循环 - loop重复执行代码 -```Rust -fn main() { - // 一直循环打印 again - loop { - println!("again!"); - } -} -``` -- 使用break终止循环 -```Rust -fn main() { - let mut counter = 0; - - loop { - println!("counter = {:?}", counter); - counter += 1; - - if counter == 10 { - break; // 将终止循环 + ```Rust + fn main() { + // 一直循环打印 again + loop { + println!("again!"); } } -} -``` -上面的代码将打印10次,遇到break后终止循环。另外,break也可以返回值,如下: -```Rust -fn main() { - let mut counter = 0; + ``` - let result = loop { - counter += 1; +- 使用`break`终止循环 - if counter == 10 { - break counter * 2; + ```Rust + fn main() { + let mut counter = 0; + + loop { + println!("counter = {:?}", counter); + counter += 1; + + if counter == 10 { + break; // 将终止循环 + } } - }; - - println!("The result is {result}"); -} -``` - -- 使用continue可以直接跳到下一轮循环 -```Rust -fn main() { - let mut x = 0; - // 此循环将只打印10以内的奇数 - loop { - x += 1; - if x == 10 { - break; - } - if x % 2 == 0 { - continue; //将直接跳到下一轮循环 - } - println!("{}", x); } -} -``` + ``` + 上面的代码将打印10次,遇到`break`后终止循环。另外,`break`也可以返回值,如下: + ```Rust + fn main() { + let mut counter = 0; + + let result = loop { + counter += 1; + + if counter == 10 { + break counter * 2; + } + }; + + println!("The result is {result}"); + } + ``` + +- 使用`continue`可以直接跳到下一轮循环 + + ```Rust + fn main() { + let mut x = 0; + // 此循环将只打印10以内的奇数 + loop { + x += 1; + if x == 10 { + break; + } + if x % 2 == 0 { + continue; //将直接跳到下一轮循环 + } + println!("{}", x); + } + } + ``` ## 3.5.3. while条件循环 -- while条件循环执行代码,当条件不满足后结束循环,如下: -```Rust -fn main() { - let mut cnt = 0u32; - while cnt < 10 { - println!("cnt = {:?}", cnt); - cnt += 1; - } -} -``` -- 在while循环中也可以使用break和continue,如下: -```Rust -fn main() { - let mut x = 0; +- `while`条件循环执行代码,当条件不满足后结束循环,如下: - while x < 10 { - x += 1; - println!("{}", x); - if x % 2 == 0 { - continue; // 跳到下一轮循环 - } - if x > 8 { - break; // 提前结束 + ```Rust + fn main() { + let mut cnt = 0u32; + while cnt < 10 { + println!("cnt = {:?}", cnt); + cnt += 1; } } -} -``` + ``` -## 3.5.4. for .. in 循环 +- 在`while`循环中也可以使用`break`和`continue`,如下: + + ```Rust + fn main() { + let mut x = 0; + + while x < 10 { + x += 1; + println!("{}", x); + if x % 2 == 0 { + continue; // 跳到下一轮循环 + } + if x > 8 { + break; // 提前结束 + } + } + } + ``` + +## 3.5.4. `for .. in` 循环 + +`for`循环用来对一个集合的每个元素执行一些代码,使用方式如下: -for循环用来对一个集合的每个元素执行一些代码,使用方式如下: ```Rust fn main() { let a = [10, 20, 30, 40, 50]; diff --git a/src/chapter_3/chapter_3_6.md b/src/chapter_3/chapter_3_6.md index 3c7cbdd..dbacc07 100644 --- a/src/chapter_3/chapter_3_6.md +++ b/src/chapter_3/chapter_3_6.md @@ -41,18 +41,27 @@ fn main() { let _r = f2(a, b); } ``` + 对于上面的代码,在执行第17行和第18行的栈帧示意图如下: + ![注释](../../assets/3.png) 这里需要注意的是两个帧对应同样的内存地址,这是因为在调用完f1函数后,其对应的栈帧释放(释放的实际意义就是这段内存可以被重新分配了),然后调用f2函数为其分配栈帧时从同样的地址进行分配。 ### 2. 堆 + 堆空间和栈空间不同,不由操作系统管理,在需要时申请,不需要时释放。申请和释放堆内存是一件困难的事情,尤其当程序代码较多时。只申请堆内存而不释放会造成内存泄露,内存泄露过多会造成内存耗尽而崩溃。错误的释放在使用的内存也会造成程序运行错误(或直接无法运行)。 + 有些编程语言使用提供垃圾管理回收器(GC)来自动回收不再使用的堆内存,有些语言必须完全由程序员在代码中手动申请和释放内存。 + Rust没有GC,但通过其独特的机制管理内存,程序员不用手动申请和释放堆内存。 ### 3. Rust如何使用堆和栈 + 栈中存储的所有数据都必须占用(在编译时就)已知且固定的大小。编译时大小未知或可能变化的数据,存储在堆上。 + 数据存放到栈上时,是直接将数据放到栈内存。 + 当数据需要存放到堆上时,内存分配器则是根据数据的大小,在堆内存找到合适大小的空区域存放,把它标记为已使用,并返回一个表示该位置地址的指针。该指针存储在栈上,当需要访问具体的数据时,必须先访问指针,然后通过指针找到堆上的位置,从而访问数据。这个过程可以用下图表示: + ![注释](../../assets/4.png) diff --git a/src/chapter_3/chapter_3_7_1.md b/src/chapter_3/chapter_3_7_1.md index 78278f2..3eb5816 100644 --- a/src/chapter_3/chapter_3_7_1.md +++ b/src/chapter_3/chapter_3_7_1.md @@ -1,4 +1,5 @@ # 3.7.1 所有权介绍 + 所有权是Rust最为与众不同的特性,它让Rust无需垃圾回收即可保证内存安全。 ## 1. 所有权规则 @@ -8,6 +9,7 @@ Rust所有权的规则如下: - Rust中的每个值都有一个被称为其所有者的变量,即值的所有者是某个变量; - 值在任何时刻有且仅有一个所有者; - 当所有者离开作用域后,这个值将丢弃。 + ```Rust fn main() { let a: u32 = 8; @@ -21,7 +23,9 @@ fn main() { 注意:b是`String::from("hello")`的所有者,但是b不是字符串`"hello"`的所有者。同理,c是`vec![1, 2, 3]`的所有者,但不是`[1, 2, 3]`的所有者。至于为什么,后续内容(String类型部分)会进行讲解。 ## 2. 变量的作用域 + 变量作用域是变量在程序中有效的范围。一对花括号表示的范围就是作用域,变量有效的范围就是从创建开始,到离开作用域结束。 + 示例1: ```Rust @@ -62,11 +66,11 @@ fn main() { ## 3. String类型 -- String类型的创建有下面三种方式: +### 3.1 String类型的创建有下面三种方式: - - `String::from` - - `to_string` - - `String::new` +- `String::from` +- `to_string` +- `String::new` ```Rust fn main() { @@ -87,7 +91,7 @@ fn main() { } ``` -- String类型的本质 +### 3.2 String类型的本质 Rust标准库中,String类型的定义如下: @@ -138,7 +142,7 @@ struct String { 所以String类型本质是三个字段:一个指针,一个容量大小,一个长度大小。 -- 内存分配 +### 3.3 内存分配 在Rust中,编译时大小确定的数据放在栈上,编译时大小不能确定的数据放在堆上。考虑如下代码: @@ -162,7 +166,7 @@ String类型本身是三个字段(指针、长度、容量),在编译时 Rust所有权规则第二条,在任意时刻,值有且仅有一个所有者。那么当一个变量赋给另外一个变量时发生了什么? -- 完全存储在栈上的类型 +### 4.1 完全存储在栈上的类型 考虑如下代码: ```Rust @@ -175,7 +179,7 @@ fn main() { x和y都是u32类型,在编译时知道大小,都存储在栈上。代码第2行是将5绑定到变量`x`上,第3行则是通过自动拷贝的方式将5绑定到`y`上(先拷贝`x`的值5,然后将拷贝后得到的5绑定到y上)。所以,当`let y = x`发生后,这段代码里面最后有两个值5,分别绑定到了`x`和`y`上。 -- 涉及到堆存储的类型 +### 4.2 涉及到堆存储的类型 再考虑如下代码: @@ -198,14 +202,15 @@ s是`String`类型,字符串`"Hello world"`是存储在堆内存上的,其 当`let s1 = s`执行后,就发生了所有权的转移,String类型值的所有权从`s`转移到了`s1`。此时Rust认为原来的`s`不再有效。因此,上面代码第4行打开编译将会出错。 ## 5. 浅拷贝与深拷贝 -- 浅拷贝 + +### 5.1 浅拷贝 只拷贝栈上的内容,就叫做浅拷贝。 对于上面的String类型,执行`let s1 = s`后,只把`s`的`ptr`、`len`、`cap`中的值拷贝给`s1`的`ptr`、`len`、`cap`的值,这种就叫做浅拷贝。浅拷贝发生后,`s`的`ptr`和`s1`的`ptr`都指向同样的堆内存。内存布局如下: ![注释](../../assets/8.png) -- 深拷贝 +### 5.2 深拷贝 除了拷贝栈上的内容外,还拷贝堆内存中的内容,就叫做深拷贝。 对于上面的String类型,执行`let s1 = s`后,除了把`s`的`len`、`cap`中的值拷贝给`s1`的`len`、`cap`外,还在堆上重新分配一块内存,将s的ptr指向的堆内存的内容拷贝到这块内存,然后`s1`的`ptr`指向这块内存,这种拷贝就叫做深拷贝。深拷贝发生后,`s`的`ptr`和`s1`的`ptr`指向不同的堆内存,但是堆内存中存储的内容一样。深拷贝发生后的内存布局如下: @@ -241,7 +246,8 @@ Rust中,默认实现了`Copy trait`的类型有: - 共享指针类型或共享引用类型。 ## 8. 所有权和函数 -- 将值传给函数 + +### 8.1 将值传给函数 在将值传递给函数时,和变量赋值一样会发生值的移动(或复制),如下: ```Rust @@ -264,9 +270,10 @@ fn makes_copy(some_integer: i32) { } ``` -- 返回值和作用域 +### 8.2 返回值和作用域 函数的返回值也可以转移所有权,如下: + ```Rust fn main() { let s1 = gives_ownership(); // gives_ownership 将返回值转移给 s1 diff --git a/src/chapter_3/chapter_3_7_2.md b/src/chapter_3/chapter_3_7_2.md index 5829dfd..a54ca4a 100644 --- a/src/chapter_3/chapter_3_7_2.md +++ b/src/chapter_3/chapter_3_7_2.md @@ -100,7 +100,7 @@ fn change(some_string: &String) { ## 2. 可变引用 -- 使用可变引用 +### 2.1 使用可变引用 可以通过可变引用改变变量的值,对一个变量加上`&mut`就是对其的可变引用,示例如下: ```Rust @@ -114,7 +114,7 @@ fn change(some_string: &mut String) { } ``` -- 引用的作用域 +### 2.2 引用的作用域 前文讲过变量的作用域是从定义开始到花括号结束的位置,如: @@ -149,7 +149,8 @@ fn change(some_string: &mut String) { } ``` -- 使用可变引用的限制 +### 2.3 使用可变引用的限制 + (1)限制一:**同一作用域,特定数据只能有一个可变引用**。如下代码会报错: ```Rust @@ -235,7 +236,8 @@ fn main() { 数据竞争会导致未定义行为,难以在运行时追踪,并且难以诊断和修复;Rust 避免了此情况的发生,因为它甚至不会编译存在数据竞争的代码! ## 3. 悬垂引用 -- 什么是悬垂引用(悬垂指针)? + +### 3.1 什么是悬垂引用(悬垂指针)? 在具有指针的语言中(如C/C++),很容易通过释放内存但是保留指向它的指针而错误的生成一个悬垂指针。例如有如下C代码: @@ -271,7 +273,7 @@ int main() 第14行执行后,`ptr`就变成了一个悬垂指针(或者交悬垂引用),然后在第16行继续使用`ptr`,则会发生错误。 -- 在 Rust 中,编译器确保引用永远不会变成悬垂状态。 +### 3.2 在 Rust 中,编译器确保引用永远不会变成悬垂状态。 如下代码因为会产生悬垂引用,编译将不会通过: @@ -285,18 +287,18 @@ fn dangle() -> &String { } ``` -*思考:为什么下面的代码是正确的* ? -```Rust -fn main() { - let s = no_dangle(); - println!("s = {:?}", s); -} - -fn no_dangle() -> String { - let s = String::from("hello"); - s -} // 此处s虽然离开了函数这个作用域范围,但是它的所有权是被转移出去了,值并没有释放 -``` +> *思考:为什么下面的代码是正确的* ? +> ```Rust +> fn main() { +> let s = no_dangle(); +> println!("s = {:?}", s); +>} +> +>fn no_dangle() -> String { +> let s = String::from("hello"); +> s +>} // 此处s虽然离开了函数这个作用域范围,但是它的所有权是被转移出去了,值并没有释放 +>``` ## 4. 引用的规则总结 diff --git a/src/chapter_3/chapter_3_8.md b/src/chapter_3/chapter_3_8.md index f73d7ac..8646058 100644 --- a/src/chapter_3/chapter_3_8.md +++ b/src/chapter_3/chapter_3_8.md @@ -12,146 +12,160 @@ Rust中可以通过结构体或者枚举来构造复杂的数据类型,结构 ### 1. 定义和实例化 -- 元组结构体的定义和实例化 +#### 1.1 元组结构体的定义和实例化 - 下面是定义一个元组结构体,用来表示颜色。 - ```rust - struct Color(i32, i32, i32); +下面是定义一个元组结构体,用来表示颜色。 - // 实例化元组结构体 - let color = Color(1, 1, 1); - ``` +```rust +struct Color(i32, i32, i32); - 使用元组结构体的特点是,给定元组具体的名字,可以和同类元组内的类型的元组做区分。 +// 实例化元组结构体 +let color = Color(1, 1, 1); +``` - ```rust - struct Color(i32, i32, i32); - struct Point(i32, i32, i32); +使用元组结构体的特点是,给定元组具体的名字,可以和同类元组内的类型的元组做区分。 - // 实例化元组结构体 - let color = Color(1, 1, 1); - let point = Point(1, 1, 1); - ``` - 可以看到Color 和Point虽然元组内的元素都是i32,但是给定了(i32, i32,i32)这个元组两个不同的结构体名称,所以Color和Point不是同一种类型。 - ```rust - #[derive(Debug, Eq, PartialEq)] - pub struct Color(i32, i32, i32); +```rust +struct Color(i32, i32, i32); +struct Point(i32, i32, i32); - #[derive(Debug, Eq, PartialEq)] - pub struct Point(i32, i32, i32); +// 实例化元组结构体 +let color = Color(1, 1, 1); +let point = Point(1, 1, 1); +``` - fn main() { - assert_eq!(Color(1, 1, 1), Point(1, 1, 1)); // error, 判断两个元组结构体是不是相等,rust将不同类型名称的结构体看作是不一样的结构体,虽然,结构内的元素都是一样的。 - } - ``` +可以看到Color 和Point虽然元组内的元素都是i32,但是给定了(i32, i32,i32)这个元组两个不同的结构体名称,所以Color和Point不是同一种类型。 -- 经典的C结构体的定义和初始化 +```rust +#[derive(Debug, Eq, PartialEq)] +pub struct Color(i32, i32, i32); - 结构体内的每个字段都是命名字段,使得访问和修改更直观。 - ```rust - // 定义结构体 - struct Person { - name: String, - age: u32, - height: f32, - } +#[derive(Debug, Eq, PartialEq)] +pub struct Point(i32, i32, i32); - // 结构体实例化 - let alice = Person { - name: String::from("Alice"), - age: 30, - height: 1.65, - }; - ``` +fn main() { + assert_eq!(Color(1, 1, 1), Point(1, 1, 1)); // error, 判断两个元组结构体是不是相等,rust将不同类型名称的结构体看作是不一样的结构体,虽然,结构内的元素都是一样的。 +} +``` - 思考:结构体内的数据可以用不同的类型,使用C结构和元组结构体的区别在于,要给结构内的每个字段给予名字来表明其意义。 +#### 1.2 经典的C结构体的定义和初始化 -- 单元结构体的定义和初始化 +结构体内的每个字段都是命名字段,使得访问和修改更直观。 - 这种结构体没有任何字段。它们通常用于实现特定的行为,而不是表示数据。 - ```rust - // 定义结构体 - struct Dummy; +```rust +// 定义结构体 +struct Person { + name: String, + age: u32, + height: f32, +} - // 结构体实例化 - let dummy = Dummy; - ``` +// 结构体实例化 +let alice = Person { + name: String::from("Alice"), + age: 30, + height: 1.65, +}; +``` + +>思考:结构体内的数据可以用不同的类型,使用C结构和元组结构体的区别在于,要给结构内的每个字段给予名字来表明其意义。 + +#### 1.3 单元结构体的定义和初始化 + +这种结构体没有任何字段。它们通常用于实现特定的行为,而不是表示数据。 + +```rust +// 定义结构体 +struct Dummy; + +// 结构体实例化 +let dummy = Dummy; +``` ### 2. 方法 这三类结构体的方法的定义和使用方式相同,示例如下: -- 元组结构体的方法 - ```rust - struct Color(u8, u8, u8); - impl Color { - fn print(&self) { - println!("Color: ({}, {}, {})", self.0, self.1, self.2); - } +#### 2.1 元组结构体的方法 + +```rust +struct Color(u8, u8, u8); + +impl Color { + fn print(&self) { + println!("Color: ({}, {}, {})", self.0, self.1, self.2); } +} - let red = Color(255, 0, 0); - red.print(); // 输出:Color: (255, 0, 0) - ``` -- 类C结构体的方法 - ```rust - struct Person { - name: String, - age: u32, +let red = Color(255, 0, 0); +red.print(); // 输出:Color: (255, 0, 0) +``` + +#### 2.2 类C结构体的方法 + +```rust +struct Person { + name: String, + age: u32, +} + +impl Person { + fn greet(&self) { + println!("Hello, my name is {} and I'm {} years old.", self.name, self.age); } +} - impl Person { - fn greet(&self) { - println!("Hello, my name is {} and I'm {} years old.", self.name, self.age); - } +let alice = Person { + name: String::from("Alice"), + age: 30, +}; + +alice.greet(); // 输出:Hello, my name is Alice and I'm 30 years old. +``` + +#### 2.3 单元结构体的方法 + +```rust +struct Dummy; + +impl Dummy { + fn do_something(&self) { + println!("Doing something..."); } +} - let alice = Person { - name: String::from("Alice"), - age: 30, - }; - - alice.greet(); // 输出:Hello, my name is Alice and I'm 30 years old. - ``` - -- 单元结构体的方法 - ```rust - struct Dummy; - - impl Dummy { - fn do_something(&self) { - println!("Doing something..."); - } - } - - let dummy_instance = Dummy; - dummy_instance.do_something(); // 输出:Doing something... - ``` +let dummy_instance = Dummy; +dummy_instance.do_something(); // 输出:Doing something... +``` ## 3.7.2 枚举类型 ### 1. 定义和实例化 -- 枚举的定义 - 枚举允许在一个数据类型中定义多个变量。这在表示多种可能情况时非常有用。每个枚举成员可以具有关联的数据。 - ```rust - enum Message { - Quit, - Move { x: i32, y: i32 }, - Write(String), - ChangeColor(u8, u8, u8), - } - ``` -- 枚举的实例化 +#### 1.1 枚举的定义 - 要创建枚举的实例,需要指定要使用的成员以及其关联的数据(如果有)。 - ```rust - let msg = Message::Write(String::from("Hello, Rust!")); - ``` +枚举允许在一个数据类型中定义多个变量。这在表示多种可能情况时非常有用。每个枚举成员可以具有关联的数据。 + +```rust +enum Message { + Quit, + Move { x: i32, y: i32 }, + Write(String), + ChangeColor(u8, u8, u8), +} +``` + +#### 1.2 枚举的实例化 + +要创建枚举的实例,需要指定要使用的成员以及其关联的数据(如果有)。 +```rust +let msg = Message::Write(String::from("Hello, Rust!")); +``` ### 2. 方法 与结构体类似,也可以为枚举定义方法。 + ```rust impl Message { fn process(&self) { @@ -168,135 +182,144 @@ impl Message { msg.process(); // 输出:Write: Hello, Rust! ``` - ### 3. 控制流 -- Match +#### 3.1 Match - Rust 中有一个特殊的控制流结构,叫做 match。它用于匹配枚举成员并针对每个成员执行相应的代码。 - ```rust - match msg { - Message::Quit => println!("Quit"), - Message::Move { x, y } => println!("Move to ({}, {})", x, y), - Message::Write(text) => println!("Write: {}", text), - Message::ChangeColor(r, g, b) => println!("Change color to ({}, {}, {})", r, g, b), +Rust 中有一个特殊的控制流结构,叫做 match。它用于匹配枚举成员并针对每个成员执行相应的代码。 + +```rust +match msg { + Message::Quit => println!("Quit"), + Message::Move { x, y } => println!("Move to ({}, {})", x, y), + Message::Write(text) => println!("Write: {}", text), + Message::ChangeColor(r, g, b) => println!("Change color to ({}, {}, {})", r, g, b), +} +``` + +match 表达式需要穷举所有可能的枚举成员,这有助于确保代码的完整性和安全性。在某些情况下,如果不需要处理所有枚举成员,可以使用 `_` 通配符来匹配任何未明确指定的成员。 + +```rust +match msg { + Message::Write(text) => println!("Write: {}", text), + _ => println!("Other message"), +} +``` +#### 3.2 `if let` + +除了 match 语句之外,Rust 还提供了 `if let` 语法,用于简化某些模式匹配的情况。`if let` 对于只关心单个枚举变体的情况特别有用,这样可以避免编写繁琐的 `match` 语句。`if let` 可以将值解构为变量,并在匹配成功时执行代码块。 + +下面是一个使用 `Option` 枚举的示例: + +```rust +fn main() { + let some_number = Some(42); + + // 使用 match 语句 + match some_number { + Some(x) => println!("The number is {}", x), + _ => (), } - ``` - match 表达式需要穷举所有可能的枚举成员,这有助于确保代码的完整性和安全性。在某些情况下,如果不需要处理所有枚举成员,可以使用 _ 通配符来匹配任何未明确指定的成员。 - ```rust - match msg { - Message::Write(text) => println!("Write: {}", text), - _ => println!("Other message"), + // 使用 if let 语句 + if let Some(x) = some_number { + println!("The number is {}", x); } - ``` -- `if let` +} +``` - 除了 match 语句之外,Rust 还提供了 `if let` 语法,用于简化某些模式匹配的情况。`if let` 对于只关心单个枚举变体的情况特别有用,这样可以避免编写繁琐的 `match` 语句。`if let` 可以将值解构为变量,并在匹配成功时执行代码块。 +在这个示例中,`if let` 语法让代码更简洁,因为只关心 `Some` 变体。这里还可以使用 `else` 子句处理未匹配的情况。 - 下面是一个使用 `Option` 枚举的示例: - ```rust - fn main() { - let some_number = Some(42); +```rust +fn main() { + let some_number: Option = None; - // 使用 match 语句 - match some_number { - Some(x) => println!("The number is {}", x), - _ => (), - } - - // 使用 if let 语句 - if let Some(x) = some_number { - println!("The number is {}", x); - } + if let Some(x) = some_number { + println!("The number is {}", x); + } else { + println!("No number found"); } - ``` +} +``` - 在这个示例中,`if let` 语法让代码更简洁,因为只关心 `Some` 变体。这里还可以使用 `else` 子句处理未匹配的情况。 - ```rust - fn main() { - let some_number: Option = None; +在此示例中,由于 `some_number` 是 `None`,`if let` 语句不匹配,因此将执行 `else` 子句,输出 `"No number found"`。 - if let Some(x) = some_number { - println!("The number is {}", x); - } else { - println!("No number found"); - } +`if let` 可以与 `Result` 枚举一起使用,以便更简洁地处理错误。当只关心 `Ok` 或 `Err` 变体之一时,这特别有用。以下是一个处理 `Result` 枚举的示例。 +定义一个可能返回错误的函数: +```rust +fn divide(numerator: f64, denominator: f64) -> Result { + if denominator == 0.0 { + Err(String::from("Cannot divide by zero")) + } else { + Ok(numerator / denominator) } - ``` +} +``` - 在此示例中,由于 `some_number` 是 `None`,`if let` 语句不匹配,因此将执行 `else` 子句,输出 `"No number found"`。 +接下来,我们使用 `if let` 处理成功的情况: - `if let` 可以与 `Result` 枚举一起使用,以便更简洁地处理错误。当只关心 `Ok` 或 `Err` 变体之一时,这特别有用。以下是一个处理 `Result` 枚举的示例。 - 定义一个可能返回错误的函数: - ```rust - fn divide(numerator: f64, denominator: f64) -> Result { - if denominator == 0.0 { - Err(String::from("Cannot divide by zero")) - } else { - Ok(numerator / denominator) - } - } - ``` +```rust +let result = divide(4.0, 2.0); - 接下来,我们使用 `if let` 处理成功的情况: - ```rust - let result = divide(4.0, 2.0); +if let Ok(value) = result { + println!("The result is {}", value); +} +``` - if let Ok(value) = result { - println!("The result is {}", value); - } - ``` +在这种情况下,由于除法操作成功,`if let` 语句将匹配 `Ok` 变体,并输出结果。如果只关心错误情况,可以使用 `if let` 匹配 `Err` 变体: - 在这种情况下,由于除法操作成功,`if let` 语句将匹配 `Ok` 变体,并输出结果。如果只关心错误情况,可以使用 `if let` 匹配 `Err` 变体: +```rust +let result = divide(4.0, 0.0); - ```rust - let result = divide(4.0, 0.0); +if let Err(error) = result { + println!("Error: {}", error); +} +``` - if let Err(error) = result { - println!("Error: {}", error); - } - ``` - - 在这种情况下,由于除法操作失败,`if let` 语句将匹配 `Err` 变体,并输出错误消息。 - 使用 `if let` 处理 `Result` 可以简化错误处理,特别是当只关心 `Ok` 或 `Err` 变体之一时。然而,请注意,对于更复杂的错误处理逻辑,`match` 语句或 `?` 运算符可能更适合。 +在这种情况下,由于除法操作失败,`if let` 语句将匹配 `Err` 变体,并输出错误消息。 +使用 `if let` 处理 `Result` 可以简化错误处理,特别是当只关心 `Ok` 或 `Err` 变体之一时。然而,请注意,对于更复杂的错误处理逻辑,`match` 语句或 `?` 运算符可能更适合。 ### 4. 常用的枚举类型 Rust 标准库中有一些常用的枚举类型,例如 `Option` 和 `Result`。 -- Option:表示一个值可能存在或不存在。其成员为 `Some(T)`(其中 `T` 是某种类型)和 `None`。 - ```rust - fn divide(numerator: f64, denominator: f64) -> Option { - if denominator == 0.0f64 { - None - } else { - Some(numerator / denominator) - } - } +#### 4.1 Option:表示一个值可能存在或不存在。其成员为 `Some(T)`(其中 `T` 是某种类型)和 `None`。 - let result = divide(4.0, 2.0); - match result { - Some(value) => println!("The result is {}", value), - None => println!("Cannot divide by zero"), +```rust +fn divide(numerator: f64, denominator: f64) -> Option { + if denominator == 0.0f64 { + None + } else { + Some(numerator / denominator) } - ``` +} -- `Result`:表示一个操作可能成功或失败。其成员为 `Ok(T)`(其中 `T` 是某种类型)和 `Err(E)`(其中 `E` 是错误类型)。 +let result = divide(4.0, 2.0); +match result { + Some(value) => println!("The result is {}", value), + None => println!("Cannot divide by zero"), +} +``` - ```rust - fn divide_result(numerator: f64, denominator: f64) -> Result { - if denominator == 0.0f64 { - Err(String::from("Cannot divide by zero")) - } else { - Ok(numerator / denominator) - } +#### 4.2 `Result`:表示一个操作可能成功或失败。其成员为 `Ok(T)`(其中 `T` 是某种类型)和 `Err(E)`(其中 `E` 是错误类型)。 + + + +```rust +fn divide_result(numerator: f64, denominator: f64) -> Result { + if denominator == 0.0f64 { + Err(String::from("Cannot divide by zero")) + } else { + Ok(numerator / denominator) } +} + +let result = divide_result(4.0, 0.0); +match result { + Ok(value) => println!("The result is {}", value), + Err(error) => println!("Error: {}", error), +} +``` + - let result = divide_result(4.0, 0.0); - match result { - Ok(value) => println!("The result is {}", value), - Err(error) => println!("Error: {}", error), - } - ``` 这些枚举类型有助于更安全地处理可能出现的错误情况,避免在代码中使用不安全的值(如空指针)。 diff --git a/src/chapter_3/chapter_3_9.md b/src/chapter_3/chapter_3_9.md index 0fce6fc..e2846f7 100644 --- a/src/chapter_3/chapter_3_9.md +++ b/src/chapter_3/chapter_3_9.md @@ -5,6 +5,7 @@ ## 3.9.1 函数定义中的泛型 如果没有泛型,当为不同的类型定义逻辑相同的函数时,可能如下: + ```rust fn return_i8(v: i8) -> i8 { v } fn return_i16(v: i16) -> i16 { v } @@ -27,7 +28,9 @@ fn main() { let _h = return_u64(2u64); } ``` + 使用泛型后,可以在函数定义时使用泛型,在调用函数的地方指定具体的类型,如下: + ```rust fn return_value(v: T) -> T{ v } @@ -47,6 +50,7 @@ fn main() { ## 3.9.2 结构体定义中的泛型 在结构体中使用泛型的示例如下: + ```rust #[derive(Debug)] struct Point { @@ -64,6 +68,7 @@ fn main() { ``` 也可以像如下方式使用: + ```rust #[derive(Debug)] struct Point { // Point的两个字段可以指定为不同的类型 @@ -82,21 +87,25 @@ fn main() { ## 3.9.3 枚举定义中的泛型 -标准库的Option类型就是使用泛型的枚举类型,其定义如下: +标准库的`Option`类型就是使用泛型的枚举类型,其定义如下: + ```rust enum Option { Some(T), None, } ``` -同样的还有Result类型,其定义如下: +同样的还有`Result`类型,其定义如下: + ```rust enum Result { Ok(T), Err(E), } ``` + 下面再举一个枚举类型中使用泛型的例子: + ```rust enum Message { Msg1(u32), @@ -116,6 +125,7 @@ fn main() { ## 3.9.4 方法定义中的泛型 还可以在方法中使用泛型,例子1: +` ```rust struct Point { x: T, @@ -139,6 +149,7 @@ fn main() { ``` 方法中的泛型不一定和结构体中的一样,示例如下: + ```rust struct Point { x: T,