pull/237/head
sunface 3 years ago
commit f3e1c2cfdd

@ -2,11 +2,11 @@
和动态数组一样,`HashMap` 也是 Rust 标准库中提供的集合类型,但是又与动态数组不同,`HashMap` 中存储的是一一映射的 `KV `键值对,并提供了平均复杂度为 `O(1)` 的查询方法,当我们希望通过一个 `Key` 去查询值时,该类型非常有用,以致于 `Go语言` 将该类型设置成了语言级别的内置特性。
Rust中哈希类型为`HashMap<K,V>`, 在其它语言中,也有类似的数据结构,例如`hash map``map``object`,`hash table`,`字典`等等,引用小品演员孙涛的一句台词:大家都是本地狐狸,别搁那装貂:)。
Rust 中哈希类型(哈希映射)为 `HashMap<K,V>`,在其它语言中,也有类似的数据结构,例如 `hash map``map``object``hash table``字典`等等,引用小品演员孙涛的一句台词:大家都是本地狐狸,别搁那装貂 :)。
## 创建HashMap
跟创建动态数组`Vec`的方法类似, 可以使用`new`方法来创建`HashMap`,然后通过`insert`方法插入键值对.
跟创建动态数组 `Vec` 的方法类似,可以使用 `new` 方法来创建` HashMap`,然后通过` insert` 方法插入键值对。
#### 使用new方法创建
```rust
@ -21,20 +21,20 @@ my_gems.insert("蓝宝石", 2);
my_gems.insert("河边捡的误以为是宝石的破石头", 18);
```
很简单对吧?跟其它语言没有区别,聪明的同学甚至能够猜到该`HashMap`的类型:`HashMap<&str,i32>`.
很简单对吧?跟其它语言没有区别,聪明的同学甚至能够猜到该 `HashMap` 的类型: `HashMap<&str,i32>`
但是还有一点,你可能没有注意,那就是使用`HashMap`需要手动通过`use ...`从标准库中引入到我们当前的作用域中来,仔细回忆下,之前使用另外两个集合类型`String`和`Vec`时,我们是否有手动引用过?答案是`No`, 因为`HashMap`并没有包含在Rust的[`prelude`](../../appendix/prelude.md)中(Rust为了简化用户使用提前将最常用的类型自动引入到作用域中)。
但是还有一点,你可能没有注意,那就是使用 `HashMap` 需要手动通过 `use ...` 从标准库中引入到我们当前的作用域中来,仔细回忆下,之前使用另外两个集合类型 `String` 和` Vec` 时,我们是否有手动引用过?答案是 `No`,因为 `HashMap` 并没有包含在Rust的[`prelude`](../../appendix/prelude.md)中(Rust为了简化用户使用提前将最常用的类型自动引入到作用域中)。
所有的集合类型都是动态的,意味着它们没有固定的内存大小,因此它们底层的数据都存储在内存堆上,然后通过一个存储在栈中的引用类型来访问。同时,跟其它集合类型一致,`HashMap`也是内聚性的, 即所有的`K`必须拥有同样的类型,`V`也是如此。
所有的集合类型都是动态的,意味着它们没有固定的内存大小,因此它们底层的数据都存储在内存堆上,然后通过一个存储在栈中的引用类型来访问。同时,跟其它集合类型一致,`HashMap` 也是内聚性的,即所有的`K`必须拥有同样的类型,`V`也是如此。
> 跟Vec一样如果预先知道要存储的KV对个数可以使用 `HashMap::with_capacity(capacity)` 创建指定大小的HashMap避免频繁的内存分配和拷贝提升性能
#### 使用迭代器和collect方法创建
在实际使用中,不是所有的场景都能`new`一个哈希表后,然后悠哉悠哉的依次插入对应的键值对, 而是可能会从另外一个数据结构中,获取到对应的数据,最终生成`HashMap`.
在实际使用中,不是所有的场景都能 `new` 一个哈希表后,然后悠哉悠哉的依次插入对应的键值对,而是可能会从另外一个数据结构中,获取到对应的数据,最终生成 `HashMap`
例如考虑一个场景有一张表格中记录了足球联赛中各队伍名称和积分的信息这张表如果被导入到Rust项目中一个合理的数据结构是 `Vec<(String,u32)>` 类型,该数组中的元素是一个个元组,该数据结构跟表格数据非常契合:表格中的数据都是逐行存储,每一个行都存有一个 `(队伍名称,积分)` 的信息。
但是在很多时候,又需要通过队伍名称来查询对应的积分,此时动态数组又不适用了,因此可以用`HashMap`来保存相关的**队伍名称 -> 积分**映射关系。 理想很骨感,现实很丰满,如果将`Vec<(String, u32)>`中的数据快速写入到`HashMap<String, u32>`中?
但是在很多时候,又需要通过队伍名称来查询对应的积分,此时动态数组就不适用了,因此可以用 `HashMap` 来保存相关的**队伍名称 -> 积分**映射关系。 理想很骨感,现实很丰满,如何将 `Vec<(String, u32)>` 中的数据快速写入到`HashMap<String, u32>`中?
一个动动脚趾头就能想到的笨方法如下:
```rust
@ -56,9 +56,9 @@ fn main() {
}
```
遍历列表,将每一个元组作为一对`KV`插入到`HashMap`中,很简单,但是。。。也不太聪明的样子, 换个词说就是 - 不够`rusty`.
遍历列表,将每一个元组作为一对 `KV `插入到 `HashMap` 中,很简单,但是。。。也不太聪明的样子,换个词说就是 - 不够`rusty`。
好在Rust为我们提供了一个非常精妙的解决办法先将`Vec`转为迭代器,接着通过`collect`方法,将迭代器中的元素收集后,转成`HashMap`:
好在Rust为我们提供了一个非常精妙的解决办法先将 `Vec` 转为迭代器,接着通过 `collect` 方法,将迭代器中的元素收集后,转成 `HashMap`
```rust
fn main() {
use std::collections::HashMap;
@ -75,9 +75,9 @@ fn main() {
}
```
代码很简单,`into_iter`方法将列表转为迭代器,接着通过`collect`进行收集,不过需要注意的是,`collect`方法在内部实际上支持生成多种类型的目标集合,因为我们需要通过类型标注`HashMap<_,_>`来告诉编译器:请帮我们收集为`HashMap`集合类型,具体的`KV`类型,麻烦编译器你老人家帮我们推导。
代码很简单,`into_iter` 方法将列表转为迭代器,接着通过 `collect` 进行收集,不过需要注意的是,`collect` 方法在内部实际上支持生成多种类型的目标集合,因为我们需要通过类型标注 `HashMap<_,_>` 来告诉编译器:请帮我们收集为 `HashMap` 集合类型,具体的 `KV` 类型,麻烦编译器您老人家帮我们推导。
由此可见Rust中的编译器时而小聪明时而大聪明不过好在它大聪明的时候会自家人知道自己事总归会通知你一声:
由此可见Rust中的编译器时而小聪明时而大聪明不过好在它大聪明的时候会自家人知道自己事总归会通知你一声
```console
error[E0282]: type annotations needed 需要类型标注
--> src/main.rs:10:9
@ -88,8 +88,8 @@ error[E0282]: type annotations needed 需要类型标注
## 所有权转移
`HashMap`的所有权规则与其它Rust类型没有区别:
- 若类型实现`Copy`特征,该类型会被复制进`HashMap`, 因此无所谓所有权
`HashMap` 的所有权规则与其它 Rust 类型没有区别:
- 若类型实现 `Copy` 特征,该类型会被复制进 `HashMap`因此无所谓所有权
- 若没实现 `Copy` 特征,所有权将被转移给`HashMap`中
例如我参选帅气男孩时的场景再现:
@ -123,10 +123,10 @@ error[E0382]: borrow of moved value: `name`
| ^^^^ value borrowed here after move
```
提示很清晰,`name`是`String`类型,因此它受到所有权的限制,在`insert`时,它的所有权被转移给`handsome_boys`,最后在使用时,无情但是意料之中的报错。
提示很清晰,`name` `String` 类型,因此它受到所有权的限制,在 `insert` 时,它的所有权被转移给 `handsome_boys`所以最后在使用时,会遇到这个无情但是意料之中的报错。
**如果你使用引用类型放入HashMap中**, 请确保该类型至少跟`HashMap`获得一样久:
**如果你使用引用类型放入HashMap中**,请确保该引用的生命周期至少跟 `HashMap` 活得一样久:
```rust
fn main() {
use std::collections::HashMap;
@ -187,7 +187,7 @@ for (key, value) in &scores {
println!("{}: {}", key, value);
}
```
最终输出:
最终输出
```console
Yellow: 50
Blue: 10
@ -240,9 +240,9 @@ for word in text.split_whitespace() {
println!("{:?}", map);
```
上面代码中,新建一个`map`用于保存词语出现的次数插入一个词语时会进行判断若之前没有插入过则使用该词语作Key,插入次数0若之前插入过则取出之前统计的该词语出现的次数。 最后,对该词语出现的次数进行加一。
上面代码中,新建一个 `map` 用于保存词语出现的次数插入一个词语时会进行判断若之前没有插入过则使用该词语作Key插入次数0作为Value若之前插入过则取出之前统计的该词语出现的次数对其加一。
有两点值得注意:
有两点值得注意
- `or_insert` 返回了 `&mut v` 引用,因此可以通过该可变引用直接修改 `map` 中对应的值
- 使用 `count` 引用时,需要先进行解引用 `*count`,否则会出现类型不匹配
@ -250,19 +250,19 @@ println!("{:?}", map);
## 哈希函数
你肯定比较好奇,为何叫哈希表,到底什么是哈希。
先来设想下,如果要实现`Key`与`Value`的一一对应,是不是意味着我们要能比较两个`Key`的相等性?例如"a"和"b",1和2,当这些做Key且能比较时可以很容易知道1对应的值不会错误的映射到2上因为`1`不等于`2`. 因此,一个类型能否作为`Key`的关键就是是否能进行相等比较, 或者说该类型是否实现了`std::cmp::Eq`特征。
先来设想下,如果要实现 `Key` `Value` 的一一对应,是不是意味着我们要能比较两个 `Key` 的相等性?例如"a"和"b"1和2当这些类型做Key且能比较时可以很容易知道 `1` 对应的值不会错误的映射到 `2` 上,因为 `1` 不等于 `2`。因此,一个类型能否作为 `Key` 的关键就是是否能进行相等比较,或者说该类型是否实现了 `std::cmp::Eq` 特征。
> f32和f64浮点数没有实现`std::cmp::Eq`特征因此不可以用作HashMap的Key
> f32和f64浮点数没有实现 `std::cmp::Eq` 特征,因此不可以用作 `HashMap` `Key`
好了理解完这个再来设想一点若一个复杂点的类型作为Key那怎么在底层对它进行存储怎么使用它进行查询和比较 是不是很棘手?好在我们有哈希函数:通过它把 `Key` 计算后映射为哈希值,然后使用该哈希值来进行存储、查询、比较等操作。
但是问题又来了,如何保证不同`Key`通过哈希后的两个值不会相同如果相同那意味着我们使用不同的Key却查到了同一个结果这种明显是错误的行为。
但是问题又来了,如何保证不同 `Key` 通过哈希后的两个值不会相同?如果相同,那意味着我们使用不同的 `Key`,却查到了同一个结果,这种明显是错误的行为。
此时,就涉及到安全性跟性能的取舍了。
若要追求安全,尽可能减少冲突,同时防止拒绝服务(Denial of Service, DoS)攻击,就要使用密码学安全的哈希函数,`HashMap` 就是使用了这样的哈希函数。反之若要追求性能,就需要使用没有那么安全的算法。
#### 高性能三方库
因此若性能测试显示当前标准库默认的哈希函数不能满足你的性能需求,就需要去[`crates.io`](https://crates.io)上寻找其它的哈希函数实现, 使用方法很简单:
因此若性能测试显示当前标准库默认的哈希函数不能满足你的性能需求,就需要去[`crates.io`](https://crates.io)上寻找其它的哈希函数实现使用方法很简单:
```rust
use std::hash::BuildHasherDefault;
use std::collections::HashMap;
@ -275,6 +275,6 @@ hash.insert(42, "the answer");
assert_eq!(hash.get(&42), Some(&"the answer"));
```
> 目前,`HashMap`使用的哈希函数是`SipHash`,它的性能不是很高,但是安全性很高。`SipHash`在中等大小的key上性能相当不错但是对于小型的key(例如整数)或者大型key(例如字符串)来说,性能还是不够好。若你需要极致性能,例如实现算法,可以考虑这个库:[ahash](https://github.com/tkaitchuck/ahash)
> 目前,`HashMap` 使用的哈希函数是 `SipHash`,它的性能不是很高,但是安全性很高。`SipHash` 在中等大小的 `Key` 上,性能相当不错,但是对于小型的 `Key` (例如整数)或者大型 `Key` (例如字符串)来说,性能还是不够好。若你需要极致性能,例如实现算法,可以考虑这个库:[ahash](https://github.com/tkaitchuck/ahash)
最后,如果你想要了解 `HashMap` 更多的用法,请参见本书的标准库解析章节:[HashMap常用方法](../../std/hashmap.md)

@ -17,22 +17,22 @@ fn main() {
能跟着这本书一直学习到这里,说明你对 Rust 已经有了一定的理解,那么一眼就能看出这段代码注定会报错,因为 `a``b` 拥有不同的类型Rust 不允许两种不同的类型进行比较。
解决办法很简单,只要把`b`转换成`i32`类型即可,这里使用`as`操作符来完成:`if a < (b as i32) {...}`. 那么为什么不把`a`转换成`u16`类型呢?
解决办法很简单,只要把 `b` 转换成 `i32` 类型即可,Rust中内置了一些基本类型之间的转换这里使用 `as` 操作符来完成:`if a < (b as i32) {...}`。那么为什么不把 `a` 转换成 `u16` 类型呢?
因为每个类型能表达的大小不一样,如果把大的类型转换成小的类型,会造成错误, 因此我们需要把小的类型转换成大的类型,来避免这些问题的发生.
因为每个类型能表达的数据范围不同,如果把范围较大的类型转换成较小的类型,会造成错误,因此我们需要把范围较小的类型转换成较大的类型,来避免这些问题的发生。
> 使用类型转换需要小心,因为如果执行以下操作`300_i32 as i8`,你将获得`44`这个值,而不是`300`,因为`i8`类型能表达的的最大值为`2^7 - 1`, 使用以下代码可以查看`i8`的最大值:
> 使用类型转换需要小心,因为如果执行以下操作 `300_i32 as i8`,你将获得 `44` 这个值,而不是 `300`,因为 `i8` 类型能表达的的最大值为 `2^7 - 1`,使用以下代码可以查看 `i8` 的最大值:
```rust
let a = i8::MAX;
println!("{}",a);
```
下面列出了常用的转换形式:
下面列出了常用的转换形式
```rust
fn main() {
let a = 3.1 as i8;
let b = 100_i8 as i32;
let c = 'a' as u8; // 将字符'a'转换为整数, 97
let c = 'a' as u8; // 将字符'a'转换为整数97
println!("{},{},{}",a,b,c)
}
@ -43,7 +43,7 @@ fn main() {
let mut values: [i32; 2] = [1, 2];
let p1: *mut i32 = values.as_mut_ptr();
let first_address = p1 as usize; // 将p1内存地址转换为一个整数
let second_address = first_address + 4; // 4 == std:mem::size_of::<i32>(), i32类型占用4个字节因此将内存地址 + 4
let second_address = first_address + 4; // 4 == std:mem::size_of::<i32>()i32类型占用4个字节因此将内存地址 + 4
let p2 = second_address as *mut i32; // 访问该地址指向的下一个整数p2
unsafe {
*p2 += 1;
@ -62,10 +62,10 @@ fn main() {
```
2. 转换不具有传递性
就算`e as U1 as U2`是合法的,也不能说明`e as U2`是合法的。
就算 `e as U1 as U2` 是合法的,也不能说明 `e as U2` 是合法的`e` 不能直接转换成 `U2`
## TryInto转换
在一些场景中,使用`as`关键字会有比较大的限制,因为你想要在类型转换上拥有完全的控制,例如处理转换错误,那么你将需要`TryInto`:
在一些场景中,使用 `as` 关键字会有比较大的限制。如果你想要在类型转换上拥有完全的控制而不依赖内置的转换,例如处理转换错误,那么可以使用 `TryInto`
```rust
use std::convert::TryInto;
@ -82,11 +82,11 @@ fn main() {
}
```
上面代码中引入了`std::convert::TryInto`特征,但是却没有使用它,可能有些同学会为此困惑,主要原因在于**如果你要使用一个特征的方法,那么你需要引入该特征到当前的作用域中**,我们在上面用到了`try_into`方法因此需要引入对应的特征。但是Rust又提供了一个非常便利的办法把最常用的标准库中的特征通过[`std::prelude`](std::convert::TryInto)模块提前引入到当前作用域中,其中包括了`std::convert::TryInto`,你可以尝试删除第一行的代码`use ...`,看看是否会报错.
上面代码中引入了 `std::convert::TryInto` 特征,但是却没有使用它,可能有些同学会为此困惑,主要原因在于**如果你要使用一个特征的方法,那么你需要引入该特征到当前的作用域中**,我们在上面用到了 `try_into` 方法,因此需要引入对应的特征。但是 Rust 又提供了一个非常便利的办法,把最常用的标准库中的特征通过[`std::prelude`](std::convert::TryInto)模块提前引入到当前作用域中,其中包括了 `std::convert::TryInto`,你可以尝试删除第一行的代码 `use ...`,看看是否会报错。
`try_into`会尝试进行一次转换,如果失败,则会返回一个`Result`,然后你可以进行相应的错误处理,但是因为我们的例子只是为了快速测试,因此使用了`unwrap`方法,该方法在发现错误时,会直接调用`panic`导致程序的崩溃退出,在实际项目中,请不要这么使用,具体见[panic](./exception-error.md#panic)部分.
`try_into` 会尝试进行一次转换,如果失败,则会返回一个 `Result`,然后你可以进行相应的错误处理,但是因为我们的例子只是为了快速测试,因此使用了 `unwrap` 方法,该方法在发现错误时,会直接调用 `panic` 导致程序的崩溃退出,在实际项目中,请不要这么使用,具体见[panic](./exception-error.md#panic)部分
最主要的是`try_into`转换会捕获大类型向小类型转换时导致的溢出错误:
最主要的是 `try_into` 转换会捕获大类型向小类型转换时导致的溢出错误
```rust
fn main() {
let b: i16 = 1500;
@ -100,11 +100,11 @@ fn main() {
};
}
```
运行后输出如下`"out of range integral type conversion attempted"`, 在这里我们程序捕获了错误,编译器告诉我们类型范围超出的转换是不被允许的,因为我们试图把`1500_i16`转换为`u8`类型,后者明显不足以承载这么大的值。
运行后输出如下 `"out of range integral type conversion attempted"`在这里我们程序捕获了错误,编译器告诉我们类型范围超出的转换是不被允许的,因为我们试图把 `1500_i16` 转换为 `u8` 类型,后者明显不足以承载这么大的值。
## 通用类型转换
虽然`as`和`TryInto`很强大但是只能应用在数值类型上可是Rust有如此多的类型想要为这些类型实现转换我们需要另谋出路,先来看看在一个笨办法,将一个结构体转换为另外一个结构体:
虽然 `as` `TryInto` 很强大,但是只能应用在数值类型上,可是 Rust 有如此多的类型,想要为这些类型实现转换,我们需要另谋出路先来看看在一个笨办法,将一个结构体转换为另外一个结构体:
```rust
struct Foo {
x: u32,
@ -125,9 +125,9 @@ fn reinterpret(foo: Foo) -> Bar {
简单粗暴但是从另外一个角度来看也挺啰嗦的好在Rust为我们提供了更通用的方式来完成这个目的。
#### 强制类型转换
在某些情况下,类型是可以进行隐式强制转换的,但是这些转换其实弱化了Rust的类型系统它们的存在是为了让Rust在大多数场景可以工作(说白了,帮助用户省事),而不是报各种类型上的编译错误。
在某些情况下,类型是可以进行隐式强制转换的,虽然这些转换弱化了 Rust 的类型系统,但是它们的存在是为了让Rust在大多数场景可以工作(说白了,帮助用户省事),而不是报各种类型上的编译错误。
首先,在匹配特征时,不会做任何强制转换(除了方法)。如果有一个类型`T`可以强制转换为`U`,不代表`impl T`可以强制转换为`impl U`,例如以下的代码就无法通过编译检查:
首先,在匹配特征时,不会做任何强制转换(除了方法)。一个类型 `T` 可以强制转换为 `U`,不代表 `impl T` 可以强制转换为 `impl U`,例如下面的代码就无法通过编译检查:
```rust
trait Trait {}
@ -154,16 +154,16 @@ error[E0277]: the trait bound `&mut i32: Trait` is not satisfied
= note: `Trait` is implemented for `&i32`, but not for `&mut i32`
```
`&i32`实现了特征`Trait``&mut i32`可以转换为`&i32`,但是`&mut i32`依然无法作为`Trait`来使用。
`&i32`实现了特征`Trait``&mut i32`可以转换为`&i32`但是`&mut i32`依然无法作为`Trait`来使用。<!-- 这一段没读懂,代码中的例子好像和上面的文字描述关系不大 -->
#### 点操作符
方法调用的点操作符看起来简单,实际上非常不简单,它在调用时,会发生很多魔法般的类型转换,例如:自动引用、自动解引用,强制类型转换直到类型能匹配等。
假设有一个方法`foo`,它有一个接收器(接收器就是`self`、`&self`、`&mut self`参数)。如果调用`value.foo()`,编译器在调用`foo`之前,需要决定到底使用哪个`Self`类型来调用。现在假设`value`拥有类型`T`.
假设有一个方法 `foo`,它有一个接收器(接收器就是 `self`、`&self`、`&mut self` 参数)。如果调用 `value.foo()`,编译器在调用 `foo` 之前,需要决定到底使用哪个 `Self` 类型来调用。现在假设 `value` 拥有类型 `T`
再进一步,我们使用[完全限定语法](https://course.rs/basic/trait/advance-trait.html#完全限定语法)来进行准确的函数调用:
1. 首先,编译器检查它是否可以直接调用`T::foo(value)`, 称之为**值方法调用**
2. 如果上一步调用无法完成(例如方法类型错误或者特征没有针对`Self`进行实现,上文提到过特征不能进行强制转换),那么编译器会尝试增加自动引用,以为着编译器会尝试以下调用:`<&T>::foo(value)`和`<&mut T>::foo(value)`, 称之为**引用方法调用**
1. 首先,编译器检查它是否可以直接调用`T::foo(value)`称之为**值方法调用**
2. 如果上一步调用无法完成(例如方法类型错误或者特征没有针对 `Self` 进行实现,上文提到过特征不能进行强制转换),那么编译器会尝试增加自动引用,以为着编译器会尝试以下调用:`<&T>::foo(value)`和`<&mut T>::foo(value)`称之为**引用方法调用**
3. 若上面两个方法依然不工作,编译器会试着解引用`T`,然后再进行尝试。这里使用了`Deref`特征 - 若`T: Deref<Target = U>`(`T`可以被解引用为`U`),那么编译器会使用`U`类型进行尝试,称之为**解引用方法调用**
4. 若`T`不能被解引用,且`T`是一个定长类型(在编译器类型长度是已知的),那么编译器也会尝试将`T`从定长类型转为不定长类型,例如将`[i32; 2]`转为`[i32]`
5. 若还是不行,那...没有那了,最后编译器大喊一声:汝欺我甚,不干了!
@ -175,10 +175,10 @@ let first_entry = array[0];
```
`array`数组的底层数据隐藏在了重重封锁之后,那么编译器如何使用`array[0]`这种数组原生访问语法通过重重封锁,准确的访问到数组中的第一个元素?
1. 首先,`array[0]`只是[`Index`](https://doc.rust-lang.org/std/ops/trait.Index.html)特征的语法糖: 编译器会将`array[0]`转换为`array.index(0)`调用, 当然在调用之前,编译器会先检查`array`是否实现了`Index`特征.
1. 首先,`array[0]`只是[`Index`](https://doc.rust-lang.org/std/ops/trait.Index.html)特征的语法糖: 编译器会将`array[0]`转换为`array.index(0)`调用当然在调用之前,编译器会先检查`array`是否实现了`Index`特征.
2. 接着,编译器检查`Rc<Box<[T; 3]>>`是否有否实现`Index`特征,结果是否,不仅如此,`&Rc<Box<[T; 3]>> `与`&mut Rc<Box<[T; 3]>>`也没有实现.
3. 上面的都不能工作,编译器开始对`Rc<Box<[T; 3]>>`进行解引用,把它转变成`Box<[T; 3]>`
4. 此时继续对`Box<[T; 3]>`进行上面的操作:`Box<[T; 3]>`, `&Box<[T; 3]>`, and `&mut Box<[T; 3]>`都没有实现`Index`特征,所以编译器开始对`Box<[T; 3]>`进行解引用,然后我们得到了`[T; 3]`
4. 此时继续对`Box<[T; 3]>`进行上面的操作:`Box<[T; 3]>``&Box<[T; 3]>`,和`&mut Box<[T; 3]>`都没有实现`Index`特征,所以编译器开始对`Box<[T; 3]>`进行解引用,然后我们得到了`[T; 3]`
5. `[T; 3]`以及它的各种引用都没有实现`Index`索引(是不是很反直觉:D在直觉中数组都可以通过索引访问实际上只有数组切片才可以!),它也不能再进行解引用,因此编译器只能祭出最后的大杀器:将定长转为不定长,因此`[T; 3]`被转换成`[T]`,也就是数组切片,它实现了`Index`特征,因此最终我们可以通过`index`方法访问到对应的元素.
过程看起来很复杂但是也还好挺好理解如果你先不能彻底理解也不要紧等以后对Rust理解更深了同时需要深入理解类型转换时再来细细品读本章。
@ -189,7 +189,7 @@ fn do_stuff<T: Clone>(value: &T) {
let cloned = value.clone();
}
```
上面例子中`cloned`的类型时什么?首先编译器检查能不能进行**值方法调用**, `value`的类型是`&T`,同时`clone`方法的签名也是`&T`: `fn clone(&T) -> T`,因此可以进行值方法调用, 再加上编译器知道了`T`实现了`Clone`,因此`cloned`的类型是`T`.
上面例子中`cloned`的类型时什么?首先编译器检查能不能进行**值方法调用**`value`的类型是`&T`,同时`clone`方法的签名也是`&T`: `fn clone(&T) -> T`,因此可以进行值方法调用,再加上编译器知道了`T`实现了`Clone`,因此`cloned`的类型是`T`。
如果`T: Clone`的特征约束被移除呢?
```rust
@ -200,7 +200,7 @@ fn do_stuff<T>(value: &T) {
首先,从直觉上来说,该方法会报错,因为`T`没有实现`Clone`特征,但是真实情况是什么呢?
我们先来推导一番。 首先通过值方法调用就不再可行,因此`T`没有实现`Clone`特征,也就无法调用`T`的`clone`方法。接着编译器尝试**引用方法调用**,此时`T`变成`&T`,在这种情况下,`clone`方法的签名如下:`fn clone(&&T) -> &T`, 记着我们现在对`value`进行了引用。 编译器发现`&T`实现了`Clone`类型(所有的引用类型都可以被复制,因为其实就是复制一份地址),因此可以可以推出`cloned`也是`&T`类型。
我们先来推导一番。 首先通过值方法调用就不再可行,因此`T`没有实现`Clone`特征,也就无法调用`T`的`clone`方法。接着编译器尝试**引用方法调用**,此时`T`变成`&T`,在这种情况下,`clone`方法的签名如下:`fn clone(&&T) -> &T`记着我们现在对`value`进行了引用。 编译器发现`&T`实现了`Clone`类型(所有的引用类型都可以被复制,因为其实就是复制一份地址),因此可以可以推出`cloned`也是`&T`类型。
最终,我们复制出一份引用指针,这很合理,因为值类型`T`没有实现`Clone`,只能去复制一个指针了。
@ -215,13 +215,13 @@ fn clone_containers<T>(foo: &Container<i32>, bar: &Container<T>) {
}
```
推断下上面的`foo_cloned`和`bar_cloned`是什么类型?提示: 关键在`Container`的泛型参数,一个是`i32`的具体类型,一个是泛型类型,其中`i32`实现了`Clone`,但是`T`并没有.
推断下上面的`foo_cloned`和`bar_cloned`是什么类型?提示: 关键在`Container`的泛型参数,一个是`i32`的具体类型一个是泛型类型,其中`i32`实现了`Clone`,但是`T`并没有.
首先要复习一下复杂类型派生`Clone`的规则:一个复杂类型能否派生`Clone`,需要它内部的所有子类型都能进行`Clone`。因此`Container<T>(Arc<T>)`是否实现`Clone`的关键在于`T`类型是否实现了`Clone`.
上面代码中,`Container<i32>`实现了`Clone`特征,因此编译器可以直接进行值方法调用,此时相当于直接调用`foo.clone`,其中`clone`的函数签名是`fn clone(&T) -> T`,由此可以看出`foo_cloned`的类型是`Container<i32>`.
上面代码中,`Container<i32>`实现了`Clone`特征,因此编译器可以直接进行值方法调用,此时相当于直接调用`foo.clone`,其中`clone`的函数签名是`fn clone(&T) -> T`由此可以看出`foo_cloned`的类型是`Container<i32>`.
然而,`bar_cloned`的类型却是`&Container<T>`.这个不合理啊,明明我们为`Container<T>`派生了`Clone`特征,因此它也应该是`Container<T>`类型才对。万事皆有因,我们先来看下`derive`宏最终生成的代码大概是啥样的:
然而`bar_cloned`的类型却是`&Container<T>`.这个不合理啊,明明我们为`Container<T>`派生了`Clone`特征,因此它也应该是`Container<T>`类型才对。万事皆有因,我们先来看下`derive`宏最终生成的代码大概是啥样的:
```rust
impl<T> Clone for Container<T> where T: Clone {
fn clone(&self) -> Self {
@ -232,7 +232,7 @@ impl<T> Clone for Container<T> where T: Clone {
从上面代码可以看出,派生`Clone`能实现的[根本是`T`实现了`Clone`特征](https://doc.rust-lang.org/std/clone/trait.Clone.html#derivable):`where T: Clone` 因此`Container<T>`就没有实现`Clone`特征。
编译器接着会去尝试引用方法调用,此时`&Container<T>`引用实现了`Clone`,最终可以得出`bar_cloned`的类型是`&Container<T>`,
编译器接着会去尝试引用方法调用,此时`&Container<T>`引用实现了`Clone`,最终可以得出`bar_cloned`的类型是`&Container<T>`
当然,也可以为`Container<T>`手动实现`Clone`特征:
```rust
@ -251,7 +251,7 @@ impl<T> Clone for Container<T> {
前方危险,敬请绕行!
类型系统,你让开!我要自己转换这些类型,不成功便成仁!虽然本书大多是关于安全的内容,我还是希望你能仔细考虑避免使用本章讲到的内容。这是你在 Rust 中所能做到的真真正正、彻彻底底、最最可怕的非安全行为, 在这里,所有的保护机制都形同虚设。
类型系统,你让开!我要自己转换这些类型,不成功便成仁!虽然本书大多是关于安全的内容,我还是希望你能仔细考虑避免使用本章讲到的内容。这是你在 Rust 中所能做到的真真正正、彻彻底底、最最可怕的非安全行为在这里,所有的保护机制都形同虚设。
先让你看看深渊长什么样,开开眼,然后你再决定是否深入: `mem::transmute<T, U>`将类型`T`直接转成类型`U`,唯一的要求就是,这两个类型占用同样大小的字节数!我的天,这也算限制?这简直就是无底线的转换好吧?看看会导致什么问题:
1. 首先也是最重要的,转换后创建一个任意类型的实例会造成无法想象的混乱,而且根本无法预测。不要把`3`转换成`bool`类型,就算你根本不会去使用该`bool`类型,也不要去这样转换。
@ -265,7 +265,7 @@ impl<T> Clone for Container<T> {
对于第5条你该如何知道内存的排列布局是一样的呢对于`repr(C)`类型和`repr(transparent)`类型来说,它们的布局是有着精确定义的。但是对于你自己的"普通却自信"的Rust类型`repr(Rust)`来说,它可不是有着精确定义的。甚至同一个泛型类型的不同实例都可以有不同的内存布局。`Vec<i32>`和`Vec<u32>`它们的字段可能有着相同的顺序也可能没有。对于数据排列布局来说什么能保证什么不能保证目前还在Rust开发组的[工作任务](https://rust-lang.github.io/unsafe-code-guidelines/layout.html)中呢.
你以为你之前凝视的是深渊吗?不,你凝视的只是深渊的大门。`mem::transmute_copy<T, U>`才是真正的深渊,它比之前的还要更加危险和不安全。它从`T`类型中拷贝出`U`类型所需的字节数,然后转换成`U`。`mem::transmute`尚有大小检查,能保证两个数据的内存大小一致,现在这哥们干脆连这个也丢了,只不过`U`的尺寸若是比`T`大,会是一个未定义行为。
你以为你之前凝视的是深渊吗?不,你凝视的只是深渊的大门。`mem::transmute_copy<T, U>`才是真正的深渊,它比之前的还要更加危险和不安全。它从`T`类型中拷贝出`U`类型所需的字节数,然后转换成`U`。`mem::transmute`尚有大小检查,能保证两个数据的内存大小一致,现在这哥们干脆连这个也丢了只不过`U`的尺寸若是比`T`大,会是一个未定义行为。
当然,你也可以通过原生指针转换和`unions`(todo!)获得所有的这些功能,但是你将无法获得任何编译提示或者检查。原生指针转换和`unions`也不是魔法,无法逃避上面说的规则。

@ -1,11 +1,11 @@
# panic深入剖析
在正式开始之前,先来思考一个问题: 假设我们想要从文件读取数据,如果失败,你有没有好的办法通知调用者为何失败?如果成功,你有没有好的办法把读取的结果返还给调用者?
在正式开始之前,先来思考一个问题假设我们想要从文件读取数据,如果失败,你有没有好的办法通知调用者为何失败?如果成功,你有没有好的办法把读取的结果返还给调用者?
## panic!与不可恢复错误
上面的问题在真实场景,其实挺复杂的,让我们先做一个假设:文件读取操作发生在系统启动阶段。那么可以轻易得出一个结论,一旦文件读取失败,那么系统启动也将失败,这意味着该失败是不可恢复的错误,无论是因为文件不存在还是操作系统硬盘的问题,这些只是错误的原因不同,但是归根到底都是不可恢复的错误(梳理清楚当前场景的错误类型非常重要).
上面的问题在真实场景会经常遇到,其实处理起来挺复杂的,让我们先做一个假设:文件读取操作发生在系统启动阶段。那么可以轻易得出一个结论一旦文件读取失败,那么系统启动也将失败,这意味着该失败是不可恢复的错误,无论是因为文件不存在还是操作系统硬盘的问题,这些只是错误的原因不同,但是归根到底都是不可恢复的错误(梳理清楚当前场景的错误类型非常重要)
既然是不可恢复错误那么一旦发生只需让程序崩溃即可。对此Rust为我们提供了`panic!`宏,当调用执行该宏时,**程序会打印出一个错误信息,展开报错点往前的函数调用堆栈,最后退出程序**.
既然是不可恢复错误那么一旦发生只需让程序崩溃即可。对此Rust 为我们提供了 `panic!` 宏,当调用执行该宏时,**程序会打印出一个错误信息,展开报错点往前的函数调用堆栈,最后退出程序**
切记,一定是不可恢复的错误,才调用 `panic!` 处理,你总不想系统仅仅因为用户随便传入一个非法参数就崩溃吧?所以,**只有当你不知道该如何处理时再去调用panic!**.
@ -23,9 +23,9 @@ thread 'main' panicked at 'crash!!1', src/main.rs:3:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```
以上信息包含了两条重要信息:
以上信息包含了两条重要信息
- `main` 函数所在的线程崩溃了,发生的代码位置是 `src/main.rs` 中的第3行第5个字符(去除该行前面的空字符)
- 在使用时加上一个环境变量可以获取更详细的栈展开信息: `RUST_BACKTRACE=1 cargo run`
- 在使用时加上一个环境变量可以获取更详细的栈展开信息`RUST_BACKTRACE=1 cargo run`
下面让我们针对第二点进行详细展开讲解。
@ -40,17 +40,17 @@ fn main() {
```
上面的代码很简单,数组只有`3`个元素,我们却尝试去访问它的第`100`号元素(数组索引从`0`开始),那自然会崩溃。
我们的读者里不乏正义之士,此时肯定要质疑,一个简单的数组越界访问,为何要直接让程序崩溃?是不是有些大题小做了?
我们的读者里不乏正义之士,此时肯定要质疑,一个简单的数组越界访问,为何要直接让程序崩溃?是不是有些小题大作了?
如果有过C语言的经验即使你越界了问题不大我依然尝试去访问至于这个值是不是你想要的(`100`号内存地址也有可能有值,只不过是其它变量或者程序的!),抱歉,不归我管,我只负责取,你要负责管理好自己的索引访问范围。上面这种情况被称为**缓冲区溢出**,并可能会导致安全漏洞,例如攻击者可以通过索引来访问到数组后面不被允许的数据。
如果有过C语言的经验即使你越界了问题不大我依然尝试去访问至于这个值是不是你想要的(`100`号内存地址也有可能有值,只不过是其它变量或者程序的!),抱歉,不归我管我只负责取,你要负责管理好自己的索引访问范围。上面这种情况被称为**缓冲区溢出**,并可能会导致安全漏洞,例如攻击者可以通过索引来访问到数组后面不被允许的数据。
说实话我宁愿程序崩溃为什么当你取到了一个不属于你的值这在很多时候会导致程序上的逻辑bug! 有编程经验的人都知道这种逻辑上的bug是多么难发现和修复因此程序直接崩溃然后告诉我们问题发生的位置最后我们对此进行修复这才是最合理的软件开发流程而不是把问题藏着掖着:
说实话我宁愿程序崩溃为什么当你取到了一个不属于你的值这在很多时候会导致程序上的逻辑bug! 有编程经验的人都知道这种逻辑上的bug是多么难发现和修复!因此程序直接崩溃,然后告诉我们问题发生的位置,最后我们对此进行修复,这才是最合理的软件开发流程,而不是把问题藏着掖着
```console
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```
好的,现在成功知道问题发生的位置,但是如果我们想知道该问题之前经过了哪些调用环节,该怎么办?那就按照提示使用`RUST_BACKTRACE=1 cargo run`来再一次运行程序:
好的,现在成功知道问题发生的位置,但是如果我们想知道该问题之前经过了哪些调用环节,该怎么办?那就按照提示使用 `RUST_BACKTRACE=1 cargo run` 来再一次运行程序:
```console
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5
stack backtrace:
@ -73,29 +73,29 @@ stack backtrace:
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
```
上面的代码就是一次栈展开(也称栈回溯),它包含了函数调用的顺序,当然按照逆序排列:最近调用的函数排在列表的最上方. 因为咱们的`main`函数基本是最先调用的函数了,所以排在了倒数第二位,还有一个关注点,排在最顶部最后一个调用的函数是`rust_begin_unwind`,该函数的目的就是进行栈展开,呈现这些列表信息给我们。
上面的代码就是一次栈展开(也称栈回溯),它包含了函数调用的顺序,当然按照逆序排列:最近调用的函数排在列表的最上方。因为咱们的 `main` 函数基本是最先调用的函数了,所以排在了倒数第二位,还有一个关注点,排在最顶部最后一个调用的函数是 `rust_begin_unwind`,该函数的目的就是进行栈展开,呈现这些列表信息给我们。
要获取到栈回溯信息,你还需要开启`debug`标志,该标志在使用`cargo run`或者`cargo build`时自动开启(这两个方式是`Debug`运行方式). 同时栈展开信息在不同操作系统或者Rust版本上也所有不同。
要获取到栈回溯信息,你还需要开启 `debug` 标志,该标志在使用 `cargo run` 或者 `cargo build` 时自动开启(这两个操作默认是 `Debug` 运行方式). 同时,栈展开信息在不同操作系统或者 Rust 版本上也所有不同。
## panic时的两种终止方式
当出现`panic!`时,程序提供了两种方式来处理终止流程: **栈展开** 和 **直接终止**.
当出现 `panic!` 时,程序提供了两种方式来处理终止流程: **栈展开** 和 **直接终止**
其中,默认的方式就是`栈展开`这意味着Rust会回溯栈上数据和函数调用因此也意味着更多的善后工作好处是给于充分的报错信息和栈调用信息,便于事后的问题复盘。`直接终止`,顾名思义,不清理数据就直接出程序,善后工作交与操作系统来负责。
其中,默认的方式就是 `栈展开`,这意味着 Rust 会回溯栈上数据和函数调用,因此也意味着更多的善后工作,好处是可以给出充分的报错信息和栈调用信息,便于事后的问题复盘。`直接终止`,顾名思义,不清理数据就直接退出程序,善后工作交与操作系统来负责。
对于绝大多数用户,使用默认选择是最好的,但是当你关心最终编译出的二进制可执行文件大小时,那么可以尝试去使用直接终止的方式,例如下面的配置修改`Cargo.toml`文件,实现在[`release`](../first-try/cargo.md#手动编译和运行项目)模式下遇到`panic`直接终止:
对于绝大多数用户,使用默认选择是最好的,但是当你关心最终编译出的二进制可执行文件大小时,那么可以尝试去使用直接终止的方式,例如下面的配置修改 `Cargo.toml` 文件,实现在[`release`](../first-try/cargo.md#手动编译和运行项目)模式下遇到 `panic` 直接终止:
```rust
[profile.release]
panic = 'abort'
```
## 线程`panic`后,程序会否终止?
长话短说,如果是`main`线程,则程序会终止,如果是其它子线程,该线程会终止。因此,尽量不要在`main`线程中做太多任务,将这些任务交由子线程去做,就算`panic`也不会导致整个程序的结束。
长话短说,如果是 `main` 线程,则程序会终止,如果是其它子线程,该线程会终止,但是不会影响 `main` 线程。因此,尽量不要在 `main` 线程中做太多任务,将这些任务交由子线程去做,就算子线程 `panic` 也不会导致整个程序的结束。
具体解析见[panic原理剖析](#panic原理剖析)
## 何时该使用panic!
下面让我们大概罗列下合适适合使用`panic`,虽然原则上,你理解了之前的内容后,会自己作出合适的选择,但是罗列出来可以帮助你强化这一点
下面让我们大概罗列下何时适合使用 `panic`,也许经过之前的学习,你已经能够对 `panic` 的使用有了自己的看法,但是我们还是会罗列一些常见的用法来加深你的理解
先来一点背景知识,在前面章节我们粗略讲过 `Result<T,E>` 这个枚举类型,它是用来表示函数的返回结果:
```rust
@ -104,20 +104,20 @@ enum Result<T, E> {
Err(E),
}
```
当没有错误发生时,函数返回一个用`Result`类型包裹的值`Ok(T)`,当错误时,返回一个`Err(E)`。对于`Result`返回我们有很多处理方法,最简单粗暴的就是`unwrap`和`expect`,这两个函数非常类似,我们以`unwrap`举例:
当没有错误发生时,函数返回一个用 `Result` 类型包裹的值 `Ok(T)`,当错误时,返回一个 `Err(E)`。对于 `Result` 返回我们有很多处理方法,最简单粗暴的就是 `unwrap``expect`,这两个函数非常类似,我们以 `unwrap` 举例:
```rust
use std::net::IpAddr;
let home: IpAddr = "127.0.0.1".parse().unwrap();
```
上面的`parse`方法试图将字符串`"127.0.0.1"`解析为一个IP地址类型`IpAddr`,它返回一个`Result<IpAddr,E>`类型,如果解析成功,则把`Ok(IpAddr)`中的值赋给`home`,如果失败,则不处理`Err(E)`,而是直接`panic`
上面的 `parse` 方法试图将字符串 `"127.0.0.1" `解析为一个IP地址类型 `IpAddr`,它返回一个 `Result<IpAddr,E>` 类型,如果解析成功,则把 `Ok(IpAddr)` 中的值赋给 `home`,如果失败,则不处理 `Err(E)`,而是直接 `panic`
因此`unwrap`简而言之:成功则返回值,失败则`panic`, 总之不进行任何错误处理。
因此 `unwrap` 简而言之:成功则返回值,失败则 `panic`总之不进行任何错误处理。
#### 示例、原型、测试
些场景,需要快速的搭建代码,错误处理反而会拖慢实现速度,也不是特别有必要,因此通过`unwrap`、`expect`等方法来处理是最快的。
几个场景下,需要快速地搭建代码,错误处理会拖慢编码的速度,也不是特别有必要,因此通过`unwrap`、`expect`等方法来处理是最快的。
同时,当我们准备做错误处理时,全局搜索这些方法,也可以不遗漏的进行替换。
同时,当我们回头准备做错误处理时,可以全局搜索这些方法,不遗漏地进行替换。
#### 你确切的知道你的程序是正确时可以使用panic
因为 `panic` 的触发方式比错误处理要简单,因此可以让代码更清晰,可读性也更加好,当我们的代码注定是正确时,你可以用 `unrawp` 等方法直接进行处理,反正也不可能`panic`
@ -133,14 +133,14 @@ let home: IpAddr = "127.0.0.1".parse().unwrap();
#### 可能导致全局有害状态时
有害状态大概分为几类:
- 非预期的错误
- 之后代码的运行会受到明显可见的影响
- 后续代码的运行会受到显著影响
- 内存安全的问题
当错误预期会出现时返回一个错误较为合适例如解析器接收到格式错误的数据HTTP请求接收到错误的参数甚至该请求内的任何错误(不会导致整个程序有问题,只影响该此请求)。 **因为错误是可预期的,因此也是可以处理的**。
当启动时某个流程发生了错误,导致了后续代码的运行造成影响,那么就应该使用`panic`,而不是处理错误后,继续运行,当然你可以通过重试的方式来继续。
当启动时某个流程发生了错误,对后续代码的运行造成了影响,那么就应该使用 `panic`,而不是处理错误后继续运行,当然你可以通过重试的方式来继续。
上面提到过,数组访问越界,就要`panic`的原因,这个就是属于内存安全的范畴,一旦内存访问不安全,那么我们无法保证自己的程序会怎么运行下去,也无法保证逻辑和数据的正确性。
上面提到过,数组访问越界,就要 `panic` 的原因,这个就是属于内存安全的范畴,一旦内存访问不安全,那么我们无法保证自己的程序会怎么运行下去,也无法保证逻辑和数据的正确性。
## panic原理剖析
@ -149,11 +149,11 @@ let home: IpAddr = "127.0.0.1".parse().unwrap();
当调用 `panic!` 宏时,它会
1. 格式化 `panic` 信息,然后使用该信息作为参数,调用 `std::panic::panic_any()` 函数
2. `panic_any`会检查应用是否使用了`panic hook`,如果使用了,该`hook`函数会被调用
3. 当`hook`函数返回后,当前的线程就开始进行栈展开:从`panic_any`开始,如果寄存器或者栈因为某些原因信息错乱了,那很可能该展开会发生异常,最终线程会直接停止,展开也无法继续进行
4. 展开的过程是一帧一帧的去回溯整个栈,每个帧的数据都会随之被丢弃,但是在展开过程中,你可能会遇到被用户标记为`catching`的帧(通过`std::panic::catch_unwind()`函数标记),此时用户提供的`catch`函数会被调用,展开也随之停止: 当然,如果`catch`选择在内部调用`std::panic::resume_unwind()`函数,则展开还会继续。
2. `panic_any` 会检查应用是否使用了 `panic hook`,如果使用了,该 `hook` 函数会被调用hook是一个钩子函数是外部代码设置的用于在panic触发时执行外部代码所需的功能
3. 当 `hook` 函数返回后,当前的线程就开始进行栈展开:从 `panic_any` 开始,如果寄存器或者栈因为某些原因信息错乱了,那很可能该展开会发生异常,最终线程会直接停止,展开也无法继续进行
4. 展开的过程是一帧一帧的去回溯整个栈,每个帧的数据都会随之被丢弃,但是在展开过程中,你可能会遇到被用户标记为 `catching` 的帧(通过 `std::panic::catch_unwind()` 函数标记),此时用户提供的 `catch` 函数会被调用,展开也随之停止:当然,如果 `catch` 选择在内部调用 `std::panic::resume_unwind()` 函数,则展开还会继续。
还有一种情况,在展开过程中,如果展开本身 `panic` 了,那展开线程会终止,展开也随之停止。
一旦线程展开被终止或者完成,最终的输出结果是取决于哪个线程`panic`:对于`main`线程,操作系统提供的终止功能`core::intrinsics::abort()`会被调用,最终结束当前的`panic`进程;如果是其它子线程,那么线程就会简单的终止,同时信息会在稍后通过`std::thread::join()`进行收集.
一旦线程展开被终止或者完成,最终的输出结果是取决于哪个线程 `panic对于 `main` 线程,操作系统提供的终止功能 `core::intrinsics::abort()` 会被调用,最终结束当前的 `panic` 进程;如果是其它子线程,那么线程就会简单的终止,同时信息会在稍后通过 `std::thread::join()` 进行收集。

Loading…
Cancel
Save