diff --git a/README.md b/README.md index ec472a12..52c81a51 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ - 国内镜像: https://book.rust.team - 知乎: [支持章节内目录跳转,很好用!](https://www.zhihu.com/column/c_1452781034895446017) -- 最近修订: 新增章节 [Tokio使用指南 - IO操作](https://zhuanlan.zhihu.com/p/462387088) +- 最近修订: 新增章节 [Tokio使用指南 - 解析数据帧](https://zhuanlan.zhihu.com/p/462387088) - Rust版本: Rust edition 2021 - QQ交流群:1009730433 diff --git a/book/contents/SUMMARY.md b/book/contents/SUMMARY.md index 1162750f..6025bd28 100644 --- a/book/contents/SUMMARY.md +++ b/book/contents/SUMMARY.md @@ -110,6 +110,7 @@ - [共享状态](tokio/shared-state.md) - [消息传递](tokio/channels.md) - [I/O](tokio/io.md) + - [解析数据帧](tokio/frame.md) diff --git a/book/contents/advance/formatted-output.md b/book/contents/advance/formatted-output.md index ff3ec8e2..0d1d5276 100644 --- a/book/contents/advance/formatted-output.md +++ b/book/contents/advance/formatted-output.md @@ -1,5 +1,5 @@ # 格式化输出 -提到格式化输出,可能很多人立刻就想到`"{}"`,但是Rust能做到的远比这个多的多,本章节我们将深入讲解格式化输出的各个方面。 +提到格式化输出,可能很多人立刻就想到 `"{}"`,但是 Rust 能做到的远比这个多的多,本章节我们将深入讲解格式化输出的各个方面。 ## 满分初印象 先来一段代码,看看格式化输出的初印象: @@ -13,16 +13,16 @@ println!("{} {}", 1, 2); // => "1 2" println!("{:04}", 42); // => "0042" with leading zeros ``` -可以看到`println!`宏接受的是可变参数,第一个参数是一个字符串常量,它表示最终输出字符串的格式, 包含其中形如`{}`的符号是**占位符**, 会被`println!`后面的参数依次替换。 +可以看到 `println!` 宏接受的是可变参数,第一个参数是一个字符串常量,它表示最终输出字符串的格式,包含其中形如 `{}` 的符号是**占位符**,会被 `println!` 后面的参数依次替换。 -## `print!`, `println!`, `format!` -它们是Rust中用来格式化输出的三大金刚,用途如下: +## `print!`,`println!`,`format!` +它们是 Rust 中用来格式化输出的三大金刚,用途如下: -- `print!`, 将格式化文本输出到标准输出,不带换行符 -- `println!`, 同上,但是在行的末尾添加换行符 -- `format!`, 将格式化文本输出到`String`字符串 +- `print!` 将格式化文本输出到标准输出,不带换行符 +- `println!` 同上,但是在行的末尾添加换行符 +- `format!` 将格式化文本输出到 `String` 字符串 -在实际项目中,最常用的是`println!`及`format!`,前者常用来调试输出,后者用来生成格式化的字符串: +在实际项目中,最常用的是 `println!` 及 `format!`,前者常用来调试输出,后者常用来生成格式化的字符串: ```rust fn main() { let s = "hello"; @@ -33,32 +33,32 @@ fn main() { } ``` -其中, `s1`是通过`format!`生成的`String`字符串,最终输出如下: +其中,`s1` 是通过 `format!` 生成的 `String` 字符串,最终输出如下: ```console hello, wolrd hello, world! ``` -#### `eprint!`, `eprintln!` -除了三大金刚外,还有两大护法,使用方式跟`print!`,`println!`很像,但是它们输出到标准错误输出: +#### `eprint!`,`eprintln!` +除了三大金刚外,还有两大护法,使用方式跟 `print!`,`println!` 很像,但是它们输出到标准错误输出: ```rust eprintln!("Error: Could not complete task") ``` -它们仅应该被用于输出错误信息和进度信息,其它场景都应该使用`print!`系列。 +它们仅应该被用于输出错误信息和进度信息,其它场景都应该使用 `print!` 系列。 -## {}与{:?} -与其它语言常用的`%d`,`%s`不同,Rust特立独行的选择了`{}`作为格式化占位符(说到这个,有点想吐槽下,Rust中自创的概念其实还挺多的,真不知道该夸奖还是该吐槽- , -),事实证明,这种选择非常正确,它帮助用户减少了很多使用成本,你无需再为特定的类型选择特定的占位符,统一用`{}`来替代即可,剩下的类型推导等细节只要交给Rust去做。 +## {} 与 {:?} +与其它语言常用的 `%d`,`%s` 不同,Rust 特立独行地选择了 `{}` 作为格式化占位符(说到这个,有点想吐槽下,Rust 中自创的概念其实还挺多的,真不知道该夸奖还是该吐槽-,-),事实证明,这种选择非常正确,它帮助用户减少了很多使用成本,你无需再为特定的类型选择特定的占位符,统一用 `{}` 来替代即可,剩下的类型推导等细节只要交给 Rust 去做。 -与`{}`类似,`{:?}`也是占位符: +与 `{}` 类似,`{:?}` 也是占位符: -- `{}`适用于实现了`std::fmt::Display`特征的类型,用来以更优雅、更友好的方式格式化文本,例如展示给用户 -- `{:?}`适用于实现了`std::fmt::Debug`特征的类型,用于调试场景 +- `{}` 适用于实现了 `std::fmt::Display` 特征的类型,用来以更优雅、更友好的方式格式化文本,例如展示给用户 +- `{:?}` 适用于实现了 `std::fmt::Debug` 特征的类型,用于调试场景 -其实两者的选择很简单,当你在写代码需要调试时,使用`{:?}`,剩下的场景,选择`{}`。 +其实两者的选择很简单,当你在写代码需要调试时,使用 `{:?}`,剩下的场景,选择 `{}`。 -#### `Debug`特征 -事实上,为了方便我们调试,大多数Rust类型都实现了`Debug`特征或者支持派生该特征: +#### `Debug` 特征 +事实上,为了方便我们调试,大多数 Rust 类型都实现了 `Debug` 特征或者支持派生该特征: ```rust #[derive(Debug)] struct Person { @@ -75,29 +75,32 @@ fn main() { } ``` -对于数值、字符串、数组,可以直接使用`{:?}`进行输出,但是对于结构体,需要[派生`Debug`](../appendix/derive.md)特征后,才能进行输出,总之很简单. +对于数值、字符串、数组,可以直接使用 `{:?}` 进行输出,但是对于结构体,需要[派生`Debug`](../appendix/derive.md)特征后,才能进行输出,总之很简单。 -#### `Display`特征 -与大部分类型实现了`Debug`不同,实现了`Display`特征的Rust类型并没有那么多,往往需要我们自定义想要的格式化方式: +#### `Display` 特征 +与大部分类型实现了 `Debug` 不同,实现了 `Display` 特征的 Rust 类型并没有那么多,往往需要我们自定义想要的格式化方式: ```rust let i = 3.1415926; let s = String::from("hello"); -let v = vec![1,2,3]; -let p = Person{name: "sunface".to_string(),age: 18}; -println!("{}, {}, {},{}",i,s,v,p); +let v = vec![1, 2, 3]; +let p = Person { + name: "sunface".to_string(), + age: 18, +}; +println!("{}, {}, {},{}", i, s, v, p); ``` -运行后可以看到`v`和`p`都无法通过编译,因为没有实现`Display`特征,但是你又不能像派生`Debug`一般派生`Display`,只能另寻他法: +运行后可以看到 `v` 和 `p` 都无法通过编译,因为没有实现 `Display` 特征,但是你又不能像派生 `Debug` 一般派生 `Display`,只能另寻他法: -- 使用`{:?}`或`{:#?}` -- 为自定义类型实现`Display`特征 -- 使用`newtype`为外部类型实现`Display`特征 +- 使用 `{:?}` 或 `{:#?}` +- 为自定义类型实现 `Display` 特征 +- 使用 `newtype` 为外部类型实现 `Display` 特征 下面来一一看看这三种方式。 #### {:#?} -`{:#?}`与`{:?}`几乎一样,唯一的区别在于它能更优美的输出内容: +`{:#?}` 与 `{:?}` 几乎一样,唯一的区别在于它能更优美地输出内容: ```console // {:?} [1, 2, 3],Person { name: "sunface", age: 18 } @@ -112,42 +115,49 @@ println!("{}, {}, {},{}",i,s,v,p); } ``` -因此对于`Display`不支持的类型,可以考虑使用`{:#?}`进行格式化,虽然理论上它更适合进行调试输出。 +因此对于 `Display` 不支持的类型,可以考虑使用 `{:#?}` 进行格式化,虽然理论上它更适合进行调试输出。 -#### 为自定义类型实现`Display`特征 -如果你的类型是定义在当前作用域中的,那么可以为其实现`Display`特征,即可用于格式化输出: +#### 为自定义类型实现 `Display` 特征 +如果你的类型是定义在当前作用域中的,那么可以为其实现 `Display` 特征,即可用于格式化输出: ```rust struct Person { name: String, - age: u8 + age: u8, } use std::fmt; -impl fmt::Display for Person { +impl fmt::Display for Person { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "大佬在上,请受我一拜,小弟姓名{},年芳{},家里无田又无车,生活苦哈哈",self.name,self.age) + write!( + f, + "大佬在上,请受我一拜,小弟姓名{},年芳{},家里无田又无车,生活苦哈哈", + self.name, self.age + ) } } fn main() { - let p = Person{name: "sunface".to_string(),age: 18}; + let p = Person { + name: "sunface".to_string(), + age: 18, + }; println!("{}", p); } ``` -如上所示,只要实现`Display`特征中的`fmt`方法,即可为自定义结构体`Person`添加自定义输出: +如上所示,只要实现 `Display` 特征中的 `fmt` 方法,即可为自定义结构体 `Person` 添加自定义输出: ```console 大佬在上,请受我一拜,小弟姓名sunface,年芳18,家里无田又无车,生活苦哈哈 ``` -#### 为外部类型实现`Display`特征 -在Rust中,无法直接为外部类型实现外部特征,但是可以使用[`newtype`](./custom-type.md#newtype)解决此问题: +#### 为外部类型实现 `Display` 特征 +在 Rust 中,无法直接为外部类型实现外部特征,但是可以使用[`newtype`](./custom-type.md#newtype)解决此问题: ```rust struct Array(Vec); use std::fmt; -impl fmt::Display for Array { +impl fmt::Display for Array { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "数组是:{:?}",self.0) + write!(f, "数组是:{:?}", self.0) } } fn main() { @@ -156,15 +166,15 @@ fn main() { } ``` -`Array`就是我们的`newtype`,它将想要格式化输出的`Vec`包裹在内,最后只要为`Arraw`实现`Display`特征,即可进行格式化输出: +`Array` 就是我们的 `newtype`,它将想要格式化输出的 `Vec` 包裹在内,最后只要为 `Array` 实现 `Display` 特征,即可进行格式化输出: ```console 数组是:[1, 2, 3] ``` -至此,关于`{}`与`{:?}`的内容已介绍完毕,下面让我们正式开始格式化输出的旅程。 +至此,关于 `{}` 与 `{:?}` 的内容已介绍完毕,下面让我们正式开始格式化输出的旅程。 ## 指定位置参数 -除了按照依次顺序使用值去替换占位符之外,还能让指定位置的参数去替换某个占位符,例如`{1}`,表示用第二个参数替换该占位符(索引从0开始): +除了按照依次顺序使用值去替换占位符之外,还能让指定位置的参数去替换某个占位符,例如 `{1}`,表示用第二个参数替换该占位符(索引从0开始): ```rust fn main() { println!("{}{}",1,2); // =>"12" @@ -185,13 +195,13 @@ fn main() { } ``` -需要注意的是: **带名称的参数必须放在不带名称参数的后面**,例如下面代码将报错: +需要注意的是:**带名称的参数必须放在不带名称参数的后面**,例如下面代码将报错: ```rust println!("{abc} {1}", abc = "def", 2); ``` ## 格式化参数 -格式化输出,意味着对输出格式会有更多的要求,例如只输出浮点数的小数点后两位: +格式化输出,意味着对输出格式会有更多的要求,例如只输出浮点数的小数点后两位: ```rust fn main() { let v = 3.1415926; @@ -202,15 +212,15 @@ fn main() { } ``` -上面代码只输出小数点后两位。同时我们还展示了`{}`和`{:?}`的用法,后面如无特殊区别,就只针对`{}`提供格式化参数说明。 +上面代码只输出小数点后两位。同时我们还展示了 `{}` 和 `{:?}` 的用法,后面如无特殊区别,就只针对 `{}` 提供格式化参数说明。 -接下来,让我们一起来看看Rust中有哪些格式化参数。 +接下来,让我们一起来看看 Rust 中有哪些格式化参数。 #### 宽度 -宽度用来指示输出目标的长度,如果长度不够,则进行填充和对齐: +宽度用来指示输出目标的长度,如果长度不够,则进行填充和对齐: ##### 字符串填充 -字符串格式化默认使用空格进行填充,并且进行左对齐. +字符串格式化默认使用空格进行填充,并且进行左对齐。 ```rust fn main() { //----------------------------------- @@ -225,7 +235,7 @@ fn main() { println!("Hello {:width$}!", "x", width = 5); //----------------------------------- - // 使用参数5为参数x指定宽度,同时在结尾输出参数5 => Hello x !5 + // 使用参数5为参数x指定宽度,同时在结尾输出参数5 => Hello x !5 println!("Hello {:1$}!{}", "x", 5); } ``` @@ -285,7 +295,7 @@ fn main() { ``` #### 进制 -可以使用`#`号来控制数字的进制输出: +可以使用 `#` 号来控制数字的进制输出: - `#b`, 二进制 - `#o`, 八进制 @@ -329,7 +339,7 @@ println!("{:p}",v.as_ptr()) // => 0x600002324050 ``` #### 转义 -有时需要输出`{`和`}`,但这两个字符是特殊字符,需要进行转义: +有时需要输出 `{`和`}`,但这两个字符是特殊字符,需要进行转义: ```rust fn main() { // {使用{转义,}使用} => Hello {} @@ -342,7 +352,7 @@ fn main() { ## 总结 -把这些格式化都牢记在脑中是不太现实的,也没必要,我们要做的就是知道Rust支持相应的格式化输出,在需要之时,读者再来查阅本文即可。 +把这些格式化都牢记在脑中是不太现实的,也没必要,我们要做的就是知道 Rust 支持相应的格式化输出,在需要之时,读者再来查阅本文即可。 -还是那句话,[<>](https://github.com/sunface/rust-course)不仅仅是Rust学习书籍,还是一本厚重的工具书! +还是那句话,[<>](https://github.com/sunface/rust-course)不仅仅是 Rust 学习书籍,还是一本厚重的工具书! diff --git a/book/contents/advance/smart-pointer/box.md b/book/contents/advance/smart-pointer/box.md index 224e7b3b..090ca75b 100644 --- a/book/contents/advance/smart-pointer/box.md +++ b/book/contents/advance/smart-pointer/box.md @@ -1,16 +1,16 @@ -# `Box`堆对象分配 -关于作者帅不帅,估计争议还挺多的,但是如果说`Box`是不是Rust中最常见的智能指针,那估计没有任何争议。因为`Box`允许你将一个值分配到堆上,然后在栈上保留一个智能指针指向堆上的数据。 +# `Box` 堆对象分配 +关于作者帅不帅,估计争议还挺多的,但是如果说 `Box` 是不是Rust中最常见的智能指针,那估计没有任何争议。因为 `Box` 允许你将一个值分配到堆上,然后在栈上保留一个智能指针指向堆上的数据。 之前我们在[所有权章节](https://course.rs/basic/ownership/ownership.html#栈stack与堆heap)简单讲过堆栈的概念,这里再补充一些。 -## Rust中的堆栈 -高级语言Python/Java等往往会弱化堆栈的概念,但是要用好C/C++/Rust,就必须对堆栈有深入的了解,原因是两者的内存管理方式不同: 前者有GC垃圾回收机制, 因此无需你去关心内存的细节。 +## Rust 中的堆栈 +高级语言 Python/Java 等往往会弱化堆栈的概念,但是要用好 C/C++/Rust,就必须对堆栈有深入的了解,原因是两者的内存管理方式不同:前者有 GC 垃圾回收机制,因此无需你去关心内存的细节。 -栈内存从高位地址向下增长,且栈内存是连续分配的,一般来说**操作系统对栈内存的大小都有限制**,因此C语言中无法创建任意长度的数组。在Rust中, `main`线程的[栈大小是`8MB`](https://course.rs/pitfalls/stack-overflow.html),普通线程是`2MB`,在函数调用时会在其中创建一个临时栈空间,调用结束后Rust会让这个栈空间里的对象自动进入`Drop`流程,最后栈顶指针自动移动到上一个调用栈顶,无需程序员手动干预,因而栈内存申请和释放是非常高效的。 +栈内存从高位地址向下增长,且栈内存是连续分配的,一般来说**操作系统对栈内存的大小都有限制**,因此 C 语言中无法创建任意长度的数组。在 Rust 中,`main` 线程的[栈大小是 `8MB`](https://course.rs/pitfalls/stack-overflow.html),普通线程是 `2MB`,在函数调用时会在其中创建一个临时栈空间,调用结束后 Rust 会让这个栈空间里的对象自动进入 `Drop` 流程,最后栈顶指针自动移动到上一个调用栈顶,无需程序员手动干预,因而栈内存申请和释放是非常高效的。 -与栈相反,堆上内存则是从低位地址向上增长,**堆内存通常只受物理内存限制**,而且通常是不连续的, 因此从性能的角度看,栈往往比对堆更高。 +与栈相反,堆上内存则是从低位地址向上增长,**堆内存通常只受物理内存限制**,而且通常是不连续的,因此从性能的角度看,栈往往比对堆更高。 -相比其它语言,Rust堆上对象还有一个特殊之处,它们都拥有一个所有者,因此受所有权规则的限制:当赋值时,发生的是所有权的转移(只需浅拷贝栈上的引用或智能指针即可), 例如以下代码: +相比其它语言,Rust 堆上对象还有一个特殊之处,它们都拥有一个所有者,因此受所有权规则的限制:当赋值时,发生的是所有权的转移(只需浅拷贝栈上的引用或智能指针即可),例如以下代码: ```rust fn main() { let b = foo("world"); @@ -23,19 +23,19 @@ fn foo(x: &str) -> String { } ``` -在`foo`函数中,`a`是`String`类型,它其实是一个智能指针结构体,该智能指针存储在函数栈中,指向堆上的字符串数据。当被从`foo`函数转移给`main`中的`b`变量时,栈上的智能指针被复制一份赋予给`b`,而底层数据无需发生改变,这样就完成了所有权从`foo`函数内部到`b`的转移. +在 `foo` 函数中,`a` 是 `String` 类型,它其实是一个智能指针结构体,该智能指针存储在函数栈中,指向堆上的字符串数据。当被从 `foo` 函数转移给 `main` 中的 `b` 变量时,栈上的智能指针被复制一份赋予给 `b`,而底层数据无需发生改变,这样就完成了所有权从 `foo` 函数内部到 `b` 的转移。 #### 堆栈的性能 -很多人可能会觉得栈的性能肯定比堆高,其实未必。 由于我们在后面的性能专题会专门讲解堆栈的性能问题,因此这里就大概给出结论: +很多人可能会觉得栈的性能肯定比堆高,其实未必。 由于我们在后面的性能专题会专门讲解堆栈的性能问题,因此这里就大概给出结论: - 小型数据,在栈上的分配性能和读取性能都要比堆上高 -- 中型数据,栈上分配性能高,但是读取性能和堆上并无区别,因为无法利用寄存器或CPU高速缓存,最终还是要经过一次内存寻址 +- 中型数据,栈上分配性能高,但是读取性能和堆上并无区别,因为无法利用寄存器或 CPU 高速缓存,最终还是要经过一次内存寻址 - 大型数据,只建议在堆上分配和使用 -总之栈的分配速度肯定比堆上快,但是读取速度往往取决于你的数据能不能放入寄存器或CPU高速缓存。 因此不要仅仅因为堆上性能不如栈这个印象,就总是优先选择栈,导致代码更复杂的实现。 +总之,栈的分配速度肯定比堆上快,但是读取速度往往取决于你的数据能不能放入寄存器或 CPU 高速缓存。 因此不要仅仅因为堆上性能不如栈这个印象,就总是优先选择栈,导致代码更复杂的实现。 -## Box的使用场景 -由于`Box`是简单的封装,除了将值存储在堆上外,并没有其它性能上的损耗。而性能和功能往往是鱼和熊掌,因此`Box`相比其它智能指针,功能较为单一,可以在以下场景中使用它: +## Box 的使用场景 +由于 `Box` 是简单的封装,除了将值存储在堆上外,并没有其它性能上的损耗。而性能和功能往往是鱼和熊掌,因此 `Box` 相比其它智能指针,功能较为单一,可以在以下场景中使用它: - 特意的将数据分配在堆上 - 数据较大时,又不想在转移所有权时进行数据拷贝 @@ -44,8 +44,8 @@ fn foo(x: &str) -> String { 以上场景,我们在本章将一一讲解,后面车速较快,请系好安全带。 -#### 使用`Box`将数据存储在堆上 -如果一个变量拥有一个数值`let a = 3`, 那变量`a`必然是存储在栈上的,那如果我们想要`a`的值存储在堆上就需要使用`Box`: +#### 使用 `Box` 将数据存储在堆上 +如果一个变量拥有一个数值 `let a = 3`,那变量 `a` 必然是存储在栈上的,那如果我们想要 `a` 的值存储在堆上就需要使用 `Box`: ```rust fn main() { let a = Box::new(3); @@ -56,46 +56,46 @@ fn main() { } ``` -这样就可以创建一个智能指针指向了存储在堆上的`3`,并且`a`持有了该指针。在本章的引言中,我们提到了智能指针往往都实现了`Deref`和`Drop`特征,因此: +这样就可以创建一个智能指针指向了存储在堆上的 `3`,并且 `a` 持有了该指针。在本章的引言中,我们提到了智能指针往往都实现了 `Deref` 和 `Drop` 特征,因此: -- `println!`可以正常打印出`a`的值,是因为它隐式的调用了`Deref`对智能指针`a`进行了解引用 -- 最后一行代码`let b = a + 1`报错,是因为在表达式中,我们无法自动隐式的执行`Deref`解引用操作, 你需要使用`*`操作符`let b = *a + 1`,来显式的进行解引用 -- `a`持有的智能指针将在作用结束(`main`函数结束)时,被释放掉,这是因为`Box`实现了`Drop`特征 +- `println!` 可以正常打印出 `a` 的值,是因为它隐式地调用了 `Deref` 对智能指针 `a` 进行了解引用 +- 最后一行代码 ` let b = a + 1` 报错,是因为在表达式中,我们无法自动隐式地执行 `Deref` 解引用操作,你需要使用 `*` 操作符 `let b = *a + 1`,来显式的进行解引用 +- `a` 持有的智能指针将在作用结束(`main` 函数结束)时,被释放掉,这是因为 `Box` 实现了 `Drop` 特征 -以上的例子在实际代码中其实很少会存在,因为将一个简单的值分配到堆上并没有太大的意义。将其分配在栈上,由于寄存器、CPU缓存的原因,它的性能将更好,而且代码可读性也更好。 +以上的例子在实际代码中其实很少会存在,因为将一个简单的值分配到堆上并没有太大的意义。将其分配在栈上,由于寄存器、CPU 缓存的原因,它的性能将更好,而且代码可读性也更好。 #### 避免栈上数据的拷贝 当栈上数据转移所有权时,实际上是把数据拷贝了一份,最终新旧变量各自拥有不同的数据,因此所有权并未转移。 -而堆上则不然,底层数据并不会被拷贝,转移所有权仅仅是复制一份栈中的指针,再将新的指针赋予新的变量,然后让拥有旧指针的变量失效,最终完成了所有权的转移: +而堆上则不然,底层数据并不会被拷贝,转移所有权仅仅是复制一份栈中的指针,再将新的指针赋予新的变量,然后让拥有旧指针的变量失效,最终完成了所有权的转移: ```rust fn main() { // 在栈上创建一个长度为1000的数组 let arr = [0;1000]; - // 将arr所有权转移arr1,由于`arr`分配在栈上,因此这里实际上是直接重新深拷贝了一份数据 + // 将arr所有权转移arr1,由于 `arr` 分配在栈上,因此这里实际上是直接重新深拷贝了一份数据 let arr1 = arr; - // arr和arr1都拥有各自的栈上数组,因此不会报错 + // arr 和 arr1 都拥有各自的栈上数组,因此不会报错 println!("{:?}",arr.len()); println!("{:?}",arr1.len()); // 在堆上创建一个长度为1000的数组,然后使用一个智能指针指向它 let arr = Box::new([0;1000]); - // 将堆上数组的所有权转移给arr1, 由于数据在堆上,因此仅仅拷贝了智能指针的结构体,底层数据并没有被拷贝 - // 所有权顺利转移给arr1,arr不再拥有所有权 + // 将堆上数组的所有权转移给 arr1,由于数据在堆上,因此仅仅拷贝了智能指针的结构体,底层数据并没有被拷贝 + // 所有权顺利转移给 arr1,arr 不再拥有所有权 let arr1 = arr; println!("{:?}",arr1.len()); - // 由于arr不再拥有底层数组的所有权,因此下面代码将报错 + // 由于 arr 不再拥有底层数组的所有权,因此下面代码将报错 // println!("{:?}",arr.len()); } ``` -从以上代码,可以清晰看出大块的数据为何应该放入堆中,此时`Box`就成为了我们最好的帮手. +从以上代码,可以清晰看出大块的数据为何应该放入堆中,此时 `Box` 就成为了我们最好的帮手。 -#### 将动态大小类型变为Sized固定大小类型 -Rust需要在编译时知道类型占用多少空间, 如果一种类型在编译时无法知道具体的大小,那么被称为动态大小类型DST。 +#### 将动态大小类型变为 Sized 固定大小类型 +Rust 需要在编译时知道类型占用多少空间,如果一种类型在编译时无法知道具体的大小,那么被称为动态大小类型 DST。 -其中一种无法在编译时知道大小的类型是**递归类型**:在类型定义中又使用到了自身,或者说该类型的值的一部分可以是相同类型的其它值,这种值的嵌套理论上可以无限进行下去,所以Rust不知道递归类型需要多少空间: +其中一种无法在编译时知道大小的类型是**递归类型**:在类型定义中又使用到了自身,或者说该类型的值的一部分可以是相同类型的其它值,这种值的嵌套理论上可以无限进行下去,所以 Rust 不知道递归类型需要多少空间: ```rust enum List { Cons(i32, List), @@ -103,9 +103,9 @@ enum List { } ``` -以上就是函数式语言中常见的`Cons List`,它的每个节点包含一个`i32`值,还包含了一个新的`List`,因此这种嵌套可以无限进行下去,然后Rust认为该类型是一个DST类型,并给予报错: +以上就是函数式语言中常见的 `Cons List`,它的每个节点包含一个 `i32` 值,还包含了一个新的 `List`,因此这种嵌套可以无限进行下去,Rust 认为该类型是一个 DST 类型,并给予报错: ```console -error[E0072]: recursive type `List` has infinite size //递归类型`List`拥有无限长的大小 +error[E0072]: recursive type `List` has infinite size //递归类型 `List` 拥有无限长的大小 --> src/main.rs:3:1 | 3 | enum List { @@ -114,7 +114,7 @@ error[E0072]: recursive type `List` has infinite size //递归类型`List`拥有 | ---- recursive without indirection ``` -此时若想解决这个问题,就可以使用我们的`Box`: +此时若想解决这个问题,就可以使用我们的 `Box`: ```rust enum List { Cons(i32, Box), @@ -122,10 +122,10 @@ enum List { } ``` -只需要将`List`存储到堆上,然后使用一个智能指针指向它,即可完成从DST到Sized类型(固定大小类型)的华丽转变. +只需要将 `List` 存储到堆上,然后使用一个智能指针指向它,即可完成从 DST 到 Sized 类型(固定大小类型)的华丽转变。 #### 特征对象 -在Rust中,想实现不同类型组成的数组只有两个办法:枚举和特征对象,前者限制较多,因此后者往往是最常用的解决办法。 +在 Rust 中,想实现不同类型组成的数组只有两个办法:枚举和特征对象,前者限制较多,因此后者往往是最常用的解决办法。 ```rust trait Draw { @@ -133,29 +133,26 @@ trait Draw { } struct Button { - id: u32 + id: u32, } impl Draw for Button { fn draw(&self) { - println!("这是屏幕上第{}号按钮",self.id) + println!("这是屏幕上第{}号按钮", self.id) } } struct Select { - id: u32 + id: u32, } impl Draw for Select { fn draw(&self) { - println!("这个选择框贼难用{}",self.id) + println!("这个选择框贼难用{}", self.id) } } fn main() { - let elems: Vec> = vec![ - Box::new(Button{id: 1}), - Box::new(Select{id: 2}) - ]; + let elems: Vec> = vec![Box::new(Button { id: 1 }), Box::new(Select { id: 2 })]; for e in elems { e.draw() @@ -163,12 +160,12 @@ fn main() { } ``` -以上代码将不同类型的`Button`和`Select`包装成`Draw`特征的特征对象,放入一个数组中,`Box`就是特征对象。 +以上代码将不同类型的 `Button` 和 `Select` 包装成 `Draw` 特征的特征对象,放入一个数组中,`Box` 就是特征对象。 -其实,特征也是DST类型,而特征对象在做的也是将DST类型转换为固定大小类型。 +其实,特征也是 DST 类型,而特征对象在做的就是将 DST 类型转换为固定大小类型。 -## Box内存布局 -先来看看`Vec`的内存布局: +## Box 内存布局 +先来看看 `Vec` 的内存布局: ```rust (stack) (heap) ┌──────┐ ┌───┐ @@ -182,9 +179,9 @@ fn main() { └───┘ ``` -之前提到过`Vec`和`String`都是智能指针,从上图可以看出,该智能指针存储在栈中,然后指向堆上的数组数据。 +之前提到过 `Vec` 和 `String` 都是智能指针,从上图可以看出,该智能指针存储在栈中,然后指向堆上的数组数据。 -那如果数组中每个元素都是一个`Box`对象呢?来看看`Vec>`的内存布局: +那如果数组中每个元素都是一个 `Box` 对象呢?来看看 `Vec>` 的内存布局: ```rust (stack) (heap) ┌───┐ ┌──────┐ ┌───┐ ┌─→│ 1 │ @@ -200,11 +197,11 @@ fn main() { └───┘ ``` -上面的`B1`代表被`Box`分配到堆上的值`1`。 +上面的 `B1` 代表被 `Box` 分配到堆上的值 `1`。 -可以看出智能指针`vec2`依然是存储在栈上,然后指针指向一个堆上的数组,该数组中每个元素都是一个`Box`智能指针,最终`Box`智能指针又指向了存储在堆上的实际值。 +可以看出智能指针 `vec2` 依然是存储在栈上,然后指针指向一个堆上的数组,该数组中每个元素都是一个 `Box` 智能指针,最终 `Box` 智能指针又指向了存储在堆上的实际值。 -因此当我们从数组中取出某个元素时,取到的是对应的智能指针`Box`,需要对该智能指针进行解引用,才能取出最终的值: +因此当我们从数组中取出某个元素时,取到的是对应的智能指针 `Box`,需要对该智能指针进行解引用,才能取出最终的值: ```rust fn main() { let arr = vec![Box::new(1), Box::new(2)]; @@ -213,16 +210,16 @@ fn main() { } ``` -以上代码有几个值得注意的点: +以上代码有几个值得注意的点: -- 使用`&`借用数组中的元素,否则会报所有权错误 -- 表达式不能隐式的解引用,因此必须使用`**`做两次解引用,第一次将`&Box`类型转成`Box`,第二次将`Box`转成`i32` +- 使用 `&` 借用数组中的元素,否则会报所有权错误 +- 表达式不能隐式的解引用,因此必须使用 `**` 做两次解引用,第一次将 `&Box` 类型转成 `Box`,第二次将 `Box` 转成 `i32` ## Box::leak -`Box`中还提供了一个非常有用的关联函数:`Box::leak`,它可以消费掉`Box`并且强制目标值从内存中泄漏,读者可能会觉得,这有啥用啊? +`Box` 中还提供了一个非常有用的关联函数:`Box::leak`,它可以消费掉 `Box` 并且强制目标值从内存中泄漏,读者可能会觉得,这有啥用啊? -其实还真有点用,例如,你可以把一个`String`类型,变成一个`'static`生命周期的`&str`类型: +其实还真有点用,例如,你可以把一个 `String` 类型,变成一个 `'static` 生命周期的 `&str` 类型: ```rust fn main() { let s = gen_static_str(); @@ -237,20 +234,20 @@ fn gen_static_str() -> &'static str{ } ``` -在之前的代码中,如果`String`创建于函数中,那么返回它的唯一方法就是转移所有权给调用者`fn move_str() -> String`,而通过`Box::leak`我们不仅返回了一个`&str`字符串切片,它还是`'static`类型的! +在之前的代码中,如果 `String` 创建于函数中,那么返回它的唯一方法就是转移所有权给调用者 `fn move_str() -> String`,而通过 `Box::leak` 我们不仅返回了一个 `&str` 字符串切片,它还是 `'static` 类型的! -要知道真正具有`'static`生命周期的往往都是编译期就创建的值,例如`let v = "hello,world"`, 这里`v`是直接打包到二进制可执行文件中的,因此该字符串具有`'static`生命周期,再比如`const`常量。 +要知道真正具有 `'static` 生命周期的往往都是编译期就创建的值,例如 `let v = "hello,world"`,这里 `v` 是直接打包到二进制可执行文件中的,因此该字符串具有 `'static` 生命周期,再比如 `const` 常量。 -又有读者要问了,我还可以手动为变量标注`'static`啊。其实你标注的`'static`只是用来忽悠编译器的,但是超出作用域,一样被释放回收。而使用`Box::leak`就可以将一个运行期的值转为`'static`。 +又有读者要问了,我还可以手动为变量标注 `'static` 啊。其实你标注的 `'static` 只是用来忽悠编译器的,但是超出作用域,一样被释放回收。而使用 `Box::leak` 就可以将一个运行期的值转为 `'static`。 #### 使用场景 光看上面的描述,大家可能还是云里雾里、一头雾水。 -那么我说一个简单的场景,**你需要一个在运行期初始化的值,但是可以全局有效,也就是和整个程序活得一样久**, 那么久可以使用`Box::leak`,例如有一个存储配置的结构体实例,它是在运行期动态插入内容,那么就可以将其转为全局有效,虽然`Rc/Arc`也可以实现此功能,但是`Box::leak`是性能最高的。 +那么我说一个简单的场景,**你需要一个在运行期初始化的值,但是可以全局有效,也就是和整个程序活得一样久**,那么久可以使用 `Box::leak`,例如有一个存储配置的结构体实例,它是在运行期动态插入内容,那么就可以将其转为全局有效,虽然 `Rc/Arc` 也可以实现此功能,但是 `Box::leak` 是性能最高的。 ## 总结 -`Box`背后是调用`jemalloc`来做内存管理,所以堆上的空间无需我们的手动管理。与此类似,带GC的语言中的对象也是借助于box概念来实现的,一切皆对象 = 一切皆box, 只不过我们无需自己去box罢了。 +`Box` 背后是调用 `jemalloc` 来做内存管理,所以堆上的空间无需我们的手动管理。与此类似,带 GC 的语言中的对象也是借助于 `Box` 概念来实现的,一切皆对象 = 一切皆 Box, 只不过我们无需自己去 `Box` 罢了。 -其实很多时候,编译器的鞭笞可以助我们更快的成长,例如所有权规则里的借用、move、生命周期就是编译器在教我们做人,哦不是,是教我们深刻理解堆栈、内存布局、作用域等你在其它GC语言无需去关注的东西。刚开始是很痛苦,但是一旦熟悉了这套规则,写代码的效率和代码本身的质量将飞速上升,直到你用Java开发的效率写出Java代码不可企及的性能和安全性,最终Rust语言所谓的开发效率低、心智负担高,对你来说终究不是个事。 +其实很多时候,编译器的鞭笞可以助我们更快的成长,例如所有权规则里的借用、move、生命周期就是编译器在教我们做人,哦不是,是教我们深刻理解堆栈、内存布局、作用域等等你在其它 GC 语言无需去关注的东西。刚开始是很痛苦,但是一旦熟悉了这套规则,写代码的效率和代码本身的质量将飞速上升,直到你可以用 Java 开发的效率写出 Java 代码不可企及的性能和安全性,最终 Rust 语言所谓的开发效率低、心智负担高,对你来说终究不是个事。 -因此,不要怪Rust,**它只是在帮我们成为那个更好的程序员,而这些苦难终究成为我们走向优秀的垫脚石**。 +因此,不要怪 Rust,**它只是在帮我们成为那个更好的程序员,而这些苦难终究成为我们走向优秀的垫脚石**。 diff --git a/book/contents/advance/smart-pointer/deref.md b/book/contents/advance/smart-pointer/deref.md index 27415ea1..a4b7cb6e 100644 --- a/book/contents/advance/smart-pointer/deref.md +++ b/book/contents/advance/smart-pointer/deref.md @@ -1,16 +1,16 @@ -# Deref解引用 -何为智能指针?能不让你写出&&&&&&s形式的解引用,我认为就是智能: ) 智能指针的名称来源,主要就在于它实现了`Deref`和`Drop`特征,这两个特征可以智能地帮助我们节省使用上的负担: +# Deref 解引用 +何为智能指针?能不让你写出 &&&&&&s 形式的解引用,我认为就是智能: ),智能指针的名称来源,主要就在于它实现了 `Deref` 和 `Drop` 特征,这两个特征可以智能地帮助我们节省使用上的负担: -- `Deref`可以让智能指针像引用那样工作,这样你就就可以写出同时支持智能指针和引用的代码, 例如`&T` -- `Drop`允许你指定智能指针超出作用域后自动执行的代码,例如做一些数据清除等收尾工作 +- `Deref` 可以让智能指针像引用那样工作,这样你就可以写出同时支持智能指针和引用的代码,例如 `&T` +- `Drop` 允许你指定智能指针超出作用域后自动执行的代码,例如做一些数据清除等收尾工作 -下面先来看看`Deref`特征是如何工作的。 +下面先来看看 `Deref` 特征是如何工作的。 -## 通过`*`获取引用背后的值 -在正式讲解`Deref`之前,我们先来看下常规引用的解引用。 +## 通过 `*` 获取引用背后的值 +在正式讲解 `Deref` 之前,我们先来看下常规引用的解引用。 -常规引用是一个指针类型,包含了目标数据存储的内存地址。对常规引用使用`*`操作符,就可以通过解引用的方式获取到内存地址对应的数据值: +常规引用是一个指针类型,包含了目标数据存储的内存地址。对常规引用使用 `*` 操作符,就可以通过解引用的方式获取到内存地址对应的数据值: ```rust fn main() { let x = 5; @@ -21,7 +21,7 @@ fn main() { } ``` -这里`y`就是一个常规引用,包含了值`5`所在的内存地址, 然后通过解引用`*y`,我们获取到了值`5`。如果你试图执行`assert_eq!(5, y);`,代码就会无情报错,因为你无法将一个引用与一个数值做比较: +这里 `y` 就是一个常规引用,包含了值 `5` 所在的内存地址,然后通过解引用 `*y`,我们获取到了值 `5`。如果你试图执行 `assert_eq!(5, y);`,代码就会无情报错,因为你无法将一个引用与一个数值做比较: ```console error[E0277]: can't compare `{integer}` with `&{integer}` //无法将{integer} 与&{integer}进行比较 --> src/main.rs:6:5 @@ -34,9 +34,9 @@ error[E0277]: can't compare `{integer}` with `&{integer}` //无法将{integer} ``` ## 智能指针解引用 -上面所说的解引用方式和其它大多数语言并无区别,但是Rust中将解引用提升到了一个新高度。考虑一下智能指针,它是一个结构体类型,如果你直接对它进行`*myStruct`,显然编译器不知道该如何办,因此我们可以为智能指针结构体实现`Deref`特征。 +上面所说的解引用方式和其它大多数语言并无区别,但是 Rust 中将解引用提升到了一个新高度。考虑一下智能指针,它是一个结构体类型,如果你直接对它进行 `*myStruct`,显然编译器不知道该如何办,因此我们可以为智能指针结构体实现 `Deref` 特征。 -实现`Deref`后的智能指针结构体,就可以像普通引用一样,通过`*`进行解引用,例如`Box`智能指针: +实现 `Deref` 后的智能指针结构体,就可以像普通引用一样,通过 `*` 进行解引用,例如 `Box` 智能指针: ```rust fn main() { let x = Box::new(1); @@ -44,10 +44,10 @@ fn main() { } ``` -智能指针`x`被`*`解引用为`i32`类型的值`1`,然后再进行求和。 +智能指针 `x` 被 `*` 解引用为 `i32` 类型的值 `1`,然后再进行求和。 #### 定义自己的智能指针 -现在,让我们一起来实现一个智能指针,功能上类似`Box`。由于`Box`本身很简单,并没有包含类如长度、最大长度等信息,因此用一个元组结构体即可。 +现在,让我们一起来实现一个智能指针,功能上类似 `Box`。由于 `Box` 本身很简单,并没有包含类如长度、最大长度等信息,因此用一个元组结构体即可。 ```rust struct MyBox(T); @@ -59,7 +59,7 @@ impl MyBox { } ``` -跟`Box`一样,我们的智能指针也持有一个`T`类型的值,然后使用关联函数`MyBox::new`来创建智能指针。由于还未实现`Deref`特征,此时使用`*`肯定会报错: +跟 `Box` 一样,我们的智能指针也持有一个 `T` 类型的值,然后使用关联函数 `MyBox::new` 来创建智能指针。由于还未实现 `Deref` 特征,此时使用 `*` 肯定会报错: ```rust fn main() { let y = MyBox::new(5); @@ -77,8 +77,8 @@ error[E0614]: type `MyBox<{integer}>` cannot be dereferenced | ^^ ``` -##### 为智能指针实现Deref特征 -现在来为`MyBox`实现`Deref`特征, 以支持`*`解引用操作符: +##### 为智能指针实现 Deref 特征 +现在来为 `MyBox` 实现 `Deref` 特征,以支持 `*` 解引用操作符: ```rust use std::ops::Deref; @@ -91,28 +91,28 @@ impl Deref for MyBox { } ``` -很简单,当解引用`MyBox`智能指针时,返回元组结构体中的元素`&self.0`, 有几点要注意的: +很简单,当解引用 `MyBox` 智能指针时,返回元组结构体中的元素 `&self.0`,有几点要注意的: -- 为了可读性, 我们声明了关联类型`Target` -- `deref`返回的是一个常规引用,可以被`*`进行解引用 +- 为了可读性,我们声明了关联类型 `Target` +- `deref` 返回的是一个常规引用,可以被 `*` 进行解引用 之前报错的代码此时已能顺利编译通过。当然,标准库实现的智能指针要考虑很多边边角角情况,肯定比我们的实现要复杂。 -## `*`背后的原理 -当我们对智能指针`Box`进行解引用时, 实际上Rust为我们调用了以下方法: +## `*` 背后的原理 +当我们对智能指针 `Box` 进行解引用时,实际上 Rust 为我们调用了以下方法: ```rust *(y.deref()) ``` -首先调用`deref`方法返回值的常规引用,然后通过`*`对常规引用进行解引用,最终获取到目标值。 +首先调用 `deref` 方法返回值的常规引用,然后通过 `*` 对常规引用进行解引用,最终获取到目标值。 -至于Rust为何要使用这个有点啰嗦的方式实现,原因是因为所有权系统的存在。如果`deref`方法直接返回一个值,而不是引用,那么该值的所有权将被转移给调用者,而我们不希望调用者仅仅只是`*T`一下,就拿走了智能指针中包含的值。 +至于 Rust 为何要使用这个有点啰嗦的方式实现,原因在于所有权系统的存在。如果 `deref` 方法直接返回一个值,而不是引用,那么该值的所有权将被转移给调用者,而我们不希望调用者仅仅只是 `*T` 一下,就拿走了智能指针中包含的值。 -需要注意的是,`*`不会无限递归替换,从`*y`到`*(y.deref())`只会发生一次,而不会继续进行替换然后产生形如`*((y.deref()).deref())`的怪物。 +需要注意的是,`*` 不会无限递归替换,从 `*y` 到 `*(y.deref())` 只会发生一次,而不会继续进行替换然后产生形如 `*((y.deref()).deref())` 的怪物。 -## 函数和方法中的隐式Deref转换 -在函数和方法中,Rust提供了一个极其有用的隐式转换:`Deref`转换。简单来说,当一个实现了`Deref`特征的值被传给函数或方法时,会根据函数参数的要求,来决定使用该值原本的类型还是`Deref`后的类型,例如: +## 函数和方法中的隐式 Deref 转换 +在函数和方法中,Rust 提供了一个极其有用的隐式转换:`Deref `转换。简单来说,当一个实现了 `Deref` 特征的值被传给函数或方法时,会根据函数参数的要求,来决定使用该值原本的类型还是 `Deref` 后的类型,例如: ```rust fn main() { let s = String::from("hello world"); @@ -126,12 +126,12 @@ fn display(s: &str) { 以上代码有几点值得注意: -- `String`实现了`Deref`特征,能被转换成一个`&str` -- `s`是一个`String`类型,当它被传给`display`函数时,自动通过`Deref`转换成了`&str` -- 必须使用`&s`的方式来触发`Deref` +- `String` 实现了 `Deref` 特征,能被转换成一个 `&str` +- `s` 是一个 `String` 类型,当它被传给 `display` 函数时,自动通过 `Deref` 转换成了 `&str` +- 必须使用 `&s` 的方式来触发 `Deref`(`&s` 相当于调用 `s` 的 `deref` 方法) -#### 连续的隐式Deref转换 -如果你以为`Deref`仅仅这点作用,那就大错特错了。`Deref`可以支持连续的隐式转换,直到找到适合的形式为止: +#### 连续的隐式 Deref 转换 +如果你以为 `Deref` 仅仅这点作用,那就大错特错了。`Deref` 可以支持连续的隐式转换,直到找到适合的形式为止: ```rust fn main() { let s = MyBox::new(String::from("hello world")); @@ -143,9 +143,9 @@ fn display(s: &str) { } ``` -这里我们使用了之前自定义的智能指针`MyBox`,并将其通过连续的隐式转换变成`&str`类型:首先`MyBox`被`Deref`成`String`类型,结果并不能满足`display`函数参数的要求,编译器发现`String`还可以继续`Deref`成`&str`,最终成功的匹配了函数参数。 +这里我们使用了之前自定义的智能指针 `MyBox`,并将其通过连续的隐式转换变成 `&str` 类型:首先 `MyBox` 被 `Deref` 成 `String` 类型,结果并不能满足 `display` 函数参数的要求,编译器发现 `String` 还可以继续 `Deref` 成 `&str`,最终成功的匹配了函数参数。 -想象一下,假如`Rust`没有提供这种隐式转换,我们该如何调用`display`函数? +想象一下,假如 `Rust` 没有提供这种隐式转换,我们该如何调用 `display` 函数? ```rust fn main() { let m = MyBox::new(String::from("Rust")); @@ -153,11 +153,11 @@ fn main() { } ``` -结果不言而喻,肯定是`&s`的方式优秀的多。总之,当参与其中的类型定义了`Deref`特征时,Rust会分析该类型并且连续使用`Deref`直到最终获得一个引用来匹配函数或者方法的参数类型,这种行为完全不会造成任何的性能损耗, 因为完全是在编译期完成。 +结果不言而喻,肯定是 `&s` 的方式优秀得多。总之,当参与其中的类型定义了 `Deref` 特征时,Rust 会分析该类型并且连续使用 `Deref` 直到最终获得一个引用来匹配函数或者方法的参数类型,这种行为完全不会造成任何的性能损耗,因为完全是在编译期完成。 -但是`Deref`并不是没有缺点,缺点就是:如果你不知道某个类型实现了`Deref`特征,那么在看到某段代码时,并不能在第一时间反应过来该代码发生了隐式的`Deref`转换。事实上,不仅仅是`Deref`,在Rust中还有各种`From/Into`等等会给阅读代码带来一定负担的特征。还是那句话,一切选择都是权衡,有得必有失,得了代码的简洁性,往往就失去了可读性,Go语言就是一个刚好相反的例子。 +但是 `Deref` 并不是没有缺点,缺点就是:如果你不知道某个类型是否实现了 `Deref` 特征,那么在看到某段代码时,并不能在第一时间反应过来该代码发生了隐式的 `Deref` 转换。事实上,不仅仅是 `Deref`,在 Rust 中还有各种 `From/Into` 等等会给阅读代码带来一定负担的特征。还是那句话,一切选择都是权衡,有得必有失,得了代码的简洁性,往往就失去了可读性,Go 语言就是一个刚好相反的例子。 -再来看一下在方法、赋值中自动应用`Deref`的例子: +再来看一下在方法、赋值中自动应用 `Deref` 的例子: ```rust fn main() { let s = MyBox::new(String::from("hello, world")); @@ -166,25 +166,25 @@ fn main() { } ``` -对于`s1`,我们通过两次`Deref`将`&str`类型的值赋给了它;而对于`s2`,我们在其上直接调用方法`to_string`, 实际上`MyBox`根本没有没有实现该方法,能调用`to_string`,完全是因为编译器对`MyBox`应用了`Deref`的结果。 +对于 `s1`,我们通过两次 `Deref` 将 `&str` 类型的值赋给了它(**赋值操作需要手动解引用**);而对于 `s2`,我们在其上直接调用方法 `to_string`,实际上 `MyBox` 根本没有没有实现该方法,能调用 `to_string`,完全是因为编译器对 `MyBox` 应用了 `Deref` 的结果(**方法调用会自动解引用**)。 -## Deref规则总结 -在上面,我们零碎的介绍了不少关于`Deref`特征的知识,下面来通过较为正式的方式来对其规则进行下总结。 +## Deref 规则总结 +在上面,我们零碎的介绍了不少关于 `Deref` 特征的知识,下面来通过较为正式的方式来对其规则进行下总结。 -一个类型为`T`的对象`foo`,如果`T: Deref`,那么,相关`foo`的引用`&foo`在应用的时候会自动转换为`&U`。 +一个类型为 `T` 的对象 `foo`,如果 `T: Deref`,那么,相关 `foo` 的引用 `&foo` 在应用的时候会自动转换为 `&U`。 -粗看这条规则,貌似有点类似于`AsRef`,而跟`解引用`似乎风马牛不相及, 实际里面有些玄妙之处。 +粗看这条规则,貌似有点类似于 `AsRef`,而跟 `解引用` 似乎风马牛不相及,实际里面有些玄妙之处。 #### 引用归一化 -Rust编译器实际上只能对 `&v` 形式的引用进行解引用操作,那么问题来了,如果是一个智能指针或者 `&&&&v` 类型的呢? 该如何对这两个进行解引用? +Rust 编译器实际上只能对 `&v` 形式的引用进行解引用操作,那么问题来了,如果是一个智能指针或者 `&&&&v` 类型的呢? 该如何对这两个进行解引用? -答案是:Rust 会在解引用时自动把智能指针和 `&&&&v` 做引用归一化操作,转换成 `&v` 形式,最终再对 `&v` 进行解引用: +答案是:Rust 会在解引用时自动把智能指针和 `&&&&v` 做引用归一化操作,转换成 `&v` 形式,最终再对 `&v` 进行解引用: -- 把智能指针(比如在库中定义的,Box, Rc, Arc, Cow 等)从结构体脱壳为内部的引用类型,也就是转成结构体内部的`&v` -- 把多重`&` ,例如 `&&&&&&&v`,归一成`&v` +- 把智能指针(比如在库中定义的,Box、Rc、Arc、Cow 等)从结构体脱壳为内部的引用类型,也就是转成结构体内部的 `&v` +- 把多重`&`,例如 `&&&&&&&v`,归一成 `&v` -关于第二种情况,这么干巴巴的说,也许大家会迷迷糊糊的,我们来看一段标准库源码: +关于第二种情况,这么干巴巴的说,也许大家会迷迷糊糊的,我们来看一段标准库源码: ```rust impl Deref for &T { type Target = T; @@ -195,7 +195,7 @@ impl Deref for &T { } ``` -在这段源码中,`&T` 被自动解引用为 `T` , 也就是 `&T: Deref` 。 按照这个代码,`&&&&T` 会被自动解引用为 `&&&T`, 然后再自动解引用为 `&&T`,以此类推, 直到最终变成 `&T`。 +在这段源码中,`&T` 被自动解引用为 `T`,也就是 `&T: Deref` 。 按照这个代码,`&&&&T` 会被自动解引用为 `&&&T`,然后再自动解引用为 `&&T`,以此类推, 直到最终变成 `&T`。 PS: 以下是 `LLVM` 编译后的部分中间层代码: ```rust @@ -215,7 +215,7 @@ bb0: { // 由于 String 实现了 Deref let owned = "Hello".to_string(); - // 因此下面的函数可以正常运行: + // 因此下面的函数可以正常运行: foo(&owned); ``` @@ -249,23 +249,21 @@ bb0: { (&&&&&&&&f).foo(); ``` -## 三种Deref转换 -在之前,我们讲的都是不可变的`Deref`转换,实际上Rust还支持将一个可变的引用转换成另一个可变的引用以及将一个可变引用转换成不可变的引用,规则如下: -- 当`T: Deref`,可以将`&T`转换成`&U`,也就是我们之前看到的例子 -- 当`T: DerefMut`,可以将`&mut T`转换成`&mut U` -- 当`T: Deref`,可以将`&mut T`转换成`&U` +## 三种 Deref 转换 +在之前,我们讲的都是不可变的 `Deref` 转换,实际上 Rust 还支持将一个可变的引用转换成另一个可变的引用以及将一个可变引用转换成不可变的引用,规则如下: +- 当 `T: Deref`,可以将 `&T` 转换成 `&U`,也就是我们之前看到的例子 +- 当 `T: DerefMut`,可以将 `&mut T` 转换成 `&mut U` +- 当 `T: Deref`,可以将 `&mut T` 转换成 `&U` -来看一个关于`DerefMut`的例子: +来看一个关于 `DerefMut` 的例子: ```rust struct MyBox { - v: T + v: T, } impl MyBox { fn new(x: T) -> MyBox { - MyBox{ - v: x - } + MyBox { v: x } } } @@ -294,21 +292,21 @@ fn main() { fn display(s: &mut String) { s.push_str("world"); - println!("{}",s); + println!("{}", s); } ``` 以上代码有几点值得注意: -- 要实现`DerefMut`必须要先实现`Deref`特征: `pub trait DerefMut: Deref` -- `T: DerefMut`解读:将`&mut T`类型通过`DerefMut`特征的方法转换为`&mut U`类型,对应上例中,就是将`&mut MyBox`转换为`&mut String` +- 要实现 `DerefMut` 必须要先实现 `Deref` 特征:`pub trait DerefMut: Deref` +- `T: DerefMut` 解读:将 `&mut T` 类型通过 `DerefMut` 特征的方法转换为 `&mut U` 类型,对应上例中,就是将 `&mut MyBox` 转换为 `&mut String` -对于上述三条规则中的第三条,它比另外两条稍微复杂了点:Rust可以把可变引用隐式的转换成不可变引用,但反之则不行。 +对于上述三条规则中的第三条,它比另外两条稍微复杂了点:Rust 可以把可变引用隐式的转换成不可变引用,但反之则不行。 -如果从Rust的所有权和借用规则的角度考虑,当你拥有一个可变的引用,那该引用肯定是对应数据的唯一借用,那么此时将可变引用变成不可变引用并不会破坏借用规则;但是如果你拥有一个不可变引用,那同时可能还存在其它几个不可变的引用,如果此时将其中一个不可变引用转换成可变引用,就变成了可变引用与不可变引用的共存,最终破坏了借用规则。 +如果从 Rust 的所有权和借用规则的角度考虑,当你拥有一个可变的引用,那该引用肯定是对应数据的唯一借用,那么此时将可变引用变成不可变引用并不会破坏借用规则;但是如果你拥有一个不可变引用,那同时可能还存在其它几个不可变的引用,如果此时将其中一个不可变引用转换成可变引用,就变成了可变引用与不可变引用的共存,最终破坏了借用规则。 ## 总结 -`Deref`可以说是Rust中最常见的隐式类型转换,而且它可以连续的实现如`Box -> String -> &str`的隐式转换,只要链条上的类型实现了`Deref`特征。 +`Deref` 可以说是 Rust 中最常见的隐式类型转换,而且它可以连续的实现如 `Box -> String -> &str` 的隐式转换,只要链条上的类型实现了 `Deref` 特征。 -我们也可以为自己的类型实现`Deref`特征, 但是原则上来说,只应该为自定义的智能指针实现`Deref`。例如,虽然你可以为自己的自定义数组类型实现`Deref`以避免`myArr.0[0]`的使用形式,但是Rust官方并不推荐这么做,特别是在你开发三方库时。 \ No newline at end of file +我们也可以为自己的类型实现 `Deref` 特征,但是原则上来说,只应该为自定义的智能指针实现 `Deref`。例如,虽然你可以为自己的自定义数组类型实现 `Deref` 以避免 `myArr.0[0]` 的使用形式,但是 Rust 官方并不推荐这么做,特别是在你开发三方库时。 diff --git a/book/contents/basic/base-type/numbers.md b/book/contents/basic/base-type/numbers.md index f2877da9..07559796 100644 --- a/book/contents/basic/base-type/numbers.md +++ b/book/contents/basic/base-type/numbers.md @@ -126,8 +126,8 @@ fn main() { 42.0_f32, ]; -// 打印数组中第一个值,其中控制小数位为2位 -println!("{:02}", forty_twos[0]); + // 打印数组中第一个值,并控制小数位为2位 + println!("{:.2}", forty_twos[0]); } ``` diff --git a/book/contents/basic/ownership/ownership.md b/book/contents/basic/ownership/ownership.md index b1abecf1..dc338043 100644 --- a/book/contents/basic/ownership/ownership.md +++ b/book/contents/basic/ownership/ownership.md @@ -37,9 +37,9 @@ int* foo() { #### 栈 -栈按照顺序存储值并以相反顺序取出值,这也被称作 **后进先出**。想象一下一叠盘子:当增加更多盘子时,把它们放在盘子堆的顶部,当需要盘子时,再从顶部拿走。不能从中间也不能从底部增加或拿走盘子! +栈按照顺序存储值并以相反顺序取出值,这也被称作**后进先出**。想象一下一叠盘子:当增加更多盘子时,把它们放在盘子堆的顶部,当需要盘子时,再从顶部拿走。不能从中间也不能从底部增加或拿走盘子! -增加数据叫做 **进栈**,移出数据则叫做 **出栈**。 +增加数据叫做**进栈**,移出数据则叫做**出栈**。 因为上述的实现方式,栈中的所有数据都必须占用已知且固定大小的内存空间,假设数据大小是未知的,那么在取出数据时,你将无法取到你想要的数据。 @@ -48,9 +48,9 @@ int* foo() { 与栈不同,对于大小未知或者可能变化的数据,我们需要将它存储在堆上。 -当向堆上放入数据时,需要请求一定大小的内存空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的 **指针**, 该过程被称为 **在堆上分配内存**,有时简称为 “分配”(allocating)。 +当向堆上放入数据时,需要请求一定大小的内存空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的**指针**, 该过程被称为**在堆上分配内存**,有时简称为 “分配”(allocating)。 -接着,该指针会被推入 **栈** 中,因为指针的大小是已知且固定的,在后续使用过程中,你将通过栈中的 **指针**,来获取数据在堆上的实际内存位置,进而访问该数据。 +接着,该指针会被推入**栈**中,因为指针的大小是已知且固定的,在后续使用过程中,你将通过栈中的**指针**,来获取数据在堆上的实际内存位置,进而访问该数据。 由上可知,堆是一种缺乏组织的数据结构。想象一下去餐馆就座吃饭: 进入餐馆,告知服务员有几个人,然后服务员找到一个够大的空桌子(堆上分配的内存空间)并领你们过去。如果有人来迟了,他们也可以通过桌号(栈上的指针)来找到你们坐在哪。 @@ -68,7 +68,7 @@ int* foo() { 因为堆上的数据缺乏组织,因此跟踪这些数据何时分配和释放是非常重要的,否则堆上的数据将产生内存泄漏 —— 这些数据将永远无法被回收。这就是 Rust 所有权系统为我们提供的强大保障。 -对于其他很多编程语言,你确实无需理解堆栈的原理,但是 **在 Rust 中,明白堆栈的原理,对于我们理解所有权的工作原理会有很大的帮助**。 +对于其他很多编程语言,你确实无需理解堆栈的原理,但是**在 Rust 中,明白堆栈的原理,对于我们理解所有权的工作原理会有很大的帮助**。 @@ -146,20 +146,20 @@ let s2 = s1; ``` 此时,可能某个大聪明(善意昵称)已经想到了:嗯,把 `s1` 的内容拷贝一份赋值给 `s2`,实际上,并不是这样。之前也提到了,对于基本类型(存储在栈上),Rust 会自动拷贝,但是 `String` 不是基本类型,而且是存储在堆上的,因此不能自动拷贝。 -实际上, `String` 类型是一个复杂类型,由 **存储在栈中的堆指针**、 **字符串长度**、 **字符串容量**共同组成,其中 **堆指针**是最重要的,它指向了真实存储字符串内容的堆内存,至于长度和容量,如果你有 Go 语言的经验,这里就很好理解:容量是堆内存分配空间的大小,长度是目前已经使用的大小。 +实际上, `String` 类型是一个复杂类型,由**存储在栈中的堆指针**、**字符串长度**、**字符串容量**共同组成,其中**堆指针**是最重要的,它指向了真实存储字符串内容的堆内存,至于长度和容量,如果你有 Go 语言的经验,这里就很好理解:容量是堆内存分配空间的大小,长度是目前已经使用的大小。 总之 `String` 类型指向了一个堆上的空间,这里存储着它的真实数据, 下面对上面代码中的 `let s2 = s1` 分成两种情况讨论: 1. 拷贝 `String` 和存储在堆上的字节数组 如果该语句是拷贝所有数据(深拷贝),那么无论是 `String` 本身还是底层的堆上数据,都会被全部拷贝,这对于性能而言会造成非常大的影响 2. 只拷贝 `String` 本身 -这样的拷贝非常快,因为在 64 位机器上就拷贝了 `8字节的指针`、`8字节的长度`、`8字节的容量`,总计 24 字节,但是带来了新的问题,还记得我们之前提到的所有权规则吧?其中有一条就是: **一个值只允许有一个所有者**,而现在这个值(堆上的真实字符串数据)有了两个所有者:`s1` 和 `s2`。 +这样的拷贝非常快,因为在 64 位机器上就拷贝了 `8字节的指针`、`8字节的长度`、`8字节的容量`,总计 24 字节,但是带来了新的问题,还记得我们之前提到的所有权规则吧?其中有一条就是:**一个值只允许有一个所有者**,而现在这个值(堆上的真实字符串数据)有了两个所有者:`s1` 和 `s2`。 好吧,就假定一个值可以拥有两个所有者,会发生什么呢? -当变量离开作用域后,Rust 会自动调用 `drop` 函数并清理变量的堆内存。不过由于两个 `String` 变量指向了同一位置。这就有了一个问题:当 `s1` 和 `s2` 离开作用域,它们都会尝试释放相同的内存。这是一个叫做 **二次释放(double free)**的错误,也是之前提到过的内存安全性 BUG 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。 +当变量离开作用域后,Rust 会自动调用 `drop` 函数并清理变量的堆内存。不过由于两个 `String` 变量指向了同一位置。这就有了一个问题:当 `s1` 和 `s2` 离开作用域,它们都会尝试释放相同的内存。这是一个叫做**二次释放(double free)**的错误,也是之前提到过的内存安全性 BUG 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。 -因此,Rust 这样解决问题: **当 `s1` 赋予 `s2` 后,Rust 认为 `s1` 不再有效,因此也无需在 `s1` 离开作用域后 `drop` 任何东西,这就是把所有权从 `s1` 转移给了 `s2`,`s1` 在被赋予 `s2` 后就马上失效了**。 +因此,Rust 这样解决问题:**当 `s1` 赋予 `s2` 后,Rust 认为 `s1` 不再有效,因此也无需在 `s1` 离开作用域后 `drop` 任何东西,这就是把所有权从 `s1` 转移给了 `s2`,`s1` 在被赋予 `s2` 后就马上失效了**。 再来看看,在所有权转移后再来使用旧的所有者,会发生什么: ```rust @@ -188,18 +188,18 @@ error[E0382]: use of moved value: `s1` > 1. Rust 中每一个值都 `有且只有` 一个所有者(变量) > 2. 当所有者(变量)离开作用域范围时,这个值将被丢弃(free) -如果你在其他语言中听说过术语 **浅拷贝(shallow copy)**和 **深拷贝(deep copy)**,那么拷贝指针、长度和容量而不拷贝数据听起来就像浅拷贝,但是又因为 Rust 同时使第一个变量 `s1` 无效了,因此这个操作被称为 **移动(move)**,而不是浅拷贝。上面的例子可以解读为 `s1` 被 **移动**到了 `s2` 中。那么具体发生了什么,用一张图简单说明: +如果你在其他语言中听说过术语**浅拷贝( shallow copy )**和**深拷贝( deep copy )**,那么拷贝指针、长度和容量而不拷贝数据听起来就像浅拷贝,但是又因为 Rust 同时使第一个变量 `s1` 无效了,因此这个操作被称为**移动(move)**,而不是浅拷贝。上面的例子可以解读为 `s1` 被**移动**到了 `s2` 中。那么具体发生了什么,用一张图简单说明: s1 moved to s2 -这样就解决了我们之前的问题,`s1` 不再指向任何数据,只有 `s2` 是有效的,当 `s2` 离开作用域,它就会释放内存。 相信此刻,你应该明白了,为什么 Rust 称呼 `let a = b` 为 **变量绑定**了吧? +这样就解决了我们之前的问题,`s1` 不再指向任何数据,只有 `s2` 是有效的,当 `s2` 离开作用域,它就会释放内存。 相信此刻,你应该明白了,为什么 Rust 称呼 `let a = b` 为**变量绑定**了吧? #### 克隆(深拷贝) -首先,**Rust 永远也不会自动创建数据的 “深拷贝”**。因此,任何 **自动**的复制都不是深拷贝,可以被认为对运行时性能影响较小。 +首先,**Rust 永远也不会自动创建数据的 “深拷贝”**。因此,任何**自动**的复制都不是深拷贝,可以被认为对运行时性能影响较小。 -如果我们 **确实**需要深度复制 `String` 中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 `clone` 的方法。 +如果我们**确实**需要深度复制 `String` 中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 `clone` 的方法。 ```rust let s1 = String::from("hello"); @@ -230,7 +230,7 @@ println!("x = {}, y = {}", x, y); Rust 有一个叫做 `Copy` 的特征,可以用在类似整型这样在栈中存储的类型。如果一个类型拥有 `Copy` 特征,一个旧的变量在被赋值给其他变量后仍然可用。 -那么什么类型是可 `Copy` 的呢?可以查看给定类型的文档来确认,不过作为一个通用的规则: **任何基本类型的组合可以是 `Copy` 的,不需要分配内存或某种形式资源的类型是 `Copy` 的**。如下是一些 `Copy` 的类型: +那么什么类型是可 `Copy` 的呢?可以查看给定类型的文档来确认,不过作为一个通用的规则:**任何基本类型的组合可以是 `Copy` 的,不需要分配内存或某种形式资源的类型是 `Copy` 的**。如下是一些 `Copy` 的类型: * 所有整数类型,比如 `u32`。 * 布尔类型,`bool`,它的值是 `true` 和 `false`。 @@ -297,5 +297,5 @@ fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用 ``` -所有权很强大,避免了内存的不安全性,但是也带来了一个新麻烦: **总是把一个值传来传去来使用它**。 传入一个函数,很可能还要从该函数传出去,结果就是语言表达变得非常啰嗦,幸运的是,Rust 提供了新功能解决这个问题。 +所有权很强大,避免了内存的不安全性,但是也带来了一个新麻烦:**总是把一个值传来传去来使用它**。 传入一个函数,很可能还要从该函数传出去,结果就是语言表达变得非常啰嗦,幸运的是,Rust 提供了新功能解决这个问题。 diff --git a/book/contents/basic/variable.md b/book/contents/basic/variable.md index 70894753..5709b254 100644 --- a/book/contents/basic/variable.md +++ b/book/contents/basic/variable.md @@ -117,7 +117,7 @@ fn main() { 变量的值不能更改可能让你想起其他另一个很多语言都有的编程概念:**常量**(*constant*)。与不可变变量一样,常量也是绑定到一个常量名且不允许更改的值,但是常量和变量之间存在一些差异: -- 常量不允许使用 `mut`。**常量不仅仅默认不可变,而且自始至终不可变**。 +- 常量不允许使用 `mut`。**常量不仅仅默认不可变,而且自始至终不可变**,因为常量在编译完成后,已经确定它的值。 - 常量使用 `const` 关键字而不是 `let` 关键字来声明,并且值的类型**必须**标注。 我们将在下一节[数据类型](./base-type/index.md)中介绍,因此现在暂时无需关心细节。 diff --git a/book/contents/tokio/frame.md b/book/contents/tokio/frame.md new file mode 100644 index 00000000..0486954d --- /dev/null +++ b/book/contents/tokio/frame.md @@ -0,0 +1,337 @@ +# 解析数据帧 +现在,鉴于大家已经掌握了 Tokio 的基本 I/O 用法,我们可以开始实现 `mini-redis` 的帧 `frame`。通过帧可以将字节流转换成帧组成的流。每个帧就是一个数据单元,例如客户端发送的一次请求就是一个帧。 +```rust +use bytes::Bytes; + +enum Frame { + Simple(String), + Error(String), + Integer(u64), + Bulk(Bytes), + Null, + Array(Vec), +} +``` + +可以看到帧除了数据之外,并不具备任何语义。命令解析和实现会在更高的层次进行(相比帧解析层)。我们再来通过 HTTP 的帧来帮大家加深下相关的理解: +```rust +enum HttpFrame { + RequestHead { + method: Method, + uri: Uri, + version: Version, + headers: HeaderMap, + }, + ResponseHead { + status: StatusCode, + version: Version, + headers: HeaderMap, + }, + BodyChunk { + chunk: Bytes, + }, +} +``` + +为了实现 `mini-redis` 的帧,我们需要一个 `Connection` 结构体,里面包含了一个 `TcpStream` 以及对帧进行读写的方法: +```rust +use tokio::net::TcpStream; +use mini_redis::{Frame, Result}; + +struct Connection { + stream: TcpStream, + // ... 这里定义其它字段 +} + +impl Connection { + /// 从连接读取一个帧 + /// + /// 如果遇到EOF,则返回 None + pub async fn read_frame(&mut self) + -> Result> + { + // 具体实现 + } + + /// 将帧写入到连接中 + pub async fn write_frame(&mut self, frame: &Frame) + -> Result<()> + { + // 具体实现 + } +} +``` + +关于Redis协议的说明,可以看看[官方文档](https://redis.io/topics/protocol),`Connection` 代码的完整实现见[这里](https://github.com/tokio-rs/mini-redis/blob/tutorial/src/connection.rs). + +## 缓冲读取(Buffered Read) +`read_frame` 方法会等到一个完整的帧都读取完毕后才返回,与之相比,它底层调用的`TcpStream::read` 只会返回任意多的数据(填满传入的缓冲区 buffer ),它可能返回帧的一部分、一个帧、多个帧,总之这种读取行为是不确定的。 + +当 `read_frame` 的底层调用 `TcpStream::read` 读取到部分帧时,会将数据先缓冲起来,接着继续等待并读取数据。如果读到多个帧,那第一个帧会被返回,然后剩下的数据依然被缓冲起来,等待下一次 `read_frame` 被调用。 + +为了实现这种功能,我们需要为 `Connection` 增加一个读取缓冲区。数据首先从 `socket` 中读取到缓冲区中,接着这些数据会被解析为帧,当一个帧被解析后,该帧对应的数据会从缓冲区被移除。 + +这里使用 [`BytesMut`](https://docs.rs/bytes/1/bytes/struct.BytesMut.html) 作为缓冲区类型,它是 [`Bytes`](https://docs.rs/bytes/1/bytes/struct.Bytes.html) 的可变版本。 + +```rust +use bytes::BytesMut; +use tokio::net::TcpStream; + +pub struct Connection { + stream: TcpStream, + buffer: BytesMut, +} + +impl Connection { + pub fn new(stream: TcpStream) -> Connection { + Connection { + stream, + // 分配一个缓冲区,具有4kb的缓冲长度 + buffer: BytesMut::with_capacity(4096), + } + } +} +``` + +接下来,实现 `read_frame` 方法: +```rust +use tokio::io::AsyncReadExt; +use bytes::Buf; +use mini_redis::Result; + +pub async fn read_frame(&mut self) + -> Result> +{ + loop { + // 尝试从缓冲区的数据中解析出一个数据帧, + // 只有当数据足够被解析时,才返回对应的帧 + if let Some(frame) = self.parse_frame()? { + return Ok(Some(frame)); + } + + // 如果缓冲区中的数据还不足以被解析为一个数据帧, + // 那么我们需要从 socket 中读取更多的数据 + // + // 读取成功时,会返回读取到的字节数,0 代表着读到了数据流的末尾 + if 0 == self.stream.read_buf(&mut self.buffer).await? { + // 代码能执行到这里,说明了对端关闭了连接, + // 需要看看缓冲区是否还有数据,若没有数据,说明所有数据成功被处理, + // 若还有数据,说明对端在发送帧的过程中断开了连接,导致只发送了部分数据 + if self.buffer.is_empty() { + return Ok(None); + } else { + return Err("connection reset by peer".into()); + } + } + } +} +``` + +`read_frame` 内部使用循环的方式读取数据,直到一个完整的帧被读取到时,才会返回。当然,当远程的对端关闭了连接后,也会返回。 + +#### `Buf` 特征 +在上面的 `read_frame` 方法中,我们使用了 `read_buf` 来读取 socket 中的数据,该方法的参数是来自 [`bytes`](https://docs.rs/bytes/) 包的 `BufMut`。 + +可以先来考虑下该如何使用 `read()` 和 `Vec` 来实现同样的功能 : +```rust +use tokio::net::TcpStream; + +pub struct Connection { + stream: TcpStream, + buffer: Vec, + cursor: usize, +} + +impl Connection { + pub fn new(stream: TcpStream) -> Connection { + Connection { + stream, + // 4kb 大小的缓冲区 + buffer: vec![0; 4096], + cursor: 0, + } + } +} +``` + +下面是相应的 `read_frame` 方法: +```rust +use mini_redis::{Frame, Result}; + +pub async fn read_frame(&mut self) + -> Result> +{ + loop { + if let Some(frame) = self.parse_frame()? { + return Ok(Some(frame)); + } + + // 确保缓冲区长度足够 + if self.buffer.len() == self.cursor { + // 若不够,需要增加缓冲区长度 + self.buffer.resize(self.cursor * 2, 0); + } + + // 从游标位置开始将数据读入缓冲区 + let n = self.stream.read( + &mut self.buffer[self.cursor..]).await?; + + if 0 == n { + if self.cursor == 0 { + return Ok(None); + } else { + return Err("connection reset by peer".into()); + } + } else { + // 更新游标位置 + self.cursor += n; + } + } +} +``` + +在这段代码中,我们使用了非常重要的技术:通过游标( cursor )跟踪已经读取的数据,并将下次读取的数据写入到游标之后的缓冲区中,只有这样才不会让新读取的数据将之前读取的数据覆盖掉。 + +一旦缓冲区满了,还需要增加缓冲区的长度,这样才能继续写入数据。还有一点值得注意,在 `parse_frame` 方法的内部实现中,也需要通过游标来解析数据: `self.buffer[..self.cursor]`,通过这种方式,我们可以准确获取到目前已经读取的全部数据。 + +在网络编程中,通过字节数组和游标的方式读取数据是非常普遍的,因此 `bytes` 包提供了一个 `Buf` 特征,如果一个类型可以被读取数据,那么该类型需要实现 `Buf` 特征。与之对应,当一个类型可以被写入数据时,它需要实现 `ButMut` 。 + +当 `T: BufMut` ( 特征约束,说明类型 `T` 实现了 `BufMut` 特征 ) 被传给 `read_buf()` 方法时,缓冲区 `T` 的内部游标会自动进行更新。正因为如此,在使用了 `BufMut` 版本的 `read_frame` 中,我们并不需要管理自己的游标。 + +除了游标之外,`Vec` 的使用也值得关注,该缓冲区在使用时必须要被初始化: `vec![0; 4096]`,该初始化会创建一个 4096 字节长度的数组,然后将数组的每个元素都填充上 0 。当缓冲区长度不足时,新创建的缓冲区数组依然会使用 0 被重新填充一遍。 事实上,这种初始化过程会存在一定的性能开销。 + +与 `Vec` 相反, `BytesMut` 和 `BufMut` 就没有这个问题,它们无需被初始化,而且 `BytesMut` 还会阻止我们读取未初始化的内存。 + +## 帧解析 +在理解了该如何读取数据后, 再来看看该如何通过两个部分解析出一个帧: + +- 确保有一个完整的帧已经被写入了缓冲区,找到该帧的最后一个字节所在的位置 +- 解析帧 + +```rust +use mini_redis::{Frame, Result}; +use mini_redis::frame::Error::Incomplete; +use bytes::Buf; +use std::io::Cursor; + +fn parse_frame(&mut self) + -> Result> +{ + // 创建 `T: Buf` 类型 + let mut buf = Cursor::new(&self.buffer[..]); + + // 检查是否读取了足够解析出一个帧的数据 + match Frame::check(&mut buf) { + Ok(_) => { + // 获取组成该帧的字节数 + let len = buf.position() as usize; + + // 在解析开始之前,重置内部的游标位置 + buf.set_position(0); + + // 解析帧 + let frame = Frame::parse(&mut buf)?; + + // 解析完成,将缓冲区该帧的数据移除 + self.buffer.advance(len); + + // 返回解析出的帧 + Ok(Some(frame)) + } + // 缓冲区的数据不足以解析出一个完整的帧 + Err(Incomplete) => Ok(None), + // 遇到一个错误 + Err(e) => Err(e.into()), + } +} +``` + +完整的 `Frame::check` 函数实现在[这里](https://github.com/tokio-rs/mini-redis/blob/tutorial/src/frame.rs#L63-L100),感兴趣的同学可以看看,在这里我们不会对它进行完整的介绍。 + +值得一提的是, `Frame::check` 使用了 `Buf` 的字节迭代风格的 API。例如,为了解析一个帧,首先需要检查它的第一个字节,该字节用于说明帧的类型。这种首字节检查是通过 `Buf::get_u8` 函数完成的,该函数会获取游标所在位置的字节,然后将游标位置向右移动一个字节。 + +## 缓冲写入(Buffered writes) +关于帧操作的另一个 API 是 `write_frame(frame)` 函数,它会将一个完整的帧写入到 socket 中。 每一次写入,都会触发一次或数次系统调用,当程序中有大量的连接和写入时,系统调用的开销将变得非常高昂,具体可以看看 SyllaDB 团队写过的一篇[性能调优文章](https://www.scylladb.com/2022/01/12/async-rust-in-practice-performance-pitfalls-profiling/)。 + +为了降低系统调用的次数,我们需要使用一个写入缓冲区,当写入一个帧时,首先会写入该缓冲区,然后等缓冲区数据足够多时,再集中将其中的数据写入到 socket 中,这样就将多次系统调用优化减少到一次。 + +还有,缓冲区也不总是能提升性能。 例如,考虑一个 `bulk` 帧(多个帧放在一起组成一个bulk,通过批量发送提升效率),该帧的特点就是:由于由多个帧组合而成,因此帧体数据可能会很大。因此我们不能将其帧体数据写入到缓冲区中,因此数据较大时,先写入缓冲区再写入 socket 会有较大的性能开销(实际上缓冲区就是为了批量写入,既然 bulk 已经是批量了,因此不使用缓冲区也很正常)。 + + +为了实现缓冲写,我们将使用 [`BufWriter`](https://docs.rs/tokio/1/tokio/io/struct.BufWriter.html) 结构体。该结构体实现了 `AsyncWrite` 特征,当 `write` 方法被调用时,不会直接写入到 socket 中,而是先写入到缓冲区中。当缓冲区被填满时,其中的内容会自动刷到(写入到)内部的 socket 中,然后再将缓冲区清空。当然,其中还存在某些优化,通过这些优化可以绕过缓冲区直接访问 socket。 + +由于篇幅有限,我们不会实现完整的 `write_frame` 函数,想要看完整代码可以访问[这里](https://github.com/tokio-rs/mini-redis/blob/tutorial/src/connection.rs#L159-L184)。 + +首先,更新下 `Connection` 的结构体: +```rust +use tokio::io::BufWriter; +use tokio::net::TcpStream; +use bytes::BytesMut; + +pub struct Connection { + stream: BufWriter, + buffer: BytesMut, +} + +impl Connection { + pub fn new(stream: TcpStream) -> Connection { + Connection { + stream: BufWriter::new(stream), + buffer: BytesMut::with_capacity(4096), + } + } +} +``` + +接着来实现 `write_frame` 函数: +```rust +use tokio::io::{self, AsyncWriteExt}; +use mini_redis::Frame; + +async fn write_frame(&mut self, frame: &Frame) + -> io::Result<()> +{ + match frame { + Frame::Simple(val) => { + self.stream.write_u8(b'+').await?; + self.stream.write_all(val.as_bytes()).await?; + self.stream.write_all(b"\r\n").await?; + } + Frame::Error(val) => { + self.stream.write_u8(b'-').await?; + self.stream.write_all(val.as_bytes()).await?; + self.stream.write_all(b"\r\n").await?; + } + Frame::Integer(val) => { + self.stream.write_u8(b':').await?; + self.write_decimal(*val).await?; + } + Frame::Null => { + self.stream.write_all(b"$-1\r\n").await?; + } + Frame::Bulk(val) => { + let len = val.len(); + + self.stream.write_u8(b'$').await?; + self.write_decimal(len as u64).await?; + self.stream.write_all(val).await?; + self.stream.write_all(b"\r\n").await?; + } + Frame::Array(_val) => unimplemented!(), + } + + self.stream.flush().await; + + Ok(()) +} +``` + +这里使用的方法由 `AsyncWriteExt` 提供,它们在 `TcpStream` 中也有对应的函数。但是在没有缓冲区的情况下最好避免使用这种逐字节的写入方式!不然,每写入几个字节就会触发一次系统调用,写完整个数据帧可能需要几十次系统调用,可以说是丧心病狂! + +- `write_u8` 写入一个字节 +- `write_all` 写入所有数据 +- `write_decimal`由 mini-redis 提供 + +在函数结束前,我们还额外的调用了一次 `self.stream.flush().await`,原因是缓冲区可能还存在数据,因此需要手动刷一次数据:`flush` 的调用会将缓冲区中剩余的数据立刻写入到 socket 中。 + +当然,当帧比较小的时候,每写一次帧就 flush 一次的模式性能开销会比较大,此时我们可以选择在 `Connection` 中实现 `flush` 函数,然后将等帧积累多个后,再一次性在 `Connection` 中进行 flush。当然,对于我们的例子来说,简洁性是非常重要的,因此选了将 `flush` 放入到 `write_frame` 中。 \ No newline at end of file diff --git a/exercise/exercises/tests/README.md b/exercise/exercises/tests/README.md index 27c6818d..d7bcb406 100644 --- a/exercise/exercises/tests/README.md +++ b/exercise/exercises/tests/README.md @@ -1,7 +1,7 @@ -# Tests +# 测试 -Going out of order from the book to cover tests -- many of the following exercises will ask you to make tests pass! +这次不按书本上的顺序介绍测试——接下来的很多练习都会要求你通过测试! -## Further information +## 更多信息 - [Writing Tests](https://doc.rust-lang.org/book/ch11-01-writing-tests.html) diff --git a/exercise/exercises/tests/tests1.rs b/exercise/exercises/tests/tests1.rs index 50586a19..820f5abc 100644 --- a/exercise/exercises/tests/tests1.rs +++ b/exercise/exercises/tests/tests1.rs @@ -1,10 +1,10 @@ // tests1.rs -// Tests are important to ensure that your code does what you think it should do. -// Tests can be run on this file with the following command: +// 测试对于确保代码实现了预期功能非常重要。 +// 可以用下面的命令对当前文件中的代码进行测试: // rustlings run tests1 -// This test has a problem with it -- make the test compile! Make the test -// pass! Make the test fail! Execute `rustlings hint tests1` for hints :) +// 关于测试还有个问题——如何成功编译测试、通过测试或者使测试失败? +// 执行 `rustlings hint tests1` 获取提示 :) // I AM NOT DONE diff --git a/exercise/exercises/tests/tests2.rs b/exercise/exercises/tests/tests2.rs index 0d981ad1..5617cb1a 100644 --- a/exercise/exercises/tests/tests2.rs +++ b/exercise/exercises/tests/tests2.rs @@ -1,6 +1,6 @@ // tests2.rs -// This test has a problem with it -- make the test compile! Make the test -// pass! Make the test fail! Execute `rustlings hint tests2` for hints :) +// 让测试能够编译然后通过测试和使测试失败! +// 执行 `rustlings hint tests2` 获取提示 :) // I AM NOT DONE diff --git a/exercise/exercises/tests/tests3.rs b/exercise/exercises/tests/tests3.rs index 3424f940..6800ff8c 100644 --- a/exercise/exercises/tests/tests3.rs +++ b/exercise/exercises/tests/tests3.rs @@ -1,8 +1,7 @@ // tests3.rs -// This test isn't testing our function -- make it do that in such a way that -// the test passes. Then write a second test that tests whether we get the result -// we expect to get when we call `is_even(5)`. -// Execute `rustlings hint tests3` for hints :) +// 这个测试不是在测试我们的函数——想些方法让它的返回值可以通过测试。 +// 在第二个测试判断调用 `is_even(5)` 是否得到了预期的结果。 +// 执行 `rustlings hint tests3` 获取提示 :) // I AM NOT DONE @@ -15,12 +14,12 @@ mod tests { use super::*; #[test] - fn is_true_when_even() { + fn is_true_when_even() {// 偶数将返回 true assert!(); } #[test] - fn is_false_when_odd() { + fn is_false_when_odd() {// 奇数将返回 false assert!(); } } diff --git a/exercise/info.toml b/exercise/info.toml index aa5e3474..71a9a4d8 100644 --- a/exercise/info.toml +++ b/exercise/info.toml @@ -612,30 +612,28 @@ name = "tests1" path = "exercises/tests/tests1.rs" mode = "test" hint = """ -You don't even need to write any code to test -- you can just test values and run that, even -though you wouldn't do that in real life :) `assert!` is a macro that needs an argument. -Depending on the value of the argument, `assert!` will do nothing (in which case the test will -pass) or `assert!` will panic (in which case the test will fail). So try giving different values -to `assert!` and see which ones compile, which ones pass, and which ones fail :)""" +你甚至不需要写任何用于测试的代码——直接填入某个值然后运行,即使在现实中这样做没什么意义 :) +`assert!` 是需要一个参数的宏。根据参数的值,`assert!` 可能什么也不会发生(这意味将通 +过测试)或者 `assert!` 引发了 panic (测试失败)。所以试着去给 `assert!` 赋予不同的值,看 +看哪些可以编译,哪些能够通过,哪些将造成失败 :)""" [[exercises]] name = "tests2" path = "exercises/tests/tests2.rs" mode = "test" hint = """ -Like the previous exercise, you don't need to write any code to get this test to compile and -run. `assert_eq!` is a macro that takes two arguments and compares them. Try giving it two -values that are equal! Try giving it two arguments that are different! Try giving it two values -that are of different types! Try switching which argument comes first and which comes second!""" +和前面的练习一样,你无需编写任何代码就可以编译和运行这个测试。 +`assert_eq!` 是一个接受两个参数并比较两者的宏。尝试给它两个相等、两个不同的 +值和两个不同类型的值!也不妨试试调换下两个参数的位置""" [[exercises]] name = "tests3" path = "exercises/tests/tests3.rs" mode = "test" hint = """ -You can call a function right where you're passing arguments to `assert!` -- so you could do -something like `assert!(having_fun())`. If you want to check that you indeed get false, you -can negate the result of what you're doing using `!`, like `assert!(!having_fun())`.""" +你可以在 `assert!` 接受参数的位置直接调用一个函数——所以你可以做一些类似于 +`assert!(having_fun())` 的事情。如果你想检查得到的值是不是 false ,可以 +用 `!` 来取反结果,例如 `assert!(!having_fun())`。""" # TEST 3