Merge branch 'sunface:main' into main

pull/355/head
tomoat 3 years ago committed by GitHub
commit 8d68d90c55
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -110,6 +110,7 @@
- [共享状态](tokio/shared-state.md)
- [消息传递](tokio/channels.md)
- [I/O](tokio/io.md)
- [解析数据帧](tokio/frame.md)
<!-- - [异步消息流](tokio/stream.md)) -->

@ -13,16 +13,16 @@ println!("{} {}", 1, 2); // => "1 2"
println!("{:04}", 42); // => "0042" with leading zeros
```
可以看到`println!`宏接受的是可变参数,第一个参数是一个字符串常量,它表示最终输出字符串的格式, 包含其中形如`{}`的符号是**占位符**, 会被`println!`后面的参数依次替换。
可以看到 `println!` 宏接受的是可变参数,第一个参数是一个字符串常量,它表示最终输出字符串的格式,包含其中形如 `{}` 的符号是**占位符**,会被 `println!` 后面的参数依次替换。
## `print!`, `println!`, `format!`
## `print!``println!``format!`
它们是 Rust 中用来格式化输出的三大金刚,用途如下:
- `print!`, 将格式化文本输出到标准输出,不带换行符
- `println!`, 同上,但是在行的末尾添加换行符
- `format!`, 将格式化文本输出到`String`字符串
- `print!` 将格式化文本输出到标准输出,不带换行符
- `println!` 同上,但是在行的末尾添加换行符
- `format!` 将格式化文本输出到 `String` 字符串
在实际项目中,最常用的是`println!`及`format!`,前者常用来调试输出,后者用来生成格式化的字符串:
在实际项目中,最常用的是 `println!``format!`,前者常用来调试输出,后者常用来生成格式化的字符串:
```rust
fn main() {
let s = "hello";
@ -33,14 +33,14 @@ 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")
```
@ -48,17 +48,17 @@ eprintln!("Error: Could not complete task")
它们仅应该被用于输出错误信息和进度信息,其它场景都应该使用 `print!` 系列。
## {} 与 {:?}
与其它语言常用的`%d`,`%s`不同Rust特立独行的选择了`{}`作为格式化占位符(说到这个有点想吐槽下Rust中自创的概念其实还挺多的真不知道该夸奖还是该吐槽- , -),事实证明,这种选择非常正确,它帮助用户减少了很多使用成本,你无需再为特定的类型选择特定的占位符,统一用`{}`来替代即可剩下的类型推导等细节只要交给Rust去做。
与其它语言常用的 `%d``%s` 不同Rust 特立独行地选择了 `{}` 作为格式化占位符(说到这个有点想吐槽下Rust 中自创的概念其实还挺多的,真不知道该夸奖还是该吐槽-,-),事实证明,这种选择非常正确,它帮助用户减少了很多使用成本,你无需再为特定的类型选择特定的占位符,统一用 `{}` 来替代即可,剩下的类型推导等细节只要交给 Rust 去做。
`{}`类似,`{:?}`也是占位符:
`{}` 类似,`{:?}` 也是占位符:
- `{}`适用于实现了`std::fmt::Display`特征的类型,用来以更优雅、更友好的方式格式化文本,例如展示给用户
- `{}` 适用于实现了 `std::fmt::Display` 特征的类型,用来以更优雅、更友好的方式格式化文本,例如展示给用户
- `{:?}` 适用于实现了 `std::fmt::Debug` 特征的类型,用于调试场景
其实两者的选择很简单,当你在写代码需要调试时,使用 `{:?}`,剩下的场景,选择 `{}`
#### `Debug` 特征
事实上,为了方便我们调试,大多数Rust类型都实现了`Debug`特征或者支持派生该特征:
事实上,为了方便我们调试,大多数 Rust 类型都实现了 `Debug` 特征或者支持派生该特征:
```rust
#[derive(Debug)]
struct Person {
@ -75,20 +75,23 @@ fn main() {
}
```
对于数值、字符串、数组,可以直接使用`{:?}`进行输出,但是对于结构体,需要[派生`Debug`](../appendix/derive.md)特征后,才能进行输出,总之很简单.
对于数值、字符串、数组,可以直接使用 `{:?}` 进行输出,但是对于结构体,需要[派生`Debug`](../appendix/derive.md)特征后,才能进行输出,总之很简单
#### `Display` 特征
与大部分类型实现了`Debug`不同,实现了`Display`特征的Rust类型并没有那么多往往需要我们自定义想要的格式化方式:
与大部分类型实现了 `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};
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` 特征
@ -97,7 +100,7 @@ println!("{}, {}, {},{}",i,s,v,p);
下面来一一看看这三种方式。
#### {:#?}
`{:#?}`与`{:?}`几乎一样,唯一的区别在于它能更优美输出内容:
`{:#?}` `{:?}` 几乎一样,唯一的区别在于它能更优美输出内容:
```console
// {:?}
[1, 2, 3],Person { name: "sunface", age: 18 }
@ -115,21 +118,28 @@ println!("{}, {}, {},{}",i,s,v,p);
因此对于 `Display` 不支持的类型,可以考虑使用 `{:#?}` 进行格式化,虽然理论上它更适合进行调试输出。
#### 为自定义类型实现 `Display` 特征
如果你的类型是定义在当前作用域中的,那么可以为其实现`Display`特征,即可用于格式化输出:
如果你的类型是定义在当前作用域中的,那么可以为其实现 `Display` 特征,即可用于格式化输出:
```rust
struct Person {
name: String,
age: u8
age: u8,
}
use std::fmt;
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);
}
```
@ -156,7 +166,7 @@ fn main() {
}
```
`Array`就是我们的`newtype`,它将想要格式化输出的`Vec`包裹在内,最后只要为`Arraw`实现`Display`特征,即可进行格式化输出:
`Array` 就是我们的 `newtype`,它将想要格式化输出的 `Vec` 包裹在内,最后只要为 `Array` 实现 `Display` 特征,即可进行格式化输出:
```console
数组是:[1, 2, 3]
```
@ -164,7 +174,7 @@ fn main() {
至此,关于 `{}``{:?}` 的内容已介绍完毕,下面让我们正式开始格式化输出的旅程。
## 指定位置参数
除了按照依次顺序使用值去替换占位符之外,还能让指定位置的参数去替换某个占位符,例如`{1}`,表示用第二个参数替换该占位符(索引从0开始):
除了按照依次顺序使用值去替换占位符之外,还能让指定位置的参数去替换某个占位符,例如 `{1}`,表示用第二个参数替换该占位符(索引从0开始)
```rust
fn main() {
println!("{}{}",1,2); // =>"12"
@ -191,7 +201,7 @@ println!("{abc} {1}", abc = "def", 2);
```
## 格式化参数
格式化输出,意味着对输出格式会有更多的要求,例如只输出浮点数的小数点后两位:
格式化输出,意味着对输出格式会有更多的要求,例如只输出浮点数的小数点后两位
```rust
fn main() {
let v = 3.1415926;
@ -207,10 +217,10 @@ fn main() {
接下来,让我们一起来看看 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 {}

@ -4,11 +4,11 @@
之前我们在[所有权章节](https://course.rs/basic/ownership/ownership.html#栈stack与堆heap)简单讲过堆栈的概念,这里再补充一些。
## Rust 中的堆栈
高级语言Python/Java等往往会弱化堆栈的概念但是要用好C/C++/Rust就必须对堆栈有深入的了解原因是两者的内存管理方式不同: 前者有GC垃圾回收机制 因此无需你去关心内存的细节。
高级语言 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
@ -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 高速缓存。 因此不要仅仅因为堆上性能不如栈这个印象,就总是优先选择栈,导致代码更复杂的实现。
## Box 的使用场景
由于`Box`是简单的封装,除了将值存储在堆上外,并没有其它性能上的损耗。而性能和功能往往是鱼和熊掌,因此`Box`相比其它智能指针,功能较为单一,可以在以下场景中使用它:
由于 `Box` 是简单的封装,除了将值存储在堆上外,并没有其它性能上的损耗。而性能和功能往往是鱼和熊掌,因此 `Box` 相比其它智能指针,功能较为单一,可以在以下场景中使用它
- 特意的将数据分配在堆上
- 数据较大时,又不想在转移所有权时进行数据拷贝
@ -45,7 +45,7 @@ fn foo(x: &str) -> String {
以上场景,我们在本章将一一讲解,后面车速较快,请系好安全带。
#### 使用 `Box<T>` 将数据存储在堆上
如果一个变量拥有一个数值`let a = 3`, 那变量`a`必然是存储在栈上的,那如果我们想要`a`的值存储在堆上就需要使用`Box<T>`:
如果一个变量拥有一个数值 `let a = 3`,那变量 `a` 必然是存储在栈上的,那如果我们想要 `a` 的值存储在堆上就需要使用 `Box<T>`
```rust
fn main() {
let a = Box::new(3);
@ -58,8 +58,8 @@ fn main() {
这样就可以创建一个智能指针指向了存储在堆上的 `3`,并且 `a` 持有了该指针。在本章的引言中,我们提到了智能指针往往都实现了 `Deref``Drop` 特征,因此:
- `println!`可以正常打印出`a`的值,是因为它隐式的调用了`Deref`对智能指针`a`进行了解引用
- 最后一行代码`let b = a + 1`报错,是因为在表达式中,我们无法自动隐式的执行`Deref`解引用操作, 你需要使用`*`操作符`let b = *a + 1`,来显式的进行解引用
- `println!` 可以正常打印出 `a` 的值,是因为它隐式地调用了 `Deref` 对智能指针 `a` 进行了解引用
- 最后一行代码 ` let b = a + 1` 报错,是因为在表达式中,我们无法自动隐式地执行 `Deref` 解引用操作,你需要使用 `*` 操作符 `let b = *a + 1`,来显式的进行解引用
- `a` 持有的智能指针将在作用结束(`main` 函数结束)时,被释放掉,这是因为 `Box<T>` 实现了 `Drop` 特征
以上的例子在实际代码中其实很少会存在因为将一个简单的值分配到堆上并没有太大的意义。将其分配在栈上由于寄存器、CPU 缓存的原因,它的性能将更好,而且代码可读性也更好。
@ -67,7 +67,7 @@ fn main() {
#### 避免栈上数据的拷贝
当栈上数据转移所有权时,实际上是把数据拷贝了一份,最终新旧变量各自拥有不同的数据,因此所有权并未转移。
而堆上则不然,底层数据并不会被拷贝,转移所有权仅仅是复制一份栈中的指针,再将新的指针赋予新的变量,然后让拥有旧指针的变量失效,最终完成了所有权的转移:
而堆上则不然,底层数据并不会被拷贝,转移所有权仅仅是复制一份栈中的指针,再将新的指针赋予新的变量,然后让拥有旧指针的变量失效,最终完成了所有权的转移
```rust
fn main() {
// 在栈上创建一个长度为1000的数组
@ -81,7 +81,7 @@ fn main() {
// 在堆上创建一个长度为1000的数组然后使用一个智能指针指向它
let arr = Box::new([0;1000]);
// 将堆上数组的所有权转移给arr1, 由于数据在堆上,因此仅仅拷贝了智能指针的结构体,底层数据并没有被拷贝
// 将堆上数组的所有权转移给 arr1由于数据在堆上,因此仅仅拷贝了智能指针的结构体,底层数据并没有被拷贝
// 所有权顺利转移给 arr1arr 不再拥有所有权
let arr1 = arr;
println!("{:?}",arr1.len());
@ -90,12 +90,12 @@ fn main() {
}
```
从以上代码,可以清晰看出大块的数据为何应该放入堆中,此时`Box`就成为了我们最好的帮手.
从以上代码,可以清晰看出大块的数据为何应该放入堆中,此时 `Box` 就成为了我们最好的帮手。
#### 将动态大小类型变为 Sized 固定大小类型
Rust需要在编译时知道类型占用多少空间, 如果一种类型在编译时无法知道具体的大小那么被称为动态大小类型DST。
Rust 需要在编译时知道类型占用多少空间,如果一种类型在编译时无法知道具体的大小,那么被称为动态大小类型 DST。
其中一种无法在编译时知道大小的类型是**递归类型**:在类型定义中又使用到了自身,或者说该类型的值的一部分可以是相同类型的其它值,这种值的嵌套理论上可以无限进行下去,所以Rust不知道递归类型需要多少空间:
其中一种无法在编译时知道大小的类型是**递归类型**:在类型定义中又使用到了自身,或者说该类型的值的一部分可以是相同类型的其它值,这种值的嵌套理论上可以无限进行下去,所以 Rust 不知道递归类型需要多少空间:
```rust
enum List {
Cons(i32, List),
@ -103,7 +103,7 @@ enum List {
}
```
以上就是函数式语言中常见的`Cons List`,它的每个节点包含一个`i32`值,还包含了一个新的`List`因此这种嵌套可以无限进行下去然后Rust认为该类型是一个DST类型并给予报错:
以上就是函数式语言中常见的 `Cons List`,它的每个节点包含一个 `i32` 值,还包含了一个新的 `List`因此这种嵌套可以无限进行下去Rust 认为该类型是一个 DST 类型,并给予报错:
```console
error[E0072]: recursive type `List` has infinite size //递归类型 `List` 拥有无限长的大小
--> src/main.rs:3:1
@ -114,7 +114,7 @@ error[E0072]: recursive type `List` has infinite size //递归类型`List`拥有
| ---- recursive without indirection
```
此时若想解决这个问题,就可以使用我们的`Box<T>`:
此时若想解决这个问题,就可以使用我们的 `Box<T>`
```rust
enum List {
Cons(i32, Box<List>),
@ -122,7 +122,7 @@ enum List {
}
```
只需要将`List`存储到堆上,然后使用一个智能指针指向它,即可完成从DST到Sized类型(固定大小类型)的华丽转变.
只需要将 `List` 存储到堆上,然后使用一个智能指针指向它,即可完成从 DST 到 Sized 类型(固定大小类型)的华丽转变。
#### 特征对象
在 Rust 中,想实现不同类型组成的数组只有两个办法:枚举和特征对象,前者限制较多,因此后者往往是最常用的解决办法。
@ -133,7 +133,7 @@ trait Draw {
}
struct Button {
id: u32
id: u32,
}
impl Draw for Button {
fn draw(&self) {
@ -142,7 +142,7 @@ impl Draw for Button {
}
struct Select {
id: u32
id: u32,
}
impl Draw for Select {
@ -152,10 +152,7 @@ impl Draw for Select {
}
fn main() {
let elems: Vec<Box<dyn Draw>> = vec![
Box::new(Button{id: 1}),
Box::new(Select{id: 2})
];
let elems: Vec<Box<dyn Draw>> = vec![Box::new(Button { id: 1 }), Box::new(Select { id: 2 })];
for e in elems {
e.draw()
@ -165,10 +162,10 @@ fn main() {
以上代码将不同类型的 `Button``Select` 包装成 `Draw` 特征的特征对象,放入一个数组中,`Box<dyn Draw>` 就是特征对象。
其实,特征也是DST类型而特征对象在做的也是将DST类型转换为固定大小类型。
其实,特征也是 DST 类型,而特征对象在做的就是将 DST 类型转换为固定大小类型。
## Box 内存布局
先来看看`Vec<i32>`的内存布局:
先来看看 `Vec<i32>` 的内存布局:
```rust
(stack) (heap)
┌──────┐ ┌───┐
@ -184,7 +181,7 @@ fn main() {
之前提到过 `Vec``String` 都是智能指针,从上图可以看出,该智能指针存储在栈中,然后指向堆上的数组数据。
那如果数组中每个元素都是一个`Box`对象呢?来看看`Vec<Box<i32>>`的内存布局:
那如果数组中每个元素都是一个 `Box` 对象呢?来看看 `Vec<Box<i32>>` 的内存布局:
```rust
(stack) (heap) ┌───┐
┌──────┐ ┌───┐ ┌─→│ 1 │
@ -204,7 +201,7 @@ fn main() {
可以看出智能指针 `vec2` 依然是存储在栈上,然后指针指向一个堆上的数组,该数组中每个元素都是一个 `Box` 智能指针,最终 `Box` 智能指针又指向了存储在堆上的实际值。
因此当我们从数组中取出某个元素时,取到的是对应的智能指针`Box`,需要对该智能指针进行解引用,才能取出最终的值:
因此当我们从数组中取出某个元素时,取到的是对应的智能指针 `Box`,需要对该智能指针进行解引用,才能取出最终的值
```rust
fn main() {
let arr = vec![Box::new(1), Box::new(2)];
@ -213,7 +210,7 @@ fn main() {
}
```
以上代码有几个值得注意的点:
以上代码有几个值得注意的点
- 使用 `&` 借用数组中的元素,否则会报所有权错误
- 表达式不能隐式的解引用,因此必须使用 `**` 做两次解引用,第一次将 `&Box<i32>` 类型转成 `Box<i32>`,第二次将 `Box<i32>` 转成 `i32`
@ -222,7 +219,7 @@ fn main() {
## Box::leak
`Box` 中还提供了一个非常有用的关联函数:`Box::leak`,它可以消费掉 `Box` 并且强制目标值从内存中泄漏,读者可能会觉得,这有啥用啊?
其实还真有点用,例如,你可以把一个`String`类型,变成一个`'static`生命周期的`&str`类型:
其实还真有点用,例如,你可以把一个 `String` 类型,变成一个 `'static` 生命周期的 `&str` 类型:
```rust
fn main() {
let s = gen_static_str();
@ -239,18 +236,18 @@ fn gen_static_str() -> &'static str{
在之前的代码中,如果 `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`
#### 使用场景
光看上面的描述,大家可能还是云里雾里、一头雾水。
那么我说一个简单的场景,**你需要一个在运行期初始化的值,但是可以全局有效,也就是和整个程序活得一样久**, 那么久可以使用`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**它只是在帮我们成为那个更好的程序员,而这些苦难终究成为我们走向优秀的垫脚石**。

@ -1,7 +1,7 @@
# Deref 解引用
何为智能指针?能不让你写出&&&&&&s形式的解引用我认为就是智能: ) 智能指针的名称来源,主要就在于它实现了`Deref`和`Drop`特征,这两个特征可以智能地帮助我们节省使用上的负担:
何为智能指针?能不让你写出 &&&&&&s 形式的解引用,我认为就是智能: )智能指针的名称来源,主要就在于它实现了 `Deref` `Drop` 特征,这两个特征可以智能地帮助我们节省使用上的负担
- `Deref`可以让智能指针像引用那样工作,这样你就可以写出同时支持智能指针和引用的代码, 例如`&T`
- `Deref` 可以让智能指针像引用那样工作,这样你就可以写出同时支持智能指针和引用的代码,例如 `&T`
- `Drop` 允许你指定智能指针超出作用域后自动执行的代码,例如做一些数据清除等收尾工作
下面先来看看 `Deref` 特征是如何工作的。
@ -10,7 +10,7 @@
在正式讲解 `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
@ -36,7 +36,7 @@ error[E0277]: can't compare `{integer}` with `&{integer}` //无法将{integer}
## 智能指针解引用
上面所说的解引用方式和其它大多数语言并无区别,但是 Rust 中将解引用提升到了一个新高度。考虑一下智能指针,它是一个结构体类型,如果你直接对它进行 `*myStruct`,显然编译器不知道该如何办,因此我们可以为智能指针结构体实现 `Deref` 特征。
实现`Deref`后的智能指针结构体,就可以像普通引用一样,通过`*`进行解引用,例如`Box<T>`智能指针:
实现 `Deref` 后的智能指针结构体,就可以像普通引用一样,通过 `*` 进行解引用,例如 `Box<T>` 智能指针:
```rust
fn main() {
let x = Box::new(1);
@ -59,7 +59,7 @@ impl<T> MyBox<T> {
}
```
`Box<T>`一样,我们的智能指针也持有一个`T`类型的值,然后使用关联函数`MyBox::new`来创建智能指针。由于还未实现`Deref`特征,此时使用`*`肯定会报错:
`Box<T>` 一样,我们的智能指针也持有一个 `T` 类型的值,然后使用关联函数 `MyBox::new` 来创建智能指针。由于还未实现 `Deref` 特征,此时使用 `*` 肯定会报错:
```rust
fn main() {
let y = MyBox::new(5);
@ -78,7 +78,7 @@ error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
```
##### 为智能指针实现 Deref 特征
现在来为`MyBox`实现`Deref`特征, 以支持`*`解引用操作符:
现在来为 `MyBox` 实现 `Deref` 特征,以支持 `*` 解引用操作符:
```rust
use std::ops::Deref;
@ -91,28 +91,28 @@ impl<T> Deref for MyBox<T> {
}
```
很简单,当解引用`MyBox`智能指针时,返回元组结构体中的元素`&self.0`, 有几点要注意的:
很简单,当解引用 `MyBox` 智能指针时,返回元组结构体中的元素 `&self.0`有几点要注意的:
- 为了可读性, 我们声明了关联类型`Target`
- 为了可读性,我们声明了关联类型 `Target`
- `deref` 返回的是一个常规引用,可以被 `*` 进行解引用
之前报错的代码此时已能顺利编译通过。当然,标准库实现的智能指针要考虑很多边边角角情况,肯定比我们的实现要复杂。
## `*` 背后的原理
当我们对智能指针`Box`进行解引用时, 实际上Rust为我们调用了以下方法:
当我们对智能指针 `Box` 进行解引用时,实际上 Rust 为我们调用了以下方法:
```rust
*(y.deref())
```
首先调用 `deref` 方法返回值的常规引用,然后通过 `*` 对常规引用进行解引用,最终获取到目标值。
至于Rust为何要使用这个有点啰嗦的方式实现原因是因为所有权系统的存在。如果`deref`方法直接返回一个值,而不是引用,那么该值的所有权将被转移给调用者,而我们不希望调用者仅仅只是`*T`一下,就拿走了智能指针中包含的值。
至于 Rust 为何要使用这个有点啰嗦的方式实现,原因在于所有权系统的存在。如果 `deref` 方法直接返回一个值,而不是引用,那么该值的所有权将被转移给调用者,而我们不希望调用者仅仅只是 `*T` 一下,就拿走了智能指针中包含的值。
需要注意的是,`*` 不会无限递归替换,从 `*y``*(y.deref())` 只会发生一次,而不会继续进行替换然后产生形如 `*((y.deref()).deref())` 的怪物。
## 函数和方法中的隐式 Deref 转换
在函数和方法中Rust提供了一个极其有用的隐式转换`Deref`转换。简单来说,当一个实现了`Deref`特征的值被传给函数或方法时,会根据函数参数的要求,来决定使用该值原本的类型还是`Deref`后的类型,例如:
在函数和方法中Rust 提供了一个极其有用的隐式转换:`Deref `转换。简单来说,当一个实现了 `Deref` 特征的值被传给函数或方法时,会根据函数参数的要求,来决定使用该值原本的类型还是 `Deref` 后的类型,例如:
```rust
fn main() {
let s = String::from("hello world");
@ -128,10 +128,10 @@ fn display(s: &str) {
- `String` 实现了 `Deref` 特征,能被转换成一个 `&str`
- `s` 是一个 `String` 类型,当它被传给 `display` 函数时,自动通过 `Deref` 转换成了 `&str`
- 必须使用`&s`的方式来触发`Deref`
- 必须使用 `&s` 的方式来触发 `Deref`(`&s` 相当于调用 `s``deref` 方法)
#### 连续的隐式 Deref 转换
如果你以为`Deref`仅仅这点作用,那就大错特错了。`Deref`可以支持连续的隐式转换,直到找到适合的形式为止:
如果你以为 `Deref` 仅仅这点作用,那就大错特错了。`Deref` 可以支持连续的隐式转换,直到找到适合的形式为止
```rust
fn main() {
let s = MyBox::new(String::from("hello world"));
@ -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` 特征的知识,下面来通过较为正式的方式来对其规则进行下总结。
一个类型为 `T` 的对象 `foo`,如果 `T: Deref<Target=U>`,那么,相关 `foo` 的引用 `&foo` 在应用的时候会自动转换为 `&U`
粗看这条规则,貌似有点类似于`AsRef`,而跟`解引用`似乎风马牛不相及, 实际里面有些玄妙之处。
粗看这条规则,貌似有点类似于 `AsRef`,而跟 `解引用` 似乎风马牛不相及,实际里面有些玄妙之处。
#### 引用归一化
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<T: ?Sized> Deref for &T {
type Target = T;
@ -195,7 +195,7 @@ impl<T: ?Sized> Deref for &T {
}
```
在这段源码中,`&T` 被自动解引用为 `T` , 也就是 `&T: Deref<Target=T>` 。 按照这个代码,`&&&&T` 会被自动解引用为 `&&&T`, 然后再自动解引用为 `&&T`,以此类推, 直到最终变成 `&T`
在这段源码中,`&T` 被自动解引用为 `T`也就是 `&T: Deref<Target=T>` 。 按照这个代码,`&&&&T` 会被自动解引用为 `&&&T`然后再自动解引用为 `&&T`,以此类推, 直到最终变成 `&T`
PS: 以下是 `LLVM` 编译后的部分中间层代码:
```rust
@ -215,7 +215,7 @@ bb0: {
// 由于 String 实现了 Deref<Target=str>
let owned = "Hello".to_string();
// 因此下面的函数可以正常运行:
// 因此下面的函数可以正常运行
foo(&owned);
```
@ -258,14 +258,12 @@ bb0: {
来看一个关于 `DerefMut` 的例子:
```rust
struct MyBox<T> {
v: T
v: T,
}
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox{
v: x
}
MyBox { v: x }
}
}
@ -300,8 +298,8 @@ fn display(s: &mut String) {
以上代码有几点值得注意:
- 要实现`DerefMut`必须要先实现`Deref`特征: `pub trait DerefMut: Deref`
- `T: DerefMut<Target=U>`解读:将`&mut T`类型通过`DerefMut`特征的方法转换为`&mut U`类型,对应上例中,就是将`&mut MyBox<String>`转换为`&mut String`
- 要实现 `DerefMut` 必须要先实现 `Deref` 特征:`pub trait DerefMut: Deref`
- `T: DerefMut<Target=U>` 解读:将 `&mut T` 类型通过 `DerefMut` 特征的方法转换为 `&mut U` 类型,对应上例中,就是将 `&mut MyBox<String>` 转换为 `&mut String`
对于上述三条规则中的第三条它比另外两条稍微复杂了点Rust 可以把可变引用隐式的转换成不可变引用,但反之则不行。
@ -311,4 +309,4 @@ fn display(s: &mut String) {
## 总结
`Deref` 可以说是 Rust 中最常见的隐式类型转换,而且它可以连续的实现如 `Box<String> -> String -> &str` 的隐式转换,只要链条上的类型实现了 `Deref` 特征。
我们也可以为自己的类型实现`Deref`特征, 但是原则上来说,只应该为自定义的智能指针实现`Deref`。例如,虽然你可以为自己的自定义数组类型实现`Deref`以避免`myArr.0[0]`的使用形式但是Rust官方并不推荐这么做特别是在你开发三方库时。
我们也可以为自己的类型实现 `Deref` 特征,但是原则上来说,只应该为自定义的智能指针实现 `Deref`。例如,虽然你可以为自己的自定义数组类型实现 `Deref` 以避免 `myArr.0[0]` 的使用形式,但是 Rust 官方并不推荐这么做,特别是在你开发三方库时。

@ -126,8 +126,8 @@ fn main() {
42.0_f32,
];
// 打印数组中第一个值,其中控制小数位为2位
println!("{:02}", forty_twos[0]);
// 打印数组中第一个值,并控制小数位为2位
println!("{:.2}", forty_twos[0]);
}
```

@ -188,7 +188,7 @@ 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` 中。那么具体发生了什么,用一张图简单说明:
<img alt="s1 moved to s2" src="/img/ownership01.svg" class="center" style="width: 50%;" />

@ -117,7 +117,7 @@ fn main() {
变量的值不能更改可能让你想起其他另一个很多语言都有的编程概念:**常量***constant*)。与不可变变量一样,常量也是绑定到一个常量名且不允许更改的值,但是常量和变量之间存在一些差异:
- 常量不允许使用 `mut`。**常量不仅仅默认不可变,而且自始至终不可变**。
- 常量不允许使用 `mut`。**常量不仅仅默认不可变,而且自始至终不可变**,因为常量在编译完成后,已经确定它的值
- 常量使用 `const` 关键字而不是 `let` 关键字来声明,并且值的类型**必须**标注。
我们将在下一节[数据类型](./base-type/index.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<Frame>),
}
```
可以看到帧除了数据之外,并不具备任何语义。命令解析和实现会在更高的层次进行(相比帧解析层)。我们再来通过 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<Option<Frame>>
{
// 具体实现
}
/// 将帧写入到连接中
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<Option<Frame>>
{
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<u8>` 来实现同样的功能 :
```rust
use tokio::net::TcpStream;
pub struct Connection {
stream: TcpStream,
buffer: Vec<u8>,
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<Option<Frame>>
{
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<u8>` 的使用也值得关注,该缓冲区在使用时必须要被初始化: `vec![0; 4096]`,该初始化会创建一个 4096 字节长度的数组,然后将数组的每个元素都填充上 0 。当缓冲区长度不足时,新创建的缓冲区数组依然会使用 0 被重新填充一遍。 事实上,这种初始化过程会存在一定的性能开销。
`Vec<u8>` 相反, `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<Option<Frame>>
{
// 创建 `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<TcpStream>,
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` 中。

@ -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)

@ -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

@ -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

@ -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!();
}
}

@ -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

Loading…
Cancel
Save