pull/95/head
sunface 3 years ago
parent 0c9118d4ee
commit e929c8c011

@ -16,5 +16,5 @@ line-numbers = true
[output.html.fold]
enable = true
level = 0
level = 1

@ -13,7 +13,7 @@
## Rust学习三部曲
- [基本语法 doing](basic/intro.md)
- [Rust基础入门](basic/intro.md)
- [变量绑定与结构](basic/variable.md)
- [基本类型](basic/base-type/index.md)
- [数值类型](basic/base-type/numbers.md)
@ -41,27 +41,31 @@
- [特征Trait](basic/trait/trait.md)
- [特征对象](basic/trait/trait-object.md)
- [进一步深入特征](basic/trait/advance-trait.md)
- [类型转换 todo](basic/type-converse.md)
- [返回、异常和错误(todo)](basic/exception-error.md)
- [类型转换](basic/converse.md)
- [返回和错误处理](basic/result-error/intro.md)
- [不可恢复错误panic!](basic/result-error/panic.md)
- [可恢复的错误Result](basic/result-error/result.md)
- [进阶语法 todo](advance/intro.md)
- [Rust高级进阶 doing](advance/intro.md)
- [自定义类型和动态类型 todo](advance/custom-type.md)
- [集合类型(todo)](advance/collection.md)
- [格式化输出(todo)](advance/formatted-output.md)
- [文档注释(todo)](advance/comment.md)
- [包和模块(todo)](advance/crate-module.md)
- [生命周期(todo)](advance/lifetime.md)
- [生命周期(todo)](advance/lifetime/intro.md)
- [认识生命周期](advance/lifetime/basic.md)
- [深入生命周期](advance/lifetime/advance.md)
- [迭代器(todo)](advance/interator.md)
- [函数式编程(todo)](advance/functional-programing.md)
- [智能指针(todo)](advance/smart-pointer.md)
- [全局变量](advance/global-variable.md)
- [一些写代码的技巧](advance/coding-tips.md)
## 专题内容,每个专题都配套一个小型项目进行实践
- [错误处理 todo](errors/intro.md)
- [panic!](errors/panic.md)
- [适用Result返回错误](errors/result.md)
- [简化错误处理](errors/simplify.md)
- [自定义错误](errors/user-define.md)
- [让错误输出更优雅](errors/pretty-format.md)
- [会导致panic的代码](errors/panic-codes.md)
@ -198,4 +202,6 @@
- [B-运算符与符号](appendix/operators.md)
- [C-表达式](appendix/expressions.md)
- [D-派生特征derive](appendix/derive.md)
- [E-Rust版本发布](appendix/rust-version.md)
- [E-prelude模块 todo](appendix/prelude.md)
- [F-难点索引](appendix/difficulties.md)
- [G-Rust版本发布](appendix/rust-version.md)

@ -0,0 +1,25 @@
# 一些写代码的技巧
## 如何获知变量类型或者函数的返回类型
有几种常用的方式:
- 第一种是查询标准库或者三方库文档,搜索`File`,然后找到它的`open`方法,但是此处更推荐第二种方法:
- 在[Rust IDE]章节,我们推荐了`VSCode` IED和`rust-analyze`插件,如果你成功安装的话,那么就可以在`VScode`中很方便的通过代码跳转的方式查看代码,同时`rust-analyze`插件还会对代码中的类型进行标注,非常方便好用!
- 你还可以尝试故意标记一个错误的类型,然后让编译器告诉你:
```rust
let f: u32 = File::open("hello.txt");
```
错误提示如下:
```console
error[E0308]: mismatched types
--> src/main.rs:4:18
|
4 | let f: u32 = File::open("hello.txt");
| ^^^^^^^^^^^^^^^^^^^^^^^ expected u32, found enum
`std::result::Result`
|
= note: expected type `u32`
found type `std::result::Result<std::fs::File, std::io::Error>`
```

@ -0,0 +1,4 @@
# 自定义类型和动态类型todo
参考https://rustwiki.org/en/book/ch19-04-advanced-types.html

@ -0,0 +1,5 @@
# 深入生命周期
# 无界生命周期
https://doc.rust-lang.org/nomicon/unbounded-lifetimes.html

@ -0,0 +1 @@
# 认识生命周期

@ -0,0 +1,6 @@
# F-难点索引
不可否认Rust难点很多而且知识点也很多随着时间的进行或多或少都会对一些难点逐渐模糊这些难点无法通过目录章节名进行索引因此集中放在此附录中进行索引方便读者朋友查阅。
### ?
?主要用于简化错误传播,在[这里](../basic/result-error/result.md#传播界的大明星:)有详细讲解。

@ -0,0 +1 @@
# E-prelude模块

@ -1,4 +1,4 @@
# 附录 ERust版本发布
# 附录 GRust版本发布
## Rust版本说明
早在第一章,我们见过 `cargo new`*Cargo.toml* 中增加了一些有关 `edition` 的元数据。本附录将解释其意义!

@ -0,0 +1,272 @@
# 类型转换
Rust是类型安全的语言因此在Rust中做类型转换不是一件简单的事这一章节我们将对Rust中的类型转换进行详尽讲解。
## `as`转换
先来看一段代码:
```rust
fn main() {
let a: i32 = 10;
let b: u16 = 100;
if a < b {
println!("Ten is less than one hundred.");
}
}
```
能跟着这本书一直学习到这里说明你对Rust已经有了一定的理解那么一眼就能看出这段代码注定会报错因为`a`和`b`拥有不同的类型Rust不允许两种不同的类型进行比较。
解决办法很简单,只要把`b`转换成`i32`类型即可,这里使用`as`操作符来完成:`if a < (b as i32) {...}`. 那么为什么不把`a`转换成`u16`类型呢?
因为每个类型能表达的大小不一样,如果把大的类型转换成小的类型,会造成错误, 因此我们需要把小的类型转换成大的类型,来避免这些问题的发生.
> 使用类型转换需要小心,因为如果执行以下操作`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
println!("{},{},{}",a,b,c)
}
```
#### 内存地址转换为指针
```rust
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 p2 = second_address as *mut i32; // 访问该地址指向的下一个整数p2
unsafe {
*p2 += 1;
}
assert_eq!(values[1], 3);
```
#### 强制类型转换的边角知识
1. 数组切片原生指针之间的转换,不会改变数组占用的内存字节数,尽管数组元素的类型发生了改变:
```rust
fn main() {
let a: *const [u16] = &[1,2,3,4,5];
let b = a as *const[u8];
assert_eq!(std::mem::size_of_val(&a),std::mem::size_of_val(&b))
}
```
2. 转换不具有传递性
就算`e as U1 as U2`是合法的,也不能说明`e as U2`是合法的。
## TryInto转换
在一些场景中,使用`as`关键字会有比较大的限制,因为你想要在类型转换上拥有完全的控制,例如处理转换错误,那么你将需要`TryInto`:
```rust
use std::convert::TryInto;
fn main() {
let a: u8 = 10;
let b: u16 = 1500;
let b_: u8 = b.try_into().unwrap();
if a < b_ {
println!("Ten is less than one hundred.");
}
}
```
上面代码中引入了`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`转换会捕获大类型向小类型转换时导致的溢出错误:
```rust
fn main() {
let b: i16 = 1500;
let b_: u8 = match b.try_into() {
Ok(b1) => b1,
Err(e) => {
println!("{:?}", e.to_string());
0
}
};
}
```
运行后输出如下`"out of range integral type conversion attempted"`, 在这里我们程序捕获了错误,编译器告诉我们类型范围超出的转换是不被允许的,因为我们试图把`1500_i16`转换为`u8`类型,后者明显不足以承载这么大的值。
## 通用类型转换
虽然`as`和`TryInto`很强大但是只能应用在数值类型上可是Rust有如此多的类型想要为这些类型实现转换我们需要另谋出路,先来看看在一个笨办法,将一个结构体转换为另外一个结构体:
```rust
struct Foo {
x: u32,
y: u16,
}
struct Bar {
a: u32,
b: u16,
}
fn reinterpret(foo: Foo) -> Bar {
let Foo { x, y } = foo;
Bar { a: x, b: y }
}
```
简单粗暴但是从另外一个角度来看也挺啰嗦的好在Rust为我们提供了更通用的方式来完成这个目的。
#### 强制类型转换
在某些情况下类型是可以进行隐式强制转换的但是这些转换其实弱化了Rust的类型系统它们的存在是为了让Rust在大多数场景可以工作(说白了,帮助用户省事),而不是报各种类型上的编译错误。
首先,在匹配特征时,不会做任何强制转换(除了方法)。如果有一个类型`T`可以强制转换为`U`,不代表`impl T`可以强制转换为`impl U`,例如以下的代码就无法通过编译检查:
```rust
trait Trait {}
fn foo<X: Trait>(t: X) {}
impl<'a> Trait for &'a i32 {}
fn main() {
let t: &mut i32 = &mut 0;
foo(t);
}
```
报错如下:
```console
error[E0277]: the trait bound `&mut i32: Trait` is not satisfied
--> src/main.rs:9:9
|
9 | foo(t);
| ^ the trait `Trait` is not implemented for `&mut i32`
|
= help: the following implementations were found:
<&'a i32 as Trait>
= note: `Trait` is implemented for `&i32`, but not for `&mut i32`
```
`&i32`实现了特征`Trait``&mut i32`可以转换为`&i32`,但是`&mut i32`依然无法作为`Trait`来使用。
#### 点操作符
方法调用的点操作符看起来简单,实际上非常不简单,它在调用时,会发生很多魔法般的类型转换,例如:自动引用、自动解引用,强制类型转换直到类型能匹配等。
假设有一个方法`foo`,它有一个接收器(接收器就是`self`、`&sef`、`&mut self`参数)。如果调用`value.foo()`,编译器在调用`foo`之前,需要决定到底使用哪个`Self`类型来调用。现在假设`value`拥有类型`T`.
再进一步,我们使用[完全限定语法](https://course.rs/basic/trait/advance-trait.html#完全限定语法)来进行准确的函数调用:
1. 首先,编译器检查它是否可以直接调用`T::foo(value)`, 称之为**值方法调用**
2. 如果上一步调用无法完成(例如方法类型错误或者特征没有针对`Self`进行实现,上文提到过特征不能进行强制转换),那么编译器会尝试增加自动引用,以为着编译器会尝试以下调用:`<&T>::foo(value)`和`<&mut T>::foo(value)`, 称之为**引用方法调用**
3. 若上面两个方法依然不工作,编译器会试着解引用`T`,然后再进行尝试。这里使用了`Deref`特征 - 若`T: Deref<Target = U>`(`T`可以被解引用为`U`),那么编译器会使用`U`类型进行尝试,称之为**解引用方法调用**
4. 若`T`不能被解引用,且`T`是一个定长类型(在编译器类型长度是已知的),那么编译器也会尝试将`T`从定长类型转为不定长类型,例如将`[i32; 2]`转为`[i32]`
5. 若还是不行,那...没有那了,最后编译器大喊一声:汝欺我甚,不干了!
下面我们来用一个例子来解释上面的方法查找算法:
```rust
let array: Rc<Box<[T; 3]>> = ...;
let first_entry = array[0];
```
`array`数组的底层数据隐藏在了重重封锁之后,那么编译器如何使用`array[0]`这种数组原生访问语法通过重重封锁,准确的访问到数组中的第一个元素?
1. 首先,`array[0]`只是[`Index`](https://doc.rust-lang.org/std/ops/trait.Index.html)特征的语法糖: 编译器会将`array[0]`转换为`array.index(0)`调用, 当然在调用之前,编译器会先检查`array`是否实现了`Index`特征.
2. 接着,编译器检查`Rc<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]`
5. `[T; 3]`以及它的各种引用都没有实现`Index`索引(是不是很反直觉:D在直觉中数组都可以通过索引访问实际上只有数组切片才可以!),它也不能再进行解引用,因此编译器只能祭出最后的大杀器:将定长转为不定长,因此`[T; 3]`被转换成`[T]`,也就是数组切片,它实现了`Index`特征,因此最终我们可以通过`index`方法访问到对应的元素.
过程看起来很复杂但是也还好挺好理解如果你先不能彻底理解也不要紧等以后对Rust理解更深了同时需要深入理解类型转换时再来细细品读本章。
再来看看以下更复杂的例子:
```rust
fn do_stuff<T: Clone>(value: &T) {
let cloned = value.clone();
}
```
上面例子中`cloned`的类型时什么?首先编译器检查能不能进行**值方法调用**, `value`的类型是`&T`,同时`clone`方法的签名也是`&T`: `fn clone(&T) -> T`,因此可以进行值方法调用, 再加上编译器知道了`T`实现了`Clone`,因此`cloned`的类型是`T`.
如果`T: Clone`的特征约束被移除呢?
```rust
fn do_stuff<T>(value: &T) {
let cloned = value.clone();
}
```
首先,从直觉上来说,该方法会报错,因为`T`没有实现`Clone`特征,但是真实情况是什么呢?
我们先来推导一番。 首先通过值方法调用就不再可行,因此`T`没有实现`Clone`特征,也就无法调用`T`的`clone`方法。接着编译器尝试**引用方法调用**,此时`T`变成`&T`,在这种情况下,`clone`方法的签名如下:`fn clone(&&T) -> &T`, 记着我们现在对`value`进行了引用。 编译器发现`&T`实现了`Clone`类型(所有的引用类型都可以被复制,因为其实就是复制一份地址),因此可以可以推出`cloned`也是`&T`类型。
最终,我们复制出一份引用指针,这很合理,因为值类型`T`没有实现`Clone`,只能去复制一个指针了。
下面的例子也是自动引用生效的地方:
```rust
#[derive(Clone)]
struct Container<T>(Arc<T>);
fn clone_containers<T>(foo: &Container<i32>, bar: &Container<T>) {
let foo_cloned = foo.clone();
let bar_cloned = bar.clone();
}
```
推断下上面的`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>`.
然而,`bar_cloned`的类型却是`&Container<T>`.这个不合理啊,明明我们为`Container<T>`派生了`Clone`特征,因此它也应该是`Container<T>`类型才对。万事皆有因,我们先来看下`derive`宏最终生成的代码大概是啥样的:
```rust
impl<T> Clone for Container<T> where T: Clone {
fn clone(&self) -> Self {
Self(Arc::clone(&self.0))
}
}
```
从上面代码可以看出,派生`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`特征:
```rust
impl<T> Clone for Container<T> {
fn clone(&self) -> Self {
Self(Arc::clone(&self.0))
}
}
```
此时,编译器首次尝试值方法调用即可通过,因此`bar_cloned`的类型变成`Container<T>`.
这一块儿内容真的挺复杂每一个坚持看完的读者都是真正的勇士我也是为了写好这块儿内容作者足足花了4个小时
#### 变形记(Transmutes)
前方危险,敬请绕行!
类型系统,你让开!我要自己转换这些类型,不成功便成仁!虽然本书都是关于非安全的内容,我还是希望你能仔细考虑避免使用本章讲到的内容。这是你在 Rust 中所能做到的真真正正、彻彻底底、最最可怕的非安全行为, 在这里,所有的保护机制都形同虚设。
先让你看看深渊长什么样,开开眼,然后你再决定是否深入: `mem::transmute<T, U>`将类型`T`直接转成类型`U`,唯一的要求就是,这两个类型占用同样大小的字节数!我的天,这也算限制?这简直就是无底线的转换好吧?看看会导致什么问题:
1. 首先也是最重要的,转换后创建一个任意类型的实例会造成无法想象的混乱,而且根本无法预测。不要把`3`转换成`bool`类型,就算你根本不会去使用该`bool`类型,也不要去这样转换。
2. 变形后会有一个重载的返回类型,即使你没有指定返回类型,为了满足类型推导的需求,依然会产生千奇百怪的类型
3. 将`&`变形为`&mut`是未定义的行为
- 这种转换永远都是未定义的
- 不,你不能这么做
- 不要多想,你没有那种幸运
4. 变形为一个未指定生命周期的引用会导致[无界生命周期](../advance/lifetime/advance.md)
5. 在复合类型之间互相变换时你需要保证它们的排列布局是一模一样的一旦不一样那么字段就会得到不可预期的值这也是未定义的行为至于你会不会因此愤怒who cares你都用了变形了老兄
对于第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`大,会是一个未定义行为。
当然,你也可以通过原生指针转换获得`unions`(todo!)获得所有的这些功能,但是你将无法获得任何编译提示或者检查。原生指针转换和`unions`也不是魔法,无法逃避上面说的规则。

@ -1 +0,0 @@
# exception-error.md

@ -0,0 +1,16 @@
# 返回和错误处理
飞鸽传书、八百里加急,自古以来,掌权者最需要的都是及时获得对某个事物的信息反馈,在此过程中,也定义了相应的应急处理措施。
社会演变至今,这种思想依然没变,甚至来到计算中的微观世界,也是如此。及时、准确的获知系统在发生什么,是程序设计的重中之重。因此能够准确的分辨函数返回值是正确的还是错误的、以及在发生错误时该怎么快速处理,成了程序设计语言的必备功能。
Go语言为人诟病的其中一点就是`if err != nil {}`的大量使用缺乏一些程序设计的美感不过我倒是觉得这种简单的方式也有其好处就是阅读代码时的流畅感很强你不需要过多的思考各种语法是什么意思。与Go语言不同Rust博采众家之长整出了颇具自身色彩的返回值和错误处理体系本章我们就从高屋建瓴的角度来学习更加深入的讲解见[此章](../errors/intro.md).
## Rust的错误哲学
错误对于软件来说是不可避免的因此一门优秀的编程语言必须有其完整的错误处理哲学。在很多情况下Rust需要你承认自己的代码可能会出错并提前采取行动来处理这些错误。
Rust中的错误主要分为两类
- **可恢复错误**, 通常用于从系统全局角度来看可以接受的错误,例如处理用户的访问、操作等错误,这些错误只会影响某个用户自身的操作进程,而不会对系统的全局稳定性产生影响
- **不可恢复错误**,刚好相反,该错误通常是全局性或者系统性的错误,例如数组越界访问,系统启动时发生了影响启动流程的错误等等,这些错误的影响往往对于系统来说是致命的
很多编程语言并不会区分这些错误而是直接采用异常的方式去处理。Rust没有异常但是Rust也有自己的卧龙凤雏`Result<T,e>`用于可恢复错误,`panic!`用于不可恢复错误。

@ -0,0 +1,148 @@
# 不可恢复错误panic!
在正式开始之前,先来思考一个问题: 假设我们想要从文件读取数据,如果失败,你有没有好的办法通知调用者为何失败?如果成功,你有没有好的办法把读取的结果返还给调用者?
## panic!与不可恢复错误
上面的问题在真实场景,其实挺复杂的,让我们先做一个假设:文件读取操作发生在系统启动阶段。那么可以轻易得出一个结论,一旦文件读取失败,那么系统启动也将失败,这意味着该失败是不可恢复的错误,无论是因为文件不存在还是操作系统硬盘的问题,这些只是错误的原因不同,但是归根到底都是不可恢复的错误(梳理清楚当前场景的错误类型非常重要).
既然是不可恢复错误那么一旦发生只需让程序崩溃即可。对此Rust为我们提供了`panic!`宏,当调用执行该宏时,**程序会打印出一个错误信息,展开报错点往前的函数调用堆栈,最后退出程序**.
切记,一定是不可恢复的错误,才调用`panic!`处理,你总不想系统仅仅因为用户随便传入一个非法参数就崩溃吧?所以,**只有当你不知道该如何处理时再去调用panic!**.
## 调用panic!
首先,来调用一下`panic!`,这里使用了最简单的代码实现,实际上你在程序的任何地方都可以这样调用:
```rust
fn main() {
panic!("crash and burn");
}
```
运行后输出:
```console
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`
下面让我们针对第二点进行详细展开讲解。
## backtrace栈展开
在真实场景中,错误往往涉及到很长的调用链甚至会深入第三方库,如果没有栈展开技术,错误将难以跟踪处理,下面我们来看一个真实的崩溃例子:
```rust
fn main() {
let v = vec![1, 2, 3];
v[99];
}
```
上面的代码很简单,数组只有`3`个元素,我们却尝试去访问它的第`100`号元素(数组索引从`0`开始),那自然会崩溃。
我们的读者里不乏正义之士,此时肯定要质疑,一个简单的数组越界访问,为何要直接让程序崩溃?是不是有些大题小做了?
如果有过C语言的经验即使你越界了问题不大我依然尝试去访问至于这个值是不是你想要的(`100`号内存地址也有可能有值,只不过是其它变量或者程序的!),抱歉,不归我管,我只负责取,你要负责管理好自己的索引访问范围。上面这种情况被称为**缓冲区溢出**,并可能会导致安全漏洞,例如攻击者可以通过索引来访问到数组后面不被允许的数据。
说实话我宁愿程序崩溃为什么当你取到了一个不属于你的值这在很多时候会导致程序上的逻辑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`来再一次运行程序:
```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:
0: rust_begin_unwind
at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35/library/std/src/panicking.rs:517:5
1: core::panicking::panic_fmt
at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35/library/core/src/panicking.rs:101:14
2: core::panicking::panic_bounds_check
at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35/library/core/src/panicking.rs:77:5
3: <usize as core::slice::index::SliceIndex<[T]>>::index
at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35/library/core/src/slice/index.rs:184:10
4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35/library/core/src/slice/index.rs:15:9
5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35/library/alloc/src/vec/mod.rs:2465:9
6: world_hello::main
at ./src/main.rs:4:5
7: core::ops::function::FnOnce::call_once
at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35/library/core/src/ops/function.rs:227:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
```
上面的代码就是一次栈展开(也称栈回溯),它包含了函数调用的顺序,当然按照逆序排列:最近调用的函数排在列表的最上方. 因为咱们的`main`函数基本是最先调用的函数了,所以排在了倒数第二位,还有一个关注点,排在最顶部最后一个调用的函数是`rust_begin_unwind`,该函数的目的就是进行栈展开,呈现这些列表信息给我们。
要获取到栈回溯信息,你还需要开启`debug`标志,该标志在使用`cargo run`或者`cargo build`时自动开启(这两个方式是`Debug`运行方式). 同时栈展开信息在不同操作系统或者Rust版本上也所有不同。
## panic时的两种终止方式
当出现`panic!`时,程序提供了两种方式来处理终止流程: **栈展开** 和 **直接终止**.
其中,默认的方式就是`栈展开`这意味着Rust会回溯栈上数据和函数调用因此也意味着更多的善后工作好处是给于充分的报错信息和栈调用信息便于事后的问题复盘。`直接终止`,顾名思义,不清理数据就直接推出程序,善后工作交与操作系统来负责。
对于绝大多数用户,使用默认选择是最好的,但是当你关心最终编译出的二进制可执行文件大小时,那么可以尝试去使用直接终止的方式,例如下面的配置修改`Cargo.toml`文件,实现在[`release`](../first-try/cargo.md#手动编译和运行项目)模式下遇到`panic`直接终止:
```rust
[profile.release]
panic = 'abort'
```
## 何时该使用panic!
下面让我们大概罗列下合适适合使用`panic`,虽然原则上,你理解了之前的内容后,会自己作出合适的选择,但是罗列出来可以帮助你强化这一点。
先来一点背景知识,在前面章节我们粗略讲过`Result<T,E>`这个枚举类型,它是用来表示函数的返回结果:
```rust
enum Result<T, E> {
Ok(T),
Err(E),
}
```
当没有错误发生时,函数返回一个用`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`。
因此`unwrap`简而言之:成功则返回值,失败则`panic`, 总之不进行任何错误处理。
#### 示例、原型、测试
这些场景,需要快速的搭建代码,错误处理反而会拖慢实现速度,也不是特别有必要,因此通过`unrap`、`expect`等方法来处理是最快的。
同时,当我们准备做错误处理时,全局搜索这些方法,也可以不遗漏的进行替换。
#### 你确切的知道你的程序是正确时可以使用panic
因为`panic`的触发方式比错误处理要简单,因此可以让代码更清晰,可读性也更加好,当我们的代码注定是正确时,你可以用`unrawp`等方法直接进行处理,反正也不可能`panic`
```rust
use std::net::IpAddr;
let home: IpAddr = "127.0.0.1".parse().unwrap();
```
例如上面的例子,`"127.0.0.1"`就是`ip`地址,因此我们知道`parse`方法一定会成功,那么就可以直接用`unwrap`方法进行处理。
当然,如果该字符串是来自于用户输入,那在实际项目中,就必须用错误处理的方式,而不是`unwrap`,否则你的程序一天要崩溃几十万次吧!
#### 可能导致全局有害状态时
有害状态大概分为几类:
- 非预期的错误
- 之后代码的运行会受到明显可见的影响
- 内存安全的问题
当错误预期会出现时返回一个错误较为合适例如解析器接收到格式错误的数据HTTP请求接收到错误的参数甚至该请求内的任何错误(不会导致整个程序有问题,只影响该此请求)。 **因为错误是可预期的,因此也是可以处理的**。
当启动时某个流程发生了错误,导致了后续代码的允许造成影响,那么就应该使用`panic`,而不是处理错误后,继续运行,当然你可以通过重试的方式来继续。
上面提到过,数组访问越界,就要`panic`的原因,这个就是属于内存安全的范畴,一旦内存访问不安全,那么我们无法保证自己的程序会怎么运行下去,也无法保证逻辑和数据的正确性。
## panic原理剖析
Not sure exactly what you're asking, but I'll try to describe the panic mechanism as I understand it. I'm sure folks will correct my mistakes.
When you call the panic!() macro it formats the panic message and then calls std::panic::panic_any() with the message as the argument. panic_any() first checks to see if there's a "panic hook" installed by the application: if so, the hook function is called. Assuming that the hook function returns, the unwinding of the current thread begins with the parent stack frame of the caller of panic_any(). If the registers or the stack are messed up, likely trying to start unwinding will cause an exception of some kind, at which point the thread will be aborted instead of unwinding.
Unwinding is a process of walking back up the stack, frame-by-frame. At each frame, any data owned by the frame is dropped. (I believe things are dropped in reverse static order, as they would be at the end of a function.)
One exceptional case during unwinding is that the unwinding may hit a frame marked as "catching" the unwind via std::panic::catch_unwind(). If so, the supplied catch function is called and unwinding ceases: the catching frame may continue the unwinding with std::panic::resume_unwind() or not.
Another exceptional case during unwinding is that some drop may itself panic. In this case the unwinding thread is aborted.
Once unwinding of a thread is aborted or completed (no more frames to unwind), the outcome depends on which thread panicked. For the main thread, the operating environment's abort functionality is invoked to terminate the panicking process via core::intrinsics::abort(). Child threads, on the other hand, are simply terminated and can be collected later with std::thread::join()

@ -0,0 +1,321 @@
# 可恢复的错误Result
还记得上一节中,提到的关于文件读取的思考题吧?当时我们解决了读取中如果遇到不可恢复错误该怎么处理,现在来看看,读取过程中,正常返回和遇到可以恢复的错误时该如何处理。
假设我们有一台消息服务器每个用户都通过websocket连接到该服务器来接收和发送消息该过程就涉及到socket文件的读写那么此时如果一个用户的读写发生了错误显然不能直接panic否则服务器会直接崩溃所有用户都会断开连接因此我们需要一种更温和的错误处理方式: `Result<T,E>`.
之前章节有提到过,`Result<T,E>`是一个枚举类型,定义如下:
```rust
enum Result<T, E> {
Ok(T),
Err(E),
}
```
泛型参数`T`代表成功时存入的正确值,存放方式是`Ok(T)``E`代表错误是存入的错误值,存放方式是`Err(E)`,枯燥的讲解永远不及代码生动准确,因此先来看下打开文件的例子:
```rust
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
}
```
以上`File::open`返回一个`Result`类型, 那么问题来了:
> #### 如何获知变量类型或者函数的返回类型
>
> 有几种常用的方式:
> - 第一种是查询标准库或者三方库文档,搜索`File`,然后找到它的`open`方法,但是此处更推荐第二种方法:
> - 在[Rust IDE](../../first-try/editor.md)章节,我们推荐了`VSCode` IED和`rust-analyze`插件,如果你成功安装的话,那么就可以在`VScode`中很方便的通过代码跳转的方式查看代码,同时`rust-analyze`插件还会对代码中的类型进行标注,非常方便好用!
> - 你还可以尝试故意标记一个错误的类型,然后让编译器告诉你:
```rust
let f: u32 = File::open("hello.txt");
```
错误提示如下:
```console
error[E0308]: mismatched types
--> src/main.rs:4:18
|
4 | let f: u32 = File::open("hello.txt");
| ^^^^^^^^^^^^^^^^^^^^^^^ expected u32, found enum
`std::result::Result`
|
= note: expected type `u32`
found type `std::result::Result<std::fs::File, std::io::Error>`
```
上面代码,故意将`f`类型标记成整形,编译器立刻不乐意了,你是在忽悠我吗?打开文件操作返回一个整形?来,大哥来告诉你返回什么:`std::result::Result<std::fs::File, std::io::Error>`,我的天呐,怎么这么长的类型!
别慌,其实很简单,首先`Result`本身是定义在`std::result`中的,但是因为`Result`很常用,就被包含在了[`prelude`](../../appendix/prelude.md)中(将常用的东东提前引入到当前作用域内),因此无需手动引入`std::result::Result`,那么返回类型可以简化为`Result<std::fs::File,std::io::Error>`,你看看是不是很像标准的`Result<T,E>`枚举定义?只不过`T`被替换成了具体的类型`std::fs::File`,是一个文件句柄类型,`E`被替换成`std::io::Error`是一个IO错误类型.
这个返回值类型说明`File::open`调用如果成功则返回一个可以进行读写的文件句柄如果失败则返回一个IO错误: 文件不存在或者没有访问文件的权限等。总之`File::open`需要一个方式告知调用者是成功还是失败,并同时返回具体的文件句柄(成功)或错误信息(失败), 万幸的是,这些信息`Result`枚举可以提供:
```rust
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => {
panic!("Problem opening the file: {:?}", error)
},
};
}
```
代码很清晰,对打开文件后的`Result<T,E>`类型进行匹配取值,如果是成功,则将`Ok(file)`中存放的的文件句柄`file`赋值给`f`,如果失败,则将`Err(error)`中存放的错误信息`error`使用`panic`抛出来,进而结束程序,这非常符合上文提到过的`panic`使用场景。
好吧,也没有那么合理:)
## 对返回的错误进行处理
直接`panic`还是过于粗暴因为实际上IO的错误有很多种我们需要对部分错误进行特殊处理而不是所有错误都直接崩溃:
```rust
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {:?}", e),
},
other_error => panic!("Problem opening the file: {:?}", other_error),
},
};
}
```
上面代码在匹配出`error`后,又对`error`进行了详细的匹配解析,最终结果:
- 如果是文件不存在错误`ErrorKind::NotFound`,就创建文件,这里创建文件`File::create`也是返回`Result`,因此继续用`match`对其进行处理:创建成功,将新的文件句柄赋值给`f`,如果失败,则`panic`
- 剩下的错误,一律`panic`
虽然很清晰,但是代码还是有些啰嗦,我们会在[简化错误处理](../../errors/simplify.md)一章重点讲述如何写出更优雅的错误.
## 失败就panic: unwrap和expect
上一节中,已经看到过这两兄弟的简单介绍,这里再来回顾下。
在不需要处理错误的场景,例如写原型、示例时,我们不想使用`match`去匹配`Result<T,E>`以获取其中的`T`值,因为`match`的穷尽匹配特性,你总要去处理下`Err`分支。那么有没有办法简化这个过程?有,答案就是`unwrap`和`expect`。
它们的作用就是,如果返回成功,就将`Ok(T)`中的值取出来,如果失败,就直接`panic`真的勇士决不多BB直接崩溃.
```rust
use std::fs::File;
fn main() {
let f = File::open("hello.txt").unwrap();
}
```
如果调用这段代码时不存在 *hello.txt* 文件,那么`unwrap`就将直接`panic`
```console
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:37
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```
`expect`跟`unwrap`很像,只不过它允许指定`panic!`时的报错信息:
```rust
use std::fs::File;
fn main() {
let f = File::open("hello.txt").expect("Failed to open hello.txt");
}
```
报错如下
```console
thread 'main' panicked at 'Failed to open hello.txt: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:37
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```
可以看出,`expect`相比`unwrap`能提供更精确的错误信息,在有些场景也会更加实用。
## 传播错误
咱们的程序几乎不太可能只有`A->B`形式的函数调用,一个设计良好的程序,一个功能涉及十来层的函数调用都有可能。而错误处理也往往不是哪里调用出错,就在哪里处理,实际应用中,大概率会把错误层层上传然后交给调用链的上游函数进行处理,由此,错误传播将极为常见.
例如以下函数从文件中读取用户名,然后将结果进行返回:
```rust
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
// 打开文件f是`Result<文件句柄,io::Error>`
let f = File::open("hello.txt");
let mut f = match f {
// 打开文件成功将file句柄赋值给f
Ok(file) => file,
// 打开文件失败,将错误返回(向上传播)
Err(e) => return Err(e),
};
// 创建动态字符串s
let mut s = String::new();
// 从f文件句柄读取数据并写入s中
match f.read_to_string(&mut s) {
// 读取成功返回Ok封装的字符串
Ok(_) => Ok(s),
// 将错误向上传播
Err(e) => Err(e),
}
}
```
有几点值得注意:
- 该函数返回一个`Result<String, io::Error>`类型,当读取用户名成功时,返回`Ok(String)`,失败时,返回`Err(io:Error)`
- `File::open`和`f.read_to_string`返回的`Result<T,E>`中的`E`就是`io::Error`
由此可见,该函数将`io::Error`的错误往上进行传播, 该函数的调用者最终会对`Result<String,io::Error>`进行再处理,至于怎么处理就是调用者的事,如果是错误,它可以选择继续向上传播错误,也可以直接`panic`亦或将具体的错误原因包装后写入socket中呈现给终端用户。
但是上面的代码也有自己的问题,那就是太长了(优秀的程序员身上的优点极多,其中最大的优点就是*懒*),我自认为也有那么一点点优秀,因此见不到这么啰嗦的代码,下面咱们来讲讲如何简化它。
### 传播界的大明星: ?
大明星出场,必需得有排面,来看看`?`的排面:
```rust
use std::fs::File;
use std::io;
use std::io::Read;
fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
```
看到没,这就是排面,相比前面的`match`处理错误的函数,代码直接减少了一半不止,但是,一山更比一山难,看不懂啊!
其实`?`就是一个宏,它的作用跟上面的`match`几乎一模一样:
```rust
let mut f = match f {
// 打开文件成功将file句柄赋值给f
Ok(file) => file,
// 打开文件失败,将错误返回(向上传播)
Err(e) => return Err(e),
};
```
如果结果是`Ok(T)`,则把`T`赋值给`f`,如果结果是`Err(E)`,则返回该错误,所以`?`特别适合用来传播错误。
虽然`?`和`match`功能一致,但是事实上`?`会更胜一筹。何解?
想象一下,一个设计良好的系统中,肯定有自定义的错误特征,错误之间很可能会存在上下级关系,例如标准库中的`std::io::Error`和`std::error::Error`前者是io相关的错误结构体后者是一个最最通用的标准错误特征同时前者实现了后者因此`std::io::Error`可以转换为`std:error::Error`。
明白了以上的错误转换,`?`的更胜一筹就很好理解了,它可以自动进行类型提升:
```rust
fn open_file() -> Result<File, Box<dyn std::error::Error>> {
let mut f = File::open("hello.txt")?;
Ok(f)
}
```
上面代码中`File::open`报错时返回的错误是`std::io::Error`类型,但是`open_file`函数返回的错误类型是`std::error::Error`的特征对象,可以看到一个错误类型通过`?`返回后,变成了另一个错误类型,这就是`?`的神奇之处。
根本原因是在于标准库中定义的`From`特征,该特征有一个方法`from`,该方法用于把一个类型转成另外一个类型,`?`可以自动调用该方法,然后进行隐式类型转换。因此只要函数返回的错误`ReturnError`实现了`From<OtherError>`特征,那么`?`就会自动把`OtherError`转换为`ReturnError`。
这种转换非常好用,意味着你可以用一个大而全的`ReturnError`来覆盖所有错误类型,只需要为各种子错误类型实现这种转换即可。
强中自有强中手,一码更比一码短:
```rust
use std::fs::File;
use std::io;
use std::io::Read;
fn read_username_from_file() -> Result<String, io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}
```
瞧见没? `?`还能实现链式调用,`File::open`遇到错误就返回,没有错误就将`Ok`中的值取出来用于下一个方法调用简直太精妙了从Go语言过来的我内心狂喜(其实学Rust的苦和痛我才不会告诉你们)。
不仅有更强,还要有最强,我不信还有人比我更短((不要误解)
```rust
use std::fs;
use std::io;
fn read_username_from_file() -> Result<String, io::Error> {
// read_to_string是定义在std::io中的方法因此需要在上面进行引用
fs::read_to_string("hello.txt")
}
```
从文件读取数据到字符串中是比较常见的操作因此Rust标准库为我们提供了`fs::read_to_string`函数,该函数内部会打开一个文件、创建`String`、读取文件内容最后写入字符串并返回,因为该函数无法帮助我们学习该章的内容,因此放在最后来讲,其实只是我想震你们一下:)
#### ?用于Option的返回
`?`不仅仅可以用于`Result`的传播,还能用于`Option`的传播,再来回忆下`Option`的定义:
```rust
pub enum Option<T> {
Some(T),
None
}
```
`Result`通过`?`返回错误,那么`Option`就通过`?`返回`None`
```rust
fn first(arr: &[i32]) -> Option<&i32> {
let v = arr.get(0)?;
Some(v)
}
```
上面的函数中,`arr.get`返回一个`Option<&i32>`类型,因为`?`的使用,如果`get`的结果是`None`,则直接返回`None`,如果是`Some(&i32)`,则把里面的值赋给`v`。
其实这个函数有些画蛇添足,我们完全可以写出更简单的版本:
```rust
fn first(arr: &[i32]) -> Option<&i32> {
arr.get(0)
}
```
有一句话怎么说?没有需求,制造需求也要上。。。大家别跟我学习,这是软件开发大忌. 只能用代码洗洗眼了:
```rust
fn last_char_of_first_line(text: &str) -> Option<char> {
text.lines().next()?.chars().last()
}
```
上面代码展示了在链式调用中使用`?`提前返回`None`的用法,`.next`方法返回的是`Option`类型:如果返回`Some(&str)`,那么继续调用`chars`方法,如果返回`None`,则直接从整个函数中返回`None`,不再继续进行链式调用.
#### 新手用?常会犯的错误
初学者在用`?`时,老是会犯错,例如写出这样的代码:
```rust
fn first(arr: &[i32]) -> Option<&i32> {
arr.get(0)?
}
```
这段代码无法通过编译,切记:`?`操作符需要一个变量来承载正确的值,只有错误值能直接返回,正确的值不行。因此`?`只能用于以下形式:
- `let v = xxx()?;`
- `xxx()?.yyy()?;`
#### 带返回值的main函数
因为刚才讲的`?`使用限制,这段代码你很容易看出它无法编译:
```rust
use std::fs::File;
fn main() {
let f = File::open("hello.txt")?;
}
```
因为`?`要求`Result<T,E>`形式的返回值,而`main`函数的返回是`()`,因此无法满足,那是不是就无解了呢?
实际上Rust还支持另外一种形式的`main`函数:
```rust
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let f = File::open("hello.txt")?;
Ok(())
}
```
这样就能使用`?`提前返回了,同时我们又一次看到了`Box<dyn Error>`特征对象,因为`std::error:Error`是Rust中抽象层次最高的错误其它标准库中的错误都实现了该特征因此我们可以用该特征对象代表一切错误就算`main`函数中调用任何标准库函数发生错误,都可以通过`Box<dyn Error>`这个特征对象进行返回.
至于`main`函数可以有多种返回值,那是因为实现了[std::process::Termination]特征,目前为止该特征还没进入稳定版Rust中也许未来你可以为自己的类型实现该特征
至此Rust的基础内容学习已经全部完成下面我们将学习Rust的高级进阶内容正式开启你的高手之路。

@ -1,6 +1,6 @@
# 进一步了解特征
# 深入了解特征
特征之于Rust更甚于接口之于其他语言因此特征在Rust中很重要也相对较为复杂我们决定把特征分为两篇进行介绍该篇就是关于特征的进阶篇,会讲述一些你不常用到但是该了解的特性。
特征之于Rust更甚于接口之于其他语言因此特征在Rust中很重要也相对较为复杂我们决定把特征分为两篇进行介绍[第一篇](./trait.md)在之前已经讲过,现在就是第二篇:关于特征的进阶篇,会讲述一些你不常用到但是该了解的特性。
## 关联类型
在方法一章中,我们将到了[关联函数](../method.md#关联函数),但是实际上关联类型和关联函数并没有任何交集,虽然它们的名字有一半的交集。
@ -341,20 +341,23 @@ impl fmt::Display for Point {
`newtype`不仅仅能实现以上的功能,而且它在运行时没有任何性能损耗,因为在编译期,该类型会被自动忽略。
下面来看一个例子,我们有一个动态数组类型:`Vec<T>`,它定义在标准库中,还有一个特征`Display`,它也定义在标准库中,如果没有`newtype`,我们是无法为`Vec<T>`实现`Display`的:
```rust
error[E0117]: only traits defined in the current crate can be implemented for arbitrary types
--> src/main.rs:5:1
|
```console
error[E0117]: only traits defined in the current crate can be implemented for arbitrary types
--> src/main.rs:5:1
|
5 | impl<T> std::fmt::Display for Vec<T> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^------
| | |
| | `Vec` is not defined in the current crate
| impl doesn't use only types from inside the current crate
|
= note: define and implement a trait or new type instead
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^------
| | |
| | Vec is not defined in the current crate
| impl doesn't use only types from inside the current crate
|
= note: define and implement a trait or new type instead
```
编译器给了我们提示:`define and implement a trait or new type instead`,重新定义一个特征,或者使用`new type`,前者当然不可行,那么来试试后者:
```rust
use std::fmt;

@ -394,6 +394,30 @@ fn main() {
详细的`derive`列表参加[附录-派生特征](../../appendix/derive.md).
## 调用方法需要引入特征
在一些场景中,使用`as`关键字做类型转换会有比较大的限制,因为你想要在类型转换上拥有完全的控制,例如处理转换错误,那么你将需要`TryInto`:
```rust
use std::convert::TryInto;
fn main() {
let a: i32 = 10;
let b: u16 = 100;
let b_ = b.try_into()
.unwrap();
if a < b_ {
println!("Ten is less than one hundred.");
}
}
```
上面代码中引入了`std::convert::TryInto`特征,但是却没有使用它,可能有些同学会为此困惑,主要原因在于**如果你要使用一个特征的方法,那么你需要引入该特征到当前的作用域中**,我们在上面用到了`try_into`方法,因此需要引入对应的特征。
但是Rust又提供了一个非常便利的办法即把最常用的标准库中的特征通过[`std::prelude`](std::convert::TryInto)模块提前引入到当前作用域中,其中包括了`std::convert::TryInto`,你可以尝试删除第一行的代码`use ...`,看看是否会报错.
## 几个综合例子
#### 为自定义类型实现`+`操作

@ -1,29 +0,0 @@
# 类型转换
In some cases, using the as keyword is too restrictive. Its possible to regain fuller control over the type conversion process at the cost of introducing some bureaucracy. The following listing shows a Rust method to use instead of the as keyword when the conversion might fail.
```rust
use std::convert::TryInto;
fn main() {
let a: i32 = 10;
let b: u16 = 100;
let b_ = b.try_into()
.unwrap();
if a < b_ {
println!("Ten is less than one hundred.");
}
}
```
Listing 2.5 introduces two new Rust concepts: traits and error handling. On line 1, the use keyword brings the std::convert::TryInto trait into local scope. This unlocks the try_into() method of the b variable. Well bypass a full explanation of why this occurs for now. In the meantime, consider a trait as a collection of methods. If you are from an object-oriented background, traits can be thought of as abstract classes or interfaces. If your programming experience is in functional languages, you can think of traits as type classes.
Line 7 provides a glimpse of error handling in Rust. b.try_into() returns an i32 value wrapped within a Result. Result is introduced properly in chapter 3. It can contain either a success value or an error value. The unwrap() method can handle the success value and returns the value of b as an i32 here. If the conversion between u16 and i32 were to fail, then calling unsafe() would crash the program. As the book progresses, you will learn safer ways of dealing with Result rather than risking the programs stability!
A distinguishing characteristic of Rust is that it only allows a types methods to be called when the trait is within local scope. An implicit prelude enables common operations such as addition and assignment to be used without explicit imports.
> TIP
>
> To understand what is included in local scope by default, you should investigate the std::prelude module. Its >documentation is available online at https://doc.rust-lang.org/std/prelude/index.html.

@ -1 +1,6 @@
# 项目结构
## main函数的正确位置
目前发现部分人会搞不懂main函数应该放在哪个文件中结果导致编译器报错找不到main函数

@ -1,11 +0,0 @@
# panic!
## panic背后的原理
Not sure exactly what you're asking, but I'll try to describe the panic mechanism as I understand it. I'm sure folks will correct my mistakes.
When you call the panic!() macro it formats the panic message and then calls std::panic::panic_any() with the message as the argument. panic_any() first checks to see if there's a "panic hook" installed by the application: if so, the hook function is called. Assuming that the hook function returns, the unwinding of the current thread begins with the parent stack frame of the caller of panic_any(). If the registers or the stack are messed up, likely trying to start unwinding will cause an exception of some kind, at which point the thread will be aborted instead of unwinding.
Unwinding is a process of walking back up the stack, frame-by-frame. At each frame, any data owned by the frame is dropped. (I believe things are dropped in reverse static order, as they would be at the end of a function.)
One exceptional case during unwinding is that the unwinding may hit a frame marked as "catching" the unwind via std::panic::catch_unwind(). If so, the supplied catch function is called and unwinding ceases: the catching frame may continue the unwinding with std::panic::resume_unwind() or not.
Another exceptional case during unwinding is that some drop may itself panic. In this case the unwinding thread is aborted.
Once unwinding of a thread is aborted or completed (no more frames to unwind), the outcome depends on which thread panicked. For the main thread, the operating environment's abort functionality is invoked to terminate the panicking process via core::intrinsics::abort(). Child threads, on the other hand, are simply terminated and can be collected later with std::thread::join()

@ -1 +0,0 @@
# 适用Result返回错误

@ -0,0 +1 @@
# 简化错误处理

@ -14,7 +14,10 @@
- [示例](https://github.com/barabadzhi/rust-in-action)
8. [Rust高级编程](https://learnku.com/docs/nomicon/2018/310-phantom-data/4721?show_current_version=yes)
[Rust nomicon](https://doc.rust-lang.org/nomicon/dot-operator.html)
9. [Rust Primer](https://rustcc.gitbooks.io/rustprimer/content/1st-glance/)
10. [Rust Forge](https://forge.rust-lang.org/infra/other-installation-methods.html#other-rust-installation-methods)
10. [Rust Forge](https://forge.rust-lang.org/infra/other-installation-methods.html#other-rust-installation-methods)
11. [rustc开发者之书](https://rustc-dev-guide.rust-lang.org/method-lookup.html)

@ -4,4 +4,6 @@
## crates
1. https://github.com/Kimundi/owning-ref-rs
2. https://github.com/joshua-maros/ouroboros
2. https://github.com/joshua-maros/ouroboros
https://www.reddit.com/r/learnrust/comments/rf4qdz/is_it_possible_to_hold_an_arena_and_references/
Loading…
Cancel
Save