Merge pull request #439 from 1132719438/main

Some fixes in advance and test chapter
pull/443/head
孙飞Sunface 3 years ago committed by GitHub
commit 867e60ae21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,15 +1,15 @@
# 整数转换为枚举
在Rust中从枚举到整数的转换很容易但是反过来就没那么容易甚至部分实现还挺邪恶, 例如使用`transmute`。
Rust 中,从枚举到整数的转换很容易,但是反过来,就没那么容易,甚至部分实现还挺邪恶, 例如使用`transmute`。
## 一个真实场景的需求
在实际场景中,从枚举到整数的转换有时还是非常需要的,例如你有一个枚举类型,然后需要从外面穿入一个整数,用于控制后续的流程走向,此时就需要用整数去匹配相应的枚举(你也可以用整数匹配整数-, -,看看会不会被喷)。
在实际场景中,从枚举到整数的转换有时还是非常需要的,例如你有一个枚举类型,然后需要从外面入一个整数,用于控制后续的流程走向,此时就需要用整数去匹配相应的枚举(你也可以用整数匹配整数-, -,看看会不会被喷)。
既然有了需求,剩下的就是看看该如何实现,这篇文章的水远比你想象的要深,且看八仙过海各显神通。
## C语言的实现
对于C语言来说万物皆邪恶因此我们不讨论安全只看实现不得不说很简洁
对于 C 语言来说,万物皆邪恶,因此我们不讨论安全,只看实现,不得不说很简洁:
```C
#include <stdio.h>
@ -32,7 +32,7 @@ int main(void)
}
```
但是在Rust中以下代码
但是在 Rust 中,以下代码:
```rust
enum MyEnum {
A = 1,
@ -57,7 +57,7 @@ fn main() {
就会报错: `MyEnum::A => {} mismatched types, expected i32, found enum MyEnum`
## 使用三方库
首先可以想到的肯定是三方库毕竟Rust的生态目前已经发展的很不错类似的需求总是有的这里我们先使用`num-traits`和`num-derive`来试试。
首先可以想到的肯定是三方库,毕竟 Rust 的生态目前已经发展的很不错,类似的需求总是有的,这里我们先使用`num-traits`和`num-derive`来试试。
在`Cargo.toml`中引入:
```toml
@ -93,7 +93,7 @@ fn main() {
除了上面的库,还可以使用一个较新的库: [`num_enums`](https://github.com/illicitonion/num_enum)。
## TryFrom + 宏
在Rust1.34后,可以实现`TryFrom`特征来做转换:
Rust 1.34 后,可以实现`TryFrom`特征来做转换:
```rust
use std::convert::TryFrom;
@ -180,7 +180,7 @@ enum MyEnum {
fn main() {
let x = MyEnum::C;
let y = x as i32;
let z: MyEnum = unsafe { ::std::mem::transmute(y) };
let z: MyEnum = unsafe { std::mem::transmute(y) };
// match the enum that came from an int
match z {
@ -191,10 +191,10 @@ fn main() {
}
```
既然是邪恶之王当然得有真本事无需标准库、也无需unstable的Rust版本我们就完成了转换awesome!??
既然是邪恶之王,当然得有真本事,无需标准库、也无需 unstable Rust 版本我们就完成了转换awesome!??
## 总结
本文列举了常用(其实差不多也是全部了还有一个unstable特性没提到)的从整数转换为枚举的方式,推荐度按照出现的先后顺序递减。
本文列举了常用(其实差不多也是全部了,还有一个 unstable 特性没提到)的从整数转换为枚举的方式,推荐度按照出现的先后顺序递减。
但是推荐度最低,不代表它就没有出场的机会,只要使用边界清晰,一样可以大光彩,例如最后的`transmute`函数.
但是推荐度最低,不代表它就没有出场的机会,只要使用边界清晰,一样可以大光彩,例如最后的`transmute`函数.

@ -1,2 +1,2 @@
# 进阶类型转换
Rust是强类型语言同时也是强安全语言因此这些决定了Rust的类型转换注定比一般语言要更困难再加上Rust的繁多的类型和类型转换特征用于很难对这块内容了若指掌因此我们专门整了一个专题来讨论Rust中那些不太容易的类型转换, 容易的请看[这一章](../basic/converse.md).
Rust 是强类型语言,同时也是强安全语言,这些特性导致了 Rust 的类型转换注定比一般语言要更困难,再加上 Rust 的繁多的类型和类型转换特征,因此大家很难对这块内容了如指掌,为此我们专门整了一个专题来讨论 Rust 中那些不太容易的类型转换, 容易的请看[这一章](../basic/converse.md).

@ -16,13 +16,13 @@ fn main() {
虽然三种使用形式皆可,但是 Rust 内置的宏都有自己约定俗成的使用方式,例如 `vec![...]`、`assert_eq!(...)` 等。
在 Rust 中宏分为两大类:声明式宏 `macro_rules!` 和三种过程宏( *procedural macros* ):
在 Rust 中宏分为两大类:**声明式宏( *declarative macros* )** `macro_rules!` 和三种**过程宏( *procedural macros* )**:
- `#[derive]`,在之前多次见到的派生宏,可以为目标结构体或枚举派生指定的代码,例如 `Debug` 特征
- 类属性宏(Attribute-like macro),用于为目标添加自定义的属性
- 类函数宏(Function-like macro),看上去就像是函数调用
如果感觉难以理解,也不必担心,接下来我们将逐个看看它们的庐山真面目,在之前,先来看下为何需要宏,特别是 Rust 的函数明明已经很强大了。
如果感觉难以理解,也不必担心,接下来我们将逐个看看它们的庐山真面目,在之前,先来看下为何需要宏,特别是 Rust 的函数明明已经很强大了。
## 宏和函数的区别
宏和函数的区别并不少,而且对于宏擅长的领域,函数其实是有些无能为力的。
@ -30,7 +30,7 @@ fn main() {
#### 元编程
从根本上来说,宏是通过一种代码来生成另一种代码,如果大家熟悉元编程,就会发现两者的共同点。
在[附录 D](https://course.rs/appendix/derive.html)讲到的 `derive` 属性,就会自动为结构体派生出相应特征所需的代码,例如 `#[derive(Debug)]`,还有熟悉的 `println!``vec!`,所有的这些宏都会展开成相应的代码,且很可能是长得多的代码。
在[附录 D](https://course.rs/appendix/derive.html)讲到的 `derive` 属性,就会自动为结构体派生出相应特征所需的代码,例如 `#[derive(Debug)]`,还有熟悉的 `println!``vec!`,所有的这些宏都会展开成相应的代码,且很可能是长得多的代码。
总之,元编程可以帮我们减少所需编写的代码,也可以一定程度上减少维护的成本,虽然函数复用也有类似的作用,但是宏依然拥有自己独特的优势。
@ -113,7 +113,7 @@ macro_rules! vec {
1. `$()` 中包含的是模式 `$x:expr`,该模式中的 `expr` 表示会匹配任何 Rust 表达式,并给予该模式一个名称 `$x`
2. 因此 `$x` 模式可以跟整数 `1` 进行匹配,也可以跟字符串 "hello" 进行匹配: `vec!["hello", "world"]`
3. `$()` 之后的逗号,意味着`1` 和 `2` 之间可以使用逗号进行分割,也意味着 `3` 既可以没有逗号,也可以有逗号:`vec![1, 2, 3,]`
4. `*` 说明之前的模式可以出现次也可以任意次,这里出现了三次
4. `*` 说明之前的模式可以出现次也可以任意次,这里出现了三次
接下来,我们再来看看与模式相关联、在 `=>` 之后的代码:
```rust
@ -158,7 +158,7 @@ let v = {
由于绝大多数 Rust 开发者都是宏的用户而不是编写者,因此在这里我们不会对 `macro_rules` 进行更深入的学习,如果大家感兴趣,可以看看这本书 [ “The Little Book of Rust Macros”](https://veykril.github.io/tlborm/)。
## 用过程宏为属性标记生成代码
第二种常用的宏就是[*过程宏*]( *procedural macros* ),从形式上来看,过程宏跟函数较为相像,但过程宏是使用源代码作为输入参数,基于代码进行一系列操作后,再输出一段全新的代码。**注意,过程宏输出的代码并不会替换之前的代码,这一点与声明宏有很大的不同!**
第二种常用的宏就是[*过程宏*](https://doc.rust-lang.org/reference/procedural-macros.html) ( *procedural macros* ),从形式上来看,过程宏跟函数较为相像,但过程宏是使用源代码作为输入参数,基于代码进行一系列操作后,再输出一段全新的代码。**注意,过程宏输出的代码并不会替换之前的代码,这一点与声明宏有很大的不同!**
至于前文提到的过程宏的三种类型(自定义 `derive`、属性宏、函数宏),它们的工作方式都是类似的。
@ -282,7 +282,7 @@ hello_macro
由于过程宏所在的包跟我们的项目紧密相连,因此将它放在项目之中。现在,问题又来了,该如何在项目的 `src/main.rs` 中引用 `hello_macro_derive` 包的内容?
方法有两种,第一种是将 `hello_macro_derive` 发布到 `crates.io``github` 中,就像我们引用的其它依赖一样;另一种就是使用相对路径引入的本地化方式,修改 `hello_macro/Cargo.tom` 文件添加以下内容:
方法有两种,第一种是将 `hello_macro_derive` 发布到 `crates.io``github` 中,就像我们引用的其它依赖一样;另一种就是使用相对路径引入的本地化方式,修改 `hello_macro/Cargo.toml` 文件添加以下内容:
```toml
[dependencies]
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }
@ -334,9 +334,9 @@ pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
由于我们为 `hello_macro_derive` 函数标记了 `#[proc_macro_derive(HelloMacro)]`,当用户使用 `#[derive(HelloMacro)]` 标记了他的类型后,`hello_macro_derive` 函数就将被调用。这里的秘诀就是特征名 `HelloMacro`,它就像一座桥梁,将用户的类型和过程宏联系在一起。
`sync` 将字符串形式的 Rust 代码解析为一个 AST 树的数据结构,该数据结构可以在随后的 `impl_hello_macro` 函数中进行操作。最后,操作的结果又会被 `quote` 包转换回 Rust 代码。这些包非常关键,可以帮我们节省大量的精力,否则你需要自己去编写支持代码解析和还原的解析器,这可不是一件简单的任务!
`syn` 将字符串形式的 Rust 代码解析为一个 AST 树的数据结构,该数据结构可以在随后的 `impl_hello_macro` 函数中进行操作。最后,操作的结果又会被 `quote` 包转换回 Rust 代码。这些包非常关键,可以帮我们节省大量的精力,否则你需要自己去编写支持代码解析和还原的解析器,这可不是一件简单的任务!
`sync.parse` 调用会返回一个 `DeriveInput` 结构体来代表解析后的 Rust 代码:
`syn::parse` 调用会返回一个 `DeriveInput` 结构体来代表解析后的 Rust 代码:
```rust
DeriveInput {
// --snip--
@ -362,7 +362,7 @@ DeriveInput {
- `fields: Unit` 说明源代码是一个单元结构体
- `ident: "Sunfei"` 说明类型名称为 `Sunfei` `ident` 是标识符 `identifier` 的简写
如果想要了解更多的信息,可以查看 [`sync` 文档](https://docs.rs/syn/1.0/syn/struct.DeriveInput.html)。
如果想要了解更多的信息,可以查看 [`syn` 文档](https://docs.rs/syn/1.0/syn/struct.DeriveInput.html)。
大家可能会注意到在 `hello_macro_derive` 函数中有 `unwrap` 的调用,也许会以为这是为了演示目的,没有做错误处理,实际上并不是的。由于该函数只能返回 `TokenStream` 而不是 `Result`,那么在报错时直接 `panic` 来抛出错误就成了相当好的选择。当然,这里实际上还是做了简化,在生产项目中,你应该通过 `panic!``expect` 抛出更具体的报错信息。
@ -389,7 +389,7 @@ fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
特征的 `hell_macro()` 函数只有一个功能,就是使用 `println!` 打印一行欢迎语句。
其中 `stringify!` 是 Rust 提供的内置宏,可以将一个表达式(例如 `1 + 2`)在编译期转换成一个字符串字面值(`"1 + 2"`),该字面量会直接打包进编译出的二进制文件中,具有 `'static` 生命周期。而 `format!`对表达式进行求值,最终结果是一个 `String` 类型。在这里使用 `stringify!` 有两个好处:
其中 `stringify!` 是 Rust 提供的内置宏,可以将一个表达式(例如 `1 + 2`)在编译期转换成一个字符串字面值(`"1 + 2"`),该字面量会直接打包进编译出的二进制文件中,具有 `'static` 生命周期。而 `format!`对表达式进行求值,最终结果是一个 `String` 类型。在这里使用 `stringify!` 有两个好处:
- `#name` 可能是一个表达式,我们需要它的字面值形式
- 可以减少一次 `String` 带来的内存分配

@ -142,14 +142,14 @@ mod tests {
其实,原因藏在`LLVM`中: `LLVM`认为`fibonacci_u64`函数调用的结果没有使用,同时也认为该函数没有任何副作用(造成其它的影响,例如修改外部变量、访问网络等), 因此它有理由把这个函数调用优化掉!
解决很简单使用Rust标准库中的 `black_box` 函数:
解决很简单,使用 Rust 标准库中的 `black_box` 函数:
```rust
for i in 100..200 {
test::black_box(fibonacci_u64(test::black_box(i)));
}
```
通过这个函数我们告诉编译器让它尽量少做优化此时LLVM就不会再自作主张了:)
通过这个函数,我们告诉编译器,让它尽量少做优化,此时 LLVM 就不会再自作主张了:)
```shell
$ cargo bench
@ -204,10 +204,17 @@ criterion_main!(benches);
最后,使用 `cargo bench` 运行并观察结果:
```shell
Running target/release/deps/example-423eedc43b2b3a93
Benchmarking fib 20
Benchmarking fib 20: Warming up for 3.0000 s
Benchmarking fib 20: Collecting 100 samples in estimated 5.0658 s (188100 iterations)
Benchmarking fib 20: Analyzing
fib 20 time: [26.029 us 26.251 us 26.505 us]
Found 11 outliers among 99 measurements (11.11%)
6 (6.06%) high mild
5 (5.05%) high severe
slope [26.029 us 26.505 us] R^2 [0.8745662 0.8728027]
mean [26.106 us 26.561 us] std. dev. [808.98 ns 1.4722 us]
median [25.733 us 25.988 us] med. abs. dev. [234.09 ns 544.07 ns]
```
可以看出,这个结果是明显比官方的更详尽的,如果大家希望更深入的学习它的使用,可以参见[官方文档](https://bheisler.github.io/criterion.rs/book/getting_started.html)。

@ -1,5 +1,5 @@
# 用Github Actions进行持续集成
[Github Actions](https://github.com/features/actions) 是官方于 2018 推出的持续集成服务,它非常强大,本文将手把手带领大家学习如何使用 `Github Actions` 对 Rust 项目进行持续集成
[Github Actions](https://github.com/features/actions) 是官方于 2018 推出的持续集成服务,它非常强大,本文将手把手带领大家学习如何使用 `Github Actions` 对 Rust 项目进行持续集成
持续集成是软件开发中异常重要的一环,大家应该都听说过 `Jenkins`,它就是一个拥有悠久历史的持续集成工具。简单来说,持续集成会定期拉取同一个项目中所有成员的相关代码,对其进行自动化构建。
@ -15,7 +15,7 @@
若你需要某个 `action`,不必自己写复杂的脚本,直接引用他人写好的 `action` 即可,整个持续集成过程,就变成了多个 `action` 的组合,这就是` GitHub Actions` 最厉害的地方。
#### action 的分享与引用
既然 `action` 这么强大,我们就将自己的 `action` 分享给他人,也可以引用他人分享的 `action`,有以下几种方式:
既然 `action` 这么强大,我们就可以将自己的 `action` 分享给他人,也可以引用他人分享的 `action`,有以下几种方式:
1. 将你的 `action` 放在 github 上的公共仓库中,这样其它开发者就可以引用,例如 [github-profile-summary-cards](https://github.com/vn7n24fzkq/github-profile-summary-cards) 就提供了相应的 `action`,可以生成 github 用户统计信息,然后嵌入到你的个人主页中,具体效果[见这里](https://github.com/sunface)
2. Github 提供了一个[官方市场](https://github.com/marketplace?type=actions),里面收集了许多质量不错的 `actions`,并支持在线搜索

@ -1,6 +1,6 @@
# 编写测试及控制执行
在 Rust 中,测试是通过函数的方式实现的,它可以用于验证测试代码的正确性。测试函数往往依次执行以下三种行为:
在 Rust 中,测试是通过函数的方式实现的,它可以用于验证测试代码的正确性。测试函数往往依次执行以下三种行为:
1. 设置所需的数据或状态
2. 运行想要测试的代码
@ -62,7 +62,7 @@ test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; fini
- 测试用例是分批执行的,`running 1 test` 表示下面的输出 `test result` 来自一个测试用例的运行结果。
- `test tests::it_works` 中包含了测试用例的名称
- `test result: ok` 中的 `ok` 表示测试成功通过
- `1 passed` 代表成功通过一个测试用例(因为只有一个)`0 failed` : 没有测试用例失败,`0 ignored` 说明我们没有将任何测试函数标记为运行时可忽略,`0 filtered` 意味着没有对测试结果做任何过滤,`0 mesasured` 代表[性能测试(benchmark)](https://course.rs/test/benchmark.html)的结果
- `1 passed` 代表成功通过一个测试用例(因为只有一个)`0 failed` : 没有测试用例失败,`0 ignored` 说明我们没有将任何测试函数标记为运行时可忽略,`0 filtered` 意味着没有对测试结果做任何过滤,`0 mesasured` 代表[基准测试(benchmark)](https://course.rs/test/benchmark.html)的结果
关于 `filtered``ignored` 的使用,在本章节的后续内容我们会讲到,这里暂且略过。
@ -87,7 +87,7 @@ mod tests {
}
```
新的测试函数 `anothre` 相当简单粗暴,直接使用 `panic` 来报错,使用 `cargo test` 运行看看结果:
新的测试函数 `another` 相当简单粗暴,直接使用 `panic` 来报错,使用 `cargo test` 运行看看结果:
```shell
running 2 tests
test tests::another ... FAILED

Loading…
Cancel
Save