From cb2582d097cc81f52c8c2e5a5c0311b1a081c8af Mon Sep 17 00:00:00 2001 From: TwoooSix Date: Thu, 17 Oct 2024 15:02:46 +0800 Subject: [PATCH] Site updated: 2024-10-17 15:02:41 --- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- 404.html | 2 +- about/index.html | 2 +- archives/2023/03/index.html | 2 +- archives/2023/04/index.html | 2 +- archives/2023/05/index.html | 2 +- archives/2023/index.html | 2 +- archives/2023/page/2/index.html | 2 +- archives/2024/01/index.html | 2 +- archives/2024/06/index.html | 2 +- archives/2024/index.html | 2 +- archives/index.html | 2 +- archives/page/2/index.html | 2 +- bangumi/index.html | 4 +- categories/index.html | 2 +- .../index.html" | 2 +- .../index.html" | 2 +- .../page/2/index.html" | 2 +- index.html | 2 +- page/2/index.html | 2 +- search.xml | 1310 ++++++++--------- sitemap.txt | 12 +- sitemap.xml | 36 +- tags/Rust/index.html | 2 +- tags/Rust/page/2/index.html | 2 +- tags/cmake/index.html | 2 +- tags/index.html | 4 +- "tags/\345\211\215\347\253\257/index.html" | 2 +- .../index.html" | 2 +- 45 files changed, 723 insertions(+), 723 deletions(-) diff --git "a/2023/03/24/\343\200\220Rust\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2211-\345\274\200\345\217\221\347\216\257\345\242\203\346\220\255\345\273\272/index.html" "b/2023/03/24/\343\200\220Rust\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2211-\345\274\200\345\217\221\347\216\257\345\242\203\346\220\255\345\273\272/index.html" index 8158eb2..8b3ae10 100644 --- "a/2023/03/24/\343\200\220Rust\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2211-\345\274\200\345\217\221\347\216\257\345\242\203\346\220\255\345\273\272/index.html" +++ "b/2023/03/24/\343\200\220Rust\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2211-\345\274\200\345\217\221\347\216\257\345\242\203\346\220\255\345\273\272/index.html" @@ -1,2 +1,2 @@ -【Rust学习记录】1. 开发环境搭建 - TwoSix的小木屋 +【Rust学习记录】1. 开发环境搭建 - TwoSix的小木屋

【Rust学习记录】1. 开发环境搭建

TwoSix Lv3

本系列参考书目:《RUST权威指南》

久闻rust大名,趁着研究生还是学习生涯,抽空出来试试这个所谓的既高效,又安全的语言

环境安装与搭建

Rust安装

windows的安装很傻瓜式,只要进入官网 ,下载最新版本的安装包,按照进行安装即可。期间可能会提示让你安装VS的工具,照着安装即可。

安装完成后,就可以通过 rustc --version 来测试是否安装成功了。

同时,安装后rust也会在本地生成一份文档,可以通过 rustup doc 用浏览器打开。

Hello World!

接下来进行一个开启一门全新语言的必备仪式,hello world!

  1. 新建一个文件命名为 hello.rs rs就是rust代码文件的后缀
  2. 编写代码
1
2
3
fn main(){
println!("Hello, world!");
}
  1. 编译程序 rustc hello.rs ,接下来就会看到文件夹内生成了一个可执行文件 hello.exe,执行它,就能看到你的 hello world啦~跨过这一步,我们就是一名rust开发者了

稍微了解一下这个函数

首先fn就是function的缩写,用来定义函数;其他语法都和c++差不多,唯独不一样的就是函数println后带着一个感叹号,这似乎是rust里的宏机制,这个后面再学学吧。

Cargo的安装与使用

创建项目

书上写的是,cargo是构建+包管理的工具,或许可以理解成,ubuntu里的apt+cmake的组合?

在安装rust时就已经同步安装了cargo,我们可以通过 cargo --version 来检查是否正确安装

1
cargo new hello_cargo

这一个命令会使用cargo来创建新的项目架构,我们进入创建的hello_cargo文件夹,可以看到cargo帮我们初始化了一个Cargo.toml文件,一个src目录,放置了一个main.rs的源文件,还有一个.gitignore文件,说明它还帮我们配置了git版本管理

关于toml文件,我们可以用编辑器打开它

1
2
3
4
5
6
7
8
[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

我们可以看到这么些东西,上面自然就是你所创建的代码包的相关信息,而dependencies就是我们代码需要依赖的第三方包信息,不过我们一个hello world不需要什么,所以现在这里是空的。

也就是说,这是个rs项目的标准配置文件

我们再打开main.rs,就会发现cargo已经帮我们写好了一个hello world程序~

编译、运行与发布

之前用rustc来编译单个文件,现在我们来使用cargo构建整个项目,首先回到根目录 hello_cargo,输入命令

1
cargo build

img

编译完成如上图,我们就可以发现,目录里生成了一个target文件夹,在./targe/debug 目录下,就可以找到我们编译成功的可执行文件了,同样运行它,就能看到 hello world了

PS:如果想要编译+运行,可以使用 cargo run 命令(若源代码未发生改变,cargo run不会重新进行构建,而是直接运行)

另外,cargo还有一个比较好用的命令 cargo check ,这个命令可以让你在编译大型程序的时候,免去漫长的编译等待,快速的知道代码能否完成编译(真的这么神吗,cmake编译一个小时opencv最后环境出错编译失败的我如此问道)


如果你已经准备好发布你的程序了,那么就可以用 cargo build --release 来编译代码,它会在 target/release 的目录下生成可执行文件,和debug不同的是,它会花更长的编译时间来优化你的代码,使代码有更好的运行性能。也就是说,普通的build侧重于快速的编译,让你调试程序,release build侧重于可执行文件的运行性能,用来交付给用户。

img

可以看到编译后有一个optimized的标签,表示是优化过的target,同时也少了debug info。(但编译速度更快了,应该就是我之前debug编译过一次,基于那个的基础上,又花了0.84s进行优化)

PS:同理,你想编译完直接测试,可以用 cargo run --release

这么看来,cargo应该就是rust构建项目的核心工具了。

到此为止,第一章结束。

  • 标题: 【Rust学习记录】1. 开发环境搭建
  • 作者: TwoSix
  • 创建于 : 2023-03-24 22:14:11
  • 更新于 : 2024-07-04 23:52:28
  • 链接: https://twosix.page/2023/03/24/【Rust学习记录】1-开发环境搭建/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
\ No newline at end of file diff --git "a/2023/03/25/\343\200\220Rust\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2212-\347\214\234\346\225\260\346\270\270\346\210\217\342\200\224\342\200\224\345\260\235\350\257\225\344\273\243\347\240\201\347\274\226\345\206\231/index.html" "b/2023/03/25/\343\200\220Rust\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2212-\347\214\234\346\225\260\346\270\270\346\210\217\342\200\224\342\200\224\345\260\235\350\257\225\344\273\243\347\240\201\347\274\226\345\206\231/index.html" index 5574187..e5b4f9e 100644 --- "a/2023/03/25/\343\200\220Rust\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2212-\347\214\234\346\225\260\346\270\270\346\210\217\342\200\224\342\200\224\345\260\235\350\257\225\344\273\243\347\240\201\347\274\226\345\206\231/index.html" +++ "b/2023/03/25/\343\200\220Rust\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2212-\347\214\234\346\225\260\346\270\270\346\210\217\342\200\224\342\200\224\345\260\235\350\257\225\344\273\243\347\240\201\347\274\226\345\206\231/index.html" @@ -1,2 +1,2 @@ -【Rust学习记录】2. 猜数游戏——尝试代码编写 - TwoSix的小木屋 +【Rust学习记录】2. 猜数游戏——尝试代码编写 - TwoSix的小木屋

【Rust学习记录】2. 猜数游戏——尝试代码编写

TwoSix Lv3

输入与输出的尝试

创建项目

1
2
3
cargo new guess_game
cd .\guess_game\
cargo run

就是之前介绍的,用 cargo 创建项目的步骤,run 成功了就表明没问题啦

1
2
3
4
5
6
7
8
9
use std::io;

fn main(){
println!("欢迎来玩猜数游戏!");
println!("输入一个数字:");
let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("读取输入失败!");
println!("你输入的数字是:{}", guess);
}
  • use:就是 Rust 里的导入语句,这里的意思是导入 std 库里的 io 模块。
  • let:就是 Rust 里的定义语句,let a = b; 就是定义一个新的变量 a,值为 b。但值得一提的是,和其他所有程序都不一样,Rust 里直接定义的变量都是 const 常量,不可变!需要用 mut 关键词声明这个变量是可变的变量。
  • string::new():没什么好说的,new 了一个 String 类型,是空白的字符串。
  • io::stdin().read_line(&mut guess):也就是调用 io 模块里的 stdin 实例的 read_line 函数,如果没有写 use,也可以用 std::io::stdin 表示,& 和 C++ 里一样,是引用的概念,也就是说,定义了一个新的引用,和上面的 guess 指向同一个地址,用来接输入。
  • .expect 就是异常处理语句,在执行完 read_line 后,会返回一个 Result 类型的值,通常是一个枚举类型,OkErr 两个值,Ok 就是执行成功,并且附带代码产生的结果值,这里就是输入的字节数;Err 就是执行错误,附带错误原因。用 expect 就可以很方便地处理异常,而不用再写各 if-else。
  • 值得一提的是,不写 expect 的话,虽然能编译通过,但 Rust 会提示你有个地方没处理异常,就像这样:

img

(听说有人不喜欢写异常处理是吧)

最后一句,就是标准的格式化花括号占位输出,没什么好说的。

引入第三方包的尝试

声明依赖

一般语言的标准库都有生成随机数的函数,但rust没有。所以我们需要引入rust官方提供的一个随机数包——rand

打开Cargo.toml文件,在dependencies后面添加rand包。

1
2
3
4
5
6
7
8
9
[package]
name = "guess_game"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rand = "0.3.14"

0.3.14版本就是版本号。

然后再build一次项目,cargo就会自动帮你搜索包以及对应的并下载了,包的信息一般是从 crates.io 获取

cargo.lock和cargo update

cargo引入了一个独特的机制来保证依赖的版本问题,让所有人在构建这个项目的时候都得到相同的结果。你第一次构建项目的时候,cargo就会遍历我们声明的依赖以及版本号,把它写到 lock 文件里,后面再构建的时候,就会都使用这个版本的依赖了,除非手动升级到其他版本。

如果实在想升级,使用 cargo update 命令就会无视 lock 强行升级依赖

使用match进行分支控制的尝试

rust提供了match语法来进行更简洁的分支控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main(){
println!("欢迎来玩猜数游戏!");
let secret_number = rand::thread_rng().gen_range(1, 101);
println!("正确答案是:{}", secret_number);
println!("输入一个数字:");
let mut guess = String::new();
io::stdin().read_line(&mut guess)
.expect("读取输入失败!");
let guess: i32 = guess.trim().parse()
.expect("请输入正确的数字!");
match guess.cmp(&secret_number){
Ordering::Less => {
println!("猜小啦");
println!("再猜一次:");
},
Ordering::Greater => println!("猜大啦"),
Ordering::Equal => println!("猜对啦"),
Other => println!("猜错啦"),
}
println!("你输入的数字是:{}", guess);
}

这段代码里:

  • use 了一个 Ordering 的模块,这个模块提供了顺序对应的枚举类型,也就是 Less, Greater, Equal
  • trim().parse() 语句用来作类型转换,因为 gen_range 生成的 secret_numberi32 类型,无法直接与输入的字符串进行比较,所以作了一个类型转换;其中 trim() 就是去掉首尾多余的字符,空格换行什么的;parse() 是用于将字符串解析为对应数值类型的方法,同样也会抛出 Result 可以用于异常处理
  • match guess.cmp:就是通过 match 语法进行分支控制,把 guess.cmp() 的结果丢到下面去匹配,匹配到什么就执行什么的语句。其中 cmp 返回的就是 Ordering 这个枚举类型。实际中,这个枚举类型也可以自己定义,方便自己的分支控制。传统的 if-else 也能实现这个逻辑就是。

PS:大概了解了一下 if-elsematch 的比较,两者应该主要是在可读性上的区别比较明显。

在可读性上,if-else 只接受 bool 值的二类判断。当有复杂条件的时候,就需要多层嵌套 if-else 比较难看。相对的,match 可以自定义枚举类型,在多类判断的时候,写法也更简洁,可读性更佳。当然,二类判断当属 if-else

在性能上,当匹配的模式非常多的情况下,match 可以在编译时就完成判断,而 if-else 是在运行的时候完成判断,在极端的情况下,match 的性能会更佳(但我觉得在这种语句上纠性能的意义并不大)。

另外了解了一下,if-else 的语法和 C++ 基本一样,就不另外写了,书上目前也没有介绍 if-else

循环的尝试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main(){
println!("欢迎来玩猜数游戏!");
let secret_number = rand::thread_rng().gen_range(1, 101);
println!("正确答案是:{}", secret_number);
loop{
println!("输入一个数字:");
let mut guess = String::new();
io::stdin().read_line(&mut guess)
.expect("读取输入失败!");
println!("你输入的数字是:{}", guess);
let guess: i32 = guess.trim().parse()
.expect("请输入正确的数字!");
match guess.cmp(&secret_number){
Ordering::Less => {
println!("猜小啦");
println!("再猜一次:");
},
Ordering::Greater => {
println!("猜大啦");
println!("再猜一次:");
},
Ordering::Equal => {
println!("猜对啦");
break;
}
}
}
}

想使用一个 while True 循环也很简单,在 Rust 里就是一个 loop 语法,外带 break,没什么多说的,这里注意定义 guess 变量要在 loop 里,不然 read_line 会不断的在 guess 后面添加字符,就会导致无法转换成数字,报错退出。

那么在循环里有一个问题就很明显了,那就是 expect 语句不是我所理解常规的 try-catch 异常处理语句,它并没有捕获+后处理的步骤,所以它是会导致程序报错退出的。

那怎么来进行异常处理呢?之前也说过,Result 返回的是一个枚举类型 OkErr。那不就是,用 match 来处理就完了嘛?~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main(){
println!("欢迎来玩猜数游戏!");
let secret_number = rand::thread_rng().gen_range(1, 101);
println!("正确答案是:{}", secret_number);
loop{
println!("输入一个数字:");
let mut guess = String::new();
io::stdin().read_line(&mut guess)
.expect("读取输入失败!");
println!("你输入的数字是:{}", guess);
let guess: i32 = match guess.trim().parse(){
Ok(num) => num,
Err(_) => {
println!("请输入一个正确的数字!");
continue;
}
};
match guess.cmp(&secret_number){
Ordering::Less => {
println!("猜小啦");
println!("再猜一次:");
},
Ordering::Greater => {
println!("猜大啦");
println!("再猜一次:");
},
Ordering::Equal => {
println!("猜对啦");
break;
}
}
}
}

修改后的代码是这样的,我们用 match 来处理 parse() 的返回值,之前也说过,Ok 会附带执行成功的值,所以 Ok(num) 表示,用 num 来匹配 Ok 里面带的成功的返回值,=> 表示返回 num 值;Err(_) 就是表示,用 _ 来匹配错误信息,因为不需要用,所以用 _ 就完了,然后执行错误处理,输出+继续输入;值得一提的是这里好像说明了 Rust 的函数返回机制,好像不需要写 return,直接一个变量名就是返回了。

OK,到目前为止,初步的编程尝试已经结束了,看了下后面的章节介绍,应该会是更结构化的东西。

  • 标题: 【Rust学习记录】2. 猜数游戏——尝试代码编写
  • 作者: TwoSix
  • 创建于 : 2023-03-25 14:45:41
  • 更新于 : 2024-07-04 23:52:28
  • 链接: https://twosix.page/2023/03/25/【Rust学习记录】2-猜数游戏——尝试代码编写/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
\ No newline at end of file diff --git "a/2023/03/25/\343\200\220Rust\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2213-\351\200\232\347\224\250\347\274\226\347\250\213\346\246\202\345\277\265/index.html" "b/2023/03/25/\343\200\220Rust\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2213-\351\200\232\347\224\250\347\274\226\347\250\213\346\246\202\345\277\265/index.html" index 943d869..6840240 100644 --- "a/2023/03/25/\343\200\220Rust\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2213-\351\200\232\347\224\250\347\274\226\347\250\213\346\246\202\345\277\265/index.html" +++ "b/2023/03/25/\343\200\220Rust\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2213-\351\200\232\347\224\250\347\274\226\347\250\213\346\246\202\345\277\265/index.html" @@ -1,2 +1,2 @@ -【Rust学习记录】3. 通用编程概念 - TwoSix的小木屋 +【Rust学习记录】3. 通用编程概念 - TwoSix的小木屋

【Rust学习记录】3. 通用编程概念

TwoSix Lv3

变量,可变性,隐藏,常量

这部分的内容基本都在前面了解过了,大致就是一下内容

变量与可变性

Rust 默认变量是不可变的,需要可变的话需要加上 mut 关键字。

隐藏

但没有 mut 的变量也可以进行修改,那就是 shadow 机制(翻译为“隐藏”,其实我不太能接受,因为“隐藏”这个词汇常常涉及到安全隐患,但在 Rust 中并非如此)。我们可以通过再 let 一个同名变量,来修改。

1
2
let x = 4;
let x = 5;

这是可以编译通过的。

通过 shadow 来修改变量,和定义 mut 来修改变量的区别是:shadow 可以修改变量的类型,就和我们猜数游戏的 guess 一样,一开始存字符串,后面存 i32,但不同类型的变量在相互赋值时,是会报错的。

常量

和变量不同的是,常量需要显式声明类型,并且必须用常量表达式来赋值。常量使用 const 关键字声明,而不是 let

1
2
3
4
fn main() {
const MAX_NUM:u32 = 100_000;
println!("The value of MAX_NUM is: {}", MAX_NUM);
}

值得一提的是,Rust 支持在数字中间加个下划线提高可读性

数据类型

Rust 本质还是一个静态语言,需要显示的给出变量的具体类型。不过有不少情况,编译器能根据实际情况推导出我们的实际类型罢了,但像 guess 需要作类型转换的时候,还是需要给出具体的类型,上面的常量定义的时候,也需要显示给出变量类型。

定义变量类型的方式也就是上面那样,用冒号加类型

如果要加的时候没有加类型,就会报 cannot infer type for xxxx 意思就是编译器推导不出来类型了,要你给。

标量类型

基本的类型,整型,浮点型,布尔类型,字符型

整型

根据长度命名,有符号为 i8, i16, i32, i64,无符号为 u8, u16, u32, u64

比较特殊的是isize, usize,这两个的长度根据本地环境而定,如果运行的环境是32位系统就是32位,64位系统就是64位。感觉挺牛的。

浮点型

只有两种,f32f64,值得一提的时候,Rust 默认会将浮点型推导为 f64,表述方法是 IEEE-754

布尔型

没什么好说的,:bool

字符型

Rust 用的字符型是用 Unicode 的,而不是 ASCLL,占4个字节;定义和C++一样,字符是单引号,字符串是双引号。

但 Unicode 实际并没有“字符”的概念,所以有点奇怪,书上说是后面会解释

复合类型

基础提供的有 tuplearray

定义方面和 python 像,tuple 用圆括号定义,array 用方括号定义。

底层方面和 c++ 差不多,array 在栈上分配一整片内存,而 tupple 是在堆上分配不一定连续的内存

但两者都是长度不可变的,如果要数组长度可变,可以使用动态数组 vector,这里书上没介绍

tuple 的基本操作:

1
2
3
4
let a:(i32, u32, f32) = (1, 2, 3.0); // 指定类型
let b = (1, 2, 3.0); // 不指定类型,默认推导浮点为 f64
let (x, y, z) = b; // 把 b 拆出来,复制给 x, y, z 三个变量
let c = b.0 //通过下标访问元组

值得一提的是,这里的模式匹配赋值需要带括号 let (x, y, z) = b;

array 的基本操作:

1
2
3
4
let a:[i32; 5] = [3, 3, 3, 3, 3] // 指定类型
let b = [3, 3, 3, 3, 3] //不指定类型
let c = [3; 5] // 用; 重复定义5个3
let first = a[0] // 通过下标访问

Rust 在每次执行数组下标访问的时候,都会作边界检查,如果访问越界的话会抛出异常,然后终止程序运行。而不像 c++ 会自作主张的运行,造成某些难以察觉的 bug

函数

函数怎么定义的,在前面也讲过了,就是 fn,其中传入参数的类型,返回值的类型必须显示定义。

1
2
3
4
// 指定数据类型,返回值类型
fn plus_one(x:i32) -> i32{
x + 1
}

语句和表达式

在Rust里有两个基本的概念,

  1. 语句:执行操作,但不返回值
  2. 表达式:进行计算,返回计算值

let 操作就是一个语句,所以不能进行

1
let a = (let b = 1);

当然也不能写 a = b = 1 这样的语句,因为这些在Rust都是语句,不返回值,不能赋值

let b = 1 中的 1 ,本身就是表达式,返回1, 函数也是个表达式,返回函数的返回值,而我们写的花括号{},本质上也是个表达式,也能返回值,所以上面的语句我们可以写成

1
2
3
4
let a = {
let b = 1;
b + 1
}

花括号就是一个表达式吗,它执行一系列的操作,并返回一个返回值,b+1 也是一个表达式,返回 b+1 的值,值得一提的是,b + 1后面没加分号,如果加了分号,那它就是语句,不是表达式,不会返回值了,这时候编译也会报错。这应该也是之前看到的,Rust 返回值的办法,默认不需要用 return 语句,而是采用表达式的办法。

Rust 默认函数的返回值就是最后一个表达式,但可以使用 return 关键字提前返回

如果函数没有返回值,函数会返回一个空元组;没有指定返回类型,也会默认类型是空元组

若类型不匹配,自然就会编译报错

注释

没什么好说的 //

控制流

if-else表达式

语法和 c++ 几乎一样,但不需要打圆括号,好评

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let num = 3;
if num<4 && num < 5 {
println!("num is less than 4 and 5");
}
else if num > 7{
println!("num is greater than 7");
}
else{
println!("num is greater than 4 and less than 7");
}
}

值得一提的是,rust 里的 if 表达式只接受 bool 值,而不会像其他一些语言一样,把不等于 0 的值自动当成 bool 值。也就是在上例不能写成 if num{};

同样,这里if else表达式,而不是语句,也就是说,它可以返回值,所以我们也能用 if 表达式给变量赋值。但同时需要注意类型问题,两个分支不同类型的赋值,还是会编译报错

1
2
3
4
5
fn main() {
let condition = true;
let num = if condition { 5 } else { 6 };
println!("The value of num is: {}", num);
}

循环

loop循环

前面已经介绍过 loop循环表达式了,就是相当于 while true,值得一提的是,表达式,对,loop 也是一个表达式,可以通过 break 返回值。没错,在 Rust 里 break 居然可以返回值

1
2
3
4
5
6
7
fn main() {
let a = loop{
let num = 1;
break num*5;
};
println!("The value of a is: {}", a);
}

while 循环

很常规的循环,但while 就不能用来返回值了,break 不能带数值。

1
2
3
4
5
6
7
fn main() {
let mut num = 0;
while num<=5 {
num += 1;
println!("The value of num is: {}", num);
};
}

for 循环

Rust 里的 for 循环和python差不多,是通过迭代器来进行循环的,这在遍历像数组一样的结构时比较舒服,主要是1. 不会有越界的危险 2. 不需要每次执行过后作一次条件判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
let a = [1,2,3,4,5];
// 普通for循环,和python很像
for i in a.iter() {
println!("{}", i);
}
// 需要获取循环次数的时候,也和python一样,使用enumerate
for (idx, val) in a.iter().enumerate() {
println!("idx:{}, val:{}", idx, val);
}
// 循环一个范围的数的时候
let a = [1,2,3,4,5];
for i in 0..4{
println!("{}", a[i]);
}
// 也可以逆序遍历
for i in (0..4).rev(){
println!("{}", a[i]);
}
}

0..4 确实有点惊到我,抽象程度还挺高。


至此,第三章基本概念结束!这章里面,虽然基本知识很多,但也有不少Rust独特之处,挺惊喜的,可以看到很多地方有些和 python 的相似程度,在保证和 c++ 差不多效率的情况下,在一些语法简洁方面向python看齐(例如自动推导类型),处处都体现着,这确实是一个很现代化的语言。

  • 标题: 【Rust学习记录】3. 通用编程概念
  • 作者: TwoSix
  • 创建于 : 2023-03-25 19:48:36
  • 更新于 : 2024-07-04 23:52:28
  • 链接: https://twosix.page/2023/03/25/【Rust学习记录】3-通用编程概念/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
\ No newline at end of file diff --git "a/2023/03/26/\343\200\220Rust\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2214-\346\211\200\346\234\211\346\235\203/index.html" "b/2023/03/26/\343\200\220Rust\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2214-\346\211\200\346\234\211\346\235\203/index.html" index ae70694..399f134 100644 --- "a/2023/03/26/\343\200\220Rust\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2214-\346\211\200\346\234\211\346\235\203/index.html" +++ "b/2023/03/26/\343\200\220Rust\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2214-\346\211\200\346\234\211\346\235\203/index.html" @@ -1,2 +1,2 @@ -【Rust学习记录】4. 所有权 - TwoSix的小木屋 +【Rust学习记录】4. 所有权 - TwoSix的小木屋

【Rust学习记录】4. 所有权

TwoSix Lv3

所有权和生命周期据说是Rust最难学也最核心的两个概念,也是Rust在没有垃圾回收的机制下确保内存安全的秘诀,现在就能开始接触这第一咯核心概念了。

什么是所有权

前言

一般内存管理就两种:1. 自动垃圾回收:在运行的时候定期检查并回收没有使用的内存;2. 程序员手动分配和释放;Rust提出了第三种规则,这套规则目的在于能让编译器在编译的过程中就检查内存问题,不需要在运行的时候花费代价去回收垃圾。


补充概念:

  • 栈:后进先出的内存分配结构,没有办法在中间插入存放数据,所以存放在栈里的数据需要已知且固定大小
  • 堆:堆的管理比较松散,你可以在堆里请求一个特定的大小的空间,操作系统就会找到一片足够大的地方,标记为已使用,分配给你,返回你一个指向这片地方的指针,因为是指针,所以也方便再申请一块地方,然后把这两块地方串起来,实现动态大小。但因为多了指针跳转,也要不断的寻找足够大的空间,所以在堆里存取数据会比栈里慢。

一般语言都不需要深入了解这两个概念,但书上说这两个概念和Rust的所有权紧密相关,所以我们暂且先看看。


所有权规则

暂时了解,后续会逐一解释

  1. 每一个都有一个对应的变量,作为值的所有者
  2. 在同一时间内,值有且仅有一个所有者
  3. 当所有者离开了自己的作用域,它持有的值就会被释放

变量作用域

这个和其他语言是一模一样的,不费口舌了。

简单来说就是变量只在作用域里变的有效,保持有效直到离开作用域

String类型——一个例子

String是一个存储在堆上的结构,用这个举例会能更好的说明所有权的作用,这一部分主要注重于所有权的部分,而不是去了解关注String

简单例子

我们定义一个动态可变长的字符串

1
2
3
4
5
fn main() {
let mut s = String::from("hello world");
s.push_str("!!");
println!("{}", s);
}

对于一个可变长度的String变量而言,内存管理主要分两个步骤

  1. 让操作系统给 String 分配一个堆空间
  2. 使用完之后,把内存交还给操作系统

第一步在大多数语言里都是一样的,那就是让程序员去发起请求,也就是定义一个变量。

第二步就不一样了,也就是上面介绍过的,要么定期检查自动回收,要么程序员自己来完成。定期回收吧,开销太大,自己完成吧,实现起来又很困难,一不小心回收晚了——内存泄漏,回收早了——非法变量,重复回收了,也可能有无法预知的后果。

所以 Rust 的解决方案是,在变量离开作用域后,立即释放内存。(其实我看到这里还觉得很普通啊,这不是很正常的操作吗?内存泄漏一般是不小心哪里弄了点跨文件的全局变量,一直被 hold 着不释放导致的吧)

Rust 回收是通过一个叫 drop 的函数进行的,也就是说,在 main 函数执行完后,其实 Rust 在花括号后面偷偷调用了一次 drop 函数。

但是,看一下复杂的例子,就能发现一点不一样的地方了

复杂例子

让我们试着定义两个存放在栈里的变量,两个存放在堆里的String变量

1
2
3
4
5
6
7
fn main() {
let x = 5;
let y = x;
let s1 = String::from("hello world");
let s2 = s1;
println!("{}", s1);
}

存放在栈里的变量,很符合正常逻辑,我们会创建一个值5给x,然后把 x 里的值拷贝一份,再给 y ,这样我们就有两个5了,互相修改互不影响。

但堆里的不一样,为了保证效率(之前也说了在堆里存取很浪费效率),Rust 在创建堆变量的时候,会附带一个指针,指向这个堆,就像这样

img

这说明什么,说明我们创建 s2 的时候其实只是拷贝了 s1 的内容,没有拷贝值的内容,只是拷贝了一份新的字段,以及一个新的指针,指向原来的内存块,所以修改的时候,是会相互影响的。好,这一部分也很好理解,毕竟不少语言也是这么干的。

但是!重点来了,之前说过,当重复释放一片内存的时候,可能会造成不可预计的错误,那我们 s2 和 s1 不就在同一个作用域吗,按 Rust 的所有权方法,在离开的时候不就同时释放了这块内存吗?

于是,Rust 用了一个很简单粗暴的方法,解决了这个问题。那就是,当两个变量同时指向了同一块内存的时候,上一个变量就没用了!(此处印证了第一条规则,值有且仅有一个所有者)

你可以尝试一下运行之前的代码,编译器是会报错的,也就是说,在定义了 s2 之后,无法输出 s1 Rust以此来保证没有一块内存是冗余的。奇葩!

img

报错提示你,你真的要用两个变量名的话,就给编译器说明,你确定是浪费内存,去要克隆一份。

1
2
3
4
5
6
fn main() {
let s1 = String::from("hello world");
let s2 = s1.clone();
println!("s1:{}, s2:{}", s1, s2);
// 这样就合法了
}

但也正如上面所说,栈是不受影响的,因为固定长度的变量在栈里操作很快,复制一份也无所谓,所以 x,y 是可以随便用的。

所有权与函数

基于上面的例子,我们就可以发现 Rust 这套规则的作用域,和别的语言完全不同的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn main() {
let s1 = String::from("hello");
string_test(s1);
let x:i32 = 5;
i_test(x);
println!("{}", x);
println!("{}", s1);
}
fn string_test(s: String) {
println!("{}", s);
}
fn i_test(i: i32){
println!("{}", i);
}

我们可以看一下这段代码,当 s1 被传入函数 string_test 的时候,其实也相当于完成了一次复制,也就是说,把 s1 的值复制给了函数参数 s,导致两者也指向了同一片空间。这代表什么?这段代码编译肯定不会通过,因为 s1 的作用域到执行函数 string_test 就已经结束了!而 x 不会受这个影响。

果然还是大受震撼,让人不禁产生疑问,那要让人怎么随心所欲调用函数了?

同时,函数的返回值也受这个所有权影响,也就是说,当执行返回值的时候,返回值的所有权回到函数上,再交由函数赋予变量上。

针对我提出的疑问,书上马上也给出了回答,如果要让我在调用函数之后保证变量的所有权,那就需要在函数的最后加个返回值,再把所有权返回给我的变量,也就是所用权的变更路线是 s1——参数——返回值——函数——s1

这也太麻烦了,这时候就需要引入另一个概念,让这个操作变得没那么繁琐,那就是——引用。

引用和借用

引用和所有权

既然复制和移动会转移所有权,导致变量有效性消失的问题,那不复制不移动不就完了?这个操作,就需要用到引用。

1
2
3
4
5
6
7
8
fn main() {
let s1 = String::from("hello");
string_test(&s1);
println!("{}", s1);
}
fn string_test(s: &String) {
println!("{}", s);
}

这段代码就完全没有问题了。

引用和别的语言概念也一样,应该不需要多说,创建一个新的引用,它的本质是这样的。

img

也就是说,什么也不拷贝,但是多了个新的指针,指向原变量,值的所有权还是在s1上,并且引用不会持有所有权,所以当 s 离开了作用域,它的值也不会被回收。

在 Rust 里,这种通过引用传递给函数参数的方法,称为借用,也就是你用完了别人的东西,要原封不动的还给人家。没错,原封不动,这又是和其他语言不太一样的地方。

1
2
3
4
5
6
7
8
9
fn main() {
let mut s1 = String::from("hello");
string_test(&s1);
println!("{}", s1);
}
fn string_test(s: &String) {
s.push_str(" world!");
println!("{}", s);
}

让我们把 s1 修改为可变,然后通过引用传给 s ,试图修改一下值,不出意外,编译器报错!引用是不可变的。

但其实很多时候,我们确实需要在函数里修改变量,怎么办?

所以又有了可变引用

可变引用

定义

1
2
3
4
5
6
7
8
9
fn main() {
let mut s1 = String::from("hello");
string_test(&mut s1);
println!("{}", s1);
}
fn string_test(s: &mut String) {
s.push_str(" world!");
println!("{}", s);
}

&mut 就是可变引用的关键字,这里我们把参数定义为了可变引入,传入的时候也改成了可变引用,代码就合法了。但 Rust 怎么可能让你这么自由的写代码?这不安全!所以可变引用有非常大的限制。那就是可变引用只能一次声明一个

1
2
3
4
5
6
fn main() {
let mut s1 = String::from("hello");
let s2 = &mut s1;
let s3 = &mut s1;
println!("{}, {}", s2, s3);
}

不出意外,这段代码必然报错。

可变引用与数据竞争

这么做的主要原因是让我们在编译的时候避免数据竞争,当指令在满足以下三种情况的时候,就会有数据竞争的情况:

  1. 两个或两个以上的指针同时访问一片空间
  2. 其中至少有一个,要往空间写入数据
  3. 而且又没有同步数据访问的机制

看了以上三种情况应该也能大概直到数据竞争是什么了,大概就是,写和读同步进行,可能导致另一个指针读到的数据不太对,导致你完全无法察觉的bug。

这种情况在 Rust 完全不会出现。因为可能产生数据竞争的代码编译这一关就通过不了哈哈哈。(同理,以上代码如果你不使用 s2, s3 的话其实不会报错,因为你定义了两个,但都没有使用,自然也没有数据竞争,只会有警告,告诉你定义了两个没用的变量)

基于这个理由,我们也可以知道,同时存在不可变引用+可变引用也是不合法的,因为一个只读,一个可能写,也会有数据竞争。而同时存在多个不可变引用的话,就没问题,因为它们都是只读,并不会修改数据。

悬垂引用

在别的语言里,有一个概念叫 悬垂指针,也就是说,一个指针指着块内存,但是内存被释放掉了,指针还指着这块内存,就叫悬垂。在 Rust 里,同样有一套规则确保引用不会进入悬垂状态,具体做法就是,确保引用的内存不会在引用离开自己的作用域时就被释放掉。也就是说,编译器保证引用在作用域内持续有效

先来创建一个悬垂引用

1
2
3
4
5
6
7
fn main() {
let test = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}

这里面我们返回了一个引用,但是引用的数据是 s 里的,s 在离开了函数后就会被销毁,引用自然也就悬垂了。

这时候编译会报错:expected named lifetime parameter

报错涉及到了我们之前说的两大最难学的核心概念之一,生命周期,这个会在后面学,现在不管。我们只要直到,Rust 又一次成功的通过报错拦截了我们的危险代码。

所以我们需要及时的去规范我们的代码,避免危险,这里也很简单,我们不返回引用,而是创建一个字符串变量,返回它的所有权就行了。

到这里,引用就讲完了,展开下一个概念,切片。(小声逼逼,这章好长,一边看书,一边写代码,一边写博客,看了我一下午了,没办法还是想一章一章的完整看完)

切片

之前说过,引用没有所有权,但没有所有权的类型还有一个,那就是——切片。用过 python 的应该很清楚这个概念。

切片在 Rust 的本质就是引用几何里一段连续的元素序列

书里举了一个例子说明切片的好处。

假设我们需要获取一个句子里面某个单词,怎么获取?最简单的方法的方法就是找到第一个单词的索引,知道单词的长度,这样就能随时的通过下标的方式访问到单词。

但这种设计方式有一个问题,那就是单词的索引,它的意义是和单词严格绑定的,当我的句子都已经被销毁的时候,其实索引也就没有了意义,但这时候我们可能用了一个变量来存这个索引,这个变量又不随着句子而销毁,这样就造成了一些冗余的问题,就连 Rust 的编译器也没办法给你挑出毛病来(你也有今天)。

所以就有了切片,我们一次性切出来一块引用,引用这个单词相关的所有字符,当原来的句子没有用了之后,引用也自然会被销毁(没被销毁的情况编译器就报错了)

1
2
3
4
5
6
fn main() {
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
println!("{} {}", hello, world);
}

使用方法也很简单,在 python 里是冒号,这里就是两个点,也同样是左闭右开,但是注意要写引用。

语法糖也和 python 一样,如果你想从一开始就切,也可以不写第一个数字,如果你想切到最后,也可以不写最后一个数字,例如:

1
2
3
4
5
6
fn main() {
let s = String::from("hello world");
let hello = &s[..5];
let world = &s[6..];
println!("{} {}", hello, world);
}

另外,有趣的是,之前不是说编译器会保证引用持续有效吗?那我在引用离开作用域前手动销毁会怎样?

1
2
3
4
5
6
7
fn main() {
let mut s = String::from("hello world");
let hello = &s[..5];
let world = &s[6..];
s.clear();
println!("{} {}", hello, world);
}

当然,肯定会报错。但它的解决方法比较有趣,之前说过,当你定义了不可变引用的时候,就没办法定义可变引用了对吧。而clear本质也是个函数,它清空 s 的内存的话,本质上是需要修改 s 的内容,所以它需要传入一个 s 的可变引用,来对齐进行清空,但我们之前还定义了不可变引用,不可变引用还没进行使用呢,你就没有办法定义可变引用去clear了。没错,根本上还是解决数据竞争的问题,清空本质上是一个写操作,我还要读呢,你就不能写!

当然,同样的,如果你不用读,它就不会报错了,例如:

1
2
3
4
5
6
fn main() {
let mut s = String::from("hello world");
let hello = &s[..5];
let world = &s[6..];
s.clear();
}

书上还提到了其他类型,例如数组也可以切片,此乃废话,不多说。


好勒,第4章到这里就终于结束了,这一章实在太长了,毕竟涉及到核心概念,看了我半天时间,今天就差不多到这吧。

  • 标题: 【Rust学习记录】4. 所有权
  • 作者: TwoSix
  • 创建于 : 2023-03-26 20:18:45
  • 更新于 : 2024-07-04 23:52:28
  • 链接: https://twosix.page/2023/03/26/【Rust学习记录】4-所有权/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
\ No newline at end of file diff --git "a/2023/03/30/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2215-\347\273\223\346\236\204\344\275\223/index.html" "b/2023/03/30/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2215-\347\273\223\346\236\204\344\275\223/index.html" index b277ab6..0e75ba3 100644 --- "a/2023/03/30/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2215-\347\273\223\346\236\204\344\275\223/index.html" +++ "b/2023/03/30/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2215-\347\273\223\346\236\204\344\275\223/index.html" @@ -1,2 +1,2 @@ -【Rust 学习记录】5. 结构体 - TwoSix的小木屋 +【Rust 学习记录】5. 结构体 - TwoSix的小木屋

【Rust 学习记录】5. 结构体

TwoSix Lv3

定义及实例化方式

定义和创建实例

定义方法和 C++ 是一模一样了,详见代码

1
2
3
4
5
6
struct User{
username: String,
email: String,
sign_in_count: u64,
active: bool,
}

实例化方法稍显不同,方法和定义差不多,指名道姓的赋值,优势是不用对应顺序,可读性强。访问方法就还是传统的点运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct User{
username: String,
email: String,
sign_in_count: u64,
active: bool,
}

fn main() {
let user1 = User{
email: String::from("test@1.com"),
username: String::from("haha"),
sign_in_count: 1,
active: true,
};
println!("user1: {}", user1.email);
}

同理,结构体也有可变与不可变一说,可变结构体,结构体所有变量可变,不可变结构体,结构体所有变量不可变,Rust 没有让结构体部分变量可变,部分不可变的说法

一些语法糖

同名参数对应赋值

如果每次都要写一个 email: xxxx, username: xxxx 好像有点麻烦是吗?Rust 提供了一个简单的方法,当变量名和结构体内的字段名完全一样的时候,会对应赋值(有一说一,这个设计挺有想法的,语法糖+1)

所以我们可以很轻松的给 User 写一个创建用的函数,这样就可以实现带默认值,轻松的构建结构体实例了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct User{
username: String,
email: String,
sign_in_count: u64,
active: bool,
}

fn create_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
// 不写分号返回 User 变量
}
fn main() {
let user1 = create_user(String::from("hah@haha.com"), String::from("haha"));
println!("user1: {}", user1.email);
}

用之前的实例构造现在的实例

在一些情况下其实结构体内的实例都不需要怎么变动,就像上面的例子里,sign_in_countactive 参数都是采用同一个默认值来赋给所有实例的,那有没有一些方法能简化这种情况的代码书写呢?有的。

1
2
3
4
5
6
7
8
9
fn main() {
let user1 = create_user(String::from("hah@haha.com"), String::from("haha"));
let user2 = User {
email: String::from("user2@haha.com"),
username: String::from("user2"),
..user1
};
println!("user2: {}", user2.active);
}

..user1 就代表了剩下的值都和 user1 一样,把 user1 里对应字段的值赋给 user2 即可。(这个感觉不如函数封装性好吧,但可能也看情况)

元组结构体——没有字段名的结构体

其实就相当于给元组命个名字,适用于很多不需要给字段命名的情况下,例如颜色的RGB,大家都懂是吧,就用一个三元组命名 Color 就好了。定义方式如下

1
2
3
4
5
6
struct Color(i8, i8, i8);

fn main() {
let black = Color(0, 0, 0);
println!("user2: {}", black.0);
}

值得一提的是,定义成结构体后也是用点运算符访问变量,而不是 [] 运算符了

空结构体——没有字段的结构体

当你想创建一个空结构体的时候,也是不会报错的,原理说是和空元组相似,然后在某些方面会有用,后续再介绍。

我也不太懂,就不多说了,后面再看吧。

结构体的所有权

在上面举的这个例子中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct User{
username: String,
email: String,
sign_in_count: u64,
active: bool,
}

fn main() {
let user1 = User{
email: String::from("test@1.com"),
username: String::from("haha"),
sign_in_count: 1,
active: true,
};
println!("user1: {}", user1.email);
}

我们定义的所有字段都是具有值的所有权的,所以结构体实例能具有所有字段数据的所有权,能伴随着自己直到离开作用域,但也有不具有所有权的定义方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct User{
username: &str,
email: &str,
sign_in_count: u64,
active: bool,
}

fn main() {
let user1 = User{
email: String::from("test@1.com"),
username: String::from("haha"),
sign_in_count: 1,
active: true,
};
println!("user1: {}", user1.email);
}

但这种方式现在是没办法定义通过的,报错提示需要指定生命周期,这个涉及到了生命周期,所以就放到后面介绍了。

初试trait——为结构体增加更多有用的功能

说实话我不知道这个trait是什么意思,大概查了一下,是 特性(性状)的意思,用来定义一个类型可能和其他类型共享的功能,或许差不多相当于和 python 里的 xxx 差不多吗?但看样子还能自己定义的样子。先不管吧,大概了解一下概念,先学着。

打印结构体

用过 python 的应该都知道 python 类有个 __str__ 函数,可以定义一个类的字符串格式,方便输出成人类能查看的格式,而不是一串地址,Rust 的结构体也有这个功能—— Display

我们可以先跑一下这段代码

1
2
3
4
5
6
7
8
9
struct rectangle{
w: u32,
h: u32
}

fn main() {
let rect = rectangle{w: 10, h: 20};
println!("rectangle is {}", rect);
}

可以看到一个报错

1
2
= help: the trait `std::fmt::Display` is not implemented for `rectangle`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead

the trait std::fmt::Display is not implemented for rectangle意思就是这个结构体还没实现 Display 这个方法,也就是说,println! 这个宏在输出的时候,还会调用一下类型的格式化函数,来进行指定的输出,之前我们用的基本类型都是默认实现了 Display 方法的,而这个 rectangle 是我们自己定义的,没有 Display 方法,println! 就不知道怎么格式化了,所以就报错。

但除了这个还有一个有意思的提示 in format strings you may be able to use {:?} (or {:#?} for pretty-print) instead 。这句话告诉我们,可以用 {:?}{:#?} 来整一个漂亮的输出?啥意思?写一下吧~

1
2
= help: the trait `Debug` is not implemented for `rectangle`
= note: add `#[derive(Debug)]` to `rectangle` or manually `impl Debug for rectangle`

被骗了,还是报错。但我们发现了另一个有意思的 trait—— Debug 。也就是说,Rust 对格式化的方法区分了两种,Debug 是专门面向开发者调试时的输出用格式。我超,什么是现代化语法啊(后仰)。这种特性真能派上不少用途。

提示里说,要么添加[derive(Debug)] 要么自己实现一个 Debug 我们可以先添加一下这个试试

1
2
3
4
5
6
7
8
9
10
#[derive(Debug)]
struct rectangle{
w: u32,
h: u32
}

fn main() {
let rect = rectangle{w: 10, h: 20};
println!("rectangle is {:?}", rect);
}

添加在函数头,进行一个 Debug 注解就可以了,这次运行就不会报错了。我们来看看输出是怎样的

1
rectangle is rectangle { w: 10, h: 20 }

可见 Rust 标准的 Debug 格式化输出就是 结构体名{所有字段名: 对应的值},嗯,挺不错的,不用自己手动一个一个输出了。我们再来看看之前提示里提到的另一个 {:#?}

1
2
3
4
rectangle is rectangle {
w: 10,
h: 20,
}

这个输出会更好看点,有了一定的排版,对于复杂的结构体会更有可读性。

好,书上的 trait 介绍就到这里结束了(是不是把一开始的 Format 忘了),它说到第 10 章的时候会更详细的介绍 trait 的时候,可以通过像这种对结构体进行 trait 注解的方式提供很多功能,包括自带的,甚至可以自己自定义,确实期待。

方法

结构体,或者说类,当然不能少了函数,书上对普通函数和结构体里的函数区分了一下概念,结构体内的函数叫方法,因为方法的定义有局限性,例如参数要有self,只能定义在结构体或trait之类的地方,属于是一个子集吧。我们也严谨点,区分一下吧。

定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#[derive(Debug)]
struct Rectangle{
w: u32,
h: u32
}
impl Rectangle {
fn area(&self) -> u32 {
self.w * self.h
}
}

fn main() {
let rect = Rectangle{w: 10, h: 20};
println!("rectangle's area is {:#?}", rect.area());
}

(吐槽:这里把 rectangle 的首字母大写了,因为 Rust 的编译器居然会警告我的命名不规范,牛)

可以看见,方法和函数定义差不多,也是用 fn 来定义,指定哪个函数里的话倒是比较意外,居然不是写在 Rectangle定义的花括号里,而是另开一个 impl Rectangle 再来定义方法。另外,Rust 方法和 Python 也有点相似之处,也是通过 self 来指代当前实例,self 可以用三种方式来定义

  1. &self :不可变引用,这个是最常见的,我们只要读取数据,什么也不干,所以不需要用到所有权,也最方便
  2. self:获取所有权,应该最不常见,有时方法需要用来转换self类型的话,需要用到所有权,获取所有权后再进行返回;如果不返回的话,所有权在调用完方法就被回收了,实例就销毁了。
  3. &mul self:可变引用,也没什么好说的,就是有时要改变实例内字段的值时会用。

然后书上介绍了 Rust 为什么没有 -> 运算符的问题,我没太看懂,就简述一下我的理解,具体感兴趣可以自行看书或查阅资料。

C++在对于一个指针类型的结构体变量里,需要对变量进行一次解引用,也就是 ractangle->area()= (*rectangle).area() 所以额外定义了一个 -> 运算符写起来方便些。而 Rust 里,self的类型被显式定义了,所以编译器可以自动的根据你定义的 self 类型,去自动推理出 self 是否需要自动引用还是解引用,所以就不需要 -> 运算符了。

关联函数

impl 块里,除了带 self 的方法之外,Rust 还允许在块里定义不含 self 的函数,这些函数因为和结构体有关联,又不太需要 self 所以称为关联函数。和 Python 的 @staticmethod 差不多吧

这里用一个定义函数来举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#[derive(Debug)]
struct Rectangle{
w: u32,
h: u32
}
impl Rectangle {
fn area(self) -> u32 {
self.w * self.h
}
fn square(size: u32) -> Rectangle {
Rectangle { w: size, h: size }
}
}

fn main() {
let rect = Rectangle{w: 10, h: 20};
println!("rectangle's area is {:#?}", rect.area());
println!("rectangle is {:#?}", Rectangle::square(3));
}

使用也就是指定命名空间调用就可以了。

多个impl块

使用多个 impl 块也是合法的,可以编译通过。书上说后面会有应用场景介绍,那就后面再看吧,目前感觉还派不上用场?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#[derive(Debug)]
struct Rectangle{
w: u32,
h: u32
}
impl Rectangle {
fn area(self) -> u32 {
self.w * self.h
}
}
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle{w: size, h: size}
}
}


fn main() {
let rect = Rectangle{w: 10, h: 20};
println!("rectangle's area is {:#?}", rect.area());
println!("rectangle is {:#?}", Rectangle::square(3));
}

本章到此结束!没什么好总结的,是比较基础的,也很重要的一部分,下一章继续干。

  • 标题: 【Rust 学习记录】5. 结构体
  • 作者: TwoSix
  • 创建于 : 2023-03-30 21:05:00
  • 更新于 : 2024-07-04 23:52:28
  • 链接: https://twosix.page/2023/03/30/【Rust-学习记录】5-结构体/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
\ No newline at end of file diff --git "a/2023/03/30/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2216-\346\236\232\344\270\276\344\270\216\346\250\241\345\274\217\345\214\271\351\205\215/index.html" "b/2023/03/30/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2216-\346\236\232\344\270\276\344\270\216\346\250\241\345\274\217\345\214\271\351\205\215/index.html" index 2ea41a7..264f0ed 100644 --- "a/2023/03/30/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2216-\346\236\232\344\270\276\344\270\216\346\250\241\345\274\217\345\214\271\351\205\215/index.html" +++ "b/2023/03/30/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2216-\346\236\232\344\270\276\344\270\216\346\250\241\345\274\217\345\214\271\351\205\215/index.html" @@ -1,2 +1,2 @@ -【Rust 学习记录】6. 枚举与模式匹配 - TwoSix的小木屋 +【Rust 学习记录】6. 枚举与模式匹配 - TwoSix的小木屋

【Rust 学习记录】6. 枚举与模式匹配

TwoSix Lv3

定义枚举

例子:IP地址,只有 IPV4, IPV6 两个模式。

所以我们可以通过枚举类型的方式来描述 IP 地址的类型。

1
2
3
4
5
6
7
8
9
enum IpAddKind{
IPV4,
IPV6
}

fn main() {
let four = IpAddKind::IPV4;
let six = IpAddKind::IPV6;
}

这里也就两个点:

  1. 使用 enum 关键字就可以完成一个枚举类型的定义。
  2. 访问枚举类型的值是通过命名空间的方式来实现的,而不是点运算符。

接下来再谈论一个实际的问题:这里的枚举类型只定义了两个类别,没有办法对应实际的IP地址,怎么办?

一般情况下,我们首先想到的肯定是用结构体,一个字段存储类型,一个字段存储地址对吧。但是 Rust 有一个非常方便的特性:关联的数据可以直接嵌入到枚举类型里

1
2
3
4
5
6
7
8
9
enum IpAddKind{
IPV4(String),
IPV6(String)
}

fn main() {
let local = IpAddKind::IPV4(String::from("127.0.0.1"));
let loopback = IpAddKind::IPV6(String::from("::1"));
}

厉害吧。不过好像结构体也能做是吧?不完全是,枚举类型有一个结构体做不到的功能。

枚举类型可以为每个类型值指定一个类型,例如 IPV4 通常是四个数字来表示,而 IPV6 则不同,那么我们可以使用四个数字来描述 IPV4 ,使用字符串来描述 IPV6

1
2
3
4
enum IpAddKind{
IPV4(u8, u8, u8, u8),
IPV6(String)
}

这个时候,把类型和内容拆分成两个字段来描述的结构体,就没有办法实现了,只能固定一种类型。

另外,IPV4和IPV6因为很常用,所以在标准库里其实就有定义好了一套枚举类型。它的定义方法是这样的。

img

也就是先用两个结构体描述一下具体的 IPV4和IPV6 ,然后再定义一个枚举类型,把这两个结构体嵌入到枚举类型里。

另外,枚举类型还有对于结构体另外的优势是,我们可以轻松的定义一个用于处理多个数据类型的函数

例如,我们可以像上面官方一样,用两个结构体去描述 IPV4和IPV6,但如果我们要定义一个函数,同时处理IP地址的话,就不知道该指定传入参数的类型是 IPV4还是IPV6了,但我们定义一个枚举类型 IpAddr ,就可以轻松的定义函数的传入类型为 IpAddr,然后可以同时处理两个结构体的数据。

Option——一个常用的枚举类型

Option 枚举类型定义了值可能不存在的情况,或者可以说是其他语言的空值 Null 。本来就很常用了,但这个类型在 Rust 里更有用,因为编译器可以自动检查我们是不是妥善的处理了所有应该被处理的情况

Rust 并没有像其他语言一样,有空值 Null 或者 None 的概念。书上说这是个错误的设计方法,因为当你定义了一个空值,在后续可能没有给他赋予其他值就进行使用的话,就会触发问题。这种设计理念可以帮助人们更方便的去实现一套系统,但也给系统埋下了更多的隐患。

Rust 结合了一下这个理念,觉得空值是有意义的,触发空值问题的本质是实现的措施的问题,所以提供了一个具有类似概念的枚举类型——Option. 标准库里的定义是这样的

1
2
3
4
enum Option<T>{
Some(T),
None,
}

Option 是被预导入的,因为它很有用,所以我们不用 use 来导入它。所以我们可以不用指定命名空间,使用 Some 或者 None 关键词, 是后面学的语法,用来代表任意类型

我们可以用这个方法来定义一些变量

1
2
3
4
5
fn main() {
let some_number = Some(5);
let some_string = Some("hello");
let absent_number = None;
}

这段编译是不通过的,这也体现了 Option 相对于普通空值的优势。

我们来简单的通过几个例子来了解一下 Option 的设计理念

  1. Option和 T 是两个不同的类型
1
2
3
4
5
fn main() {
let a = Some(5);
let b = 5;
println!("{}", a+b);
}

可以看到这段代码里,虽然 a, b 同样都是 i32 但一个是被Some持有,那它们就是不同的类型,编译器无法理解不同类型相加,所以这就意味着什么。当我们对于一个可能有值,可能没值的变量,我们就要去使用 Option 枚举,一旦使用了Option枚举,我们再要实际使用它的时候,就要显式的把这个 Some 类型的变量转换到 i32 的变量,再去使用。相当于强迫你去对这个变量编写一段代码,这样就避免了你不经过处理就使用,结果使用到空值的情况。

当然,因为是枚举类型,我们也可以方便的用 match 来处理不同的情况,例如有值时怎么样,没值时怎么样等等。接下来就开始介绍一下 match

match运算符

match 是一个很好用的运算符,它除了提高可读性外,也方便编译器对你的代码进行安全检查,确保你对所有可能的情况都进行了处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum Coin{
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}

fn main() {
let my_coin = Coin::Dime;
println!("The value of my coin is {} cents", value_in_cents(my_coin));
}

这就是一个简单的,输入硬币类型,返回硬币价值的代码。相对于 if-else 表达式,match 的第一个优势自然就是可读性。你 if-else 需要想方设法凑一个 bool 值,可能用字符串,可能用字典,可能用数字下标,可能用结构体什么的,但 match 枚举类型就不用,可以为任意你想要的类型定义一个名字,直接用,直接返回任意值,结构还更紧凑好看。

另一个优势就是,match 运算符可以匹配枚举变量的部分值。

美国的25美分硬币里,很多个州都有不同的设计,也只有25美分的硬币有这个特点,所以我们可以给25美分加一个值:州,对应不同的设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#[derive(Debug)]
enum UsState{
Alabama,
Alaska,
}

enum Coin{
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("The state of coin is {:?}", state);
25
}
}
}

fn main() {
let my_coin = Coin::Quarter(UsState::Alaska);
println!("The value of my coin is {} cents", value_in_cents(my_coin));
}

这里我们用到了之前的 Debug 注解,让编译器自动格式化枚举类型的输出,然后在match匹配里,提取了 Quarter 枚举类型里的值,命名为 state,然后输出。

匹配Option

接下来我们就可以综合一下上面学到的东西,用match来处理一下空和非空的情况了。

1
2
3
4
5
6
7
8
9
10
11
fn pluse_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}

fn main() {
let num = Some(5);
let num2 = pluse_one(None);
}

这就是一个典型的例子:

  1. 当为空的时候,什么也不干
  2. 当不为空的时候,取出值,进行处理

这也就是 match 的相对于 if-else 的优势:可以以一个更简单,更紧凑,可读性更高的方式,进行模式匹配,进行对应值的处理。接下来就介绍 match 在安全性方面的优势:让编译器帮你检查是否处理完了所有情况。

必须穷举所有可能

我们来试着漏处理为空值的情况。

1
2
3
4
5
6
7
8
9
fn pluse_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}

fn main() {
let num = Some(5);
}

编译器马上报错:non-exhaustive patterns: None not covered 你没有覆盖处理空值!

这一段代码同时体现了 match 的优势和 Option 实现空值的优势。也就是你必须要处理所有情况,保证没有一点逻辑遗漏的地方,Option也依赖于 match 的这个特性,强迫你处理空值的情况,杜绝了大部分程序员只写一部分 if-else 结果就因为漏了部分情况没处理,导致程序 crash 的问题。

_ 通配符

但有时候我明明不用处理所有情况,但每次都要写上很麻烦怎么办?没事,Rust 作为一个现代语言还是准备了一些语法糖,那就是通配符 _ 相当于 if-else 里面的 else。

1
2
3
4
5
6
7
8
9
10
fn pluse_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
_ => None
}
}

fn main() {
let num = Some(5);
}

_ 可以匹配所有类型,所以得放最后,用于处理前面没有被处理过的情况。

但有时候我指向对一种情况处理怎么办?还是有点麻烦吧,Rust 还准备了 if let 语句

简单控制流 if-let

我们就继续用美分硬币来举例吧,之前写的代码方便些。例如我们只想知道这个硬币是不是 Penny。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum Coin{
Penny,
Nickel,
Dime,
Quarter,
}

fn main() {
let my_coin = Coin::Penny;
match my_coin{
Coin::Penny => println!("Lucky Penny!"),
_ => println!("Not a penny"),
};
}

用 match 的话,就是要定义一个硬币,匹配这个硬币,再写个通配符来检测其他情况,标准流程是吧。但这样写或许繁琐了些,if let 就是这么一个一步到位的语法

1
2
3
4
5
6
7
8
9
10
11
12
13
enum Coin{
Penny,
Nickel,
Dime,
Quarter,
}

fn main() {
let my_coin = Coin::Penny;
if let Coin::Penny = my_coin {
println!("Lucky penny!");
}
}

if let Coin::Penny 就是要匹配的值,my_coin也就是你传入的值,用等号分隔开,如果两者相等的话,执行花括号内的语句。

当然,也可以用else

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum Coin{
Penny,
Nickel,
Dime,
Quarter,
}

fn main() {
let my_coin = Coin::Penny;
if let Coin::Penny = my_coin {
println!("Lucky penny!");
}else{
println!("Not a lucky penny!");
}
}

这样就和上面的 match 完全匹配了。

虽然 if let 让代码写起来更简单了,但也失去了 match 的安全检查,所以是一个取舍吧。个人偏向于使用match,说实话我觉得这 if let 写起来有点别扭,感觉书写逻辑不太舒服。

这应该只是一个偶尔可能用到的糖,哪时候你写烦了match,可以想想原来还有 If let 这么个东西


第六章也就搞定了,大致总结一下,这章主要就是讲了一下1. 枚举类型对于结构体的优势所在,2. match对于if-else的优势所在,3. 独特的空值设计理念

今天就到这里吧,后面还有7,8,9章三章的基础通用编程知识,学完就差不多进入进阶阶段了。

  • 标题: 【Rust 学习记录】6. 枚举与模式匹配
  • 作者: TwoSix
  • 创建于 : 2023-03-30 22:38:03
  • 更新于 : 2024-07-04 23:52:28
  • 链接: https://twosix.page/2023/03/30/【Rust-学习记录】6-枚举与模式匹配/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
\ No newline at end of file diff --git "a/2023/04/01/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2217-\345\214\205\343\200\201\345\215\225\345\205\203\345\214\205\345\222\214\346\250\241\345\235\227/index.html" "b/2023/04/01/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2217-\345\214\205\343\200\201\345\215\225\345\205\203\345\214\205\345\222\214\346\250\241\345\235\227/index.html" index 3a78332..654433b 100644 --- "a/2023/04/01/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2217-\345\214\205\343\200\201\345\215\225\345\205\203\345\214\205\345\222\214\346\250\241\345\235\227/index.html" +++ "b/2023/04/01/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2217-\345\214\205\343\200\201\345\215\225\345\205\203\345\214\205\345\222\214\346\250\241\345\235\227/index.html" @@ -1,2 +1,2 @@ -【Rust 学习记录】7. 包、单元包和模块 - TwoSix的小木屋 +【Rust 学习记录】7. 包、单元包和模块 - TwoSix的小木屋

【Rust 学习记录】7. 包、单元包和模块

TwoSix Lv3

包与单元包

  1. 单元包(Crate):单元包可以被用来生成二进制的程序或者库;单元包的入口文件称为单元包的入口节点
  2. 包(Package):一个包由一个或多个单元包集合而成,用 Cargo.toml 描述包内的单元包怎么构建;一个包最多也拥有一个库单元包;包可以有多个二进制单元包;包必须至少拥有一个单元包,可以是库单元包,也可以是二进制单元包。

举个例子,我们一直用的cargo new test命令就是用来生成一个名为 test 包的,其中我们的代码文件 src/main.rs 就是 test 包下的一个单元包,叫 main,代码文件就是这个单元包的根节点,这类代码编译后可以生成一个二进制可执行文件,所以也叫二进制单元包。我们也可以通过不断的在 src 目录下写代码,创建更多的二进制单元包。

书上并没有介绍我更关心的库单元包,只是它举了个文件名例子src/lib.rs。后面再看。

同时,每个单元包内的代码都有自己的作用域(和 c++ 的命名空间相当),用来避免命名冲突。也就是我们之前 use 了 rand 包后,需要指定rand::Rng才能使用Rng模块一样。这样可以避免 Rng 这个名字和你自己定义的 Rng 结构冲突,你可以更随心所欲的命名。

模块

模块可以提供私有/公共的权限管理功能,也就是熟知的 private 和 public

模块的定义

我们现在 src 文件夹下创建一个lib.rs文件,新建一个库单元包,然后再编写代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
mod front_of_house{
mod hosting{
fn add_to_waitlist(){

}
fn seat_at_table(){

}
}

mod serving{
fn take_order(){

}
fn serve_order(){

}
fn take_payment(){

}
}
}

代码解释

  1. mod关键字:mod 关键字用来定义一个模块,我们定义了一个名为 front_of_house 的模块,用来管理餐厅的前厅部分,并且前厅部分又分为服务客户的服务员,处理订单的前台等,所以我们在模块下又定义了两个模块 hosting, serving
  2. 接着我们以模块来对代码进行分组,在不同模块下定义了对应的功能函数

这段代码就相当于我们构建了一个单元包,单元包里包含了一个模块,模块内有两个模块,而两个模块又有多个函数,构成了一个树的层级结构。

包管理架构

模块的调用

我们可以通过绝对路径和相对路径的方式来调用这个层级结构的某个函数。

Rust 通过 :: 符号来构建访问路径,当我们想调用 hostting模块下的 add_to_waitlist 函数,可以用以下方式(这段代码是暂时编译不通过的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
mod front_of_house{
mod hosting{
fn add_to_waitlist(){

}
fn seat_at_table(){

}
}

mod serving{
fn take_order(){

}
fn serve_order(){

}
fn take_payment(){

}
}
}

pub fn eat_at_restaurant() {
crate::front_of_house::hosting::add_to_waitlist();

front_of_house::hosting::add_to_waitlist();
}
  1. 绝对路径:crate::front_of_house::hosting::add_to_waitlist(); 从指定的入口文件开始访问到函数
  2. 相对路径:front_of_house::hosting::add_to_waitlist(); 用相对路径来访问当前函数同级下的函数,这里的eat_at_restaurantfront_of_house同级

绝对路径和相对路径的优缺点也不用多说了吧,当可能需要同步移动两个模块的时候,相对路径好,单独移动一个模块的时候用绝对路径好。视情况而定就好

访问权限

刚说了上面的代码是编译不通过的,我们编译一下代码,看看为什么不通过,

1
error[E0603]: module `hosting` is private

报错,hosting 是私有的

也就是说,Rust 里所有的条目,在没有特意声明的情况下,默认都是私有的,权限这块的规则大概是:父级模块无法访问私有的子模块条目,子模块可以访问父级模块的私有条目,同级之间可以互相访问。

但是注意,公有的模块不代表模块里的字段公有。例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
mod front_of_house{
pub mod hosting{
fn add_to_waitlist(){

}
fn seat_at_table(){

}
}

mod serving{
fn take_order(){

}
fn serve_order(){

}
fn take_payment(){

}
}
}

pub fn eat_at_restaurant() {
crate::front_of_house::hosting::add_to_waitlist();

front_of_house::hosting::add_to_waitlist();
}

这里我们用pub关键字声明了hosting模块公有,但实际仍然会编译错误。因为这一次声明,只是声明了父级的 front_of_house 模块可以访问 hosting 模块了,但父级 hosting 模块却依旧不能访问它的私有字段 add_to_waitlist

也就是说,父级条目是不是公有的并不影响它内部的条目的公有或私有状态

所以要代码能编译通过很简单,我们把 add_to_waitlist 也公开就行。

可能有人想问,为什么 front_of_house 没有公开也能访问?因为 front_of_house 和 eat_at_restaurant 是同级的,具有互相访问的权限。

super 关键字

super 关键字和python一样,用来查找到父模块的路径,属于相对路径的一种。应该不用多说,用一个代码来当例子吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
mod front_of_house{
pub mod hosting{
pub fn add_to_waitlist(){

}
fn seat_at_table(){

}
}

mod serving{
fn take_order(){
super::hosting::add_to_waitlist(); // 通过相对路径访问到add_to_waitlist
}
fn serve_order(){

}
fn take_payment(){

}
}
}

结构体和枚举类型的公有

结构体

结构体的权限和模块基本相同,也就是父级模块的公有,不影响子字段的公有或私有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
mod back_of_house{
pub struct Breakfast{
pub toast: String,
seasonal_fruit: String,
}
impl Breakfast{
pub fn summer(toast: &str) -> Breakfast{
Breakfast{
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}

pub fn eat_at_restaurant() {
let mut meal = back_of_house::Breakfast::summer("Rye");
meal.toast = String::from("Wheat");
println!("I'd like {} toast please", meal.toast);
println!("I'd like {} fruit please", meal.seasonal_fruit);
}

这里,我们首先定义了一个后厨模块 back_of_house,定义了一个早餐结构体 Breakfast,包含吐司和水果两个变量,一个公有一个私有,然后实现了一个结构体内的函数 summer,用来创建一个包含指定吐司和水果 Breakfast 结构体。

后续,我们在 eat_at_restaurant通过相对路径创建了一个可变的 Breakfast 结构体,并试图访问结构体的字段。

通过观察报错就可以知道,公有的 meal.toast 可以正常的访问和修改,但私有的 meal.seasonal_fruit 无法访问,也就无从谈起修改了。

值得一提的是,因为 Breakfast 有一个私有字段,所以如果我们不定义一个子级的 summer 函数,我们甚至不能创建一个 Breakfast 结构体,因为我们没办法在外部给 seasonal_fruit 赋值。

枚举类型

但枚举类型不一样,枚举公有后,所有字段都公有了,因为枚举类型这东西必须全公有才好用,一个公有一个私有,没有什么意义,例如说你 match 总要做个完整性检查吧?字段都不能全部完全访问,何谈完整的处理?所以一半私有一半公有是没有意义的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
mod back_of_house{
pub struct Breakfast{
pub toast: String,
seasonal_fruit: String,
}
impl Breakfast{
pub fn summer(toast: &str) -> Breakfast{
Breakfast{
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}

pub enum Appetizer{
Soup,
Salad,
}
}

pub fn eat_at_restaurant() {
let a = back_of_house::Appetizer::Soup;
let b = back_of_house::Appetizer::Salad;
}

这里我们新加了一个枚举类型前菜 Appetizer,字段没有声明为公有,但依旧可以正常访问。

use关键字

use的基本使用

如果我们要多次调用模块里的函数,如果一直要写一大串的路径似乎有点麻烦。use关键字就是用来简化这个步骤的。原理就叫:引入作用域

1
2
3
4
5
6
// 还是前面的代码,前厅部分,为了美观省略了
use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}

这里我们就用 use 关键字,用绝对路径的方法把 hosting 这个字段引入了当前作用域,这样 hosting 就可以作为本地的字段,直接使用就行了,当然,这样引入后自然也会和本地的 hosting 字段冲突,你不能再定义一个叫 hosting 的东西了。

或许有人想问 use 相对路径行不?可以,可以自己试试。

PS:在书上的版本还需要在相对路径前加入 self 字段来指定当前所在的作用域,但它提到了有些开发者在视图去掉这个字段。我的版本下没加也编译通过了,看来他们成功了。

后面书上还提到了一个代码规范,虽然我不会这么写,但似乎 csdn 的博客上有不少人确实会这样喜欢省事,我简单拉出来提一下。

有人可能会问,上面的代码为什么只 use 到了 hosting,既然只用 add_to_waitlist 函数为什么不直接 use 到 add_to_waitlist 函数呢?

原因很简单,我们需要告诉所有看这段代码的人,当然也包括自己,这个函数并不是在本地定义的,而是引用了哪里的一个包/模块里面的函数,既增加了一定的可读性,也防止了一部分同名。毕竟很多最底层的字段命名,一般都是十分通用的名字。这是一个好习惯。

pub use

use 字段通常是以私有的方式,引入当前作用域,也就是说,你引入之后,也只是在当前 use 的作用域生效,在其地方是不生效的,依旧要通过路径访问。pub use 字段就是解决这个问题的,让其他代码也能简单的导入代码。

这个的应用场景主要是,你为了自己更方便的管理自己的代码,所以分了很多层级结构,但其实其他代码并不是很在乎你这块代码的很多结构,就比如餐厅的例子,顾客不需要在乎你的前厅,你的后厨,顾客只在乎来餐厅吃饭的几个步骤,坐座位,点单,上菜,吃。所以你就可以使用 pub use,把这几个步骤从前厅后厨里导出来,方便顾客使用。

例子到后面跨文件的部分再举吧。

嵌套路径use

当我们使用包内的多个模块的时候,每个都写一行 use 会占用很多地方,这时候就可以用嵌套路径的方法。用标准包为例子

1
2
3
4
5
use std::cmp::Ordering;
use std::io;

// 下面的写法和上面等价
use std::{cmp::Ordering, io};

也就是用花括号把同级的模块括起来一起导入,逗号分开。

如果我导入了一个模块,又想导入模块下面的一个函数呢?可以用 self 代表当前路径

1
2
3
4
5
use std::io;
use std::io::Write;

// 下面的写法和上面等价
use std::{self, Write};

如果我想导入一个模块的所有字段的?可以用统配符*,就相当于 import * 吧。是个坏文明,因为这样你就不知道你写的字段是不是和包内的字段重名了,别用。

1
use std::io::*;

将模块拆分为不同文件

之前说过了模块的层级目录对吧,我们可以把这个层级目录转换为对应的文件层级目录,实现不同文件对应不同模块。

用之前写的代码 front_of_house->hosting->add_to_waitlist 为例子。

首先,我们需要在我们的库单元包声明一个前台模块。

lib.rs

1
2
3
4
5
6
7
mod front_of_house;

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}

然后,对应的再在 lib.rs 的同级目录下新建一个 front_of_house.rs 文件,对应模块的入口。声明 hosting 模块。

front_of_house.rs

1
pub mod hosting;

然后,我们再在同级下新建一个文件夹,front_of_house,对应front_of_house模块下面的模块代码入口。再在 front_of_house 文件夹下新建一个 hosting.rs。添加进我们的 add_to_waitlist 代码

front_of_house/hosting.rs

1
2
3
pub fn add_to_waitlist(){
println!("add_to_waitlist");
}

修改完后,再 cargo run 一下,编译没有问题,依旧能够通过路径正常访问到 add_to_waitlist 函数~这就是模块层级路径和文件层级路径的对应关系,只需要声明模块后,给出一个模块的对应入口文件,就可以通过这种方式拆分为多个模块文件啦。

好,我们再来试试跨文件使用代码吧。

还记得我们一开始 cargo new 是 new 了一个名为 test 的包吧?lib.rs 作为库单元包,其实就是相当于 test 包的入口文件,用来声明 test 包下的单元包。

还记得我们之前使用了 pub use 把 hosting 引入到了 lib.rs 的作用域,也就是相当于引入到了 test 包的作用域。基于这个前置知识,我们就可以在 main.rs 里跨文件使用 add_to_waitlist 函数了。

1
2
3
4
5
use test::hosting;
fn main() {
println!("Hello, world!");
hosting::add_to_waitlist();
}

运行成功输出 add_to_waitlist,也就是 pub use 在 test 包的作用域下声明了一个公有的字段 hosting,我们就可以通过路径,在 test 下访问到 hosting了。

当然,如果没有 pub use 我们也可以通过 use test::front_of_house::hosting; 的绝对路径去访问 hosting,但是注意,因为我们的 front_of_house 在 lib.rs 不是公有声明,所以 test 作为父级,是访问不到的,必须要声明为公有模块才可以访问(同理普通的 use 不能访问也是一个原理)。


第七章结束,也是常规一章的知识,总结一下这章的内容就是

  1. 模块的定义和使用方法
  2. 公有私有的权限管理,用结构体和枚举类型为例子更详细的说明了父子权限的关系
  3. use 关键字的原理和用法
  4. 模块层级路径和文件层级路径可以相互转换管理

今天就到这!

  • 标题: 【Rust 学习记录】7. 包、单元包和模块
  • 作者: TwoSix
  • 创建于 : 2023-04-01 22:41:12
  • 更新于 : 2024-07-04 23:52:28
  • 链接: https://twosix.page/2023/04/01/【Rust-学习记录】7-包、单元包和模块/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
\ No newline at end of file diff --git "a/2023/04/02/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2218-\351\200\232\347\224\250\351\233\206\345\220\210\347\261\273\345\236\213/index.html" "b/2023/04/02/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2218-\351\200\232\347\224\250\351\233\206\345\220\210\347\261\273\345\236\213/index.html" index 29d482d..07dbb4e 100644 --- "a/2023/04/02/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2218-\351\200\232\347\224\250\351\233\206\345\220\210\347\261\273\345\236\213/index.html" +++ "b/2023/04/02/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2218-\351\200\232\347\224\250\351\233\206\345\220\210\347\261\273\345\236\213/index.html" @@ -1,2 +1,2 @@ -【Rust 学习记录】8. 通用集合类型 - TwoSix的小木屋 +【Rust 学习记录】8. 通用集合类型 - TwoSix的小木屋

【Rust 学习记录】8. 通用集合类型

TwoSix Lv3

动态数组

定义

1
let a:Vec<i32> = Vec::new();

定义非常简单,是Vec的格式,Vec 也就是 vector,动态数组类型的关键字,用两个尖括号括住动态数组所存放的数据类型 T,代码里就是存放 i32 类型的数据。再使用new方法分配一片空间。

上面是一个创建一个指定类型的空数组,因为是空数组,所以编译器没法推理出我们数组的类型,所以要显示定义类型,如果给定数据,就不需要显示指定类型了,如下

1
let b = vec![1,2,3];

这里我们用到了Rust官方提供的宏vec!用来创建一个动态数组,用方括号给定初始值,因为给的都是整数值,所以编译器会默认推理得 b 是 i32 类型的动态数组

动态数组的所有权:动态数组的所有元素的所有权和变量绑定,当变量被销毁时,所有元素被销毁

使用

末尾添加元素

使用push方法即可在末尾添加元素

1
2
3
4
5
6
fn main() {
let mut a:Vec<i32> = Vec::new();
a.push(1);
a.push(2);
println!("{:?}", a);
}

读取元素

Rust 提供了两种 vec 的访问方法

  1. [] 运算符访问
  2. get() 方法访问
1
2
3
4
5
6
7
let e1 = &a[0];
println!("{:?}", e1);
let e2 = a.get(1);
match e2{
Some(e) => println!("{}", e),
None => println!("None"),
}

e1 通过下标访问获取 a 中的元素,而 e2 则是通过 get 方法访问获取 a 的元素,二者主要有以下区别:

  1. &[]会返回元素的引用
  2. get会返回一个Option<&T>类型

所以作用也很明显了,get 方法可以有效避免访问越界的问题,当你访问了一个不存在的元素时,它会给你返回一个 None 值,而 [] 则不会,若访问越界,会直接导致程序崩溃。两者可以视情况使用。

值得一提的是,这里使用引用借用了一下数组的元素,而之前我们说过,不可变引用不能和可变引用一起定义,所以当我们借用了数组元素后,是没有办法向数组末尾添加元素的

1
2
3
4
let mut a = vec![1, 2, 3];
let e1 = &a[0];
a.push(4);
println!("{:?}", e1);

因为push的时候,相当于传入了一个可变的引用,用于修改数组,而之前已经定义了一个不可别引用,两者无法同时存在,所以编译器会报错。

可能会有人问,我只是引用了数组的第一个元素而已,和插入的元素有什么关系?

这是因为动态数组本质上还是存储在一片堆上的连续空间,正因为是连续的,所以你变动了前面的元素的时候,就有可能会影响到后面的元素,这就是另类的数据竞争,Rust 的所有权机制杜绝了这种情况的发生(同理,你后面不会用到 e1,不存在修改前面数据情况的话,其实是可以编译通过的)

PS:

书上只提到了借用数组元素的访问方法,但实际上不用借用也是可以访问的,会通过创建一个副本的方式来进行返回

1
2
3
4
5
6
fn main() {
let mut a = vec![1, 2, 3];
let e = a[0]; // 返回的是值的副本
a.push(4); // 可以传入可变引用来修改数组
println!("{}", e);
}

记得我们在前面提过,基本类型都是存储在栈里的,所以默认赋值方式都是深拷贝返回副本。而存储在堆里的数据就不一样了,默认都是浅拷贝,所以当我们使用字符串的时候就没办法不借用访问了,编译器会报错提示我们需要使用copy方法,手动拷贝。

1
2
3
4
5
6
fn main() {
let mut a = vec![String::from("hello"), String::from("world")];
let e = a[0]; // 需要返回拷贝,但默认赋值不拷贝,报错。
a.push(String::from("!!!"));
println!("{}", e);
}

遍历数组

我们可以通过for循环来遍历动态数组

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let a = vec![1, 2, 3];
for i in &a {
println!("{}", i);
}
let mut b = a;
for i in &mut b{
*i += 5;
}
println!("{:?}", b);
}
  1. 对于不可变的动态数组a,我们用了不可变引用来遍历数组,并打印输出
  2. 对于可变数组b,我们用了可变引用来遍历数组,再进行解引用访问到对应的值,把他们每一个值都+5,再输出数组b

值得一提的是,这里遍历不用iter()方法,估计是通过某种方法内置了

用枚举类型让动态数组存储多个类型的值

一个动态数组只能存储一个类型的值,那我们需要存储多个类型的时候怎么办?记得之前介绍枚举类型的时候,我们提到过可以利用枚举类型,方便的向函数传入多个类型的参数,,那么我们可以利用一下这个特性,用于在动态数组里存储多个类型的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum MyType{
Int(i32),
Float(f32),
Text(String),
}

fn main() {
let a = vec![MyType::Int(1), MyType::Float(2.0), MyType::Text(String::from("Hello"))];
let e = &a[0];
match e {
MyType::Int(value)=> println!("Int: {}", value),
MyType::Float(value)=> println!("Float: {}", value),
MyType::Text(value)=> println!("Text: {}", value),
};
}

在这个例子里我们就把三个类型的值都定义为同一个枚举类型的值,让动态数组能够方便的存储。

同时,因为是枚举类型,所以理所当然的我们就需要在访问的时候穷举所有可能性。

字符串

我们已经在代码里用过无数次的String了,在这一节里,我们就需要更加深入的理解以下Rust里的字符串原理。

什么是字符串

Rust 核心部分只有一种字符串类型:字符串切片str,通常以&str的形态出现,用于指向别人的一段UTF-8编码的字符串引用

而我们常用的字符串String是实现在标准库里的。但这两种字符串的应用都非常广泛,我们既需要一个结构来存储完整的字符串,也很经常需要用字符串切片来引用其中一段字符串,人们一般把这两种类型都称作字符串,毕竟他们表现出来的样子就是一个人类所理解的字符串。另外再强调一下,Rust的字符串编码是UTF-8

当然标准库里还有很多乱七八糟的字符串,比如OsStringOsStrCStringCStr,后面的结尾是StringStr也表明了这个结构到底是所有权版本还是借用的版本,这些都是不同编码或者不同内存布局的字符串,书上没讲好像也不会讲,感兴趣可以自行查询官方API文档学习。

字符串的使用

很多在Vec能用的方法在String也能用

创建

首先我们就可以使用new的方法来创建空字符串:let s = String::new();

对于有初始值的情况,我们也可以用我们所熟知的String::from(),也可以用to_string的方法。

1
2
let data = "hello, world!"; // 创建一个字符串切片 &str
let s = data.to_string(); // 把&str转为String类型,获取所有权

这里我比较疑惑的是,用双引号定义的数据值在没有用to_string的时候所有权归谁?在什么时候被回收?书上说的是,这种类型叫Display trait,但没有详细说明,暂且放下。

fromto_string的效果是一样的,可以根据个人喜好使用。

更新

使用pushpush_str都能向字符串的末尾添加内容,其中push是添加字符,而push_str是添加字符串

1
2
3
4
5
6
fn main() {
let mut s = String::new();
s.push_str("hello world");
s.push('!'); // 单引号表示字符
println!("{}", s);
}

另外,push_strpush都是不取得所有权的,所以我们可以传入字符串切片就能实现添加,同时如果传入其他变量的话,也不会使得其他字符串失效。

同时,我们也可以使用+运算符来实现字符串的拼接

1
2
3
4
5
6
fn main() {
let s1 = String::from("hello");
let s2 = String::from(" world");
let s3 = s1 + &s2;
println!("{}", s3);
}

但需要注意的是,+运算符只能实现String&str的拼接,也就是说以上代码会夺取s1的所有权,然后返回给s3,然后s1就不能再使用了,+运算符的大概定义是这样的:

1
fn add(self, &str)->String{}

所以+运算符似乎看起来用着不是很方便,首先1. 所有权会有影响,2. 在多字符串拼接的时候其实不是很方便。

所以我们还有一个宏函数,format!

1
2
3
4
5
6
fn main() {
let s1 = String::from("hello");
let s2 = String::from(" world");
let s3 = format!("{}{}", s1, s2);
println!("{}", s3);
}

format!println!的用法是一样的,不同的是println是格式化输出到屏幕上,format是格式化输入到变量里存起来,而且format不会夺取任何变量的所有权

访问字符串

为什么不能索引访问?

大部分语言都是支持通过[]运算符,利用索引访问字符串的,我们可以试试

1
2
3
4
fn main() {
let s1 = String::from("hello");
let s = s1[0];
}

报错:error[E0277]: the type String cannot be indexed by {integer} 很简单,就是说String不支持索引访问。

至于为什么,就要从Rust实现String的底层来解释了。

我们可以先来看一下这个例子

1
2
3
4
5
fn main() {
let len1 = String::from("hello").len();
let len2 = String::from("你好").len();
println!("len1: {}, len2: {}", len1, len2);
}

这段代码里len1的输出值是5,这很正常,每个英文字母占用1个字节,代表一个位置,但len2却输出的是6,每个字符占用了3个字节,也就是3个位置,这意味着我们并不能通过索引,访问到我们真正想访问到的字符。

因为在Rust里,String其实是通过一个Vec的动态数组来实现的,其中u8就是对应着UTF-8编码的字节值。一个Unicode字符可以由多个UTF-8编码来表达,所以当用户访问索引0的时候,得到的只是“你”的编码的其中一部分,而不是完整“你”,是一个没有任何含义的整数值,这是没有意义的。所以为了避免返回一个不是用户期待的值,所以Rust禁用了索引访问,不进行编译。

当然,还有另外一个禁用索引访问的理由,那就是用户往往觉得索引访问的时间复杂度理所当然是O(1),但在字符串里,就需要视情况而定,不能保证,所以不用也好。

同理还有字符串切片

1
2
let s = String::from("你好");
println!("{}", &s[0..3]);

我们之前知道了你好里面每个字符占3个字节,所以我们可以通过一次性切3个字节,来得到一个“你”,那我是不是可以通过曲线救国的方式,利用切片来实现索引访问0?很不幸,Rust也掐死了这一条路,当你视图使用&s[0..1]的方式访问索引0时,程序会发生崩溃,告诉你不能这么写。

怎么访问字符串?

既然不能索引访问,那我们究竟要怎么访问字符串呢?

  1. chars()方法
1
2
3
4
5
6
fn main() {
let s = String::from("你好");
for c in s.chars() {
println!("{}", c);
}
}

chars方法能把字符串里的字节值凑成一个char类型的值再作为一个结果数组返回给你,这样你就可以放心的访问得到你所希望的字符了。

  1. bytes() 方法

可能就有人说了,那我就是想访问到索引0,我想知道这里存的字节值是什么!放心,也有办法,bytes() 方法就可以把字符串转成字节值的数组,让你访问到每一个字节值

1
2
3
4
5
6
fn main() {
let s = String::from("你好");
for c in s.bytes() {
println!("{}", c);
}
}

总的来说,Rust为了考虑字符串中的使用安全,把很多字符串内部实现的复杂性给暴露了出来,你不得不去考虑更多底层的东西。这也是一个权衡,为了安全,你不得不付出一些便利性的代价。

哈希映射

哈希也是一个非常常用的数据结构了,它存储了一个键(Key)到值(Value)的映射关系,很多时候我们并不满足于普通数组的索引下标-值的映射关系,这时候就可以用哈希映射。

创建和使用

1
2
3
4
5
6
7
use std::collections::HashMap;

fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("blue"), 1);
scores.insert(String::from("red"), 2);
}

哈希映射(HashMap)被定义在标准库的collections中,不是默认导入的,所以我们要用use来引入。

我们通过常规的new方法建立了一个哈希映射,然后通过insert方法插入了一对映射关系,其中第一个值是key,第二个值value,这里的意思就是,我们建立了一个比分映射关系,key是队伍,用字符串代表红队蓝队,value是值,代表比分。建立这么一个映射关系,我们就可以很轻松的知道队伍的对应得分。

值得一提的是,哈希映射也要求所有的键是一个类型,所有的值是一个类型

我们也可以通过动态数组来作为初始值构建哈希映射

1
2
3
4
5
6
7
8
use std::collections::HashMap;

fn main() {
let team = vec![String::from("blue"), String::from("red")];
let score = vec![1, 2];
let scores: HashMap<_,_> = team.iter().zip(score.iter()).collect();
println!("{:?}", scores);
}

这段代码里,我们首先定义了一个动态数组存储队伍,一个动态数组存储比分。然后我们使用zip方法,把队伍和比分创建一个元组一一对应起来,这里得到的结果大概是这样的:[(blue, 1), (red, 2)],然后,我们再通过collect()方法,以第一个值为key,第二个值为value,把这么一个元组数组转成哈希映射。

这里需要显示的给出类型,因为collect方法可以作用于不同的数据结构,不止是哈希表,所以我们要告诉编译器,这里我们是collect成了一个哈希表,但对于哈希表内的元素类型,我们可以写成通配符,让编译器去推理得到。

哈希映射的所有权

和之前说的差不多,对于基本类型这种存储在栈上的数据,会深复制一份进入哈希映射,而存储在堆上的数据,像字符串,会把所有权一并传入哈希映射,导致原来的变量被销毁。

当然我们可以把字符串的引用传进哈希映射,但这种做法就需要保证哈希映射的作用域结束之前字符串是一直有效的。这一部分的知识需要到后面的生命周期部分详细解释。

访问元素

和动态数组的访问一样,我们可以通过[]访问,也可以通过get访问,里面不同点和需要注意的地方也是相同的。

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let teama = String::from("blue");
let mut scores = HashMap::new();
scores.insert(teama, 1);
scores.insert(String::from("red"), 2);
println!("{:?}", scores["blue"]);
match scores.get("blue") {
Some(&score) => println!("blue team score is {}", score),
None => println!("blue team score is not found"),
}
}

遍历访问同理

1
2
3
4
5
6
7
8
9
fn main() {
let teama = String::from("blue");
let mut scores = HashMap::new();
scores.insert(teama, 1);
scores.insert(String::from("red"), 2);
for (key, value) in &scores { //注意借用,不然for完所有权被没收了
println!("{}: {}", key, value);
}
}

更新

覆盖旧值

哈希映射的一个键只能对应一个值,所以当我们往哈希映射里插入已有的键值,会覆盖原有的键的值。

1
2
3
4
5
6
7
8
9
use std::collections::HashMap;

fn main() {
let teama = String::from("blue");
let mut scores = HashMap::new();
scores.insert(teama, 1);
scores.insert(String::from("blue"), 2); // 把原本blue的1覆盖为2
println!("{:?}", scores);
}

有时我们不想覆盖掉旧值,只想在没有对应的key的时候才插入数据,这时候可以用entry方法

1
2
3
4
5
6
7
8
9
10
use std::collections::HashMap;

fn main() {
let teama = String::from("blue");
let mut scores = HashMap::new();
scores.insert(teama, 1);
scores.entry(String::from("blue")).or_insert(3);
scores.entry(String::from("red")).or_insert(3);
println!("{:?}", scores);
}

entry方法会返回一个Entry的枚举,表面这个键的值是否存在,or_insert方法返回Entry所指向的值的引用,如果值不存在,就把传入的值插入到哈希映射里,返回这个值的引用。

知道了or_insert后,我们就可以利用它做一些更灵活的应用。

1
2
3
4
5
6
7
8
9
10
11
use std::collections::HashMap;

fn main() {
let text = String::from("aaaabbbbbcc");
let mut char_count = HashMap::new();
for c in text.chars() {
let count = char_count.entry(c).or_insert(0);
*count += 1;
}
println!("{:?}", char_count);
}

这段代码就是简单的数字符串里的英文字母有多少个。我们利用entry+or_insert在字母不存在的时候,赋初值为0,然后对这个访问到的字母加1次数。之前说了,or_insert会返回一个值的引用,所以我们可以利用这个返回,轻松的访问到对应的值的空间,只需要一次解引用就可以了。并且这个可变引用会在for循环结尾就离开作用域,也满足安全的规则

值得一提的是,似乎不能通过索引的方式对哈希表进行值更改,但报错提示里提到了get_mut()方法,书上没说,或许感兴趣可以后续看看。

所使用的哈希函数

Rust 默认使用的是一个比较安全的哈希加密算法,但并不是最高效的一个算法,如果你觉得这样效率太低,当然你也可以通过 trait 的方式使用自己的哈希算法,这会在后续的章节里有讲。


结束!这一章里面主要就是学了动态数组/字符串/哈希映射三个集合结构,介绍了一下基本的使用,还有他们的所有权规则,为了满足这个所有权,很多结构的使用方式都变得有点麻烦了,放python里,基本都是一个索引就能解决的事,在这里要考虑一大堆所有权问题,肉眼可见的麻烦起来了。

下一节学错误处理,学完就到trait和生命周期了,搞完这些个rust独有的概念,也就差不多可以上手使用了。

  • 标题: 【Rust 学习记录】8. 通用集合类型
  • 作者: TwoSix
  • 创建于 : 2023-04-02 22:51:57
  • 更新于 : 2024-07-04 23:52:28
  • 链接: https://twosix.page/2023/04/02/【Rust-学习记录】8-通用集合类型/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
\ No newline at end of file diff --git "a/2023/04/06/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2219-\351\224\231\350\257\257\345\244\204\347\220\206/index.html" "b/2023/04/06/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2219-\351\224\231\350\257\257\345\244\204\347\220\206/index.html" index a39fa00..3d94015 100644 --- "a/2023/04/06/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2219-\351\224\231\350\257\257\345\244\204\347\220\206/index.html" +++ "b/2023/04/06/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\2219-\351\224\231\350\257\257\345\244\204\347\220\206/index.html" @@ -1,2 +1,2 @@ -【Rust 学习记录】9. 错误处理 - TwoSix的小木屋 +【Rust 学习记录】9. 错误处理 - TwoSix的小木屋

【Rust 学习记录】9. 错误处理

TwoSix Lv3

前言

Rust 里的错误主要分为两种:1. 不可恢复错误:主要指的就是程序Bug之类的用户不可见的错误,例如尝试访问超过数组长度的下标;2. 可恢复错误,例如文件没找到等,可以提示用户再次查找。Rust 对这两种错误进行了区分,并针对不同的场景提供了许多的特性来处理

不可恢复错误与Panic!

Panic!宏

介绍

Panic!宏是专门用于处理某个错误被检测到,而程序员不知道该怎么处理的情景。Panic!宏会首先打印一段错误提示信息,然后沿着调用的栈反向遍历,清理等待执行的指令以及它们的数据,清理完毕后退出程序。

提示

PS: Rust程序打包时会默认附带很多信息来支持Panic!的栈展开清理操作,如果你不需要 Rust 自己清理,可以接受把内存交由操作系统来回收,并且需要打包的二进制包体尽可能小的话,可以在Cargo.toml[profile]区域添加panic = 'abort'来讲panic的默认行为从展开切换为直接终止。例如

1
2
[profile.release]
panic = 'abort'

使用

1
2
3
fn main() {
panic!("crash and burn");
}

运行这段代码,会得到报错

1
thread 'main' panicked at 'crash and burn', src\main.rs:2:5note: run with RUST_BACKTRACE=1 environment variable to display a backtrace

第一句很简单,输出了我们的报错信息,并告诉了我们panic的位置在main.rs第2行第5个字符。

第二句则提示我们可以用环境变量RUST_BACKTRACE=1显示回溯信息,是什么意思呢?我们可以试一下。设置环境变量后再运行

1
2
// windows系统下,powersell环境的命令
$env:RUST_BACKTRACE=1; cargo run

我们可以看到新的报错信息

1
2
3
4
5
6
7
8
9
stack backtrace:
0: std::panicking::begin_panic_handler
at /rustc/8460ca823e8367a30dda430efda790588b8c84d3/library\std\src\panicking.rs:575
1: core::panicking::panic_fmt
at /rustc/8460ca823e8367a30dda430efda790588b8c84d3/library\core\src\panicking.rs:64
2: test_error::main
at .\src\main.rs:2
3: core::ops::function::FnOnce::call_once<void (*)(),tuple$<> >
at /rustc/8460ca823e8367a30dda430efda790588b8c84d3\library\core\src\ops\function.rs:250

可以看到这就是我们panic栈展开的时候的具体信息,首先是panic函数,再是panic标准化输出,然后是我们自己的文件.\src\main.rsmain函数,最后我也不清楚,可能是main函数入口吧。

我们就可以根据这个回溯信息,一个一个查找到错误发生的地方,进行排除(感觉用的不会很多?)

另外,后面我们还得运行代码,如果不想看到这么一长串,记得把这个环境变量设回0

可恢复错误与Result

Result枚举类型

简单使用

其实我们在之前就已经使用过了Result了,在编写随机数字时,我们在处理用户输入的时候就用了expect函数,那时候我们就简单的提到了,Result是一个枚举类型,包含OkErr两个变体。它的定义是这样的:

1
2
3
4
enum Result<T,E>{
Ok(T),
Err(E),
}

其中T,E是两个泛型参数,下一章就会讨论到泛型。总之就是1. T包含了Ok里的值,跟随着程序执行成功时返回对应的值;2. E包含了错误类型,跟随着执行失败的时候返回

当一个可能会运行失败的函数,可以将Result作为返回结果,例如标准库里的打开文件操作

1
2
3
4
5
6
7
8
9
10
11
use std::fs::File;

fn main() {
let f = File::open("hello.txt");
let f = match f{
Ok(file) => file,
Err(error) => {
panic!("There was a problem opening the file: {:?}", error)
}
};
}

File::open函数回返回一个Result类型,所以我们需要用matchf进行额外的处理,取出Result里的值,才能正确的获得打开文件,这迫使程序员必须用match枚举所有可能性,不然就无法正常编译~

处理不同错误

文件打开可能有多种错误,可能是文件不存在,可能是没有文件的读权限,那我们怎么从Err里分辨多种错误呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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(error) => panic!("Problem creating the file: {:?}", error)
}
},
other_error => panic!("Problem opening the file: {:?}", other_error)
}
}
};
}

这段代码多了不少东西,我们一个一个来说

  1. 通过use引入了io标准库的标准io相关错误类型ErrorKind
  2. error错误类型的值可以通过 .kind 函数获取它的错误类型,错误类型也是一个枚举类型
  3. 所以可以通过 match 匹配错误类型,我们针对找不到文件的类型作了特殊处理——创建一个文件(创建文件的同时也要处理创建是否成功的Result类型),对于其他类型不知道怎么处理,就调用 panic!

可以看见,写一段 rust 代码突然变得繁琐起来了,本来我们只要几行代码就搞定的东西,Rust 却逼我们写了一堆 match 来处理报错,可读性也损失了不少。

书上提到了 Rust 提供了一个闭包的特性解决这个问题,简化代码,增加错误处理的可读性,以下是一个代码示例。但具体在后面讲解闭包的时候再作解释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use std::fs::File;
use std::io::ErrorKind;

fn main() {
let f = File::open("hello.txt").map_err(|error|{
if error.kind() == ErrorKind::NotFound{
File::create("hello.txt").unwrap_or_else(|error|{
panic!("Problem creating the file: {:?}", error);
})
}
else{
panic!("Problem opening the file: {:?}", error);
}
});
}

快捷处理方式

每次都写 match 和 panic! 来处理实在太麻烦了,Rust 当然也提供了一些快捷方式——expectunwrap

unwrap

unwarp可以在返回的是 Ok 时直接返回值,是 Err 时直接自动帮你触发 panic! 报错。

1
2
3
4
5
use std::fs::File;

fn main() {
let f = File::open("hello2.txt").unwrap();
}

没有文件时,会直接触发报错

1
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "系统找不到指定的文件。" }'

expect

expect的效果和unwarp是差不多的,不同点在于expect可以让程序员自己指定一个报错的提示信息

1
thread 'main' panicked at 'Failed to open hello2.txt: Os { code: 2, kind: NotFound, message: "系统找不到指定的文件。" }', src\main.rs:4:38

可以看见 paniked at xxx 的地方发生了变化

错误传播

上面提到了可能执行失败的函数会返回一个错误类型,那我们自己写的函数怎么返回呢?以下是一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use std::fs::File;
use std::io;
use std::io::Read;

fn my_function() -> Result<String, io::Error>{
let f = File::open("hello.txt");
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut s = String::new();
match f.read_to_string(&mut s){
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}
fn main() {
my_function().unwrap();
}

这里我们定义了一个函数,返回类型是 Result 枚举,其中两个关于Ok和Err的泛型参数被我们规定为了字符串和标准io错误(这里用标准io错误是因为我们函数里相关的错误都是io错误,当然你可以自己定义其他错误,作一个额外处理然后返回)。

函数里首先对 hello.txt 文件进行读取,读取成功的话取出 Ok 里的文件句柄 file,失败的话则用 return 关键字提前结束函数,返回 Err 类型,然后读取文件内容,存到字符串里,同时也要处理是否读取成功,最后一个 match 表达式不写分号即可直接返回值,不用 return。

PS: 这里有个小细节,f 必须是可变的才能读取内容。为什么呢?我只读文件没有修改文件内容啊。因为这里读取到的 f 只是一个句柄,而我们真正读取内容的时候,需要修改偏移量什么的吧。因此需要可变。

?运算符

因为错误传播太常见了,所以这一块有着一点语法糖——?运算符

我们直接看一段例子

1
2
3
4
5
6
fn my_function() -> 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是差不多的,首先,它只能用于Reult类型的后面,且只能用于返回值的Result类型的函数。在值为Ok的时候,它会把Ok的值返回作为这个表达式的结果。在值为Err的时候,它会调用return把错误类型作为整个程序的返回。

最后我们手动构建了一个Result的Ok变体用于运行成功的返回。

?运算符也和match表达式有一点点不同。那就是它对于错误类型,也就是Err(e)里的e,会偷偷调用一次from函数,把错误类型转换成我们函数所返回的错误类型,这对于我们有不同类型的错误,但函数返回的类型只有一种时很有用,如果是match还需要我们手动进行额外处理。

当然,我们还能把代码写的更短,在实际项目中,给文件句柄赋予一个 f 变量其实也是多余的

1
2
3
4
5
fn my_function() -> Result<String, io::Error>{
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}

各种报错方法的使用场景

关于panic!和Result

调用 panic! 代表程序已经无法从这个错误中恢复了,所以选择自行了解。所以当你觉得在某种情况下无论如何也恢复不了,就可以代替使用这个函数的人直接 panic!,不然大多时候都可以选择返回一个 Result,交由使用者自己决定是不是 panic!

当然,还有另外一种情况,那就是用户的操作违反了你所编写的一些程序的“约定”,且这个非法操作会破坏你代码的一些原有行为,难以恢复,又或者是可能会触及到你代码的一些安全漏洞,这时候就可以选择直接 panic! 以终止用户的非法行为。例如数组访问越界,防止用户因为数组访问越界,访问到不属于这个数组的内存数据,以此造成一些安全问题的时候,我们通常会直接 panic!

关于expect和unwrap

对于早期开发和测试

使用 expect 和 unwrap 处理错误固然方便,但也代表我们失去了处理不同错误的机会,因为它会帮助我们直接把程序 panic! 掉。

所以一般情况下,我们都会把 expect 和 unwrap 当成是一个占位符,告诉后面,这里有个错误需要处理,但在开发的早期和需要测试的时候,我们为了方便会选择直接把程序 panic! 掉,既提高了开发效率,也可以方便测试,毕竟程序都直接崩溃了,这里的bug你总不能忽视了吧?快修bug去。

当然在后期交付用户的时候,为了用户友好性,当然不能一点小错误就直接 panic!,这时候我们就可以一个一个的查询之前留下的 expect 和 unwrap,去处理更细致的报错。

对于你确定不会发生报错的时候

在某些时候,你确定100%不可能会出现 Err 变体,那当然也能直接用 unwrap 节省代码量。例如

1
2
3
4
use std::net::IpAddr;
fn main() {
let home: IpAddr = "127.0.0.1".parse().unwrap();
}

127.0.0.1 总不可能是个非法IP地址了吧?这还要我去处理各种报错就有点不合理了。


以上,就是错误处理的全部内容了,大致总结一下就是

  1. panic! 和 Result——硬处理和软处理的方式
  2. Result处理的优化——expect, unwrap 和 ?运算符
  3. 错误类型——error.kind()
  4. 一些基本的错误处理场景原则

第10章开始就是 trait,泛型和生命周期了。

  • 标题: 【Rust 学习记录】9. 错误处理
  • 作者: TwoSix
  • 创建于 : 2023-04-06 22:23:58
  • 更新于 : 2024-07-04 23:52:28
  • 链接: https://twosix.page/2023/04/06/【Rust-学习记录】9-错误处理/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
\ No newline at end of file diff --git "a/2023/04/15/\344\275\277\347\224\250Sphinx\344\270\272\344\275\240\347\232\204\351\241\271\347\233\256\345\277\253\351\200\237\346\236\204\345\273\272\346\226\207\346\241\243/index.html" "b/2023/04/15/\344\275\277\347\224\250Sphinx\344\270\272\344\275\240\347\232\204\351\241\271\347\233\256\345\277\253\351\200\237\346\236\204\345\273\272\346\226\207\346\241\243/index.html" index 52a77d0..634c3f3 100644 --- "a/2023/04/15/\344\275\277\347\224\250Sphinx\344\270\272\344\275\240\347\232\204\351\241\271\347\233\256\345\277\253\351\200\237\346\236\204\345\273\272\346\226\207\346\241\243/index.html" +++ "b/2023/04/15/\344\275\277\347\224\250Sphinx\344\270\272\344\275\240\347\232\204\351\241\271\347\233\256\345\277\253\351\200\237\346\236\204\345\273\272\346\226\207\346\241\243/index.html" @@ -1,2 +1,2 @@ -使用Sphinx为你的项目快速构建文档 - TwoSix的小木屋 +使用Sphinx为你的项目快速构建文档 - TwoSix的小木屋

使用Sphinx为你的项目快速构建文档

TwoSix Lv3

最近写了个软件,需要写个接口文档,看到别人项目的文档有不少都是托管在 Read the Docs 上的,于是搜了一下,Read the Docs 是一个托管平台,而这个平台的文档是基于 Sphinx 构建的,所以就学了一下,以此记录。

安装Sphinx

很简单,用pip安装即可,尽量使用官方的源,国内源听说多少有点问题

1
pip install sphinx

构建Sphinx项目

快速构建

推荐在项目的根目录构建一个文件夹docs来专门存放文档的源码,然后cd docs下构建源码

构建也非常简单,一行命令即可

1
sphinx-quickstart

输入命令后,会提示是否要创建独立目录,选择y是即可,然后提示你填一些信息,包括项目名,作者名,语言等等,照实填写即可,语言是简体中文的话,填 zh_CN 即可

构建完毕后,如果之前选择的是y的话,我们就可以在目录下看到buildsource文件夹了,其中source文件夹就是存放项目源码的文件夹

source中包含了一个conf.py文件,用于填写项目配置,一个index.rst文件,是首页的源码。

若无特殊需求的话,直接根据rst格式编写你的代码,然后make html即可完成项目的构建。

但是为了写一个文档,又专门去学一个 rst 语法似乎有些不大合适,所以需要修改一下配置文件,让Sphinx支持大众都熟知的 markdown 语法。

配置文件

markdown配置

安装扩展

首先,为了支持 markdown 语法,我们需要安装一个扩展插件myst-parser

1
pip install --upgrade myst-parser

添加扩展配置

安装完成后,我们在conf.py文件内,修改一下extensions字段,引入扩展即可

1
extensions = ['myst_parser']

如果你的 markdown 文件可能非 md 结尾,则需要添加一下source_suffix字段

1
2
3
4
5
source_suffix = {
'.rst': 'restructuredtext',
'.txt': 'markdown',
'.md': 'markdown',
}

意思就是,.rst文件,使用restructuredtext进行解析,.txt.md文件则使用 markdown 进行解析(.rst不能删,首页还得用)

此外,myst-parser默认关闭了很多一些非基本markdown的语法,我们可以通过添加myst_enable_extensions字段来支持这些语法,以下是一个完整的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
myst_enable_extensions = [
"amsmath",
"attrs_inline",
"colon_fence",
"deflist",
"dollarmath",
"fieldlist",
"html_admonition",
"html_image",
"linkify",
"replacements",
"smartquotes",
"strikethrough",
"substitution",
"tasklist",
]

按需开启即可,每个语法扩展具体功能如下:

  • amsmath:LaTeX数学公式的软件包
  • attrs_inline:属性扩展,这个我不太了解,应该和HTML的写法相关
  • colon_fence:表格的语法
  • deflist:列表的语法,也就是我现在在写的这个无序列表
  • dollarmath:使用美元符号$$包围的数学公式语法
  • fieldlist:块列表语法,一般用在说明函数及其参数的功能的时候
  • html_admonition:基于html的提示框语法
  • html_image:基于html的图片显示语法
  • linkify:网址链接可点击的语法
  • replacements:这个我不太懂,看起来是可以支持字符串替换
  • smartquotes:会帮你自动把直引号转换成弯引号
  • strikethrough:删除线语法,就是这样
  • substitution:替换语法,差不多就是你可以定义一个变量,然后在后续的文本里添加占位符,构建时会帮你自动把变量的值填进占位符里
  • tasklist:todo列表语法

因篇幅有限,扩展仅作简单概述,甚至可能不准确,具体每个扩展的使用方法,以及效果示例,请查看官方文档

linkify 需要额外安装一个插件,pip install linkify-it-py

主题配置

默认的主题很丑,所以我们选择使用 read the docs 的主题配置

首先安装一下主题

1
pip install sphinx_rtd_theme

然后在conf.py文件修改一下html_theme字段即可,改为

1
html_theme = 'sphinx_rtd_theme'

编写你的文档

因为我们配置了 markdown 语法,所以我们只需要使用常规的markdown编译器,正常的写每一页文档即可。

这里我就写了两页,代码结构+如何添加算法,如下,写完放进source的目录即可

img

注意,markdown语法需要把最高级的标题留给页面标题,例如用#一级标题写了页面的标题后,文章内容就只能用二级及以下的标题了,不然后面目录显示会有问题,会把文件所有最高级标题都作为目录标题

修改主页代码

接下来我们去 index.rst 文件下,修改一下我们的主页内容以及左侧目录内容就差不多了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.. AlgorithmViewer documentation master file, created by
sphinx-quickstart on Fri Apr 14 13:50:38 2023.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.

Welcome to AlgorithmViewer's documentation!
===========================================

.. toctree::
:maxdepth: 2
:caption: Contents

代码结构
如何添加算法

rst 的语法我也不太懂,所以只简单针对这一个文件作简单的分析

.. 开头的类似于注释,不会被编译到网页上

===========================================上的一行就是我们的欢迎页标题,rst的标题等号长度不得小于文字长度

toctree:: 声明了一个树状结构,也就是我们的目录,maxdepth就是层级的最深深度,2也就是只显示两层

caption 指定目录的标题,这里的目录标题是 Contents

然后在后面接上你编写的文档文件即可,按我的写法,最终生成的页面会是这样的

img

生成HTML文件

配置完成后,我们就可以在根目录docs下执行编译命令了,也是一行代码的事

1
make html

成功 build 后,我们就可以到 build/html 文件夹下,看到我们的HTML文件了,打开 index.html,就可以看到你的文档啦

  • 标题: 使用Sphinx为你的项目快速构建文档
  • 作者: TwoSix
  • 创建于 : 2023-04-15 01:13:00
  • 更新于 : 2024-07-04 23:52:28
  • 链接: https://twosix.page/2023/04/15/使用Sphinx为你的项目快速构建文档/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
\ No newline at end of file diff --git "a/2023/05/08/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\22110-\346\263\233\345\236\213\343\200\201trait\344\270\216\347\224\237\345\221\275\345\221\250\346\234\237/index.html" "b/2023/05/08/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\22110-\346\263\233\345\236\213\343\200\201trait\344\270\216\347\224\237\345\221\275\345\221\250\346\234\237/index.html" index 5eb4517..8aabf32 100644 --- "a/2023/05/08/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\22110-\346\263\233\345\236\213\343\200\201trait\344\270\216\347\224\237\345\221\275\345\221\250\346\234\237/index.html" +++ "b/2023/05/08/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\22110-\346\263\233\345\236\213\343\200\201trait\344\270\216\347\224\237\345\221\275\345\221\250\346\234\237/index.html" @@ -1,2 +1,2 @@ -【Rust 学习记录】10. 泛型、trait与生命周期 - TwoSix的小木屋 +【Rust 学习记录】10. 泛型、trait与生命周期 - TwoSix的小木屋

【Rust 学习记录】10. 泛型、trait与生命周期

TwoSix Lv3

泛型

泛型是一种具体类型或者其他属性的抽象替代,通常用来减少代码的重复,接下来将从泛型的几个实际应用场景开始介绍泛型

应用场景

在函数定义中使用

现在假设我们要写一个寻找数组最大值的功能,我要怎么实现既能从字符数组里查找最大值,又能从整数数组里查找最大值?定义两个函数分别查找的话难免重复性有点高,这时候就需要使用泛型。

1
2
3
4
5
6
7
8
9
fn largest<T>(list: &<T>) -> T{
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}

以上代码定义了一个寻找数组最大值的泛型函数,首先

  1. 需要声明一个泛型名称T,放置在函数名和参数的圆括号之间,用尖括号括起来,largest(list: &<T>)
  2. 后续的类型声明,就都可以用T来代替了

但以上代码暂时无法提示,rust-analyzer会报错binary operation '>' cannoy be applied to type 'T',也就是说>运算符不能直接用于泛型参数,这个问题会在后续解决,现在重点先放在泛型的应用场景。

在结构体定义中使用

1
2
3
4
5
6
7
8
9
10
11
12
#[derive(Debug)]
struct Point<T> {
x: T,
y: T
}

fn main() {
let p1 = Point{ x: 5, y: 10 };
let p2 = Point{ x: 1.0, y: 4.0 };
println!("{:?}", p1);
println!("{:?}", p2);
}

结构体中,泛型名称声明在结构体名字后面,Point,这段代码是可以编译通过的。

但注意,当你使用两种类型的变量创建泛型结构体时,就无法编译通过了。

1
2
3
4
5
6
7
8
9
10
#[derive(Debug)]
struct Point<T> {
x: T,
y: T
}

fn main() {
let p1 = Point{ x: 5, y: 10.0 };
println!("{:?}", p1);
}

报错:expected integer, found floating-point number

这是因为,当你向泛型T传入第一次传入值5的时候,编译器会自动为T赋值为和5相同的类型,即整型。也就是说,泛型并不是代表能接受所有类型的变量,而是编译器自动帮你识别为第一次接收到的变量类型

但如果我们就是可能传入两个类型呢?解决这个问题也简单,我们声明两个泛型,存储两个类型即可

1
2
3
4
5
6
7
8
9
10
#[derive(Debug)]
struct Point<T, U> {
x: T,
y: U
}

fn main() {
let p1 = Point{ x: 5, y: 10.0 };
println!("{:?}", p1);
}

声明多个泛型只需要在尖括号内用逗号隔开即可。

这段代码里,我们声明了两个泛型名称T, U,这时候我们分别为类型为T, U的变量x, y传入5,10.0,对应的,此时T代表整型,U代表浮点型

在方法定义中使用

有了泛型的结构体,自然也就能有泛型的结构体方法了

1
2
3
4
5
6
7
8
9
struct Point<T> {
x: T,
y: T
}
impl<T> Point<T> {
fn x(&self, other: &Point<T>) -> &T{
&self.x
}
}

注意,这里我们使用两次,也就是说,我们需要在impl后声明一次泛型名称,再在后续指定泛型。

这是因为,在泛型结构体里我们可以单独的为某个类型实现方法,而不是一定要所有类型都使用同一个方法,例如:

1
2
3
4
5
6
7
8
9
struct Point<T> {
x: T,
y: T
}
impl Point<f32> {
fn x(&self, other: &Point<f32>) -> &f32{
&self.x
}
}

在这段代码里,我们就相当于单独的为f32类型设定了方法x,只有当T的类型为f32时可以使用这个方法。这种写法可以很经常的被用于处理不同类型的不同情况。

因此,我们需要先声明以下泛型名字,才能确保编译器知道你后面的尖括号到底是泛型还是具体类型。

当然,我们的方法也可以和函数一样,再次声明自己的泛型名称

1
2
3
4
5
6
7
8
impl <T, U>Point<T, U> {
fn mixup<V, W>(self, other:Point<V, W>)->Point<T, W>{
Point{
x: self.x,
y: other.y,
}
}
}

这段代码里,我们在mixup函数里新定义了V, W两个泛型,用来接收可能不同类型的其他Point实例,并把两个实例的类型进行混合后,作为新的Point返回

在枚举类型定义中使用

在之前章节的学习里,我们就知道了ResultOption枚举。其中Option就是典型的单泛型枚举,Result就是典型的包含两个泛型的枚举

1
2
3
4
5
6
7
8
9
enum Option<T>{
Some(T),
None,
}

enum Result<T, E>{
Ok(T),
Err(E),
}

泛型的性能

可能有人会担心,泛型会不会和Python一样,使得程序有运行时的性能影响?

实际上是不会的,Rust的泛型和c++的auto差不多,会在编译器就静态固定好对应的类型,因此不会产生运行时的损耗,只会在编译期有性能损耗

trait:定义共享行为

trait(特征?)是用来描述一个类型的功能,可以用来和多个类型共享。例如说求和,每个类型的求和都不尽相同的时候,你可以定义一个trait名为sum,然后再分别为不同的类型实现sum

trait可能和其他语言的interface功能类似,但也不完全相同

定义trait

现在假设我们有两个结构体类型,一个是文章(Article),一个是推特(Tweet),我们需要同时为这两个文字内容主体生成摘要,于是我们就可以定义一个Summary的trait,来规定一个适用于所有类型的生成摘要的接口。

接下来我们新建一个库文件lib.rs,然后定义一个trait

1
2
3
pub trait Summary {
fn summarize(&self) -> String;
}

在这段代码里,首先我们定义了一个公有的trait Summary,并规定了trait里有一个函数summarize,在 trait 里,称作签名,它传入类型实例自己,返回String,但这里我们省略了函数的具体实现,具体实现交由不同的类型按照自己的规则来进行实现。

当然一个trait里可以有多个签名,这里只定义了一个。

实现trait

接下来,我们就需要给文章和推特两个结构体类型实现一下用于提取摘要的trait

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pub struct Article{
pub title: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for Article {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.title, self.author, self.location)
}
}

pub struct Tweet{
pub username: String,
pub content: String,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}

这段代码,我们定义了两个公有的结构体ArticleTweet,并使用impl Summary for xxx语句,声明为结构体实现Summary这个trait,然后再在impl块里,实现trait内的签名summarize,这时候就可以根据实际情况来返回不同的摘要了。代码中我们是使用format!格式化返回不同的内容。

实现后,我们就可以在具体的实例里使用这个trait了

1
2
3
4
5
6
7
8
9
use test10::{Tweet, Summary};

fn main() {
let test = Tweet{
username: String::from("TwoSix"),
content: String::from("Hello, world!"),
};
println!("A new tweet: {}", test.summarize());
}

注意看我们的use代码use test10::{Tweet, Summary},这里我们同时引进Tweet类型和Summary这个trait,才能让Tweet实例使用trait对应的成员函数,否则会编译报错,不信你可以试试。(test10是我自己创建的根目录名字)

这一点和其他语言都不一样,有点让人迷惑,实现了trait之后难道不是相当于结构体的成员函数了吗?为什么成员还需要额外引进才能使用?

这是因为trait提供了相当的灵活性,以至于编译器并不好自动检查怎么使用,例如以下场景:我们实现了两个trait,Summary1 和 Summary2,并且这两个trait里都有一个签名叫做 summarize ,然后我们还在Tweet 结构体里同时实现了这两个 trait

是的,Rust允许这种场景的存在,那你说这时候调用 summarize时,应该调用的是Summary1 还是 Summary2?因此,必须显示引入,才能正常使用。

使用就如此,那实现自然也是,如果你想实现别人定义的 trait,那你就需要把别人的 trait 显示引入当前的作用域,才能实现别人的 trait。

默认实现

前面我们没有在trait内实现summarize签名,交由每个类型自己实现,但实际上我们也可以为其定义一个默认实现。

1
2
3
4
5
pub trait Summary {
fn summarize(&self) -> String{
String::from("(Read more...)")
}
}

这里我们在trait的定义内实现了summarize签名,默认返回一个 (Read more…) 的字符串

然后我们修改一下Article的实现

1
2
3
4
5
6
7
pub struct Article{
pub title: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for Article {}

我们在实现Summary的时候,直接使用空的花括号,没有实现具体的 trait 签名,然后我们再使用看一下效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use test10::{Tweet, Article, Summary};

fn main() {
let test_tweet: Tweet = Tweet{
username: String::from("TwoSix"),
content: String::from("Hello, world!"),
};
let test_article = Article{
title: String::from("Hello"),
location: String::from("World"),
author: String::from("TwoSix"),
content: String::from("This is a test"),
};
println!("A new article: {}", test_tweet.summarize());
println!("A new tweet: {}", test_article.summarize());
}

没有意外,正常的输出 (Read more…),并且 Tweet 的输出正常,不会受到影响。这个概念也很常见,也就是重载。

把trait作为参数

trait 甚至能作为函数的参数传入,见以下示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use test10::{Tweet, Article, Summary};

fn summarize(item: impl Summary) -> String{
item.summarize()
}

fn main() {
let test_tweet: Tweet = Tweet{
username: String::from("TwoSix"),
content: String::from("Hello, world!"),
};
let test_article = Article{
title: String::from("Hello"),
location: String::from("World"),
author: String::from("TwoSix"),
content: String::from("This is a test"),
};
println!("A new article: {}", summarize(test_tweet));
println!("A new tweet: {}", summarize(test_article));
}

我们定义了一个函数summarize来调用每个实现了 Summary trait 的类型的 summarize 函数。这里的impl Summary 就是指代的所有实现了 Summary trait 的类型。

trait约束

以上impl Summary实际上只是一个语法糖,它的完整声明形式称作 triat 约束,写作如下

1
2
3
fn summarize<T: Summary>(item: T) -> String{
item.summarize()
}

意思就是声明了一个泛型T,并使用:Summary对泛型T指代的类型进行了约束,使它只能代表实现了 Summary trait 的类型。

实际上在函数较复杂的时候,triat 约束要比之前的语法糖要好用

1
2
3
4
5
6
7
8
9
10
11
fn summarize<T: Summary>(item1: T, item2:T, item3: T) -> String{
item1.summarize();
item2.summarize();
item3.summarize()
}

fn summarize(item1: impl Summary, item2:impl Summary, item3: impl Summary) -> String{
item1.summarize();
item2.summarize();
item3.summarize()
}

对比一下这两种写法,是不是在复杂的情况,反而 triat 约束更简洁了一些?

多个trait约束

如果我想让泛型 T 指代实现了多个 triat 的类型怎么办?使用 + 法可以解决这个问题

1
2
3
fn summarize<T: Summary + Display>(item: T) -> String{
item.summarize()
}

这里就表示,传入的 item 必须是同时实现了 Summary trait 和标准库的 Display trait 的类型。

简化trait约束

当有多个泛型参数,每个泛型参数有多个 trait 约束的时候,会写成这样

1
fn summarize<T: Summary + Display, U: Summary + Display>(item1: T, item2: U) -> String{

这样就会导致函数定义很长又有很多重复内容,阅读费劲,难以理解,所以 rust 提供了一个 where 从句的方法,提高这种情况下的可读性

1
2
3
4
fn summarize<T, U>(item1: T, item2: U) -> String
where T: Summary + Display,
U: Summary + Display
{

我们可以在返回值的类型后面,加上一个 where 从句,把每个泛型的 trait 约束换一行之后再定义,就美观多了。

把trait作为函数返回值类型

既然能作为函数参数传入,自然也能作为函数返回值进行返回了

1
2
3
4
5
6
7
fn summarize() -> impl Summary
{
Tweet{
username: String::from("TwoSix"),
content: String::from("Hello, world!"),
}
}

这一段代码则让 summarize 函数固定返回实现了 Summary 的 Tweet 类型,但这种用法似乎感觉没有什么用?没关系,书上说后续讲解闭包等概念的时候,会使用到这种语法。

需要注意的是,Rust 编译器同样会对 impl Trait 进行静态推理保存,碍于 impl Trait 工作方式的限制,所以你只能在返回一个类型的时候,使用 trait 作为返回值类型,如果你既想返回 Tweet 也想返回 Article 是不行的。

如以下的代码就会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn summarize(switch: bool) -> impl Summary
{
if switch {
Tweet{
username: String::from("TwoSix"),
content: String::from("Hello, world!"),
}
} else {
Article{
title: String::from("Hello"),
location: String::from("World"),
author: String::from("TwoSix"),
content: String::from("This is a test"),
}
}
}

练手

还记得我们之前在讲泛型的时候使用的查找最大值的例子吗,之前代码编译不通过,但现在的我们已经有办法修复它了。

先回顾以下这段代码

1
2
3
4
5
6
7
8
9
fn largest<T>(list: &[T]) -> T{
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}

报错的是 > 号不能用于泛型T,而 > 实际上是一个叫做 PartialOrd 的 trait,所以这段代码报错的核心是,不是每个类型都实现了 PartialOrd,所以我们可以给它加个 trait 约束。

1
2
3
4
5
6
7
8
9
fn largest<T: PartialOrd>(list: &[T]) -> T{
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}

因为 PartialOrd 是预导入模块,所以我们可以直接使用,而不需要 use。我们修改完后,这段代码出现了新的报错:cannot move out of here | move occurs because list[_] has type T, which does not implement the Copy trait;意思就是,不是每个类型都实现了 Copy trait,所以我们没有办法把泛型 T 列表内的元素赋值出来,解决也很简单,那就是再加个 Copy 约束即可。

以下这段代码,就能正确的编译并找到不同类型数组的最大值了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn largest<T: PartialOrd+Copy>(list: &[T]) -> T{
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}

fn main(){
let a = vec![1, 2, 3, 4, 5];
let b = vec!['a', 'b', 'c', 'd', 'e'];
let largest_a = largest(&a);
let largest_b = largest(&b);
println!("The largest number is {}", largest_a);
println!("The largest char is {}", largest_b);
}

PS:

这里我有点迷惑,这难道不是对应赋值吗?什么类型不能赋值出来?我查了一下资料,大致得出的结论如下,不知道对不对:

翻译应该沾点锅,看了下原文的意思应该是:不是所有类型都有 Copy trait,这里的 Copy trait 指的是深拷贝,在浅拷贝的变量里,赋值操作应该是 move,而 move 则对应了所有权的转移,对于一个列表内的变量,我们把它所有权转移出来之后,但数组自己是不知道自己的元素所有权已经没有了,这不就出问题了?

所以最重要的原因还是在于这一句代码:let mut largest = list[0]; 这里把list[0]的元素所有权移出来了,自然有问题。所以我把代码改成下面这个样子,多加了一些引用的使用,不转移所有权,也就不需要使用Copy约束了。

1
2
3
4
5
6
7
8
9
fn largest<T: PartialOrd>(list: &[T]) -> &T{
let mut largest = &list[0];
for item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}

通过约束为指定类型实现方法

也就是当我们定义了一个泛型结构体时,可以让这个结构体内的一些方法只能让指定的类型调用。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
use std::fmt::Display;

struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self {
x,
y,
}
}
}

impl<T: PartialOrd+Display> Pair<T> {
fn cmp_display(&self){
if self.x >= self.y{
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}

fn main(){
let pair = Pair::new(1, 2);
pair.cmp_display();
}

这里我们就规定了 Pair 结构体里只有存放的类型是同时实现了可以比较可以打印两个 trait 的类型,才能调用cmp_display这个方法。(注意 Display trait 不是预导入的,虽然是标准库,也要自己 use)

当然,基于此,自然也可以为结构体的指定类型实现 trait。

1
2
3
4
5
6
7
8
9
10
impl<T:Display> ToString for Pair<T> {
fn to_string(&self) -> String {
format!("x = {}, y = {}", self.x, self.y)
}
}

fn main(){
let pair = Pair::new(1, 2);
println!("{}", pair.to_string());
}

生命周期

普通的泛型可以用来消除重复代码,也可以向编译器指明程序员希望这些类型拥有什么样的行为,而生命周期就是一种特殊的泛型,用来确保引用在我们使用的过程中一直有效

前言

在介绍生命周期前,我们需要介绍编译器是怎么检查因为超出作用域而导致的悬垂引用问题的。我们来看看以下这个例子

img

这个例子中,我们定义了一个变量r,在下一个花括号中,我们定义了一个变量x,并把x的所有权借给r,但x的作用域只在这个花括号为止,超出了这个花括号之后所有权就被回收了,r也就成了悬垂引用。

右边的'a,'b就分别代表了r和x的生命周期,我们可以明显的看到,x的生命周期’b明显要比r的生命周期’a短,所以编译器就可以通过检查生命周期的长短来查找你可能的悬垂引用问题,进而提出报错。

那我们为什么要指定生命周期?让我们来写一段代码

1
2
3
4
5
6
7
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}

这个函数用于比较得到长度更长的字符串,因为不想只是比较以下就夺取所有权,所以使用引用的方式传入,也使用引用的方式返回。

不出意外,会有错误提示missing lifetime specifier,我们回顾以下编译器检查悬垂引用的方式,需要比较一下引用的生命周期长短,而在以上情况中,我们有一个分支判断,既可能返回x,也可能返回y,编译器不知道会返回x还是返回y,也不知道该比较哪个和哪个引用之间的长短,也就无法进行检查,进而报错,提示我们需要指定生命周期,明确一下引用之间的关系,方便编译器进行比较。

标注生命周期

基本语法

标注生命周期的语法很简单,和我们之前举例的命名一样,生命周期的命名以'开头,如'a

1
2
3
&i32 // 这是一个普通引用
&'a i32 // 这是一个生命周期为'a的引用
&'a mut i32 // 这是一个生命周期为'a的可变引用

函数中的生命周期标注

1
2
3
4
5
6
7
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}

可见,我们用类似定义泛型的方式,定义了一个生命周期,名为'a,并且给后面的引用都指定了生命周期为'a,也就是告诉编译器,这个函数里传入的引用必然都是相同的生命周期,放心比较!于是编译器就会选择一个引用,推导出实际的生命周期'a,然后和函数外的实际拥有所有权的变量进行比较,然后发现外边的变量生命周期都比'a长,最后得出结果,这段代码可以编译通过。

那x和y是两个不同的变量啊,这里编译器最终得到的生命周期’a究竟是什么地方的生命周期?

答案也很简单,就是x和y重叠部分的生命周期

1
2
3
4
5
6
fn main(){
let a = String::from("abcd");
let b = "xyz";
let result = longest(a.as_str(), b);
println!("The longest string is {}", result);
}

如以上代码,'a的长度等同于变量b的生命周期(a,b重叠部分,也就是取最短的一个就行),我们定义了返回的引用生命周期也是'a,因此返回的result生命周期也应该是在b的生命周期范围内,这段代码里和b一起被回收,所以没有问题,编译通过。

错误示例如下,result变量的生命周期要长于b的生命周期,则无法通过编译:

1
2
3
4
5
6
7
8
9
fn main(){
let result;
{
let a = String::from("abcd");
let b = "xyz";
result = longest(a.as_str(), b);
}
println!("The longest string is {}", result);
}

深入理解生命周期

由上面我们可以知道,其实标注生命周期的作用就是为了方便编译器检查。

所以自然而然的,不需要参与检查的变量也就不是必须标注的了,如:

1
2
3
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
x
}

这里我们规定直接返回x,所以编译器只会顺着第一个参数x进行检查,所以我们只标注了x的生命周期,不标注y也是可以编译通过的,因为y和返回值没有半毛钱关系。

其次,我们标注生命周期,只是向编译器声明了以下传入的引用的生命周期关系,并没有改变任意一方的生命周期

1
2
3
4
fn longest<'a>(x: &str, y: &str) -> &'a str {
let result = String::from("really long string");
result.as_str()
}

例如这一段代码,我们给返回值声明了生命周期'a,但返回值result是在函数内定义的,他也就只能活在这个函数里,并不是说我们给他声明了一个生命周期,他就能活到外面去了。

结构体中的生命周期

一般情况下结构体都是存储自持有的变量,但实际上也可以存储引用,这时候就需要用到生命周期

1
2
3
4
5
6
7
8
9
struct ImportantExcerpt<'a> {
part: &'a str,
}

fn main(){
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt { part: first_sentence };
}

定义的语法也是类似即可。

生命周期省略

可以说,所有引用必然是需要有自己的生命周期的,但其实以前编写的很多函数都没有指明生命周期,也能传入引用,是为什么呢?

其实早期的Rust是所有引用都必须显示标注生命周期的,但随着慢慢的发展,Rust的团队发现有很多情况下,是能够使用编译器推导出返回值的生命周期的,重复的写生命周期有点烦,也就把这部分情况,写成了可省略的生命周期规则。

编译器检查生命周期的规则有以下三条:

  1. 每一个引用的参数,都有自己的生命周期
  2. 当只存在一个输入的生命周期参数时,这个生命周期会被赋予给所有输出的生命周期参数
  3. 当拥有多个输入的生命周期参数时,若其中一个是&self&mut selfself的生命周期会被赋予给所有输出的生命周期参数
  4. 若以上三条规则使用完毕,编译器仍然无法推导出所有生命周期,则报错,让用户指定。

这些规则帮助我们省略了很多生命周期的编写。为了更好了理解这些规则,我们举一些例子看看。

例如这段代码:

1
fn test(s: &str)->&str{

按照规则1,编译器先给所有输入参数赋予自己的生命周期

1
fn test<'a>(s: &'a str)->&str{

由于只有一个输入参数s,满足规则2,编译器把生命周期赋予给所有输出的参数

1
fn test<'a>(s: &'a str)->&'a str{

至此,编译器自己推导出了所有参数的生命周期,也就不用我们写了。

接着,我们再距离说明一下规则3

1
2
3
4
5
6
7
8
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl <'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
self.part
}
}

impl <'a> ImportantExcerpt<'a>这个声明语句中的生命周期声明不能省略(我也不知道为什么)

在方法announce_and_return_part中,编译器会首先按照规则1,赋予声明周期

1
fn announce_and_return_part(&'a self, announcement: &'b str) -> &str {

因为有两个参数,规则不生效

最后因为参数里有self,所以按照规则3,赋予输出参数self的声明周期

1
fn announce_and_return_part(&'a self, announcement: &'b str) -> &'a str {

这时候,所有生命周期也就推导完毕了,不需要手动指定,也可编译通过。

如果不返回self.part,返回announcement一样会报错,但此时报错的提示是announcement的生命周期不一定比self长,而不是缺少生命周期声明,可见编译器确实给输出赋予了self的声明周期,并进行检查

静态生命周期

一种特殊的生命周期,意味在整个程序的执行期中都可以存活

1
let s:&'static str = "I have a static lifetime.";

但使用需要谨慎,1:你需要确保他确实可以在整个程序的生命周期存活;2:你确定它真的需要活这么长时间。

总结

最后用一段代码,同时使用泛型、trait约束、生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use std::fmt::Display;

fn longest_with_ann<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
where T: Display
{
println!("Announcement! {}", ann);
if x.len() > y.len(){
x
}
else{
y
}
}
fn main(){
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest_with_ann(string1.as_str(), string2, 123);
println!("The longest string is {}", result);
}

简单解释一下代码

  1. 定义了一个声明周期'a,用来声明传入的两个字符串x, y的生命周期,以及返回字符串的生命周期
  2. 定义了一个泛型T
  3. 约束了泛型T只能是实现了Display这个trait的类型,方便后续直接使用println!输出
  • 标题: 【Rust 学习记录】10. 泛型、trait与生命周期
  • 作者: TwoSix
  • 创建于 : 2023-05-08 21:17:12
  • 更新于 : 2024-07-04 23:52:28
  • 链接: https://twosix.page/2023/05/08/【Rust-学习记录】10-泛型、trait与生命周期/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
\ No newline at end of file diff --git "a/2023/05/09/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\22111-\347\274\226\345\206\231\350\207\252\345\212\250\345\214\226\346\265\213\350\257\225/index.html" "b/2023/05/09/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\22111-\347\274\226\345\206\231\350\207\252\345\212\250\345\214\226\346\265\213\350\257\225/index.html" index caa9a8c..93eff7b 100644 --- "a/2023/05/09/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\22111-\347\274\226\345\206\231\350\207\252\345\212\250\345\214\226\346\265\213\350\257\225/index.html" +++ "b/2023/05/09/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\22111-\347\274\226\345\206\231\350\207\252\345\212\250\345\214\226\346\265\213\350\257\225/index.html" @@ -1,2 +1,2 @@ -【Rust 学习记录】11. 编写自动化测试 - TwoSix的小木屋 +【Rust 学习记录】11. 编写自动化测试 - TwoSix的小木屋

【Rust 学习记录】11. 编写自动化测试

TwoSix Lv3

这一章讲的就是怎么在Rust编写单元测试代码,这一部分的思想不仅适用于Rust,在绝大多数语言都是有用武之地的

如何编写测试

测试代码的构成

构成

通用测试代码通常包括三个部分

  1. 准备所需的数据或者前置状态
  2. 调用需要测试的代码
  3. 使用断言,判断运行结果是否和我们期望的一致

在Rust中,有专门用于编写测试代码的相关功能,包含test属性,测试宏,should_panic属性等等

在最简单的情况下,Rust中的测试就是一个标注有test属性的函数。只需要将#[test]添加到函数的关键字fn上,就能使用cargo test命令来运行测试。

测试命令会构建一个可执行文件,调用所有标注了test的函数,生成相关报告。

PS:

属性是一种修饰代码的一种元数据,例如之前为了输出结构体时,加入的#[derive(Debug)]就是一个属性,声明属性后,会为下面的代码自动生成一些实现,如#[derive(Debug)]修饰结构体时,就会为结构体生成Debug trait的实现

初次尝试

接下来我们就试试怎么测试

首先新建一个名为adder的项目cargo new adder --lib(–lib指生成lib.rs文件)

可能是版本比较新,lib.rs里直接生成有了以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pub fn add(left: usize, right: usize) -> usize {
left + right
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}

我们先忽略一些没讲过的关键词,这段代码里我们定义了一个it_works函数,并标注为测试函数,然后使用断言判断add函数的结果是否正确的等于4。在了解了大概功能之后,我们直接运行测试看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cargo test
Compiling adder v0.1.0 (E:\Code\rust\adder)
Finished test [unoptimized + debuginfo] target(s) in 0.29s
Running unittests src\lib.rs (target\debug\deps\adder-0a86cd050490705e.exe)

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

对应的测试结果如上

  • passed: 测试通过的函数数量,我们这里只有一个it_works函数,且测试通过,所以为1
  • failed: 测试失败的函数数量
  • ignored: 被标记为忽略的测试函数,后面会提
  • measured: Rust还提供了衡量函数性能的benchmark方法,不过编写书的时候似乎这部分还不完善,所以不会有讲解,想了解需要自行学习
  • filtered out:被过滤掉的测试函数
  • Doc-tests:文档测试,这是个很好用的特性,可以防止你在修改了函数之后,忘记修改自己的文档,保证文档能和实际代码同步。

测试时,每一个测试函数都是运行在独立的线程里的,所以发生panic时并不会影响其他的测试,我们可以写一个错误的函数看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pub fn add(left: usize, right: usize) -> usize {
left + right
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn error() {
let result = add(3, 2);
assert_eq!(result, 4);
}

#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
cargo test
Compiling adder v0.1.0 (E:\Code\rust\adder)
Finished test [unoptimized + debuginfo] target(s) in 0.24s
Running unittests src\lib.rs (target\debug\deps\adder-0a86cd050490705e.exe)

running 2 tests
test tests::it_works ... ok
test tests::error ... FAILED

failures:

---- tests::error stdout ----
thread 'tests::error' panicked at 'assertion failed: `(left == right)`
left: `5`,
right: `4`', src\lib.rs:18:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
tests::error

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

可以看见,error的panic并不影响it_works的测试通过。

assert!宏

assert!

assert!宏主要的功能是用来确保某个值为true,所以常被用于测试中。如a>b等场景,返回的是一个bool值,就完美的符合assert!的使用场景,可以使用assert!进行测试,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pub fn cmp(a: i32, b: i32) -> bool {
if a>b{
true
}
else{
false
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn cmp_test(){
let result = cmp(3, 2);
assert!(result);
}
}

assert_eq!和assert_ne!

那如果返回值不是bool值呢?前面也出现过了,我们可以使用assert_eq!或者assert_ne!来断言两个值是否相等。

eq则对应的只有相等才能通过断言,ne则对应的只有不相等才能通过断言,用例见上面的add测试即可。

但是注意,assert_eq!和assert_ne!使用了==!=来实现是否相等的判断,也就意味着,传入这两个宏的参数是必须实现了PartialEq这个trait的。同时,我们可见错误的输出中会打印出详细的不相等原因,也就是说它还同时需要实现了Debug宏帮助打印输出。一般绝大部分参数都是满足要求的,自定义的结构体时需要注意。

之前提到过属性这个概念,会为你自动实现一些功能,实际上PartialEq和Debug作为可派生的宏,也内置了属性的实现,你只需要在自己定义的结构体上加上#[derive(PartialEq, Debug)],就能自动帮你实现这两个宏

自定义错误提示代码

上面我们说到assert_eq是会有详细输出的,告诉你怎么不相等了,帮助你排除bug,但普通的assert!只判断布尔值,所以没办法有详细的输出,这时候我们可以定制一个输出,使得错误提示更人性化一点。

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pub fn cmp(a: i32, b: i32) -> bool {
if a>b{
true
}
else{
false
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn cmp_test(){
let a = 2;
let b = 3;
let result = cmp(a, b);
assert!(result, "{} is not bigger than {}", a, b);
}
}

和一般不一样,我们不需要用什么格式化字符串的方法先格式化一个字符串,再传入这个字符串,断言支持直接使用格式化的语法。这段代码的输出如下

1
2
3
running 1 test
thread 'tests::cmp_test' panicked at '2 is not bigger than 3', src\lib.rs:23:9
stack backtrace:

可见报错提示相对于单纯的panicked at更人性化了一些。

当然,自定义输出也支持在assert_eq和assert_ne里使用

should_panic

should_panic也是一个属性,用来测试代码是否能正确的在出错时发生panic。用例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pub fn positive_num(a: i32) -> i32 {
if a > 0 {
a
} else {
panic!("{} is not positive", a)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
#[should_panic]
fn pos_test(){
let a = -1;
positive_num(a);
}
}

这段代码用来检查一个数是否是正数,在不是时抛出panic,接下来我们使用#[should_panic]来检查程序是否正确的panic,这段代码运行测试通过没问题。

接下来我们修改一下a的值,让程序不抛出panic,看看会发生什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
running 1 test
test tests::pos_test - should panic ... FAILED

failures:

---- tests::pos_test stdout ----
note: test did not panic as expected

failures:
tests::pos_test

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `-p adder --lib`

测试失败,告诉你pos_test没有按照预期发生panic。这个特性可以用来检查你的代码是否能正确的处理报错,发生panic以阻止程序进一步运行,产生不可预估的后果。

但是,单纯这么使用感觉有点含糊不清,因为程序发生panic的原因可能不是我们所预期的,假如其他一些我们不知道的原因抛出了panic,也会导致测试通过。所以我们可以添加一个可选参数expected,用来检查panic发生报错的输出信息里是否包含指定的文字。

1
2
3
4
5
6
#[test]
#[should_panic(expected = "positive")]
fn pos_test(){
let a = -1;
positive_num(a);
}

这时候,should_panic就会检查发生的panic输出的报错信息是否包含”positive”这个字符串,如果是,才会测试通过,输出如下:

1
2
3
running 1 test
thread 'tests::pos_test' panicked at '-1 is not positive', src\lib.rs:9:9
stack backtrace:

可见,输出中也包含了报错的信息,更人性化了。

使用Result编写测试

之前学习Result枚举的时候我们就知道了这东西是用来处理报错的,自然也就可以用来处理测试。使用时也很简单,我们只需要声明测试函数的返回值是Result,test命令就会自动根据Result的枚举结果来判断是否测试成功了。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn it_works() -> Result<(), String>{
if 2+2 == 4 {
Ok(())
} else {
Err(String::from("two plus two does not equal four"))
}
}
}

使用Result编写测试函数的主要优势是可以使用问号表达式进行错误捕获,更方便我们去编写一些复杂的测试函数,可以让函数在任一时刻有错误被捕获到时,就返回报错。

问号表达式的使用可见【Rust 学习记录】9. 错误处理的?运算符部分,这里就不再写代码举例了(主要书上没例子,我也懒的写)

控制测试的运行方式

这一部分主要是对cargo test命令的讲解,具体的运行方式,参数的使用等。

cargo test的参数统一需要写在--后面,也就是说你想要使用--help显示参数文档时,需要使用以下命令

1
cargo test -- --help

并行或串行的执行代码

默认情况下,测试是多线程并发执行的,这可以使测试更快速的完成,且相互之间不会影响结果。但如果测试间有相互依赖关系,则需要串行执行。例如两个测试用例同时在操作一个文件,一个测试在写内容,一个测试在读内容时,则容易导致测试结果不合预期。

我们可以使用--test-threads=1来指定测试的线程数为1,即可实现串行执行,当然,你想执行的更快也可以指定更多的线程

1
cargo test -- --test-threads=1

显示函数的输出

默认情况下,test命令会捕获所有测试成功时的输出,也就是说,对于测试成功的函数,即使你使用了println!打印输出,你也无法在控制台看见你的输出,因为它被test命令捕获吞掉了。

如果你想要在控制台显示你的输出,只需要用--nocapture设置不捕获输出即可

1
cargo test -- --nocapture

只运行部分特定名称的测试

如果测试的函数越写越多,执行所有的测试可能很花时间,通常我们编写了一个新的功能并想进行测试的时候,我们只需要测试这一个功能就足够了,因此可以向test命令指定函数名称来进行测试。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
fn add_two(a: i32, b: i32) -> i32 {
a + b
}


#[cfg(test)]
mod tests {
use super::*;

#[test]
fn add_test_1() {
assert_eq!(add_two(1, 2), 3);
}

#[test]
fn add_test_2() {
assert_eq!(add_two(2, 2), 4);
}

#[test]
fn one_hundred() {
assert_eq!(100, 100);
}

}

对于这段代码,我们只想测试one_hundred这个函数,只需要对test命令指定运行one_hundred即可

1
cargo test one_hundred

输出如下:

1
2
3
4
running 1 test
test tests::one_hundred ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s

这里显示2 filtered out,代表有两个测试用例被我们过滤掉了。

当然,这个方法也并不是只能运行一个测试函数,也可以通过部分匹配的方法执行多个名称里包含相同字符串的测试函数,例如:

1
2
3
4
5
6
7
cargo test add        

running 2 tests
test tests::add_test_1 ... ok
test tests::add_test_2 ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s

我们使用cargo test add命令,则可以测试所有名字里带add的测试函数,忽略掉了one_hundred函数。

不过需要注意的是,这种方法一次只能使用一个参数进行匹配并测试,如果你想同时用多个规则匹配多类的测试函数,就需要用其他方法了。

通过显示指定来忽略某些测试

忽略部分测试函数

当有部分测试函数执行特别耗时时,我们不想每次测试都执行这个函数,我们就可以通过#[ignore]属性来显示指定忽略这个测试函数。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
fn add_two(a: i32, b: i32) -> i32 {
a + b
}


#[cfg(test)]
mod tests {
use super::*;

#[test]
fn add_test_1() {
assert_eq!(add_two(1, 2), 3);
}

#[test]
#[ignore]
fn add_test_2() {
assert_eq!(add_two(2, 2), 4);
}

#[test]
fn one_hundred() {
assert_eq!(100, 100);
}

}

此时我们直接执行cargo test,输出如下

1
2
3
4
5
6
7
8
running 3 tests
test tests::add_test_2 ... ignored
test tests::add_test_1 ... ok
test tests::one_hundred ... ok

test result: ok. 2 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s

Doc-tests adder

可见add_test_2函数被忽略不执行了,提示1 ignored。

单独执行被忽略的测试函数

如果我们想单独执行这些被忽略的函数,则可以使用--ignored命令

1
2
3
4
5
6
cargo test -- --ignored

running 1 test
test tests::add_test_2 ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s

可见,只有add_test_2被执行了。

测试的组织结构

测试通常分为两类,单元测试和集成测试。单元测试小而专注,集中于测试一个私有接口或模块;集成测试则独立于代码库之外,正常的从外部调用公共接口,一次测试可能使用多个模块

单元测试

单元测试的目的在于将一小段代码单独隔离开来,快速确定代码结果是否符合预期。一般来说,单元测试的代码和需要测试的代码存放在同一文件中。同时也约定俗成的在每个源代码文件里都会新建一个tests模块来存放测试函数,并使用cfg(test)来标注。

测试模块和#[cfg(test)]

#[cfg(test)]旨在让Rust只在执行Cargo test命令的时候编译和运行这段代码,而在cargo build的时候剔除掉它们,只用于测试,节省编译时间与空间,使得我们可以更方便的把测试代码和源代码放在同一个文件里。(集成测试时一般不需要标注,因为集成测试一般是独立的一个文件)

我们之前编写的测试模块就使用了这个属性

1
2
3
4
5
6
7
8
9
10
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}

测试私有函数

是不是应该测试私有函数一直有争议,不管你觉得要不要,但Rust提供了方法供你方便的测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn add(left: usize, right: usize) -> usize {
left + right
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}

以上代码中的add没有标注pub关键字,也就是私有的,但因为Rust的测试代码本身也属于Rust代码,所以可以通过use的方法把私有的函数引入当前作用域来测试,也就是对应的代码里的use super::*;

集成测试

集成测试通常是新建一个tests目录,只调用对外公开的那部分接口。

tests目录

tests目录需要和src文件夹并列,Cargo会自动在这个目录下面寻找测试文件。

现在,我们新建一个tests/integration_test.rs文件,保留之前的lib.rs代码(add函数如果改了私有记得改回公有),并编写测试代码。

1
2
3
4
5
6
use adder;

#[test]
fn add_two() {
assert_eq!(adder::add(2, 2), 4);
}

集成测试就不需要#[cfg(test)]了,Rust有单独为tests目录做处理,不会build这个目录下的文件。

接下来,我们再执行以下Cargo test,看看输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cargo test
Compiling adder v0.1.0 (E:\Code\rust\adder)
Finished test [unoptimized + debuginfo] target(s) in 0.32s
Running unittests src\lib.rs (target\debug\deps\adder-0a86cd050490705e.exe)

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Running tests\integration_test.rs (target\debug\deps\integration_test-57f19c149db40d76.exe)

running 1 test
test add_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

我们可以看到输出里

  1. 先输出了单元测试的结果,test tests::it_works … ok,每行输出一个单元测试结果
  2. 再输出集成测试的结果,Running tests\integration_test.rs,表示正在测试哪个文件的测试模块,后续跟着这个文件的测试结果
  3. 最后是文档测试

当编写的测试代码越多,输出也就会越多越杂,所以我们也可以使用--test参数指定集成测试的文件名,单独进行测试。如:cargo test --test integration_test

在集成测试中使用子模块

测试模块也和普通模块差不多,可以把函数分解到不同文件不同子目录里,当我们需要测试内容越来越多的时候,就会需要这么做。

但因为测试的特殊性,rust会把每个集成测试的文件编译成独立的包来隔离作用域,模拟用户实际的使用环境,这就意味着我们以前在src目录下管理文件的方法并不完全适用于tests目录了。

例如,我们需要编写一个common.rs文件,并且编写一个setup函数,这个函数将会用在多个不同的测试文件里使用,如

1
2
3
4
5
pub fn setup(){
// 一些测试所需要初始化的数据
a = 1;
a
}

我们执行cargo test时会有以下输出:

1
2
3
4
5
     Running tests\common.rs (target\debug\deps\common-15055e88a26e37ec.exe)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

可以发现,即便我们没有在common.rs里写任何测试函数,它依旧会将它作为测试文件执行,并输出无意义的running 0 tests。这明显不是我们所希望的,那如何解决呢?

我们可以使用mod.rs文件,把common.rs的文件内容移到tests/common/mod.rs里面,这样的意思是让Rust把common视作一个模块,而不是集成测试文件。

于是,我们就可以通过mod关键字引入common模块并使用其中的函数,例如

1
2
3
4
5
6
7
8
9
10
// tests/integration_test.rs
use adder;

mod common;

#[test]
fn add_two() {
common::setup();
assert_eq!(adder::add(2, 2), 4);
}

此时再运行cargo test,就不会出现common相关的测试输出了。

二进制包的集成测试

如果我们的项目只有src/main.rs而没有src/lib.rs的话,是没有办法在tests中进行集成测试的,因为只有把代码用lib.rs文件指定为一个代码包crate,才能把函数暴露给其他包来使用,而main.rs对应的是二进制包,只能单独执行自己。

所以Rust的二进制项目通常会把逻辑编写在src/lib.rs里,main.rs只对lib.rs的内容进行简单的调用。

总结

没什么好总结的,下一章写项目去了。

  • 标题: 【Rust 学习记录】11. 编写自动化测试
  • 作者: TwoSix
  • 创建于 : 2023-05-09 16:00:30
  • 更新于 : 2024-07-04 23:52:28
  • 链接: https://twosix.page/2023/05/09/【Rust-学习记录】11-编写自动化测试/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
\ No newline at end of file diff --git "a/2023/05/12/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\22112-\347\274\226\345\206\231\344\270\200\344\270\252\345\221\275\344\273\244\350\241\214\347\250\213\345\272\217/index.html" "b/2023/05/12/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\22112-\347\274\226\345\206\231\344\270\200\344\270\252\345\221\275\344\273\244\350\241\214\347\250\213\345\272\217/index.html" index c9cc88b..3783b6a 100644 --- "a/2023/05/12/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\22112-\347\274\226\345\206\231\344\270\200\344\270\252\345\221\275\344\273\244\350\241\214\347\250\213\345\272\217/index.html" +++ "b/2023/05/12/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\22112-\347\274\226\345\206\231\344\270\200\344\270\252\345\221\275\344\273\244\350\241\214\347\250\213\345\272\217/index.html" @@ -1,2 +1,2 @@ -【Rust 学习记录】12. 编写一个命令行程序 - TwoSix的小木屋 +【Rust 学习记录】12. 编写一个命令行程序 - TwoSix的小木屋

【Rust 学习记录】12. 编写一个命令行程序

TwoSix Lv3

本章节我们将开始学习编写一个小项目——开发一个能够和文件系统交互并处理命令行输入、输出的工具

基本功能实现

首先自然是新建一个项目,名为minigrep

1
cargo new minigrep

实现这一个工具的首要任务自然是接收命令行的参数,例如我们要实现在一个文件里搜索字符串,就得在运行时接收两个参数,一个待搜索的字符串,一个搜索的文件名,例如

1
cargo run string filename.txt

这种基础的功能自然是已经有一些现成的crate可以使用来实现这些功能的了,不过因为我们的主要目的是学习,所以接下来我们会从零开始实现这些功能。

读取参数值

这一部分我们需要用到标准库里的std::env::args函数,这个函数会返回一个命令行参数的迭代器,使得程序可以读取所有传递给它的命令行参数值,放到一个动态数组里。

用例:

1
2
3
4
5
6
use std::env;

fn main() {
let args: Vec<String> = env::args().collect();
println!("{:?}", args);
}

这段代码简单来说就是通过env::args()读取命令行参数的迭代器,再通过collect()函数自动遍历迭代器把参数收集到一个动态数组里并返回。

接下来我们用终端运行代码,传入参数试试

1
2
3
4
5
cargo run hahha arg   
Compiling minigrep v0.1.0 (E:\Code\rust\minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 1.81s
Running `target\debug\minigrep.exe hahha arg`
["target\\debug\\minigrep.exe", "hahha", "arg"]

这里的输出的第一个参数是当前运行的程序的二进制文件入口路径,这一功能主要方便程序员打印程序名称/路径,后续才是我们传入的参数字符串,一般情况下我们忽略第一个参数只处理后面两个即可。

1
2
3
4
5
6
7
8
9
10
11
12
use std::env;

fn main() {
let args: Vec<String> = env::args().collect();
println!("{:?}", args);

let query = &args[1];
let filename = &args[2];

println!("Searching for {}", query);
println!("In file {}", filename);
}

接下来我们再运行以下这段代码,输出一切正常的话,第一步接收命令行参数的任务我们就完成啦!

读取文件

既然是要在文件内容里搜索指定字符串,自然要读取文件的内容了。但在这之前我们要先有一个文件,现在我们在项目的根目录下,新建一个poem.txt文件,内容就选择书上给的这首诗吧:

1
2
3
4
5
6
7
8
9
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.

How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!

接下来,我们就可以在代码里读取这个文件了,也很简单,用之前使用过的fs库即可,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use std::env;
use std::fs;

fn main() {
let args: Vec<String> = env::args().collect();
println!("{:?}", args);

let query = &args[1];
let filename = &args[2];

println!("Searching for {}", query);
println!("In file {}", filename);

let contents = fs::read_to_string(filename).expect("Error in reading file");
println!("With text:\n{}", contents)
}

接下来,再运行以下这段代码,用命令行传递参数的方法传递我们的文件名poem.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cargo run bog poem.txt       
Compiling minigrep v0.1.0 (E:\Code\rust\minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.63s
Running `target\debug\minigrep.exe bog poem.txt`
["target\\debug\\minigrep.exe", "bog", "poem.txt"]
Searching for bog
In file poem.txt
With text:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.

How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!

可见,顺利的输出了我们的文件内容,代码没有问题。

到此为止,我们的基本功能就实现完了。但目前为止,我们现在写的代码都一股脑的堆积在main.rs里,非常简单,但也不具备可扩展性以及可维护性,远远称不上一个”项目”。接下来我们就要用到之前学习的模块化管理的知识,重构一下我们的代码,让它变得更规范化。

模块化与错误处理

问题与计划

接下来我们将针对以下四个问题,来制定计划重构我们的代码:

  1. 功能如果都堆积在main函数里的话,随着我们的功能越来越多,这个文件的代码将会变得越来越复杂,越来越难让人理解,更难去测试,耦合程度很高。所以我们要把函数拆分开来,一个函数负责一个任务
  2. query和filename是存储程序配置的,contents是用于业务逻辑的,当一个作用域的变量越来越多时,我们就很难准确的追踪每个变量的实际用途;所以我们最好应该把用途相同的变量合并到一个结构体里,方便管理也明确用途。
  3. 错误处理方面处理的不够详细,读取文件失败可能有很多原因,我们应该准确定位到每个原因,抛出相关的提示,给用户提供更有效的排错信息。
  4. 错误管理也应该模块化,例如当用户没有指定运行参数时,底层报错是一个数组越位时,抛出错误”index out of bounds”,这无法帮助用户有效的理解问题本身,我们最好把用于错误处理的代码集中管理,更加方便的处理相关的错误逻辑,也方便为用户打印有意义,便于理解的报错信息。

二进制项目的组织结构

Rust社区对于程序的组织结构有一套自己的原则

  1. 程序拆分为main.rs和lib.rs,实际的逻辑放在lib.rs,main.rs只作简单调用
  2. 如果逻辑相对简单,再考虑留在main.rs,保留在main中的代码量应该小到可以一眼看出这段代码的正确性

也就是,main负责运行,lib负责实际逻辑,同时由上一章自动化测试可知,如果我们把逻辑放到lib.rs也更方便我们编写集成测试的代码。

所以,我们这个项目的main函数功能大概如下

  • 处理命令行参数
  • 程序配置的变量
  • 调用lib.rs中的run函数
  • 对run函数进行错误处理

接下来我们就可以开始按照以上原则重构了

分离基本功能逻辑

提取解析参数代码

首先,我们把解析参数做成一个函数,提供main函数调用,方便后面再把这个功能转移到lib

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::env;
use std::fs;

fn main() {
let args: Vec<String> = env::args().collect();
println!("{:?}", args);

let (query, filename) = parse_config(&args);

println!("Searching for {}", query);
println!("In file {}", filename);

let contents = fs::read_to_string(filename).expect("Error in reading file");
println!("With text:\n{}", contents)
}

fn parse_config(args: &Vec<String>) -> (&str, &str){
let query = &args[1];
let filename = &args[2];
(query, filename)
}

组合参数值

上面的代码我们选择返回一个元组,又把一个元组拆分成两个变量,这还是有点意义不明,不方便使用,所以我们选择把这两个值放到一个结构体里,再用两个明确的变量名存储这两个参数,使得这个函数的返回值更加明确。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
use std::env;
use std::fs;

fn main() {
let args: Vec<String> = env::args().collect();
println!("{:?}", args);

let config = parse_config(&args);

println!("Searching for {}", config.query);
println!("In file {}", config.filename);

let contents = fs::read_to_string(config.filename).expect("Error in reading file");
println!("With text:\n{}", contents)
}

struct Config{
query: String,
filename: String,
}

fn parse_config(args: &Vec<String>) -> Config{
let config = Config{
query: args[1].clone(),
filename: args[2].clone(),
};
config
}

这里,我们定义了一个Config结构体来存储我们的配置值,同时,秉持着结构体最好还是持有自己变量所有权的理念,这里我们需要把字符串clone一下再存入config变量,这种做法虽然比起直接使用引用多用了一些时间和内存,但也省去写生命周期的麻烦,也增加了代码可读性,毕竟开发效率也是效率,该做的牺牲还得做。

可见,现在main函数里解析参数,使用参数的逻辑已经非常清晰了。

有人对运行时代价很敏感,就是不喜欢用clone解决所有权问题,这个后面在13章会学习一些其他方法更有效率的处理这种情形。但对于我们现在来说,clone是一点没有问题的,因为我们这里的字符串只有读取程序参数的时候用一次clone,而且还只是两个很短的字符串,这点性能真算不上浪费。

为Config创建一个构造器

这时候我们再回头看一下,其实parse_config这个函数和我们的Config结构体也是紧密相关的,因为parse_config的目的就是构造一个Config结构体变量,那我们为什么不在Config结构体里写一个构造函数来实现同样的功能,增强这个函数和Config结构体的相关性,更好理解呢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
use std::env;
use std::fs;

fn main() {
let args: Vec<String> = env::args().collect();
println!("{:?}", args);

let config = Config::new(&args);

println!("Searching for {}", config.query);
println!("In file {}", config.filename);

let contents = fs::read_to_string(config.filename).expect("Error in reading file");
println!("With text:\n{}", contents)
}

struct Config{
query: String,
filename: String,
}

impl Config {
fn new(args: &Vec<String>) -> Config{
let query = args[1].clone();
let filename = args[2].clone();
Config{query, filename}
}
}

于是我们的代码变成了上面这样,使用Config::new代替了之前的parse_config,并顺利运行。

分离主体逻辑

这里我们的项目主体运行逻辑就是打开文件并搜索字符串,目前我们只写了打开文件,就先把这部分拆出来,写到run函数里

1
2
3
4
5
fn run(config: Config){
let contents = fs::read_to_string(config.filename)
.expect("Error in reading file");
println!("With text:\n{}", contents);
}

错误处理

依照计划,现在我们开始修复一下错误处理相关的逻辑

改进错误提示信息

首先,第一个能想到的错误就是,用户输入的参数不够,例如没有输入参数时,程序会报错“index out of bounds: the len is 1 but the index is 1”,但这个错误会让用户不知所云,所以我们需要完善一下报错提示:

1
2
3
4
5
6
7
8
9
10
impl Config {
fn new(args: &Vec<String>) -> Config{
if args.len() < 3 {
panic!("Not enough arguments");
}
let query = args[1].clone();
let filename = args[2].clone();
Config{query, filename}
}
}

在参数数量少于3个的时候,我们主动抛出panic,提示参数不够,这时的报错信息就更人性化了。

返回Result而不是直接Panic

对于函数里的错误,使用Result作为返回结果,让调用函数的用户决定是否panic的方法会更加友好一点。所以我们再修改一下这个new函数,让它在运行成功时返回一个Config变量,在失败时返回报错信息

1
2
3
4
5
6
7
8
9
10
impl Config {
fn new(args: &Vec<String>) -> Result<Config, &str>{
if args.len() < 3 {
return Err("Not enough arguments")
}
let query = args[1].clone();
let filename = args[2].clone();
Ok(Config{query, filename})
}
}

因为修改了返回值,所以我们还需要对应修改一下main函数的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use std::env;
use std::fs;
use std::process;

fn main() {
let args: Vec<String> = env::args().collect();

let config = Config::new(&args).unwrap_or_else(|err|{
println!("Problem parsing args: {}", err);
process::exit(1);
});

println!("Searching for {}", config.query);
println!("In file {}", config.filename);

let contents = fs::read_to_string(config.filename).expect("Error in reading file");
println!("With text:\n{}", contents)
}

在以上代码里

  1. 我们新use了一个process库用来退出程序
  2. 我们使用了之前没用过的错误处理函数unwrap_or_else,其大致工作内容也很简单,主要就是捕获错误信息,把报错信息传入闭包err中,作为参数传递进下面的花括号(匿名函数)里,运行花括号的代码。如果没有报错,则自动获取Ok里存储的变量返回。大致就是,和unwrap类似,不过多了一个可以执行额外错误处理代码的功能

现在我们再来跑一个错误的命令,来看看报错提示

1
2
3
4
5
cargo run             
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target\debug\minigrep.exe`
Problem parsing args: Not enough arguments
error: process didn't exit successfully: `target\debug\minigrep.exe` (exit code: 1)

完美,提示简短且易懂。

此外,对于run函数我们也作同样的操作;

1
2
3
4
5
6
7
use std::error::Error;

fn run(config: Config) -> Result<(), Box<dyn Error>>{
let contents = fs::read_to_string(config.filename)?;
println!("With text:\n{}", contents);
Ok(())
}

对于run,我们使用了一种新的处理方式->问号表达式,之前学过,这可以自动为我们在有错误的时候返回错误,减少代码的编写量;然后我们use了一个新的东西std::error::Error,并且使用了一个叫做Box的trait,指定返回的值是一个Error的trait,同时使用了dyn关键词;这里的主要作用就是,因为问号表达式返回的错误类型不是我们容易知道的,所以使用trait的方法可以方便的,动态的帮我们捕获不同的错误类型并进行返回。最后,因为run函数不需要返回什么值,所以只需要Ok里包含空元组就可以了。

同样的,最后再修改一下main函数,处理返回的Result即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
let args: Vec<String> = env::args().collect();

let config = Config::new(&args).unwrap_or_else(|err|{
println!("Problem parsing args: {}", err);
process::exit(1);
});

println!("Searching for {}", config.query);
println!("In file {}", config.filename);

if let Err(e) = run(config){
println!("Application error: {}", e);
process::exit(1);
}
}

这里我们也使用了一种新的处理方式,也就是在6. 枚举与模式匹配里学到的if let语句,用于匹配枚举类型中的其中一种情况。这里因为我们的run函数必定返回一个空元组,所以我们没必要通过unwrap_or_else提取这个空元组,所以简单对Err这种情况进行处理即可。

当然,语言是很灵活的,我们也可以选择使用match等语句处理我们的报错。

把代码分离成独立的包

代码基本已经分离完成了,现在我们只需要把分离的代码放到lib.rs里即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::fs;
use std::error::Error;

pub struct Config{
pub query: String,
pub filename: String,
}

impl Config {
pub fn new(args: &Vec<String>) -> Result<Config, &str>{
if args.len() < 3 {
return Err("Not enough arguments");
}
let query = args[1].clone();
let filename = args[2].clone();
Ok(Config{query, filename})
}
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>>{
let contents = fs::read_to_string(config.filename)?;
println!("With text:\n{}", contents);
Ok(())
}

别忘了声明接口为pub,让外部可以调用~以及相关的use语句也要搬过来。

搬迁完毕,接下来只要到main.rs里把我们的库use进来即可,或者不用use,直接使用顶层包进行绝对路径访问也没问题,详见7. 包、单元包和模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::env;
use std::process;

use minigrep::Config;

fn main() {
let args: Vec<String> = env::args().collect();

let config = Config::new(&args).unwrap_or_else(|err|{
println!("Problem parsing args: {}", err);
process::exit(1);
});

println!("Searching for {}", config.query);
println!("In file {}", config.filename);

if let Err(e) = minigrep::run(config){
println!("Application error: {}", e);
process::exit(1);
}
}

这里的Config就是通过use导入,run函数因为处于顶层,所以使用我们顶层包名minigrep直接调用也很方便(如果你创建项目的时候名字不是minigrep记得改成你的项目名字)

运行通过!我们的基础功能就基本完成了,后面再把用于测试用的println语句删除就好。

好了,以上代码基本结合了我们学习的大部分内容了,用模块化的思路设计完基本功能逻辑完成后,接下来就该结合一下我们测试的知识了

使用测试驱动开发来编写库功能

软件主要功能是搜索文件里的字符串,那还差一个搜索的功能。本小节就主要讲的是按照测试驱动开发(TDD)的流程来开发搜索的逻辑。大概是以下步骤

  1. 编写一个必然失败的测试,运行测试,确保它一定失败
  2. 编写或修改刚好足够多的代码,让测试通过
  3. 在保证测试通过的前提下,重构代码
  4. 回到步骤1,进行开发

这只是其中一种开发技术,主要思想是:优先编写测试,再编写能通过测试的代码,有助于开发过程中保持较高的测试覆盖率。

接下来,我们就开始第一步

编写一个会失败的测试

lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mod test{
use super::*;

#[test]
fn one_result(){
let query = "duct";
let content = "
Rust:
safe, fast, productive.
Pick three.";

assert_eq!(vec!["safe, fast, productive."], search(query, content));
}
}

这里我们编写了一个测试模块,给出了测试用的查询字符串,以及内容字符串,再调用了一下实现搜索的功能函数,断言搜索的结果是一个vec数组;

现在我们还没实现search功能,所以后面就来实现一下。但因为我们首先要编写一个必然失败的测试,所以我们先来试试写一个函数,必然返回空数组,这样就和断言的结果不同,导致失败。

1
2
3
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str>{
vec![]
}

这里因为我们选择传入字符串的引用,所以需要使用生命周期,这里返回的值主要存的是contents里的部分内容,所以只需要在contents和返回值里标上生命周期即可,query并不需要

接下来我们用cargo test运行一下测试吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
running 1 test
test test::one_result ... FAILED

failures:

---- test::one_result stdout ----
thread 'test::one_result' panicked at 'assertion failed: `(left == right)`
left: `["safe, fast, productive."]`,
right: `[]`', src\lib.rs:41:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
test::one_result

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

输出测试失败,左右不相等,一切按我们计划的在走,那接下来就是第二步了,编写或修改代码,让测试通过。

编写可以通过测试的代码

接下来我们按照正常的思路实验一下search函数的功能:

  1. 遍历内容的每一行
  2. 搜索行里是否包含搜索的字符串
  3. 如果包含,就把这行添加到列表里;否则忽略
  4. 返回列表
1
2
3
4
5
6
7
8
9
pub fn search<'a>(query: & str, contents: &'a str) -> Vec<&'a str>{
let mut ret = Vec::new();
for eachline in contents.lines() {
if eachline.contains(query){
ret.push(eachline);
}
}
ret
}

大概解释一下:

  1. new了一个可变的动态列表ret,用来存返回值
  2. 用for循环遍历contents的每一行,lines()函数可以把字符串分割成若干行,返回一个迭代器
  3. 使用contains函数判断query是否是eachline的子串,如果是,push到ret里
  4. 返回ret

最后再运行一下测试,不出意外就通过了。

把search集成到run函数内

上面已经把所有要写的都写完啦,可以集成功能并试着运行了

1
2
3
4
5
6
7
pub fn run(config: Config) -> Result<(), Box<dyn Error>>{
let contents = fs::read_to_string(config.filename)?;
for (i, line) in search(&config.query, &contents).iter().enumerate(){
println!("{}: {}\n", i, line);
}
Ok(())
}

也简单解释一下:

  1. 通过for循环遍历所有的搜索结果
  2. 通过.iter().enumerate()让迭代器生成的结果附带上当前的循环次数
  3. 输出搜索的结果

接下来就通过我们熟悉的命令行参数运行程序吧:cargo run frog poem.txt

1
2
3
4
5
cargo run frog poem.txt
Compiling minigrep v0.1.0 (E:\Code\rust\minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.26s
Running `target\debug\minigrep.exe frog poem.txt`
0: How public, like a frog

完美。

处理环境变量

如果有这么一个参数,例如是否开启不分大小写搜索,那每次用户启动的时候都需要通过命令行来输入一串指令开启就有点太麻烦了。这一部分我们就基于此优化一下我们的代码,主要依赖的就是环境变量

同样基于TDD的计划流程来编写功能。

编写一个必然失败的不区分大小写搜索测试

首先是写测试,就不解释了,主要是实现了两个测试,一个大小写敏感和一个大小写不敏感。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#[cfg(test)]
mod test{
use super::*;

#[test]
fn one_result(){
let query = "duct";
let content = "
Rust:
safe, fast, productive.
Pick three.";

assert_eq!(vec!["safe, fast, productive."], search(query, content));
}

#[test]
fn case_sensitive(){
let query = "duct";
let content = "
Rust:
safe, fast, productive.
Pick three.
Duct tape.";

assert_eq!(vec!["safe, fast, productive."], search(query, content));
}

#[test]
fn case_insensitive(){
let query = "rUst";
let content = "
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(vec!["Rust:", "Trust me."], search_case_insensitive(query, content));
}

}

这里省略掉实现search_case_insensitive返回空数组,导致测试失败的部分。可以自己试试,观察是否确保测试失败。

编写能通过测试的代码

然后是实现大小写不敏感的搜索功能函数,所谓大小写不敏感,其实就是把所有字母转成小写/大小再来作比较。

1
2
3
4
5
6
7
8
9
10
pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str>{
let mut ret = Vec::new();
let query = query.to_lowercase();
for eachline in contents.lines() {
if eachline.to_lowercase().contains(&query){
ret.push(eachline);
}
}
ret
}

以上代码需要注意的是:因为to_lowercase函数把字符串原来的数据更改了,所以会创建一个新的字符串存储新的数据,所以这里我们的query变成了拥有自己所有权的String变量,而不再是原来的字符串切片,所以后续传入contains时又使用了&来借用。

运行测试通过的话,就没有问题了。

把功能搬迁到run函数内

因为用户自行决定是否开启大小写敏感的功能,所以要新增一个配置项来存储这个结果,所以我们修改一个Config结构

1
2
3
4
5
pub struct Config{
pub query: String,
pub filename: String,
pub is_sensitive: bool,
}

接下来,再结合is_sensitive把search_case_insensitive的功能搬到run函数里就差不多了

1
2
3
4
5
6
7
8
9
10
11
12
13
pub fn run(config: Config) -> Result<(), Box<dyn Error>>{
let contents = fs::read_to_string(config.filename)?;
let result = if config.is_sensitive{
search(&config.query, &contents)
}
else{
search_case_insensitive(&config.query, &contents)
};
for (i, line) in result.iter().enumerate(){
println!("{}: {}\n", i, line);
}
Ok(())
}

不过这段代码还不能编译通过,因为我们还没修改Config的new函数。这里因为我们不希望通过之前的命令行参数解析的方法来得到is_sensitive的值,所以我们要使用新的方法——读取环境变量。

环境变量可以让一个参数的值再整个会话内都有效,就不需要每次运行都输入一遍了,很方便。

1
2
3
4
5
6
7
8
9
10
11
impl Config {
pub fn new(args: &Vec<String>) -> Result<Config, &str>{
if args.len() < 3 {
return Err("Not enough arguments");
}
let query = args[1].clone();
let filename = args[2].clone();
let is_sensitive = !(env::var("SENSITIVE").is_err());
Ok(Config{query, filename, is_sensitive})
}
}
  1. 我们新use了env,用来读取环境变量
  2. 添加了新的一行,通过env的var函数,读取名为“SENSITIVE”的环境变量
  3. 用Result的is_err()来处理报错,因为我们这里的is_sensitive就是一个bool值,is_err也是返回一个布尔值,正确读取环境变量的时候返回False,读取失败的时候返回True,用取反之后整好对上我们想实现的逻辑,也就是设置了环境变量的时候,则为True大小写敏感,否则False大小写不敏感。因为我们不需要关注环境变量的值,只想知道有没有。

写完之后,我们就来运行一下看看吧

1
2
3
4
5
6
7
8
9
10
cargo run to poem.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target\debug\minigrep.exe to poem.txt`
0: Are you nobody, too?

1: How dreary to be somebody!

2: To tell your name the livelong day

3: To an admiring bog!

我们在poem.txt里搜索to,结果没有问题,大小写不敏感,搜索出了to和To的结果

接下来,我们设置一下环境变量,这对于每个系统方法都不一样,这里只说一下windows的方法,在终端里输入以下语句

1
$env:SENSITIVE=1

然后再运行代码

1
2
3
4
5
6
7
cargo run to poem.txt  
Compiling minigrep v0.1.0 (E:\Code\rust\minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.41s
Running `target\debug\minigrep.exe to poem.txt`
0: Are you nobody, too?

1: How dreary to be somebody!

也没有问题,只搜索出小写的to结果

现在,这一部分也完成了。

将错误提示信息打印到标准错误而不是标准输出

println是输出到标准输出流,也就是打印到屏幕上,另一种输出流是标准错误流,会将正常的输出保存到文件里,错误的输出依旧打印到屏幕上。

我觉得这部分不重要,就简单放代码了。

println改成eprintln即可输出到标准错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use std::env;
use std::process;

use minigrep::Config;

fn main() {
let args: Vec<String> = env::args().collect();

let config = Config::new(&args).unwrap_or_else(|err|{
eprintln!("Problem parsing arguments: {}", err);
process::exit(1);
});

if let Err(e) = minigrep::run(config){
eprintln!("Application error: {}", e);
process::exit(1);
}
}

运行错误指令,并指定输出文件,查看输出

1
2
3
4
5
cargo run > output.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target\debug\minigrep.exe`
Problem parsing arguments: Not enough arguments
error: process didn't exit successfully: `target\debug\minigrep.exe` (exit code: 1)

可以看见错误信息打印到了屏幕上,而output.txt没有内容

运行正确指令,查看输出

1
2
3
cargo run to poem.txt > output.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target\debug\minigrep.exe to poem.txt

可以发现终端没有任何内容输出,输出搜索结果都在output.txt里

总结

这章主要还是复习以前的内容吧,大概就是

  1. 结合基本的概念,如所有权,生命周期等编写基本逻辑
  2. 如何使用函数,结构体,模块等功能结构化管理程序逻辑
  3. 如何捕获并正确的处理程序错误
  4. 如何编写测试模块
  5. 一些乱七八糟的命令行处理等
  • 标题: 【Rust 学习记录】12. 编写一个命令行程序
  • 作者: TwoSix
  • 创建于 : 2023-05-12 21:34:53
  • 更新于 : 2024-07-04 23:52:28
  • 链接: https://twosix.page/2023/05/12/【Rust-学习记录】12-编写一个命令行程序/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
\ No newline at end of file diff --git "a/2024/01/18/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\22113-\351\227\255\345\214\205\344\270\216\350\277\255\344\273\243\345\231\250/index.html" "b/2024/01/18/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\22113-\351\227\255\345\214\205\344\270\216\350\277\255\344\273\243\345\231\250/index.html" index ac282d2..93751b9 100644 --- "a/2024/01/18/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\22113-\351\227\255\345\214\205\344\270\216\350\277\255\344\273\243\345\231\250/index.html" +++ "b/2024/01/18/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\22113-\351\227\255\345\214\205\344\270\216\350\277\255\344\273\243\345\231\250/index.html" @@ -1,2 +1,2 @@ -【Rust 学习记录】13. 闭包与迭代器 - TwoSix的小木屋 +【Rust 学习记录】13. 闭包与迭代器 - TwoSix的小木屋

【Rust 学习记录】13. 闭包与迭代器

TwoSix Lv3

闭包:能够捕获环境的匿名函数

如这个小节的标题所示,闭包其实就是一个匿名函数,可以接收变量,也可以返回值,主要是用来实现一些代码复用和自定义的行为。

概述

首先,闭包的基本定义方法如下

1
2
3
4
5
6
7
fn main() {
let a = 1;
let b = 2;
let c = |a, b|{a+b};
let d = |a, b| a-b;
println!("{}, {}", c(a, b), d(a, b));
}
  1. 通过**||**包裹住传入的参数,通过逗号隔开
  2. 花括号内定义函数的行为。当然,对于简短的函数,例如a+b这种一条表达式直接返回值的,也可以不用花括号,如d

闭包与函数不同的是

  1. 闭包内不强迫显示标注类型,因为考虑到闭包这种通常使用的场景就是一个狭窄的上下文内,进行一个暂时的函数定义,而不会被广泛的定义。So, rust的设计者认为这种场景下干脆默认直接交给编译器推理就行。所以,闭包也有编译期固定类型的特性,闭包第一次被调用的时候,就按照第一次传入的参数被固定了,那第二次调用的时候是无法使用其他类型的参数传入的。如下面的代码会报错:error[E0308]: arguments to this function are incorrect

    1
    2
    3
    4
    5
    6
    7
    8
    9
    fn main() {
    let a = 1;
    let b = 2;
    let closure = |a, b|{a+b};
    println!("{}", closure(a, b));
    let c = 1.1;
    let d = 2.2;
    println!("{}", closure(c, d));
    }

    如果要实现复杂的闭包,用泛型等,自行指定类型也是没有问题的

  2. 闭包可以作为变量存储,这也就增加了更多代码复用的写法,例如作为变量存到结构体里,在结构体里按需调用之类的

  3. 闭包可以捕获环境上下文;这是一个与函数相比最大的不同点,这个就稍微展开来讲讲。

使用闭包捕获上下文环境

首先,捕获上下文是什么意思?看代码就知道了:

1
2
3
4
5
fn main() {
let a = 1;
let closure = |num| num==a;
println!("{}", closure(1));
}

在代码里可以看到,我们的闭包在没有传入a的情况下,获取到了闭包外的值a来进行一系列的处理。而普通函数如果不对a进行显示的传入的话,是不可能获得a的值的,这就是所谓的捕获上下文的能力。

那么问题来了,rust里的核心机制就是所有权,参数到了函数里,所有权也会转到函数中,那a在闭包内的所有权是怎么变化的呢?

我们换个例子来讲,因为a是整型,是存储在栈里的变量,在传入传出的时候是无脑的复制一份所有权,所以不具有代表性,我们换成数组。

1
2
3
4
5
6
fn main() {
let a = vec![1,2,3];
let closure = |num| num ==a[0];
println!("{}", closure(1));
println!("{:?}", a);
}

闭包里面包含了三个trait,分别是FnOnce, FnMut, Fn,而对于每一个闭包,至少实现了这三个trait中的一个。

  1. FnOnce是在闭包定义时会调用的,而且也只会调用一次的trait,他会把捕获到的环境变量所有权转移到闭包环境内。
  2. FnMut是可变的借用捕获的变量,不夺取所有权
  3. Fn则是不可变的借用

闭包会对变量实现哪个trait是自动的,主要取决于你在闭包内用变量来干了什么。例如我的数组例子里,是可以运行成功的,因为我只简单的使用了一下数组内的元素进行==的比较,并没有设计数值修改,所以闭包自动选择了不可变借用的形式。如以下形式,就是可变借用:

1
2
3
4
5
6
7
8
9
fn main() {
let mut a = vec![1, 2, 3]; // 注意a要改成可变
let closure = |tmp| {
a = tmp;
a
};
let b = vec![3, 4, 5];
println!("{:?}", closure(b));
}

如果你希望夺取所有权的话,有一个关键词move可以实现这么一个功能。

1
2
3
4
5
6
fn main() {
let a = vec![1, 2, 3];
let closure = move |num| num == a[0];
println!("{}", closure(1));
println!("{:?}", a);
}

定义为move后因为a的所有权被移到了闭包内,因此代码报错,无法运行。

除了自动实现的以外,你也可以采用之前讲的trait相关内容,为闭包指定trait类型,也可以重载实现trait以自定义更多的行为,这里就不多做讲解了。

迭代器:处理元素序列

迭代器也并不是一个新概念了,简单来说,迭代器主要就是用来遍历序列的,主要是解决传统遍历方式需要程序员自己判断序列是什么时候结束的问题,用迭代器就可以省去每次遍历都要定义一个=0的量,获取一次序列大小的逻辑。

迭代器的本质是一个Iterator的trait,如下:

1
2
3
4
5
6
7
pub trait Iterator {
type Item;

fn next(&mut self) -> Option<Self::Item>;

// 这里省略了由Rust给出的默认实现方法
}

所以迭代器本质上就是不断的调用next方法,生成下一个元素的值,包装成Option然后返回。

迭代器的生成是lazy的,和python的yield一样,只有当你遍历到那个元素的时候,迭代器才会使用next生成一个元素,然后返回到外面提供用户使用,在没有使用到的时候什么也不会发生,这在一定程度上节约了内存的使用。

上面的代码有一些新的语法,type ItemSelf::Item等,这个Item的功能是用来指定一个数据类型,后面会讲。

创建一个迭代器

.iter()

创建一个迭代器有很多方法,最基础的就是.iter()

1
2
3
4
5
6
7
fn main() {
let a = vec![1, 2, 3];
let a_iter = a.iter();
for i in a_iter {
println!("{}", i);
}
}

以上代码就是一个最简单的,使用.iter()创建一个列表的迭代器来遍历列表的示例。

当然,既然说过迭代器的本质是不断调用next方法,所以我们也可以使用next方法来使用迭代器。

1
2
3
4
5
6
7
8
fn main() {
let a = vec![1, 2, 3];
let mut a_iter = a.iter();
assert_eq!(a_iter.next(), Some(&1));
assert_eq!(a_iter.next(), Some(&2));
assert_eq!(a_iter.next(), Some(&3));
assert_eq!(a_iter.next(), None);
}

这里需要注意的是,a_iter必须定义为可变,因为调用next是会不断的吃掉上一个变量,替换成下一个变量的迭代器。

使用迭代器

因为next方法是会不断“吃掉”上一个元素的,因此迭代器的使用也伴随着迭代器的消耗。基本的for循环就是使用迭代器的一个方法。

1
2
3
4
5
6
7
8
9
10
fn main() {
let a = vec![1, 2, 3];
let a_iter = a.iter();
for i in a_iter {
println!("{}", i);
}
for i in a_iter {
println!("{}", i);
}
}

这段代码运行将会报错,可以看到,a_iter在被第一个for循环使用完之后,就无法继续使用了。

除了基本的循环外,还有很多提供便捷功能的迭代器相关方法,它们也同样会消耗迭代器。

  1. sum方法会自动生成所有迭代器,并返回其中所有元素的和。

    1
    2
    3
    4
    5
    6
    fn main() {
    let a = vec![1, 2, 3];
    let a_iter = a.iter();
    let total: i32 = a_iter.sum();
    println!("{}", total);
    }

    需要注意的是,这里必须把a_iter.sum()的结果存储到变量中打印,并显示标注total的类型,但这是后面再讲的问题了

  2. collect方法可以自动收集所有迭代器生成的元素,返回一个列表。

除了消耗迭代器返回元素的方法外,还有一种方法可以消耗旧的迭代器,生成新的符合一定要求的迭代器。这种方法成为迭代器适配器

  1. map方法可以接收一个闭包作为参数,自定义一个新的迭代器出来。

    1
    2
    3
    4
    5
    6
    7
    fn main() {
    let a = vec![1, 2, 3];
    let a_iter = a.iter();
    let a2_iter = a_iter.map(|x| x * 2);
    let b: Vec<i32> = a2_iter.collect();
    println!("{:?}", b);
    }

    这个示例中,我们使用map方法,生成一个会把原来元素*2再返回的迭代器,再使用collect方法收集成新的列表并打印输出

  2. filter方法同样也是接收闭包为参数,但这个闭包必须是返回一个bool值,利用闭包的bool结果,filter方法会过滤掉返回结果不为true的迭代器。

    1
    2
    3
    4
    5
    6
    7
    fn main() {
    let a = vec![1, 2, 3, 4];
    let a_iter = a.into_iter();
    let a2_iter = a_iter.filter(|x| x % 2 == 0);
    let b: Vec<i32> = a2_iter.collect();
    println!("{:?}", b);
    }

    这里使用的是into_iter,因为a.iter()返回的是一个引用,所以在执行%运算的时候会报类型不匹配的错误;要么解引用,要么用into_iter直接夺取所有权,这里我偷了个懒。(PS:上面的乘法能编译通过是因为乘法这种基本运算会自动解引用。)

    当然,既然filter和map的传入参数是闭包,自然也可以捕获环境上下文,感兴趣可以自己试试,这里就不多写演示代码了。

还有很多方法,例如zip(),skip(),这里就没办法一一展开了

自定义迭代器

迭代器本质上是一个Iterator trait,所以我们自然也可以定义一个结构体,为他实现一个Iterator trait,进而实现一个我们自定义的迭代器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Counter {
Counter { count: 0 }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
self.count += 1;
if self.count < 6 {
Some(self.count)
} else {
None
}
}
}
fn main() {
let counter = Counter::new();
for i in counter {
println!("{}", i);
}
}

这段示例里,我们就定义了一个结构体Counter,内置一个u32变量count,初始值为0,然后为Counter实现了一个Iterator trait,让Counter结构体变成了一个迭代器,迭代的逻辑就是不断的递增结构体内的count变量,直到count>=6。

迭代器的性能分析

例如说我们现在有一个功能,需要在一堆字符串中,找到所有包含某个子串的字符串。那最简单的,利用循环的写法可以写成如下形式:

1
2
3
4
5
6
7
8
9
fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}

那在知道迭代器之后,我们自然也可以想到可以通过迭代器的filter方法,过滤出只包含这个子串的迭代器,迭代器的版本可以这么写:

1
2
3
4
5
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
contents.lines()
.filter(|line| line.contains(query))
.collect()
}

那这两种写法有什么区别呢,多抽象一层迭代器的话,会不会有性能损失?

答案是不会的。

Rust官方书籍里用一本小说做了一次bench mark,结果表面,迭代器的写法在经过优化后比for循环更快。

1
2
test bench_search_for  ... bench:  19,620,300 ns/iter (+/- 915,700)
test bench_search_iter ... bench: 19,234,900 ns/iter (+/- 657,200)

迭代器是Rust里的一种零开销抽象,所以我们在使用的时候根本不需要担心会不会引入额外的运行成本。

至于快了的那一点,本质上是因为迭代器在编译过程中会运行一次展开优化,例如本来要运行12次的循环代码,如果是迭代器的话,在编译之后会展开成重复执行12次的代码,减少了循环控制时候的开销。

但个人觉得可以忽略不计,大可依照个人习惯的去选择使用这两种写法,benchmark本质上只是证明了iter并不会引入额外开销,放心使用。

  • 标题: 【Rust 学习记录】13. 闭包与迭代器
  • 作者: TwoSix
  • 创建于 : 2024-01-18 20:57:44
  • 更新于 : 2024-07-04 23:52:28
  • 链接: https://twosix.page/2024/01/18/【Rust-学习记录】13-闭包与迭代器/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
\ No newline at end of file diff --git "a/2024/01/23/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\22114-\350\277\233\344\270\200\346\255\245\350\256\244\350\257\206Cargo\345\217\212crates-io/index.html" "b/2024/01/23/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\22114-\350\277\233\344\270\200\346\255\245\350\256\244\350\257\206Cargo\345\217\212crates-io/index.html" index 13bf1ab..939bec1 100644 --- "a/2024/01/23/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\22114-\350\277\233\344\270\200\346\255\245\350\256\244\350\257\206Cargo\345\217\212crates-io/index.html" +++ "b/2024/01/23/\343\200\220Rust-\345\255\246\344\271\240\350\256\260\345\275\225\343\200\22114-\350\277\233\344\270\200\346\255\245\350\256\244\350\257\206Cargo\345\217\212crates-io/index.html" @@ -1,2 +1,2 @@ -【Rust 学习记录】14. 进一步认识Cargo及crates.io - TwoSix的小木屋 +【Rust 学习记录】14. 进一步认识Cargo及crates.io - TwoSix的小木屋

【Rust 学习记录】14. 进一步认识Cargo及crates.io

TwoSix Lv3

这一章就主要讲讲Cargo这个工具的一些用途,主要是以下几个部分,没有涉猎到的可以在官网 查看cargo更全面的介绍:

  1. build时使用的release profile相关介绍
  2. 怎么将你写的包发布到creates.io上给别人用
  3. 使用工作空间组织项目
  4. 下载安装craetes.io的包
  5. 使用自定义命令来扩展cargo

release profile

Rust内置了两套release profile,一套就是我们在cargo build时候使用的debug用dev profile,一套就是cargo build --release对应的发布用release profile

我们可以在toml文件内自定义的修改这两套配置。对应的在[profile.xxx ]字段内,如下:

1
2
3
4
5
[profile.dev]
opt-level = 0

[profile.release]
opt-level = 3

opt-level对应的是编译时的优化等级,等级越高,编译耗时越长,但最终可执行文件的运行速度就越快,值只能是0,1,2,3。其中dev配置默认是0,relese配置默认是3。如果有必要,你也可以自行选择修改成1和2

怎么发布你的包到creates.io

编写有用的文档注释

一份好的文档能帮助用户快速理解这个包的具体用途和使用方法,Rust提供了一种可以快速生成HTML文档的注释方法(nb,我还以为是docstring,居然可以直接生成文档)

之前我们都知道双斜杠//可以注释,而我们的文档注释就是///三斜杠,文档注释主要用来介绍使用方法,而不是介绍包的实现细节,例如:

lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
/// 将传入的数字加1
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
}

第一行介绍了函数对应的功能,然后用类似markdown的语法表明了一块Examples区域,最后提供了一片代码块示例。写完之后,我们只需要运行命令:cargo doc就可以自动生成一份HTML文档了,再执行cargo doc --open就可以自动用浏览器打开文档了。

生成后的样子:
img

除了Examples外,还有其他一些可选的文档区域,如:

  • Panics,指出函数可能引发panic的场景。不想触发panic的调用者应当确保自己的代码不会在这些场景下调用该函数。
  • Errors,当函数返回Result作为结果时,这个区域会指出可能出现的错误,以及造成这些错误的具体原因,它可以帮助调用者在编写代码时为不同的错误采取不同的措施。
  • Safety,当函数使用了unsafe关键字(在第19章讨论)时,这个区域会指出当前函数不安全的原因,以及调用者应当确保的使用前提。

此外,有意思的是,你写的示例内的代码并不只是给用户看的。在你执行cargo test的时候,cargo会把你文档注释内的代码一并执行测试,因为没有比文档内的示例代码不能正常执行更让人恶心的了哈哈哈。


另一种文档注释的形式是//!。这种文档注释会显示在注释的最外层,一般用在包的根文件上,用来介绍整个包的大致情况。我们改成如下注释看看:

lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//! # My Crate
//!
//! my_crate是一系列工具的集合,
//! 这些工具被用来简化特定的计算操作

/// 将传入的数字加1
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
}

此时生成的文档如下:

img

可以看见//!的注释显示在了add_one对应注释的外层,点击add_one后才会进入刚才对应的add_one用例文档

使用pub use来导出合适的公共API

这一部分的内容和use章里讲的内容基本是异曲同工,不过这里可以结合文档部分再讲一下。

这一部分解决的问题主要是:假如我们的代码结构嵌套的非常复杂的时候,那么用户在调用的时候代码就会写成use::aaa::bbb::ccc::ddd::eee::fff,很长很难看也很不好用,那该怎么办呢?

结合文档来看一下,例如以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//! # Art
//!
//! 一个用来建模艺术概念的代码库

pub mod kinds {
/// RYB颜色模型的三原色
pub enum PrimaryColor {
Red,
Yellow,
Blue,
}

/// RYB模型的调和色
pub enum SecondaryColor {
Orange,
Green,
Purple,
}
}

pub mod utils {
use crate::kinds::*;

/// 将两种等量的原色混合生成调和色
pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
// --略--
}
}

会生成以下文档:

img

可见暴露在外的只有kinds和utils,而我们高频使用的应该是kinds和utils的内容,这部分却要点进二级目录才能看到具体有什么。

要解决这个问题,我们只需要在结构外use一遍,把里面的结构暴露到外面即可。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//! # Art
//!
//! 一个用来建模艺术概念的代码库

pub use self::kinds::PrimaryColor;
pub use self::kinds::SecondaryColor;
pub use self::utils::mix;

pub mod kinds {
/// RYB颜色模型的三原色
pub enum PrimaryColor {
Red,
Yellow,
Blue,
}

/// RYB模型的调和色
pub enum SecondaryColor {
Orange,
Green,
Purple,
}
}

pub mod utils {
use crate::kinds::*;

/// 将两种等量的原色混合生成调和色
pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
// --略--
}
}

以上代码我们只是在开头添加了几句pub use,生成的文档如下

img

可见我们pub use的函数也暴露在文档首页了;不过具体注释还是要点进去才能看见

同时,我们在文件外使用这些函数的时候也从:

1
2
3
pub use art::kinds::PrimaryColor;
pub use art::kinds::SecondaryColor;
pub use art::utils::mix;

变成了:

1
2
3
pub use art::PrimaryColor;
pub use art::SecondaryColor;
pub use art::mix;

发布你的包

代码完成后,发布包只需按照以下几个步骤

  1. 创建crates.io账户

    创建省略

    创建完毕后需要获取api令牌,并使用cargo进行本地身份验证,如:

    1
    cargo login abcdefghijklmnopqrstuvwxyz012345

    这句命令会把你的令牌存入~/.cargo/credentials

  2. 为包添加元数据

    Cargo.toml里面可以配置包名,描述,版本等等信息,填写示例如下:

    1
    2
    3
    4
    5
    6
    7
    [package]
    name = "guessing_game"
    version = "0.1.0"
    authors = ["Your Name <you@example.com>"]
    edition = "2018"
    description = "A fun game where you guess what number the computer has chosen."
    license = "MIT OR Apache-2.0"
  3. 发布:执行cargo publish即可完成发布

  4. 更新:只需要更改toml里的version字段,再重新publish就能完成新版本的更新了

  5. 撤回:执行cargo yank --vers 1.0.1即可撤回指定版本的包;如果操作失误了,也可以撤回你的撤回:cargo yank --vers 1.0.1 --undo;但需要注意的是,撤回不会删除任何代码,只能阻止别人的代码下载或指定这个版本的包为依赖

Cargo工作空间

工作空间的概念可以把项目的代码划分成若干个包,在互相关联的同时更方便的协同开发。

创建工作空间

这时候我们需要新建一个文件夹,名为add,直接新建一个文件夹即可,不需要cargo new命令创建。

然后,我们再自己建一个Cargo.toml文件,填入以下内容

1
2
3
4
5
[workspace]

members = [
"adder",
]

这个toml文件不像我们之前碰到的toml一样有一堆字段,他只包含一个工作空间字段,声明了工作空间下的成员有一个adder,这个adder我们还没创建,现在来创建一下。

使用cargo new字段创建一个二进制包:cargo new adder

这个时候我们就已经可以在add目录下使用cargo build构建整个工作空间了。

在工作空间里创建第二个包

我们可以用cargo new add-one --lib来创建一个代码包,当然,我们同时也得在toml文件加上

1
2
3
[workspace]

members = ["adder", "add-one"]

然后,我们再在add-one里的lib.rs写上我们可以供外部调用的接口函数,如下:

1
2
3
pub fn add_one(x: i32) -> i32 {
x + 1
}

那我们在adder里要怎么调用add-one里的接口呢?

工作空间是不会自动为你假设出依赖关系的,所以需要自己在toml里显式的指定,例如我们要在adder里调用add-one的函数,就要在adder的toml里添加以下字段

1
2
3
[dependencies]

add-one = { path = "../add-one" }

这一部分指定了adder需要依赖的包,名字是什么,对应的包路径在哪。显式指定后,我们就可以在addermain.rs里编写代码,并使用add-one里的接口了,如下:

1
2
3
4
5
6
use add_one;

fn main() {
let num = 10;
println!("Hello, world! {} plus one is {}!", num, add_one::add_one(num));
}

最后,我们再在根目录add下build编译并运行。

运行和往常有所不同,因为一个工作空间可能有很多个二进制包,所以我们需要指定运行哪个二进制包,加上-p参数即可,如:cargo run -p adder

然后就可以看到正常的输出了

在工作空间中依赖外部包

和以前一样,也是在toml里指定即可,例如这里我们给add-one引入一个rand包,如下:

1
2
3
[dependencies]

rand = "0.3.14"

再执行cargo build,就可以自动的下载安装rand包了。


这里值得一提的是,Cargo.lock文件只在add根目录下有,这是为了保证工作空间下所有包的依赖都是同一版本的,一是节约了磁盘空间,二是确保工作空间下的包互相之间是兼容的,避免奇奇怪怪的问题。

工作空间下进行测试

和往常一样编写测试代码,然后在根目录下执行cargo test即可自动完成所有测试。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
pub fn add_one(x: i32) -> i32 {
x + 1
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn it_works() {
assert_eq!(3, add_one(2));
}
}
$ cargo test
warning: virtual workspace defaulting to `resolver = "1"` despite one or more workspace members being on edition 2021 which implies `resolver = "2"`
note: to keep the current resolver, specify `workspace.resolver = "1"` in the workspace root's manifest
note: to use the edition 2021 resolver, specify `workspace.resolver = "2"` in the workspace root's manifest
note: for more details see https://doc.rust-lang.org/cargo/reference/resolver.html#resolver-versions
Compiling add-one v0.1.0 (E:\Code\rust\learning\add\add-one)
Compiling adder v0.1.0 (E:\Code\rust\learning\add\adder)
Finished test [unoptimized + debuginfo] target(s) in 0.29s
Running unittests src\lib.rs (target\debug\deps\add_one-48bf46fc7c463809.exe)

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Running unittests src\main.rs (target\debug\deps\adder-174daa247398ed5b.exe)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Doc-tests add-one

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

使用cargo install来安装可执行程序

cargo install的主要作用是用来安装别人分享的二进制包的,可以直接在命令行使用别人写好的工具。安装会装在rust安装的根目录下的bin文件夹里,所以如果要直接使用的话,需要配置一下环境变量,把这个目录加进去。

例如:cargo install ripgrep

具体就没什么好演示的了,这是一个用来搜索文件的小工具。

使用自定义命令扩展Cargo的功能

cargo xxx可以运行xxx这个二进制文件,所以如果你用cargo install安装了某个扩展,也可以用cargo xxx来运行

  • 标题: 【Rust 学习记录】14. 进一步认识Cargo及crates.io
  • 作者: TwoSix
  • 创建于 : 2024-01-23 21:55:47
  • 更新于 : 2024-07-04 23:52:28
  • 链接: https://twosix.page/2024/01/23/【Rust-学习记录】14-进一步认识Cargo及crates-io/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
\ No newline at end of file diff --git "a/2024/06/27/cmake\350\260\203\347\224\250windeployqt\345\256\236\347\216\260\350\207\252\345\212\250\346\211\223\345\214\205qt\347\232\204dll\346\226\207\344\273\266/index.html" "b/2024/06/27/cmake\350\260\203\347\224\250windeployqt\345\256\236\347\216\260\350\207\252\345\212\250\346\211\223\345\214\205qt\347\232\204dll\346\226\207\344\273\266/index.html" index 40f56e8..08bd2ec 100644 --- "a/2024/06/27/cmake\350\260\203\347\224\250windeployqt\345\256\236\347\216\260\350\207\252\345\212\250\346\211\223\345\214\205qt\347\232\204dll\346\226\207\344\273\266/index.html" +++ "b/2024/06/27/cmake\350\260\203\347\224\250windeployqt\345\256\236\347\216\260\350\207\252\345\212\250\346\211\223\345\214\205qt\347\232\204dll\346\226\207\344\273\266/index.html" @@ -1,2 +1,2 @@ -cmake调用windeployqt实现自动打包qt的dll文件 - TwoSix的小木屋 +cmake调用windeployqt实现自动打包qt的dll文件 - TwoSix的小木屋

cmake调用windeployqt实现自动打包qt的dll文件

TwoSix Lv3

最近在编写一个Qt项目,发现Qt在windows部署有一个很方便的工具windeployqt.exe,遂研究如何在cmake里调用这个工具,在install时执行,实现全自动化的发布构建。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
# 1. 找到qmake的执行路径(Qt5请更换为你实际的Qt版本)
get_target_property(qmake_exec_filepath Qt5::qmake IMPORTED_LOCATION)
# 2. 找到Qt的bin文件夹
get_filename_component(qt_exec_bin_dir "${qmake_exec_filepath}" DIRECTORY)
# 3. 找到windeployqt的路径
find_program(windeployqt_exec_filepath windeployqt HINTS "${qt_exec_bin_dir}")
# 4. 在控制台执行命令
add_custom_command(TARGET app POST_BUILD
COMMAND "${windeployqt_exec_filepath}" "--dir" "${CMAKE_INSTALL_PREFIX}/你的二进制文件存放的文件夹" "$<TARGET_FILE:你的target名>"
COMMENT "Running windeployqt..."
)

简单来说思路就是:通过库文件找到qmake的路径->通过qmake找到bin的路径->通过bin找到windeployqt的路径->就可以在控制台调用windeployqt执行命令了。

--dir是指定输出的目录,我使用install进行打包,如果不指定的话会在build目录下执行。


如果没有像qt一样这么方便的工具怎么办?另外附上一段另外写的,查找其他库的dll并复制过来的代码:

1
2
3
4
5
6
7
8
# 获取到库的bin路径
set(VTK_DLL_PATH ${VTK_DIR}/../../../bin)
# 搜索bin下的dll
file(GLOB VTK_DLLS ${VTK_DLL_PATH}/*.dll)
# 对所有dll依次install
foreach(DLL ${VTK_DLLS})
install(FILES ${DLL} DESTINATION bin)
endforeach()
  • 标题: cmake调用windeployqt实现自动打包qt的dll文件
  • 作者: TwoSix
  • 创建于 : 2024-06-27 20:11:05
  • 更新于 : 2024-07-04 23:52:28
  • 链接: https://twosix.page/2024/06/27/cmake调用windeployqt实现自动打包qt的dll文件/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
此页目录
cmake调用windeployqt实现自动打包qt的dll文件
\ No newline at end of file diff --git "a/2024/06/28/\344\270\272\344\275\240\347\232\204hexo\345\215\232\345\256\242\346\267\273\345\212\240\344\270\200\344\270\252\350\277\275\347\225\252\345\210\227\350\241\250/index.html" "b/2024/06/28/\344\270\272\344\275\240\347\232\204hexo\345\215\232\345\256\242\346\267\273\345\212\240\344\270\200\344\270\252\350\277\275\347\225\252\345\210\227\350\241\250/index.html" index 7b17b9b..53d12e9 100644 --- "a/2024/06/28/\344\270\272\344\275\240\347\232\204hexo\345\215\232\345\256\242\346\267\273\345\212\240\344\270\200\344\270\252\350\277\275\347\225\252\345\210\227\350\241\250/index.html" +++ "b/2024/06/28/\344\270\272\344\275\240\347\232\204hexo\345\215\232\345\256\242\346\267\273\345\212\240\344\270\200\344\270\252\350\277\275\347\225\252\345\210\227\350\241\250/index.html" @@ -1,2 +1,2 @@ -为你的hexo博客添加一个追番列表 - TwoSix的小木屋 +为你的hexo博客添加一个追番列表 - TwoSix的小木屋

为你的hexo博客添加一个追番列表

TwoSix Lv3

本文基于插件hexo-bilibili-bangumi 编写,并修改为适配redefine主题的样式,最终结果示例见我的追番列表

主要是闲来无事逛github的时候发现了hexo-bilibili-bangumi 这么一个插件,可以爬取bili/bangumi的数据并渲染为一个页面展示你的追番列表,整好我前段时间开始有了bangumi记录追番的习惯,所以想着上手用用。

如何使用

正常来说,按照官方的readme操作完就可以上手使用了

  1. 安装插件

    1
    $ npm install hexo-bilibili-bangumi --save
  2. _config.yml配置文件里添加你的配置(以下配置为了确保一次正常运行,与官方示例不同,完整示例见官方):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    bangumi: # 追番设置
    enable: true
    source: bangumi
    bgmInfoSource: 'bgmApi'
    path:
    vmid: 根据官方readme获取
    title: '追番列表'
    quote: '生命不息,追番不止!'
    show: 1
    lazyload: false
    srcValue: '__image__'
    lazyloadAttrName: 'data-src=__image__'
    loading:
    showMyComment: false
    pagination: false
    metaColor:
    color:
    webp:
    progress:
    extraOrder:
    order: latest
    coverMirror:
  3. 编译并生成静态文件

    1
    2
    3
    4
    hexo c
    call hexo bangumi -u # 必须要在hexo g前添加这句,爬取数据
    call hexo g
    call hexo s
  4. 然后你就可以在/bangumis后缀的页面下看见你的追番列表页面啦。(如果修改了path的话,以你实际填写path为主)

附言

实际配置过程中,lazyload选项容易和主题冲突,导致图片一直转圈;pagination选项也会有冲突,导致分页异常。因此上面的配置我都改成了默认关闭。

进阶

因为默认的样式不太好看,以及其他配置和我当前使用的主题redefine有诸多冲突,因此需要进行一些修改才能正常使用,以下是我做的部分修改分享,也是给自己作一次存档。

信息

本人传统后端出身,对前端一概不通,以下修改基本都是靠堆时间慢慢调试+GPT完成,所以改的不好或有其他方案建议的欢迎批评指出(我们GPT真是太强啦)

  1. 针对获取的番剧封面太小的问题,修改了lib/templates/bgm-template.ejs文件(因为我只用bgm源所以是这个,bili源有另一个templates文件);以及途中感觉他的布局有些奇怪,my-comments明明是在picture和右边的内容下面,但却归到右边内容的div里,用负数的padding来移到左边…所以布局也改了改

    主要是把img的width从110改成了130px,然后新增一个bangumi-block的div和mycomments纵向排列。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    <div class="bangumi-item">
    <div class="bangumi-block">
    <div class="bangumi-picture"><img src="<%= lazyload ? (loading || "https://cdn.jsdelivr.net/npm/hexo-bilibili-bangumi@1.2.0/lib/img/loading.gif") : (srcValue === '__loading__' ? (loading || "https://cdn.jsdelivr.net/npm/hexo-bilibili-bangumi@1.2.0/lib/img/loading.gif") : `https:${item.cover.replace(/^https:/, '')}`) %>" <%- lazyload ? ` data-src="${item.cover}"` : (lazyloadAttrName ? ` ${lazyloadAttrName.split('=')[0]}="${lazyloadAttrName.split('=')[1] === '__loading__' ? (loading || "https://cdn.jsdelivr.net/npm/[email protected]/lib/img/loading.gif") : (lazyloadAttrName.split('=')[1] === '__image__' ? `https:${item.cover.replace(/^https:/, '')}` : (lazyloadAttrName.split('=')[1] || ''))}"` : "") %> referrerPolicy="no-referrer" width="130" style="width:130px;margin:20px auto;" />
    </div>
    <div class="bangumi-info">
    <div class="bangumi-title">
    <a target="_blank" href="https://bangumi.tv/subject/<%= item.id %>"><%= item.title || "Unknown" %></a>
    </div>
    <div class="bangumi-meta">
    <span class="bangumi-info-items" <%- metaColor %>>
    <span class="bangumi-info-item">
    <% if(item.totalCount){ %>
    <span class="bangumi-info-total"><%= item.totalCount %></span><em
    class="bangumi-info-label-em">0</em>
    </span>
    <% } %>
    <span class="bangumi-info-item bangumi-type">
    <span class="bangumi-info-label">类型</span> <em><%= item.type %></em>
    </span>
    <span class="bangumi-info-item bangumi-wish">
    <span class="bangumi-info-label">想看</span> <em><%= item.wish %></em>
    </span>
    <span class="bangumi-info-item bangumi-doing">
    <span class="bangumi-info-label">在看</span> <em><%= item.doing || "-" %></em>
    </span>
    <span class="bangumi-info-item bangumi-collect">
    <span class="bangumi-info-label">已看</span> <em><%= item.collect || "-" %></em>
    </span>
    <span class="bangumi-info-item bangumi-info-item-score">
    <span class="bangumi-info-label">评分</span> <em><%= item.score || "-" %></em>
    </span>
    </span>
    </div>
    <div class="bangumi-comments" <%- color %>>
    <p>简介:<%= item.des || "暂无简介" %></p>
    </div>
    </div>
    </div>
    <% if (showMyComment && item.myComment) { %>
    <div class="bangumi-my-comments">我的评分:
    <% if (item.myStars) { %>
    <span class="bangumi-starstop"><span class="bangumi-starlight stars<%= item.myStars %>"></span></span>
    <% } %>
    <br>
    我的评价:<%= item.myComment %>
    </div>
    <% } %>
    </div>

  2. 因插件为对swup没作兼容,而redefine主题推荐开启swup,开启后会导致无法加载bangumi插件的js脚本,因此对隔壁的lib/templates/bangumi.ejs进行了修改

    主要是将<script>标签改成了<script data-swup-reload-script type="text/javascript">

    1
    2
    3
    4
    ···以上省略
    <script data-swup-reload-script type="text/javascript">
    ···以下省略
    </script>
  3. 对样式进行美化(自认为的)

    因bangumi插件已经支持针对不同主题的样式表,所以只需要在/src/lib/templates/theme目录下,新增一个redefine.css文件,然后填写自己重新针对该主题设置的样式表即可,以下是我自己修改的样式表:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    .bangumi-tabs {
    margin-bottom: 15px;
    margin-top: 15px;
    display: flex;
    justify-content: center;
    background-color: #f4f4f4;
    padding: 5px 5px;
    border-radius: 10px;
    width: fit-content;
    margin-left: auto;
    margin-right: auto;
    }

    .bangumi-tab {
    padding: 10px 20px;
    justify-content: center;
    text-align: center;
    cursor: pointer;
    border: none;
    background-color: transparent;
    color: #000;
    margin: 5px;
    border-radius: 8px;
    transition: background-color 0.3s, color 0.3s, box-shadow 0.3s;
    font-weight: bold;
    }

    a.bangumi-tab {
    text-decoration: none;
    }

    .bangumi-active {
    background-color: #fafafa;
    color: #005080;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    border-radius: 8px;
    font-weight: bold;
    }

    .bangumi-tab:hover {
    background-color: #e8e6e6;
    }

    .bangumi-item {
    position: relative;
    clear: both;
    padding: 15px;
    margin: 15px;
    height: fit-content;
    background-color: transparent;
    box-shadow: 0 4px 8px rgba(0,0,0,0.1);
    border: 1px solid #e1e1e1;
    border-radius: 10px;
    transform: translateZ(0);
    transition: transform 0.3s, box-shadow 0.3s;
    }

    @media screen and (max-width: 600px) {
    .bangumi-item {
    width: 100%;
    }
    }

    .bangumi-block {
    min-height: 180px;
    }

    .bangumi-picture {
    display: flex !important;
    justify-content: center;
    align-items: center;
    padding-left: 20px;
    width: 130px;
    height: auto;
    }

    .bangumi-picture img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.3);
    }

    .bangumi-info {
    padding-left: 150px;
    margin-top: 10px;
    }

    .bangumi-title {
    font-size: 1.2rem;
    }

    .bangumi-title a {
    line-height: 1;
    text-decoration: none;
    }

    .bangumi-meta {
    font-size:12px;
    padding-right:10px;
    height:45px
    }

    .bangumi-comments {
    font-size: 0.92rem;
    margin-top:12px
    }

    .bangumi-comments>p {
    word-break: break-all;
    text-overflow: ellipsis;
    overflow: hidden;

    white-space: normal;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
    }

    .bangumi-pagination {
    margin-top: 15px;
    text-align: center;
    margin-bottom: 10px;
    }

    .bangumi-button {
    padding: 5px;
    }

    .bangumi-button:hover {
    background: #657b83;
    color: #fff;
    }

    .bangumi-hide {
    display: none;
    }

    .bangumi-show {
    display: block;
    }

    .bangumi-info-items {
    font-size: 0.92rem;
    color: #005080;
    padding-top: 10px;
    line-height: 1;
    float: left;
    width: 100%;
    }

    .bangumi-item:hover {
    box-shadow: 0 6px 12px rgba(0,0,0,0.25);
    }

    .bangumi-info-item {
    display: inline-block;
    width: 13%;
    border-right: 1px solid #005080;
    text-align: center;
    height: 34px;
    }

    .bangumi-info-label {
    display: block;
    line-height: 12px;
    }

    .bangumi-info-item em {
    display: block;
    padding-top: 6px;
    line-height: 17px;
    font-style: normal;
    font-weight: 700;
    }

    .bangumi-info-total {
    padding-top: 11px;
    display: block;
    line-height: 12px;
    font-weight: bold;
    }

    .bangumi-info-item-score {
    border-right: 1px solid #0000;
    width: 50px;
    }

    .bangumi-info-label-em {
    color: rgba(0, 0, 0, 0);
    opacity: 0;
    visibility: hidden;
    line-height: 6px !important;
    padding: 0 !important;
    }

    @media (max-width:650px) {

    .bangumi-coin,
    .bangumi-type {
    display: none;
    }

    .bangumi-info-item {
    width: 16%;
    }
    }

    @media (max-width:590px) {

    .bangumi-danmaku,
    .bangumi-wish {
    display: none;
    }

    .bangumi-info-item {
    width: 19%;
    }
    }

    @media (max-width:520px) {

    .bangumi-play,
    .bangumi-doing {
    display: none;
    }

    .bangumi-info-item {
    width: 24%;
    }
    }

    @media (max-width:480px) {

    .bangumi-follow,
    .bangumi-collect {
    display: none;
    }

    .bangumi-info-item {
    width: 30%;
    }
    }

    @media (max-width:400px) {
    .bangumi-area {
    display: none;
    }

    .bangumi-info-item {
    width: 45%;
    }
    }

    .bangumi-my-comments {
    border: 1px dashed #8f8f8f;
    padding: 3px;
    border-radius: 5px;
    margin-left: 5px;
    }

    .bangumi-starstop {
    background: transparent url(https://cdn.jsdelivr.net/npm/hexo-bilibili-bangumi@1.7.9/lib/img/rate_star_2x.png);
    height: 10px;
    background-size: 10px 19.5px;
    background-position: 100% 100%;
    background-repeat: repeat-x;
    width: 50px;
    display: inline-block;
    float: none;
    }

    .bangumi-starlight {
    background: transparent url(https://cdn.jsdelivr.net/npm/hexo-bilibili-bangumi@1.7.9/lib/img/rate_star_2x.png);
    height: 10px;
    background-size: 10px 19.5px;
    background-position: 100% 100%;
    background-repeat: repeat-x;
    display: block;
    width: 100%;
    background-position: 0 0;
    }

    .bangumi-starlight.stars1 {
    width: 5px;
    }

    .bangumi-starlight.stars2 {
    width: 10px;
    }

    .bangumi-starlight.stars3 {
    width: 15px;
    }

    .bangumi-starlight.stars4 {
    width: 20px;
    }

    .bangumi-starlight.stars5 {
    width: 25px;
    }

    .bangumi-starlight.stars6 {
    width: 30px;
    }

    .bangumi-starlight.stars7 {
    width: 35px;
    }

    .bangumi-starlight.stars8 {
    width: 40px;
    }

    .bangumi-starlight.stars9 {
    width: 45px;
    }

    .bangumi-starlight.stars10 {
    width: 50px;
    }

但苦于实在不会前端,目前还遗留了一些问题:1. 不知道该怎么获取主题当前是日间还是夜间模式,然后针对夜间模式进行适配,所以夜间模式下会不太好看;2. 其实想把选页的按钮也改成redefine首页那样的模式,但也确实不会修改了,问GPT也只能做到改样式的程度了。


如果你想使用我修改后的版本,只需要在path/to/your_blot/node_modules/hexo-bilibili-bangumi目录下,完成以下步骤即可

  1. 安装依赖:npm install
  2. 作上述同样的修改
  3. 编译:npm run build

然后hexo重新生成以下就完事了。

  • 标题: 为你的hexo博客添加一个追番列表
  • 作者: TwoSix
  • 创建于 : 2024-06-28 16:42:06
  • 更新于 : 2024-07-04 23:52:28
  • 链接: https://twosix.page/2024/06/28/为你的hexo博客添加一个追番列表/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
此页目录
为你的hexo博客添加一个追番列表
\ No newline at end of file diff --git a/404.html b/404.html index 44476a3..73ac41e 100644 --- a/404.html +++ b/404.html @@ -1,2 +1,2 @@ -Page Not Found - TwoSix的小木屋 +Page Not Found - TwoSix的小木屋

404 Page Not Found

\ No newline at end of file diff --git a/about/index.html b/about/index.html index e4b5cfe..9e8caa7 100644 --- a/about/index.html +++ b/about/index.html @@ -1,2 +1,2 @@ -关于 - TwoSix的小木屋 +关于 - TwoSix的小木屋

About Me

  • 普通在读研究生,大方向是机器人,小方向未定,处于摆烂不知所措的状态似乎现在一直在用传统图像处理方法做工业图像分析已经基本确定是水下SLAM+多传感器融合;最后变成了数字孪生,做个SLAM+数据融合
  • 技术方面,懂点视觉slam和传统图像处理,沾点其他传感器slam和深度学习的皮毛;语言方面写的最多的是python,其次是c++,自学了rust但很久没写了,挺喜欢rust的设计的,希望有朝一日能用rust工作。
  • Github项目:
    • Alist-Mikananirss :一个用python写的小脚本,主要是在爬取蜜柑上的RSS源,推送到Alist上进行离线下载到对应网盘,顺便附带了个重命名为emby可识别格式+更新通知的功能,多亏了这个脚本,我的追番欲望大增;功能基本稳定,应该不会有什么大更新了,bug一直在发现一直在小修小补,觉得好用的话能给个star就最好了~同时也附上自己的追番记录,可以当个漫评看

About the blog

Server:

  • RackNerd 2c 2.5g 55g 圣何塞
  • OVH 1c 2g 20g 俄勒冈
  • 最后还是放到了github pages,静态博客的访问速度是真快啊。

Driver: WordPress
Theme: Argon
Driver: Halo
Theme: halo-theme-hao

Driver: Hexo

Theme: redefine

\ No newline at end of file diff --git a/archives/2023/03/index.html b/archives/2023/03/index.html index 9ff5fe3..b7d2978 100644 --- a/archives/2023/03/index.html +++ b/archives/2023/03/index.html @@ -1,2 +1,2 @@ -归档: 2023/3 - TwoSix的小木屋 +归档: 2023/3 - TwoSix的小木屋
\ No newline at end of file diff --git a/archives/2023/04/index.html b/archives/2023/04/index.html index 7550abd..73c2c9f 100644 --- a/archives/2023/04/index.html +++ b/archives/2023/04/index.html @@ -1,2 +1,2 @@ -归档: 2023/4 - TwoSix的小木屋 +归档: 2023/4 - TwoSix的小木屋
\ No newline at end of file diff --git a/archives/2023/05/index.html b/archives/2023/05/index.html index 4794895..b60d317 100644 --- a/archives/2023/05/index.html +++ b/archives/2023/05/index.html @@ -1,2 +1,2 @@ -归档: 2023/5 - TwoSix的小木屋 +归档: 2023/5 - TwoSix的小木屋
\ No newline at end of file diff --git a/archives/2023/index.html b/archives/2023/index.html index 8d83ab6..e0f3856 100644 --- a/archives/2023/index.html +++ b/archives/2023/index.html @@ -1,2 +1,2 @@ -归档: 2023 - TwoSix的小木屋 +归档: 2023 - TwoSix的小木屋
\ No newline at end of file diff --git a/archives/2023/page/2/index.html b/archives/2023/page/2/index.html index 6a9430b..a4cb2bf 100644 --- a/archives/2023/page/2/index.html +++ b/archives/2023/page/2/index.html @@ -1,2 +1,2 @@ -归档: 2023 - TwoSix的小木屋 +归档: 2023 - TwoSix的小木屋
\ No newline at end of file diff --git a/archives/2024/01/index.html b/archives/2024/01/index.html index 971ca99..84eb16e 100644 --- a/archives/2024/01/index.html +++ b/archives/2024/01/index.html @@ -1,2 +1,2 @@ -归档: 2024/1 - TwoSix的小木屋 +归档: 2024/1 - TwoSix的小木屋
\ No newline at end of file diff --git a/archives/2024/06/index.html b/archives/2024/06/index.html index b630894..ca022d1 100644 --- a/archives/2024/06/index.html +++ b/archives/2024/06/index.html @@ -1,2 +1,2 @@ -归档: 2024/6 - TwoSix的小木屋 +归档: 2024/6 - TwoSix的小木屋
\ No newline at end of file diff --git a/archives/2024/index.html b/archives/2024/index.html index 4654add..f665ac8 100644 --- a/archives/2024/index.html +++ b/archives/2024/index.html @@ -1,2 +1,2 @@ -归档: 2024 - TwoSix的小木屋 +归档: 2024 - TwoSix的小木屋
\ No newline at end of file diff --git a/archives/index.html b/archives/index.html index bdce477..f051d5b 100644 --- a/archives/index.html +++ b/archives/index.html @@ -1,2 +1,2 @@ -归档 - TwoSix的小木屋 +归档 - TwoSix的小木屋
\ No newline at end of file diff --git a/archives/page/2/index.html b/archives/page/2/index.html index 76c0e2b..2002cb0 100644 --- a/archives/page/2/index.html +++ b/archives/page/2/index.html @@ -1,2 +1,2 @@ -归档 - TwoSix的小木屋 +归档 - TwoSix的小木屋
\ No newline at end of file diff --git a/bangumi/index.html b/bangumi/index.html index 6279031..51d6d31 100644 --- a/bangumi/index.html +++ b/bangumi/index.html @@ -1,2 +1,2 @@ -追番列表 - TwoSix的小木屋 -

大叔,你怎么还在看二次元噢?

全12话0 类型 动画 想看 784 在看 352 已看 7 评分 8.3

简介:時は西暦1333年、武士による日本統治の礎を築いた鎌倉幕府は、信頼していた幕臣・足利高氏の謀反によって滅亡する。全てを失い、絶望の淵へと叩き落とされた幕府の正統後継者・北条時行は、神を名乗る神官・諏訪頼重の手引きで燃え落ちる鎌倉を脱出するのだった…。逃げ落ちてたどり着いた諏訪の地で、信頼できる仲間と出会い、鎌倉奪還の力を蓄えていく時行。「戦って」「死ぬ」武士の生き様とは反対に「逃げて」「生きる」ことで乗り越えていく。英雄ひしめく乱世で繰り広げられる、時行の天下を取り戻す鬼ごっこの行方は―――。

全12话0 类型 动画 想看 795 在看 674 已看 9 评分 7.3

简介:「真夜中なのにおっはよー!真夜中ぱんチです!」世界でもっとも見られている動画投稿サイト「NewTube」。3人組NewTuber「はりきりシスターズ」の「まさ吉」こと真咲まさきは、とある事件がきっかけでチャンネルをクビになってしまう。起死回生を狙う真咲の前に現れたのは、なぜか彼女に運命を感じたりぶ。超人的な能力を持つりぶと一緒なら、最高の動画が撮れるはず。目指せ、チャンネル登録100万人!

全12话0 类型 动画 想看 1450 在看 567 已看 4 评分 8.5

简介:「応援したいって気持ち…どこから湧いてくるんだろう?」 趣味も特技も性格もバラバラな6人の女子高生。それぞれの悩みを抱えつつも、走ったり、叫んだり、ぶつかったり、妄想したり…。応援したいって純粋な気持ちが揃う時、彼女たちの応援はかかわった人々の心に伝わっていく。群馬の女子高生6人の応援が、世界をちょっとだけ変える…のかも?

全12话0 类型 动画 想看 1887 在看 384 已看 12 评分 8.4

简介:たがいに助け合い、完全な小市民を目指そう。かつて“知恵働き”と称する推理活動により苦い経験をした小鳩くんは、清く慎ましい小市民を目指そうと決意していた。同じ志を立てた同級生の小佐内さんとたがいに助け合う“互恵(ごけい)関係”を密かに結び、小市民としての高校デビューを飾り平穏な日々を送るつもりでいたのだ。ところがふたりの学園生活に、なぜか不可解な事件や災難が次々と舞い込んでくる。はたして小鳩くんと小佐内さんは、小市民としての穏やかな日々を手に入れることができるのだろうか。

全12话0 类型 动画 想看 1580 在看 368 已看 8 评分 7.8

简介:想い人の恋人の座を勝ち取れなかった女の子——「負けヒロイン」。食いしん坊な幼なじみ系ヒロイン・八奈見杏菜。元気いっぱいのスポーツ系ヒロイン・焼塩檸檬。人見知りの小動物系ヒロイン・小鞠知花。ちょっと残念な負けヒロイン——マケインたちに絡まれる、新感覚・はちゃめちゃ敗走系青春ストーリーがここに幕を開ける!負けて輝け、マケインたち!

全12话0 类型 动画 想看 530 在看 166 已看 3 评分 10

简介:悪の組織――あらゆるものを侵略し、あらゆるものを滅ぼす。残忍にして狡猾なその組織のブレーンには、王の片腕たる悪の参謀がいた。地上侵略の危機に立ち上がる、薄幸の魔法少女・白夜。彼女と対峙した悪の参謀・ミラは、なんと一目ボレしてしまい……。魔法少女と悪が敵対していたのは、かつての話。殺し愛(あ)わない、ふたりの行く末は――?

全12话0 类型 动画 想看 1952 在看 452 已看 11 评分 8.8

简介:都立日野南高校に通う女子高生、虎視こし虎子とらこ。ある日の登校中、彼女は顔に冷たいものが当たるのを感じた。ふと上を見ると、そこには鼻水をたらし、ツノが電線に引っかかって身動きが取れなくなっている女の子が――!?うっかり変な『ツノ』の生えた少女・鹿乃子しかのこのこを助けたことで、優等生(の皮を被った)虎視虎子の人生がかき乱されていく……一人の少女(元ヤン)が一人のシカ(?)に出会うガール・ミーツ・シカ物語開幕!!

全12话0 类型 动画 想看 1365 在看 2596 已看 62 评分 6.9

简介:久世政近的邻座坐着一位名叫艾莉莎的女孩,她总是对他投以冰冷的目光。然而,她偶尔会小声地用俄语对他撒娇……政近当然不会错过这些话。因为他竟然拥有着母语级别的俄语听力!艾莉莎误以为政近听不懂俄语,所以才偶尔在他面前流露出真实的自己。而政近虽然明白她话中的意思,却装作毫不知情的样子。两人之间弥漫着掩饰不住的甜蜜气氛,他们的恋情将会何去何从——!?

全12话0 类型 动画 想看 2701 在看 485 已看 40 评分 7.3

简介:「この芸能界(せかい)において嘘は武器だ」地方都市で働く産婦人科医・ゴロー。ある日"推し"のアイドル「B小町」のアイが彼の前に現れた。彼女はある禁断の秘密を抱えており…。そんな二人の"最悪"の出会いから、運命が動き出していく―。

全12话0 类型 动画 想看 434 在看 1016 已看 974 评分 5.9

简介:人人所畏惧的邪恶魔术师萨冈,是个不擅言语的人,今天也一边研究魔术,一边打击领地内的恶贼。萨冈受到损友巴尔巴洛士的邀请,来到地下拍卖会会场。在那里与一位被当作魔王遗物来拍卖的白发精灵少女涅菲,命运般地相遇。萨冈花费所有财产,将涅菲带回自己的城堡里。至今从未与人相处过,且内向寡言的萨冈,不知如何对待涅菲,只是过着没什么对话又狼狈的生活。究竟两人的共同生活走向如何呢?不擅言语的魔术师与美少女精灵的欢乐喜剧就此展开。

全25话0 类型 动画 想看 1813 在看 4681 已看 329 评分 7.4

简介:若き行商人クラフト・ロレンスは、荷馬車を引く一頭の馬を相棒に、街から街へと商品を売り歩く日々を送っていた。ある日、黄金色の麦畑が広がる小さな村を訪れた彼は、耳と尻尾を有する美しい少女と出会う。「わっちの名前はホロ」自身を“賢狼”と呼ぶホロは、豊穣を司る狼の化身だった――。彼女の「遠く北にあるはずの故郷・ヨイツの森へ帰りたい」という望みを聞き、ロレンスとホロは北を目指す商売の旅の道連れとなる。だが行商人の旅には思いがけない波乱がつきもので……。孤独だった行商人と、孤独だった狼の化身を乗せた馬車が、今、騒がしく走り始める。

全24话0 类型 动画 想看 2030 在看 7054 已看 6444 评分 7.8

简介:迷宫饭,不是吃就是被吃…妹妹在迷宫深处被赤龙吃掉了!冒险者莱欧斯侥幸逃过一命回到了地面。他想要再度挑战迷宫,但是钱和食物都留在了迷宫深处…在妹妹随时可能会被消化掉情况下,莱欧斯下定了决心:「食物要在迷宫内就地取材」史莱姆、鸡尾蛇、宝箱怪、然后是龙!冒险者啊,一边吃掉袭来的魔物,一边通关迷宫吧!

全12话0 类型 动画 想看 475 在看 1436 已看 711 评分 5.7

简介:“夕阳和~”“安美的!”“高中生广播~!”“本节目是由碰巧就读于同一所高中、同一个班级的我们,为大家带来教室氛围的节目!”无论是作为配音员还是作为同班同学都关系极好的二人,放松地做配音员广播,但这仅仅是“台前”的模样。夕阳的本性是阴沉的土妹子,而安美则是地道的辣妹。容貌和性格都正好相反的二人,一开口就要吵架。“……怎么这么晃眼。你这样跟平时形象的差别真的大到吓人,平时的阴沉都到哪去了?”“你才是,别用这副模样发出可爱的声音啊。”录音结束后便就地变成修罗场,挖苦和谩骂势如风暴。和这种人一起做广播,我绝对办不到!但是,节目可不会等人,前途坎坷的配音员广播,会持续多久……靠职业精神欺骗全世界,暴露就要结束的青春配音员节目,现已播出。

我的评分:
我的评价:虽然前期作画崩,中期有一段尬的飞起,不过对于基本是蹦着水濑祈乐子来的我,在了解这番其实也就是主打一个百合营业之后其实也对剧情没什么期待,一边看美少女贴贴一边看作者玩点声优业界梗,这追番历程倒也算个轻松加愉快
全12话0 类型 动画 想看 775 在看 2217 已看 2403 评分 7

简介:贫苦的侦探镝矢惣助,遇到了操纵魔法的异世界皇女莎拉。惣助逐渐开始了与莎拉的同居生活,莎拉很快就习惯了现代日本。另一方面,跟着莎拉转移来到日本的女骑士莉薇亚,在流浪的同时,意外地度过了快乐生活。积极坚强生活的两个异世界人,不仅影响了惣助,也影响了鬼畜律师、分手工作员、宗教人士与住在这区的怪人们。

我的评分:
我的评价:很不错的养女儿番,四月休闲娱乐担当,事实证明把一个小女孩写可爱完全不需要写熊。就是女骑士那条线总要出点擦边画面,在宿舍用大屏看怪羞耻的。
全13话0 类型 动画 想看 1833 在看 12650 已看 1144 评分 8.2

简介:无论是愤怒、喜悦还是悲伤,全都倾注其中。高中二年级,从学校退学,只身在东京以大学为目标的主人公。被同伴背叛,不知如何是好的少女。被父母抛弃,一个人在大城市里打工维生的女孩子。虽然这个世界总是背叛我们。虽然什么都不能称心如意。但是,因为我们想要喜欢某个东西。因为我们相信自己的容身之处就在某个地方。所以,我们歌唱。

我的评分:
我的评价:10分只给原创,实际其实在8分左右吧。但GBC确实在四月给了我无与伦比的追番体验,这是一场名为花田的魔法。
全12话0 类型 动画 想看 110 在看 419 已看 249 评分 5.5

简介:母親の入院費を稼ぐため貧乏生活を送る、スクールカースト最底辺の高校生・志村光太。学校では不良のハマケンからゴミのように扱われ、負け組人生に日々絶望していた。そんなある日、クラスメイトのカネゴンと殴り合う様子が誤って全世界へ生配信!?底辺同士のイタすぎる喧嘩動画は瞬く間に広がり、一晩でまさかの1000万再生を突破!!!再生数が金になることを知った光太が噛みつく相手は不良に、ヤンキーに、プロ格闘家とエスカレートしていき…プロのいじめられっ子、反撃開始───ッ!!!

我的评分:
我的评价:这番给人的感觉就是很一般,包括结尾也很平淡。爽没怎么爽起来,但你要说难看倒也不难看,一集一集的过来我倒也没怎么反感的就看完了。不上不下的,这可能也是他人气冷的原因之一吧。
全13话0 类型 动画 想看 1290 在看 5023 已看 2557 评分 6.8

简介:郊外的某个小镇。这里不是一个随处可见的普通乡村……。这里的居民产生了巨大的异变。即使在这样的情况下,千仓静留也有着坚定的想法。想再一次见到行踪不明的朋友们!静留她们在被废弃且停止行驶的电车里,在不知道能不能活着回来的情况下前往外面的世界。开始运行的末日列车的终点,到底有什么?

我的评分:
我的评价:确实是很电波的一个番,纠结了很久他究竟属于还行,还是一般。但就这个浪漫的结尾+中期不乏有趣的脑洞,倒感觉也称得上一部过得去的现代童话。
全12话0 类型 动画 想看 1596 在看 5825 已看 4154 评分 5.8

简介:「我」也想像某个人一样闪耀光辉!明天要聊的话题,这周要买的衣服,这些手机(流行趋势)都会告诉我们答案。想成为不一样的存在──然而这忙碌的世界却连让我们怀有这种愿望的时间都没有。活动停止中的插画家「海月夜」、想靠歌声重新争口气的前任偶像「橘乃乃香」、自称最强Vtuber的「龙崎诺克斯」、想支持喜欢的偶像的神秘作曲家「木村酱」。这些与世界有些格格不入的少女们组成了匿名艺术家团体「JELEE」。如果成为不是自己的「我们」──也许就能够闪耀光辉。没错,这是一部以涩谷为舞台的青春群像剧,描绘着夜晚流连在涩谷的我们所展开的物语!

我的评分:
我的评价:很可惜的一部动画,动工制作拉满,可惜59的剧本是依托答辩,当一部动画的核心是大份的时候,再华丽的外表也失去的光芒
全12话0 类型 动画 想看 156 在看 248 已看 238 评分 4

简介:考虑环境问题,以安心和安全为目标的世界,至少在表面上达到了目的。就在这时,次世代赛车“NEX Race”突然发表。最高时速超过500km/h。以最新技术为安全性担保,世界舞台上的赛车场面发生了翻天覆地的变化。狂热与兴奋。挑战者的眼神,震撼着观众的心。在这里,一名少女迎来了出道战。轮堂凛。当世界得知其名,赛车将再次迎来一个崭新的时代。

我的评分:
我的评价:前面是毫无疑问的大份,剧情真的非常蠢,但后面回归一般水准。单纯看看美少女赛车服就行,对美少女赛车服不感兴趣的话看这个纯属浪费时间
全13话0 类型 动画 想看 323 在看 304 已看 474 评分 5.6

简介:故事讲述忍者 Kamui为了逃避自己的血腥过往,脱离忍者组织,隐姓埋名地生活着。然而由于他背叛了忍者古老的信条,一天夜晚,他的家人被前组织的暗杀者残忍杀害。为了给亲人复仇,Kamui重新化身为从前的自己。然而,二十一世纪的忍者是一个时代错误,因为他们要以残酷而古老的技巧对抗高科技武器。Kamui要面对训练有素的刺客、战斗机器人以及其它强大的忍者,来推翻他的前组织。

我的评分:
我的评价:老老实实走忍杀路线多好,非要搞个高达战甲,穿上战甲之后的打戏真的是烂完了,慢慢悠悠的毫无感觉。结局也莫名奇妙的,唉,浪费时间。
全14话0 类型 动画 想看 2121 在看 2528 已看 19301 评分 8.3

简介:「能一辈子和我搞乐队吗?」在高一的春季即将结束之时,羽丘女子学园里人人都在参加乐队活动。入学较晚的爱音,也为了尽快融入班级,急忙寻找乐队成员。在寻找时,她发现被称为「羽丘的不可思议少女」的灯还没有参加乐队,于是爱音不由得向她发出邀请…我们的<音乐(呐喊)>,伤痕累累而充满狼狈。迷茫也无所谓,迷茫也要前进。

全16话0 类型 动画 想看 2242 在看 3778 已看 31013 评分 8.4

简介:作为网络吉他手“吉他英雄”而广受好评的后藤一里,在现实中却是个什么都不会的沟通障碍者。一里有着组建乐队的梦想,但因为不敢向人主动搭话而一直没有成功,直到一天在公园中被伊地知虹夏发现并邀请进入缺少吉他手的“结束乐队”。可是,完全没有和他人合作经历的一里,在人前完全发挥不出原本的实力。为了努力克服沟通障碍,一里与“结束乐队”的成员们一同开始努力……

全13话0 类型 动画 想看 267 在看 278 已看 3156 评分 5.6

简介:たったひとりの身内である祖父を亡くした相馬一也は、ある日、突然、異世界に勇者として召喚されてしまう。召喚された先は、まるで中世ヨーロッパのようなエルフリーデン王国であった。勇者どころか、ごくふつうの青年のソーマだが、持ち前の合理的精神と現代知識から、次々と新しい政策を打ち出し、傾きかけていた王国の財政政治体制を立て直していく。ソーマと共に歩むのは、エルフリーデン王国の王女リーシア、王国一の武を誇るダークエルフのアイーシャ、怜悧な頭脳を持つハクヤ、大食いのポンチョ、歌姫のジュナ、動物と意思疎通できる少女トモエなど、多才で個性的な仲間たち。現代知識で窮地の王国を再生する異世界内政 ファンタジー、『現実主義勇者の王国再建記』。いよいよ開幕!

全13话0 类型 动画 想看 2309 在看 538 已看 8868 评分 7.5

简介:我们为什么要这么做……?哈尔希洛回过神来,才发现自己身处在黑暗当中,他完全不知道自己人在何处,也不明白这个地方是哪里。他的身边有一群和他一样失去记忆,只记得自己名字的男女;而离开了地底后,等待着众人的是一个「宛如游戏」的世界。为求生存,哈尔希洛与自己有着相同境遇的伙伴们组成队伍、学习技能,以义勇兵见习者的身份踏入了这个世界--「格林姆迦尔」。没有人知道未来会遇见什么……这一切,就是从灰烬之中所诞生的冒险谭。

全14话0 类型 动画 想看 632 在看 536 已看 6824 评分 4.1

简介:在她成为神的那天,世界开始走向终结——高中最后的暑假,在成神阳太的眼前,突然有一天,自称“知晓一切的神”的少女雏出现了。“30天后,这个世界就会结束。”阳太对告知这样的消息感到困惑,但是看到神一样的预知能力,确信那个力量是真的。与超常的力量相反,天真烂漫、天真无邪的小雏,不知为何决定寄居在阳太的家中,两人共同生活。向着“世界末日”,喧闹的一个夏天开始了。

全12话0 类型 动画 想看 636 在看 302 已看 6168 评分 6.2

简介:厌恶魔术的魔术讲师所展开的叛逆英雄幻想剧!魔术与科学共同发展的世界——卢瓦佛斯。位于魔导大国·阿尔扎诺帝国南部的“阿尔扎诺帝国魔法学院”,是学习世界最先端魔术的最高学舍。拥有约四百年历史的这所学院,是所有有志于魔术之道人们的憧憬,学院的讲师和学生们也因自己的身份而自豪。突然来到这所正统学院赴任的非常勤讲师,格伦·勒达斯。“我来教给你们真正的魔术吧”被称作“不正经”的这个男人的破天荒授课,就此开始。

我的评分:
我的评价:前面半部分不错,妹子可爱爆了。后面实在蠢的有点看不下去。
全15话0 类型 动画 想看 186 在看 83 已看 1763 评分 6.4

简介:无知少女穿越异世界,遭遇裸男围攻。是屈服?是挣扎?是反抗?凸变英雄LEAF之脱变英雄,看春光乍泄,照亮世界!

全4话0 类型 动画 想看 1317 在看 702 已看 6552 评分 7.5

简介:クラシック三冠レース『皐月賞』『日本ダービー』『菊花賞』一生に一度だけ挑戦できるそのレースで勝つことは、すべてのウマ娘、すべてのトレーナーにとっての夢である。『ナリタトップロード』『アドマイヤベガ』『テイエムオペラオー』三強と呼ばれたウマ娘たちが、絶対に負けられない想いを胸に頂点を目指し競い合う、熱い熱いクラシック戦線。――ウマ娘の新シリーズ配信アニメプロジェクト、いよいよ始動です。

全14话0 类型 动画 想看 1737 在看 1042 已看 8771 评分 7.1

简介:WIT STUDIO×长月达平×梅原英司Nialand,将梦想、希望和科学融为一体的,人工智能综合主题乐园。作为历史上第一个自律学习型 AI,Vivy,该设施的 AI 歌姬,每天站在舞台上唱歌。但是,它目前的能力还不足以得到人气。——其使命是「用歌声给大家带来幸福」为了这个使命,vivy一直在园区的主舞台歌唱,探究什么是用心的唱歌。某日,名为松本的AI出现在Vivy身边。松本自称“来自100年后的未来”,其使命是「与Vivy一起修正历史,并防止将在100年后发生的AI与人类的战争」。执行着不同任务的2名AI,会把世界导向怎样的未来?这是<我(Vivy)>将<我(AI)>毁灭的故事――AI『歌姬』Vivy的百年旅程即将开始。“ニーアランド”、それは夢と希望と科学が混在したAI複合テーマパーク。史上初の自律人型AIとして生み出され、施設のAIキャストとして活動するヴィヴィは日々、歌うためにステージに立ち続ける。しかし、その人気は今ひとつだった。――「歌でみんなを幸せにすること」。自らに与えられたその使命を果たすため、いつか心を込めた歌を歌い、園内にあるメインステージに立つことを目標に歌い続けるヴィヴィ。ある日、そんなヴィヴィの元に、マツモトと名乗るAIが現れる。マツモトは自らを100年後の未来からきたAIと話し、その使命は「ヴィヴィと共に歴史を修正し、100年後に起こるAIと人間との戦争を止めること」だと明かす。果たして、異なる使命を持つ2体のAIの出会いは、どんな未来を描き直すのか。これは<私>が<私>を滅ぼす物語――AIの『歌姫』ヴィヴィの、百年の旅が始まる。WIT STUDIO×長月達平×梅原英司エンターテイメントの名手たちが、引き寄せあった絆で紡ぐSFヒューマンドラマ、ここに開演。

全12话0 类型 动画 想看 1380 在看 565 已看 13619 评分 7.7

简介:北原伊织升入大学后,开始寄住在伊豆的叔父所经营的潜水用具店“Grand Blue”。传入耳中的波涛声、炽烈照射的太阳、开始一起生活的可爱表姐妹……青春的大学生活!这样的伊织所面临的却是——除了野球拳以外的猜拳一概不知的强壮男人们!!刚一入学就被大学潜水社团“Peek a Boo”所盯上了的伊织,在会长时·田信治和三年级学生·寿龙次郎的怂恿下,不知不觉已经身为社团的一员加入了他们的愚蠢喧嚣中。在大学中认识的虽然长得帅但性格却很遗憾的宅男——今村耕平也加入其中,他们的青春向着越来越奇怪的方向转进。明明在寄住的地方有着可爱的表姐妹——古手川姐妹,但自己却被妹妹千纱当成垃圾看待,而姐姐奈奈华又是重度妹控,根本无法成为对象。在可爱的全裸笨蛋们的包围之下,伊织的大学生活究竟会变得如何……。

我的评分:
我的评价:自男高之后觉得最好笑的一部搞笑番,只要有人要我推一部喜剧番,我都会毫无疑问的摆出这一部
全24话0 类型 动画 想看 1277 在看 2454 已看 3217 评分 6.6

简介:「不运要来了。不错哦,太棒了!!」风子是会使触碰到她的人遭遇不幸事故的「不运」少女。拥有绝对不会死的不死之身的「不死」的安迪,出现在因为自己的特异体质而一度打算寻短的风子面前。他为了借由风子的力量获得「真正的死」,决定与她一起行动。然而,锁定像安迪和风子这种拥有异能之力的「否定者」下手的神秘组织「UNION」,却出现在他们的面前,试图将他们带回组织。安迪为了完成心愿,带着风子展开逃亡,他们的命运将会如何?那个组织又是什么来头?这是,两人找寻着最棒的死亡的故事。

我的评分:
我的评价:第一集观感巨好,女主有种让人迫不及待和她颠鸾倒凤的美;可惜后期可能是工期比较赶,才10几集就步了海贼王几百集的后尘。其实中间的回忆穿插我还能洗成剧情需要,但后面有一集大半集都在回忆,甚至几分钟前就播过的东西也得回忆,我还是有点受不了的,间接导致末尾其实看的也没那么入戏了
全13话0 类型 动画 想看 1587 在看 3606 已看 7446 评分 7.1

简介:柊舞缇娜是非常喜欢魔法少女的普通女孩。有一天,一个奇怪的吉祥物给了她变身的能力,于是她学会了神奇的魔法,如此一来她就能成为魔法少女了。“咦,这到底是什么装扮?”“邪恶组织的女干部又是什么意思!?接下来我会变得怎么样啊!!?”心地善良且腼腆的魔法少女粉变成虐待狂,正义与邪恶的激烈SM Play就此开始。

我的评分:
我的评价:还不错的卖肉番,主打一手玩法丰富。漏两点,不能在人前看,对于我这种在公众场合一边干别的一边看番的人来说不太友好。
全13话0 类型 动画 想看 248 在看 357 已看 1212 评分 5.5

简介:―逃げぬ心と見つけたり― 灯荒仁は、かつての親友・浅観音真宝との再会をきっかけに強者たちの戦いに巻き込まれていく…。そんな中、巨大な魔人の影が現れ…?!

我的评分:
我的评价:人物塑造的太差了,后面转折感觉闹麻了。男二的黑化没道理的,魔人一对的矛盾也感觉很牵强,因为你把我当偶像,不敢超越我,所以我宁愿把你打死也要激发你的潜力,让自己死在你手上?结果俩人胜负没决出来给人暗枪毙了???真的很奇怪。此外,大伙都调侃皆大欢喜的结局是包饺子,结果最后还真让你包上饺子了,看到饺子的时候我真笑喷了。
全34话0 类型 动画 想看 3513 在看 11268 已看 18129 评分 8.6

简介:魔法使芙莉莲和勇者辛美尔等人一起,历经十年的冒险之后击败了魔王,为世界带来了和平。身为能活上一千多年的精灵族,她于是和辛美尔等人约定再次相见后,踏上了独自一人的旅途。自那以后,50年过去,芙莉莲又前往拜访辛美尔,相比于和50年前一点没变的她来说,辛美尔已经年迈体衰,进入了风烛残年。之后,她目睹了辛美尔迎来他的死亡,痛感自己以往没能“知晓人心",陷入后悔的芙莉莲,为了“知晓人心",踏上了旅途。这趟旅途会邂逅各种各样的人,也有各种各样的事情在等待着她。

我的评分:
我的评价:好。没什么好评价的,就是好看。他没有那种跌宕起伏的情节,没有紧张刺激的吸引你不断的去看下一集的感觉,他就是那种可以让你看的非常舒服,一天的所有不愉快都能在这24分钟中被治愈的感觉。属于是有事没事,点开来看一集都能获得绝佳体验的类型。真的很佩服芙莉莲的原作者,究竟是生活在什么样的幸福中才能写出这么美好的对白。
全24话0 类型 动画 想看 2102 在看 3025 已看 6146 评分 7.4

简介:大陆的中央有个大国,在那个国家的后宫之中有一位少女,其名为“猫猫”。她以前在花街做药师,现在在后宫做事。某天,她得知皇子们皆早夭,而现在的两个皇子都因为生病而逐渐衰弱。于是,在好奇心的驱使下,她开始调查事情的真相,就好像在说世上不可能存在诅咒。壬氏是一位外表俊美的宦官,他让猫猫当皇帝宠妃的试毒人。猫猫对人没有兴趣,却异常钟情毒和药,就是这样一位在花街长大的药师,被卷入了宫中的传闻事件。美丽的玫瑰长有刺,女人的庭院里到处都是毒,流言和阴谋更是不缺。虽然不断地被壬氏委托麻烦事,但猫猫还是照常处理。罕见的嗜毒姑娘今天也在后宫里奔走。

我的评分:
我的评价:虽说大伙都觉得这部番看的是名侦探猫猫,但我觉得这部番的侦探部分只是一个非常微不足道的辅料,重点还是案件背后所牵扯的各种亲情伦理爱恨情仇,属于是情绪流很到位,但禁不住细想的类型,总体来说我觉得还是不错的一部番
全26话0 类型 动画 想看 1011 在看 2755 已看 4096 评分 6.4

简介:你是为了什么而玩游戏的?如果世上有100部神作,那么也存在着1000部粪作。这是喜爱粪作,同时也被粪作所爱的男人“阳务乐郎”,向粪作的对立面——神作「香格里拉边境」发起挑战的故事。原作网络小说的总阅览数超过5亿,并少见的跳过书籍出版直接在「周刊少年MAGAZINE」上改编成漫画开始连载。在这本拥有60年以上历史的漫画杂志上,赢得了读者问卷评比有史以来第一次的四冠王!本作品通过每个人都能拥有的游戏体验来描绘出一种新的幻想世界,为沉浸在过去记忆中的大人和生活在科技最前沿的年轻人都带来了新的冒险与刺激。一名“粪作猎人”向神作发起挑战,至高无上的"游戏x幻想 "的冒险故事,开幕!!

我的评分:
我的评价:这部番主要被吐槽的地方是小剧场+开头旁白挤占正片时间,所以有水分,但个人真没什么感觉,可能是国漫看多了?就正片质量而言我觉得是过得去的,起码看着很舒服,在已完结补番的前提下前面的问题也压根不是问题,我觉得是值得去补的一部番,但这一季确实断的莫名奇妙,看到最后我都以为有下集,毫无完结的紧张感(仗着有下一季随手一断的感觉),也没有特别惊艳的地方。
全13话0 类型 动画 想看 720 在看 1164 已看 4556 评分 6.4

简介:连接异次元和现世界的通道“门”突然出现十来年后,世界上出现觉醒了超能力被称为“猎人”之人。猎人使用力量以攻占门内地下城并获得报酬为生,但在强者齐聚的猎人们中,“水篠旬”作为被称为人类最弱兵器的低等猎人而艰难生活。某日,水篠旬遭遇某个被隐藏在低等级迷宫中的高等级重迷宫,在身负濒死重伤的水篠旬面前出现了神秘的探索窗口。他在临死之际,决定接受任务之际,成为唯一能“升级”之人。

我的评分:
我的评价:前期节奏极其拖沓,导演断章鬼才。但后面爽起来之后还是可以打个及格分吧
全12话0 类型 动画 想看 1036 在看 1214 已看 2968 评分 6

简介:日本各地突然出现了神秘的“门”。门的另一面连接着异空间“魔都”,里面有着女性吃了就能获得超能力的“桃”。为了打败从“魔都”中现身的名为“丑鬼”的怪物,人类组建了由女性军人组成的战斗集团“魔防队”。希望能出人头地的男高中生和仓优希,有一天不慎走进魔都并遭到丑鬼的袭击。而魔防队七番组的美女组长羽前京香帮助他打倒了丑鬼。通过京香的奴隶化能力,优希激发了潜藏的力量,成为了一名魔防队奴隶兼宿舍管理员,开始了与丑鬼战斗的生活。“被饲养的少年”的战斗幻想剧,开幕!

我的评分:
我的评价:有种战斗又做不好,福利又越来越少的不上不下的尴尬感。但单纯看人设看后宫的话,算个勉勉强强吧。
全12话0 类型 动画 想看 275 在看 476 已看 1518 评分 6.1

简介:公爵千金莉榭·伊尔姆加德·韦尔特纳有一个秘密。那就是“她在20岁洗掉后就会回到5年前废除婚约的那刻”。虽然她前6世中拥有过商人、药师、侍女、骑士等身份过得很充实,不过她来到第7次重来的人生后想要走到底玩玩。但是,心怀决意想要冲出城堡之时,遇到了以残暴闻名的加尔克汉国的皇太子阿诺德·海因。他是在莉榭骑士生涯中杀掉她的罪魁祸首,然而如今,莫名其妙的看对眼了……“请你成为我的妻子吧!”为了生存,为了阻止战争,运用前6世得到的经验,成为敌国新娘的莉榭第7次人生开始了。

我的评分:
我的评价:前面几集靠着人设还挺吸引人的,传统的霸道总裁爱上我+女主扮猪吃老虎的剧情也够爽;但后面真的有点油腻有点无聊,男主几乎没什么爆点戏份,剧情都是那几个油腻的男配角,还挺无聊的。而且这种时间线偏古的作品,作者好像都喜欢用现代的知识去降维打击古人,以体现女主的智慧,但放观众眼里真的没感觉一点智慧,真的很蠢。感觉真不如前几集那样继续从人格魅力上塑造女主,而不是一直用所谓的“知识”去体现“智慧”,属于是有第二季也绝对不看的类型
全14话0 类型 动画 想看 357 在看 653 已看 2873 评分 5.5

简介:全ての敵を即死させる!最強主人公爆誕!!修学旅行中、高遠夜霧とクラスメイトたちは、突然異世界に召喚された。 召喚したのは賢者を名乗る女、シオン。彼女は《ギフト》と呼ばれる特殊能力を彼らに与え、 賢者になるための試練をクリアしろと一方的に宣告する。突然の事態に動揺しつつも行動を開始する一同。だが、なぜか《ギフト》を与えられなかった夜霧や壇ノ浦知千佳ら一部の生徒は、 ドラゴンが迫る草原に囮として置き去りにされてしまう。飛来したドラゴンによって次々に殺されていく残された生徒たち。 夜霧と知千佳も絶対絶命の大ピンチ!……かに思われたが……。「死ね」夜霧はその一言でドラゴンを撃退。 なんと彼は、任意の対象を即座に殺せる《即死チート》の持ち主だった!!!元いた世界への帰還を目指し、旅を始める夜霧と知千佳。 そんな彼らの前に現れるのは異世界にいるチート能力者たち。不死身の吸血鬼? やり直しの勇者? 神に溺愛されし転生者? そんなやつら、即死チートの相手にはまるでならないんですが!前代未聞のチート能力が武器のダウナー系少年と ちょっぴり残念な美少女の異世界冒険が今、幕を開ける!!

我的评分:
我的评价:动画制作水平其实很一般,可以说毫无演出,但倒是没什么压力的看完了。一开始觉得主角的秒杀让整个剧本都变得很无聊,加上动画对秒杀这一行为本身又没有加什么有趣的演出,前期看的其实挺折磨的,全靠女主可爱+捧哏坚持下来。但后续随着各路配角慢慢登场之后,发现这番的看点根本不在主角的秒杀装逼,而是配角们流水线似的开始介绍他们的逆天人设,全是异世界轻小说里的主角模板,而且每集还能登场好几个,每次都会让我感慨作者还有多少脑洞可以用,好像取之不竭似的。而主角在这时候的作用其实更像是一个“句号”,告诉这个配角,好了,你的戏份结束了,该下一个开始自我介绍了。外加这番的画风真的可以,妹子都很可爱,自然而然的就看下来了,属于是无脑作中偏上的水平吧,没什么营养,也没什么反转,看起来很轻松。
全12话0 类型 动画 想看 536 在看 911 已看 2612 评分 6.6

简介:人形装甲兵器“Titano Stride,通称TS”发达的时代。各国军队在“夏威夷瓦胡岛”集结。 陆上自卫队所属的伊萨米·阿欧与美国海军陆战队所属的路易斯·史密斯两人在战斗中相遇。然而士兵们突然受到不明所属飞机的强袭,束手无策地散开。自豪地战斗吧。 为了在与死亡为邻的战场上生存下去。 为了拯救伙伴。相信生命,燃烧“勇气”。

我的评分:
我的评价:无法理解,无法评价。我无法把他定义为烂片,我也不可能把他定义为好片,但我认为这是值得一看的片。他是另一个维度的意识形态,看完的人脑子应该不会太正常。
全12话0 类型 动画 想看 328 在看 434 已看 1329 评分 6.1

简介:令世间万物为之恐惧的魔王被击败,在这个时代,人们再也不必担心灭亡。在能够对未来抱有幻想的今天、少女被卷入了蛮横的暴力纠纷中。美丽的景色已荡然无存,城市和花草树木都陷入火海。耳边伴随着活生生被解体的挚友悲痛的临终哀嚎,少女逃出生天。最令人绝望的事情,莫过于至亲之人逝去之时,自己仅能袖手旁观。然而,少女面前出现了一位剑士。他仅用一击便轻松斩断了这份绝望以及少女无法抗衡的那份蛮横。“本大爷叫柳生宗次郎,是这地球上最后的柳生后裔。”在这世界上有这样一群人,他们站在各个力量的顶点之上,并冠以“最强”之名。剑士也是这世上数不尽的“修罗”之一。在魔王不复存在的这个世界,仍在追求战斗的“修罗”不计其数,而剑士只不过是其中之一。——这样啊,我无法原谅他,无法原谅所谓的强者啊。尽管多次在他的帮助下保住性命,但少女的心中却涌现出了错误的情感。然而对于失去了一切的少女来说,她需要一个足以支撑自身前行的理由。——杀了他。在这世上,有能够葬送他的强者。凭借对不合理的“强者”们的仇恨,少女与剑士一同踏上旅途。消灭“最强者”的旅途就此展开。

我的评分:
我的评价:总体来说还是一部合格的斗蛐蛐片,就是前期人物介绍比较慢热,这种片感觉还是要讨论氛围比较浓厚点会更好看,当你和人争论哪个设定看起来很imba的角色和什么角色之间有什么克制关系进而猜中谁赢的过程还是其乐无穷
全13话0 类型 动画 想看 1108 在看 1467 已看 3865 评分 5.7

简介:一天到晚窝在家的少女「黛拉可玛莉」一觉醒来,居然被大力提拔成为七红天帝国将军!?而且可玛莉率领的,还是一支以下犯上风气盛行、嗜血又暴力的部队!?可玛莉出生在吸血鬼名门世家,却因无法喝血而自身背负着「三不」的状态——「运动神经不行」、「身高不够高」、「不能用魔法」。正当她走投无路时,她(应该要成为)心腹的女仆「薇儿海丝」在这时建言:「请交给我吧。我一定会让那些部下产生误会的!」靠着虚张声势、幸运和周围的误解相互叠加,使屡战屡胜的可玛莉被认定为第七部队的杀戮之主,一部欢乐的幻想搞笑故事就此诞生!

我的评分:
我的评价:P9典中典之静态美人;前期其实还行,画的好看也可爱,后面原形毕露,构式的剧情+崩裂的作画+滑稽的动作,唉,P9
全12话0 类型 动画 想看 432 在看 529 已看 2287 评分 6.1

简介:头,还在,我变小了。在发生革命后数年的堤亚穆帝国,被蔑称为任性皇女的米雅被革命军推上了断头台处决……本应该被处死,醒来以后却发现自己变回了12岁的样子!看来这里是重新来过的世界――她的枕边放着的是处刑前自己所写的染血的日记。走上了第二人生的米雅,决定复兴帝国。是为了拯救百姓于饥饿之中?还是为了那些在内战中牺牲的士兵?都不是,自身也是为了躲避被送上断头台的命运!!这,这种事情很简单的!任性公主的行动居然引发了奇迹,改变历史的奇幻故事即将开始——

我的评分:
我的评价:就看傻子得乐子的角度而言,前期还是不错的,就是反复玩来玩去都是一个梗,后面就显得有点无聊有点尬了。
全12话0 类型 动画 想看 527 在看 738 已看 2481 评分 5.2

简介:最强的魔王雷欧尼斯为了将来的决战而将自己的灵魂封印了。然而他在1000年后再度醒来时,不知为何却变成了10岁少年的模样!?被隶属于〈圣剑学院〉的美少女黎榭莉亚所保护的雷欧尼斯,在对这个变得截然不同的世界一头雾水的情况下,入学了〈圣剑学院〉。未知的敌人〈Void〉、〈第07战术都市〉、外观观是武器形态却拥有异能之力的〈圣剑〉。在魔法被遗忘的这个未来世界,最强魔王与美少女们交织而成的圣剑与魔剑的学园幻想物语盛大开幕!

我的评分:
我的评价:要剧情没剧情,要战斗没战斗,配上贫穷的演出总能在严肃的时候让人忍俊不禁。但好在制作组们很懂自己的唯一长处,美少女都画的挺好的,该给的特写也给上了。
全13话0 类型 动画 想看 1079 在看 1097 已看 3077 评分 5.2

简介:这里是从异世界传承下光辉的姓名与竞跑能力的「马娘」,自久以来与人类共存着的世界的故事。凝望着同一片风景,「北部玄驹」开始追寻着仰慕的背影。背负着一族的期待,「里见光钻」开始挑战G1的悲愿。从来都是二位一体的两人,迈向新的梦想......。

我的评分:
我的评价:很一般,人设转变突然,台词奇怪且别扭,也能看,但确实不好看了。感觉比第一季还要差一点,第一季好歹摆明了纯卖萌,第三季就是故意给北黑造点挫折却翻车了
全14话0 类型 动画 想看 671 在看 602 已看 2672 评分 7.6

简介:突如《見えざる帝国》の襲撃を受けた尸魂界を救うべく、死神代行・黒崎一護は、躊躇うことなく戦いに身を投じる。だが、滅却師の始祖・ユーハバッハの命を受けた星十字騎士団の侵攻により、多くの死神たちが次々と無惨に散っていった。卍解を奪う手段をも持つ星十字騎士団の面々に、護廷十三隊は隊長格すら苦戦し、奮闘する一護もまた、ユーハバッハを前にしては力及ばず、刀を折られてしまう。斬月を治せず失意の一護に、父・一心が語った過去。己を知り、決意と共に再び立ち上がった一護は今、導かれ修練の時へ踏み出す。新たな力をその手に、大切な仲間を、全てを護るために――。一方、一護とは異なる行動をとっていた、滅却師の末裔・石田雨竜。記録を紐解き真実を求めた雨竜が辿り着いた答えとは――。ユーハバッハが指名した「後継者」が《見えざる帝国》に波紋を呼び、戦況は新たな局面へと様相を変えていく。『世界の終わる9日間』瀞霊廷は闇に消え、やがて訣別の刻が訪れる――。

全13话0 类型 动画 想看 2726 在看 2869 已看 12983 评分 7.8

简介:2024年,世界发生了史无前例的灾难,然后15年过去了。成为废墟的日本栖息着一种食人的怪物(蛭子),人们艰难地生存着。在东京·中野经营万事屋的的斩子接受了一个神秘女人的委托,“带这个孩子去天国”。这个被留下的名叫真流的孩子说“天国好像有人和我长得一模一样。”除此之外他什么都不知道。“天国”究竟在哪里?当你到达那里时会发生什么......?真流和斩子寻找天国的旅程现在开始了——!

我的评分:
我的评价:七月神中神!精妙的伏笔设计,用不经意的伏笔回收把本来就算不错的支线剧情推上另一个高度,同时也揭示了更多主线剧情里的细节。最重要的是情绪渲染能力,你把伏笔拿掉,那段剧情本身就算一个不错的悲剧情节,但在后续伏笔回收之后,你把相关的点滴铺垫逐渐收集起来,发现这部分内容其实由始至终陪伴了你这么久,情绪逐渐递进,最后再回头看那结局性的一幕,所有思绪汇源到一处,最终再爆发出来的感觉,真的,无以言喻,无与伦比。神。
\ No newline at end of file +追番列表 - TwoSix的小木屋 +

大叔,你怎么还在看二次元噢?

全25话0 类型 动画 想看 1733 在看 4681 已看 120 评分 7.2

简介:15世纪的欧洲某国。在跳级中被允许进入大学的神童拉法尔。他回应了周围的期待,宣布将专攻当时最重要的神学。但是,从以前开始就热心投入的对天文的热情一直没有被抛弃。有一天,他遇到了一位名叫休伯特的神秘学者。因为触犯了基于异端思想的禁忌而受到拷问,被关进监狱的休伯特。他研究的是关于宇宙的冲击性的“某个假说”——。

全25话0 类型 动画 想看 590 在看 1324 已看 57 评分 6.6

简介:数多の作品を生み出してきた小説投稿サイト「小説家になろう」にて、2017年より連載中の硬梨菜による大人気WEB小説。小説の書籍化を待たずしてコミカライズ化された本作は、第47回講談社漫画賞の少年部門を受賞し、「週刊少年マガジン」の読者アンケートでは史上初の四冠達成!誰もが持ちうるゲーム体験を通じ、新感覚の冒険を描く本作は、かつての思い出に浸る大人たちと、テクノロジーの最先端で生きる若者たちに、新たな冒険の興奮を呼び覚ましている。1人のクソゲーハンターが、神ゲーに挑む、至高のゲーム×ファンタジー冒険譚、開幕!!

全12话0 类型 动画 想看 225 在看 627 已看 21 评分 7.8

简介:1984年から「週刊少年ジャンプ」で約10年半にわたって連載され、常にトップを走り続ける日本を代表する漫画『DRAGON BALL(ドラゴンボール)』。コミックスは全世界累計2億6000万部超と驚異的な記録を叩き出し、連載終了後も、テレビアニメ・映画・ゲームなど様々なメディアミックスでファンを魅了しながら、全世界で桁外れの人気を誇っているモンスタータイトル。その勢いはとどまる所を知らず、2013年には17年ぶりの劇場版シリーズが復活し、立て続けに大ヒットをする中、2015年に原作者・鳥山明原案によるシリーズ「ドラゴンボール超」が放送開始。2022年には、映画『ドラゴンボール超 スーパーヒーロー』が公開、国内初週末興行収入1位はもちろん、全米初登場興行収入1位を獲得。連載開始から40周年となる2024年10月に、「DRAGON BALL」の歴史に、新たに刻む新シリーズ「ドラゴンボールDAIMA」がついにスタートします!

全12话0 类型 动画 想看 566 在看 1355 已看 74 评分 7.9

简介:まさしく「最終決戦」にふさわしい実力派スタッフ陣で挑む、ファイナル・シリーズ。はたして、黒崎一護がたどり着くのは――。

全12话0 类型 动画 想看 954 在看 2550 已看 61 评分 6.9

简介:10年前、妻を亡くした愛妻家の新島圭介はずっと失意の中にいた。だがある日、小学生の女の子、白石万理華が、自分は他界した妻、貴恵だと言ってやってくる。こうして、小学生の姿をした妻との人生が再び動き始める…

全25话0 类型 动画 想看 1054 在看 4603 已看 109 评分 7.4

简介:中高一貫のスポーツ強豪校・栄明高校に入学する、男子バドミントン部の一年生・猪股大喜。大喜は毎朝、朝練で顔を合わせる一つ上の先輩、鹿野千夏に恋をする。千夏は女子バスケットボール部のエースで、校内外問わず人気の高嶺の花。部活に恋に勉強に、大喜にとって忙しい高校生活がはじまる、そんなある日――

全12话0 类型 动画 想看 434 在看 2493 已看 49 评分 7

简介:大学生佐佐木常宏身背巨额债务,又被医生告知之余两年寿命。他终日郁郁寡欢,在一次被催债人追逐中失足坠海,幸而被热爱垂钓的少女花以及钓友贵明救助。在花的劝说下,常宏开始了人生中第一次垂钓,与其他钓友也逐渐熟悉,并开始在花以及贵明工作的便利店打工。起初,他一度苦于各种生涩的术语和糠虾的腥味,但还是逐渐喜欢上了垂钓。鱼儿中钩的触感触达手中,那也是生命的实感——“一路下坡,只能抬头仰望的人生,不可能轻易改观”抱有如此想法的常宏,会通过垂钓获得怎样的感悟?

全12话0 类型 动画 想看 287 在看 1405 已看 42 评分 6

简介:キタカガミ市に住む中学生アマツガ・ヒカルはある日、謎の”ウデ”型機械生命体 アルマと出会う。アルマはヒカルからエネルギーを得ようと彼の身体への結合を試みるも失敗。なんとヒカルが着ていたパーカーに結合してしまう。一方その頃、メカウデ達の自我を奪い兵器にすることを企む大企業”カガミグループ” はアルマの身柄を追っていた。そしてメカウデ達を救い解放するため活動する組織”ARMS(アームズ)”もアルマを探す。そんな中、記憶を失って困っているアルマを見捨てることができなかったヒカルはアルマが結合したパーカーを羽織り、メカウデ使いたちが繰り広げる大事件に巻き込まれていく……

全12话0 类型 动画 想看 1554 在看 7387 已看 201 评分 7.8

简介:出生于灵媒师家族的女高中生·小桃<绫濑桃>,与她同年级的超自然爱好者·厄卡伦<高仓健>。小桃在厄卡伦遭到班上同学欺凌时保护了他,两人以此为契机开始有了交流。“相信幽灵存在但否定外星人存在”的小桃,和“相信外星人存在但否定幽灵存在”的厄卡伦争论不休。为了让对方相信外星人和幽灵的存在,小桃去了被称为UFO圣地的废弃医院,厄卡伦去了闹鬼的隧道。在那里,他们遇到了无法理解的极其怪异的现象。在绝境中,小桃觉醒了体内潜藏的力量,厄卡伦获得了诅咒之力,二人开始挑战接连逼近的怪异!也开始了命运之恋!?超能力战斗&青春故事,开幕!霊媒師の家系に生まれた女子高生・モモ<綾瀬桃>と、同級生でオカルトマニアのオカルン<高倉健>。モモがクラスのいじめっ子からオカルンを助けたことをきっかけに話すようになった2人だったが、「幽霊は信じているが宇宙人否定派」のモモと、「宇宙人は信じているが幽霊否定派」のオカルンで口論に。互いに否定する宇宙人と幽霊を信じさせるため、モモはUFOスポットの病院廃墟へ、オカルンは心霊スポットのトンネルへ。そこで2人は、理解を超越した圧倒的怪奇に出会う。窮地の中で秘めた力を覚醒させるモモと、呪いの力を手にしたオカルンが、迫りくる怪奇に挑む!運命の恋も始まる!?オカルティックバトル&青春物語、開幕!

全13话0 类型 动画 想看 316 在看 1515 已看 36 评分 6.2

简介:トレジャーハンターになろうぜ目指すはただひとつ、世界最強の英雄だかつてそんな誓いを交わした六人の中でひとりだけ圧倒的に才能がなかった少年がいた。ある日挫折を口にした彼に、幼馴染は言った。「クライ、お前、特に役割ないんだからリーダーやれよ」才能があり過ぎる怪物達(=幼馴染達)で結成されたパーティ 《嘆きの亡霊(ストレンジ・グリーフ)》は数年にしてその名を帝都中に轟かせ、そのてっぺんに据えられた彼はあれよあれよという間に最強パーティのリーダーとして祭り上げられた。跳ね上がる周囲からの期待により彼の言動はいつだって勘違いされ、事態は予想もしない展開に…。これは、最強パーティのリーダーにして最強のクランマスターとして名を馳せるクライ・アンドリヒの栄光と苦悩に満ちた英雄譚である!

全12话0 类型 动画 想看 530 在看 166 已看 3 评分 10

简介:悪の組織――あらゆるものを侵略し、あらゆるものを滅ぼす。残忍にして狡猾なその組織のブレーンには、王の片腕たる悪の参謀がいた。地上侵略の危機に立ち上がる、薄幸の魔法少女・白夜。彼女と対峙した悪の参謀・ミラは、なんと一目ボレしてしまい……。魔法少女と悪が敵対していたのは、かつての話。殺し愛(あ)わない、ふたりの行く末は――?

全12话0 类型 动画 想看 1887 在看 384 已看 12 评分 8.4

简介:たがいに助け合い、完全な小市民を目指そう。かつて“知恵働き”と称する推理活動により苦い経験をした小鳩くんは、清く慎ましい小市民を目指そうと決意していた。同じ志を立てた同級生の小佐内さんとたがいに助け合う“互恵(ごけい)関係”を密かに結び、小市民としての高校デビューを飾り平穏な日々を送るつもりでいたのだ。ところがふたりの学園生活に、なぜか不可解な事件や災難が次々と舞い込んでくる。はたして小鳩くんと小佐内さんは、小市民としての穏やかな日々を手に入れることができるのだろうか。

全24话0 类型 动画 想看 755 在看 1576 已看 42 评分 6.6

简介:3次元の女子に興味無し!漫画研究部部長・奥村は今日も部室でひとり、画面の向こうに映る愛してやまない2次元のキャラクター・リリエルの名を叫んでいた……。そんな奥村のもとへやってきたのは「リリエルになりたい」という3次元女子・天乃リリサ。彼女は、漫画の中に登場する女の子のエッチで可愛い「衣装」が大好き。そして、奥村に負けないくらいリリエルを愛する仲間(オタク)だった!奥村に秘密の趣味がコスプレであることを明かしたリリサは、コスプレの写真や動画が詰め込まれた「ROM(ロム)」のコレクションを見せて伝える――。「私っ……これを作りたいんです!!」ふたりきりの部室で始まるコスプレ活動!リリサが変身(コスプレ)したリリエルは奥村が衝撃を受けるほど本物(リアル)で!?熱意に押された奥村もカメラを手に!?真摯に熱くコスプレに向き合う彼らが、「何かを熱烈に愛している」全てのオタクへ贈るコスプレ青春ストーリー、開幕!!

全12话0 类型 动画 想看 434 在看 1016 已看 974 评分 5.9

简介:人人所畏惧的邪恶魔术师萨冈,是个不擅言语的人,今天也一边研究魔术,一边打击领地内的恶贼。萨冈受到损友巴尔巴洛士的邀请,来到地下拍卖会会场。在那里与一位被当作魔王遗物来拍卖的白发精灵少女涅菲,命运般地相遇。萨冈花费所有财产,将涅菲带回自己的城堡里。至今从未与人相处过,且内向寡言的萨冈,不知如何对待涅菲,只是过着没什么对话又狼狈的生活。究竟两人的共同生活走向如何呢?不擅言语的魔术师与美少女精灵的欢乐喜剧就此展开。

全25话0 类型 动画 想看 1813 在看 4681 已看 329 评分 7.4

简介:若き行商人クラフト・ロレンスは、荷馬車を引く一頭の馬を相棒に、街から街へと商品を売り歩く日々を送っていた。ある日、黄金色の麦畑が広がる小さな村を訪れた彼は、耳と尻尾を有する美しい少女と出会う。「わっちの名前はホロ」自身を“賢狼”と呼ぶホロは、豊穣を司る狼の化身だった――。彼女の「遠く北にあるはずの故郷・ヨイツの森へ帰りたい」という望みを聞き、ロレンスとホロは北を目指す商売の旅の道連れとなる。だが行商人の旅には思いがけない波乱がつきもので……。孤独だった行商人と、孤独だった狼の化身を乗せた馬車が、今、騒がしく走り始める。

全24话0 类型 动画 想看 2030 在看 7054 已看 6444 评分 7.8

简介:迷宫饭,不是吃就是被吃…妹妹在迷宫深处被赤龙吃掉了!冒险者莱欧斯侥幸逃过一命回到了地面。他想要再度挑战迷宫,但是钱和食物都留在了迷宫深处…在妹妹随时可能会被消化掉情况下,莱欧斯下定了决心:「食物要在迷宫内就地取材」史莱姆、鸡尾蛇、宝箱怪、然后是龙!冒险者啊,一边吃掉袭来的魔物,一边通关迷宫吧!

全12话0 类型 动画 想看 1364 在看 2604 已看 62 评分 6.9

简介:久世政近的邻座坐着一位名叫艾莉莎的女孩,她总是对他投以冰冷的目光。然而,她偶尔会小声地用俄语对他撒娇……政近当然不会错过这些话。因为他竟然拥有着母语级别的俄语听力!艾莉莎误以为政近听不懂俄语,所以才偶尔在他面前流露出真实的自己。而政近虽然明白她话中的意思,却装作毫不知情的样子。两人之间弥漫着掩饰不住的甜蜜气氛,他们的恋情将会何去何从——!?

我的评分:
我的评价:优点只剩画的好看。好好的校园党争发糖不做,非得整这么个学生会竞选主线,还引入什么“家族”我真是有点难绷
全12话0 类型 动画 想看 2701 在看 485 已看 40 评分 7.3

简介:「この芸能界(せかい)において嘘は武器だ」地方都市で働く産婦人科医・ゴロー。ある日"推し"のアイドル「B小町」のアイが彼の前に現れた。彼女はある禁断の秘密を抱えており…。そんな二人の"最悪"の出会いから、運命が動き出していく―。

我的评分:
我的评价:制作上乘,剧情也还行,好看。为什么赤坂的漫画总能吃到这么好的资源
全12话0 类型 动画 想看 784 在看 352 已看 7 评分 8.3

简介:時は西暦1333年、武士による日本統治の礎を築いた鎌倉幕府は、信頼していた幕臣・足利高氏の謀反によって滅亡する。全てを失い、絶望の淵へと叩き落とされた幕府の正統後継者・北条時行は、神を名乗る神官・諏訪頼重の手引きで燃え落ちる鎌倉を脱出するのだった…。逃げ落ちてたどり着いた諏訪の地で、信頼できる仲間と出会い、鎌倉奪還の力を蓄えていく時行。「戦って」「死ぬ」武士の生き様とは反対に「逃げて」「生きる」ことで乗り越えていく。英雄ひしめく乱世で繰り広げられる、時行の天下を取り戻す鬼ごっこの行方は―――。

我的评分:
我的评价:画风很独特但色彩很顺眼好看,作画也很流畅,不过剧情题材比较冷门也确实没很戳我,中间疯狂点题逃跑其实和稍微有些审美疲劳,但瑕不掩瑜,整体还算优秀吧。
全12话0 类型 动画 想看 418 在看 3174 已看 141 评分 6.6

简介:里加甸魔法学院六年级生──威尔・赛尔福特,被称为「只擅长学科的书呆子」。威尔连初阶魔法都无法使用,被大家藐视为学院的吊车尾。 为了遵守与儿时玩伴──艾尔法莉亚的约定,威尔作为魔导士,以最强称号「至高五杖」为目标,但这条路却遥不可及。某日,威尔一如往常在地下城击败魔物赚取学分,却撞见了蔑视自己的同学──锡安等人,正被本来不该出现在低楼层的强敌袭击……

我的评分:
我的评价:剧情很尬但又莫名带感,不知不觉就一集又一集跟着看完了的龙傲天番,虽然后期有些后劲不足,但整体还算制作优良
全12话0 类型 动画 想看 1580 在看 368 已看 8 评分 7.8

简介:想い人の恋人の座を勝ち取れなかった女の子——「負けヒロイン」。食いしん坊な幼なじみ系ヒロイン・八奈見杏菜。元気いっぱいのスポーツ系ヒロイン・焼塩檸檬。人見知りの小動物系ヒロイン・小鞠知花。ちょっと残念な負けヒロイン——マケインたちに絡まれる、新感覚・はちゃめちゃ敗走系青春ストーリーがここに幕を開ける!負けて輝け、マケインたち!

我的评分:
我的评价:放到整个校园青春赛道也不失为上乘之作,好看
全12话0 类型 动画 想看 795 在看 674 已看 9 评分 7.3

简介:「真夜中なのにおっはよー!真夜中ぱんチです!」世界でもっとも見られている動画投稿サイト「NewTube」。3人組NewTuber「はりきりシスターズ」の「まさ吉」こと真咲まさきは、とある事件がきっかけでチャンネルをクビになってしまう。起死回生を狙う真咲の前に現れたのは、なぜか彼女に運命を感じたりぶ。超人的な能力を持つりぶと一緒なら、最高の動画が撮れるはず。目指せ、チャンネル登録100万人!

我的评分:
我的评价:平稳,没什么出彩的,也没什么要喷的,如果人设好看一点倒也不失为一个优质的美少女贴贴作品
全12话0 类型 动画 想看 68 在看 779 已看 36 评分 6.2

简介:“エンドオブザワールド”は誰の手に……!?とある海にぷっかりと浮かぶ『エグー島(とう)』。この島の唯一の娯楽は歌姫“エンドオブザワールド”。村人たちは日々、彼女の歌声に酔いしれていた。しかし、そんなのどかな時間は一瞬で壊れる。-歌姫の失踪-この事件以来、島は『怒り』『疑心』『不安』で溢れてしまう。そして、それぞれの思惑が動き始める……。“エンドオブザワールド”を巡る、ペロペロ大戦争!その運命を握るのは……、伝説の遺産『エグミレガシー』

我的评分:
我的评价:原来你才是七月最佳原创,由头到尾的伏笔还真让你收到了,在抽象搞笑和故事之间平衡的不错,挺好看的
全15话0 类型 动画 想看 1859 在看 1900 已看 11889 评分 6.9

简介:因为新能源资源的发现而建立的巨型漂浮城市・贝隆城。在那里经营着一家小公司的青年・修,因为平时花钱大手大脚而过着贫穷的生活。一位少女木更担心着这样的修,因此前往修的事务所兼家里。她在贝隆城里念着高中的同时,也照顾着事务所的工作和家务活。还有一位名叫绫野的是修以前所属公司的前辈同事、同时也是修的前女友,也很在意着修。漂浮在太平洋上的人工岛上带着略微奇怪关系的3人之间的恋爱喜剧开幕了。

我的评分:
我的评价:主线其实一般般吧,主要是看主角和女角之间的互动乐子还挺有意思的
全12话0 类型 动画 想看 475 在看 1436 已看 711 评分 5.7

简介:“夕阳和~”“安美的!”“高中生广播~!”“本节目是由碰巧就读于同一所高中、同一个班级的我们,为大家带来教室氛围的节目!”无论是作为配音员还是作为同班同学都关系极好的二人,放松地做配音员广播,但这仅仅是“台前”的模样。夕阳的本性是阴沉的土妹子,而安美则是地道的辣妹。容貌和性格都正好相反的二人,一开口就要吵架。“……怎么这么晃眼。你这样跟平时形象的差别真的大到吓人,平时的阴沉都到哪去了?”“你才是,别用这副模样发出可爱的声音啊。”录音结束后便就地变成修罗场,挖苦和谩骂势如风暴。和这种人一起做广播,我绝对办不到!但是,节目可不会等人,前途坎坷的配音员广播,会持续多久……靠职业精神欺骗全世界,暴露就要结束的青春配音员节目,现已播出。

我的评分:
我的评价:虽然前期作画崩,中期有一段尬的飞起,不过对于基本是蹦着水濑祈乐子来的我,在了解这番其实也就是主打一个百合营业之后其实也对剧情没什么期待,一边看美少女贴贴一边看作者玩点声优业界梗,这追番历程倒也算个轻松加愉快
全12话0 类型 动画 想看 775 在看 2217 已看 2403 评分 7

简介:贫苦的侦探镝矢惣助,遇到了操纵魔法的异世界皇女莎拉。惣助逐渐开始了与莎拉的同居生活,莎拉很快就习惯了现代日本。另一方面,跟着莎拉转移来到日本的女骑士莉薇亚,在流浪的同时,意外地度过了快乐生活。积极坚强生活的两个异世界人,不仅影响了惣助,也影响了鬼畜律师、分手工作员、宗教人士与住在这区的怪人们。

我的评分:
我的评价:很不错的养女儿番,四月休闲娱乐担当,事实证明把一个小女孩写可爱完全不需要写熊。就是女骑士那条线总要出点擦边画面,在宿舍用大屏看怪羞耻的。
全13话0 类型 动画 想看 1833 在看 12650 已看 1144 评分 8.2

简介:无论是愤怒、喜悦还是悲伤,全都倾注其中。高中二年级,从学校退学,只身在东京以大学为目标的主人公。被同伴背叛,不知如何是好的少女。被父母抛弃,一个人在大城市里打工维生的女孩子。虽然这个世界总是背叛我们。虽然什么都不能称心如意。但是,因为我们想要喜欢某个东西。因为我们相信自己的容身之处就在某个地方。所以,我们歌唱。

我的评分:
我的评价:10分只给原创,实际其实在8分左右吧。但GBC确实在四月给了我无与伦比的追番体验,这是一场名为花田的魔法。
全12话0 类型 动画 想看 110 在看 419 已看 249 评分 5.5

简介:母親の入院費を稼ぐため貧乏生活を送る、スクールカースト最底辺の高校生・志村光太。学校では不良のハマケンからゴミのように扱われ、負け組人生に日々絶望していた。そんなある日、クラスメイトのカネゴンと殴り合う様子が誤って全世界へ生配信!?底辺同士のイタすぎる喧嘩動画は瞬く間に広がり、一晩でまさかの1000万再生を突破!!!再生数が金になることを知った光太が噛みつく相手は不良に、ヤンキーに、プロ格闘家とエスカレートしていき…プロのいじめられっ子、反撃開始───ッ!!!

我的评分:
我的评价:这番给人的感觉就是很一般,包括结尾也很平淡。爽没怎么爽起来,但你要说难看倒也不难看,一集一集的过来我倒也没怎么反感的就看完了。不上不下的,这可能也是他人气冷的原因之一吧。
全13话0 类型 动画 想看 1290 在看 5023 已看 2557 评分 6.8

简介:郊外的某个小镇。这里不是一个随处可见的普通乡村……。这里的居民产生了巨大的异变。即使在这样的情况下,千仓静留也有着坚定的想法。想再一次见到行踪不明的朋友们!静留她们在被废弃且停止行驶的电车里,在不知道能不能活着回来的情况下前往外面的世界。开始运行的末日列车的终点,到底有什么?

我的评分:
我的评价:确实是很电波的一个番,纠结了很久他究竟属于还行,还是一般。但就这个浪漫的结尾+中期不乏有趣的脑洞,倒感觉也称得上一部过得去的现代童话。
全12话0 类型 动画 想看 1596 在看 5825 已看 4154 评分 5.8

简介:「我」也想像某个人一样闪耀光辉!明天要聊的话题,这周要买的衣服,这些手机(流行趋势)都会告诉我们答案。想成为不一样的存在──然而这忙碌的世界却连让我们怀有这种愿望的时间都没有。活动停止中的插画家「海月夜」、想靠歌声重新争口气的前任偶像「橘乃乃香」、自称最强Vtuber的「龙崎诺克斯」、想支持喜欢的偶像的神秘作曲家「木村酱」。这些与世界有些格格不入的少女们组成了匿名艺术家团体「JELEE」。如果成为不是自己的「我们」──也许就能够闪耀光辉。没错,这是一部以涩谷为舞台的青春群像剧,描绘着夜晚流连在涩谷的我们所展开的物语!

我的评分:
我的评价:很可惜的一部动画,动工制作拉满,可惜59的剧本是依托答辩,当一部动画的核心是大份的时候,再华丽的外表也失去的光芒
全12话0 类型 动画 想看 156 在看 248 已看 238 评分 4

简介:考虑环境问题,以安心和安全为目标的世界,至少在表面上达到了目的。就在这时,次世代赛车“NEX Race”突然发表。最高时速超过500km/h。以最新技术为安全性担保,世界舞台上的赛车场面发生了翻天覆地的变化。狂热与兴奋。挑战者的眼神,震撼着观众的心。在这里,一名少女迎来了出道战。轮堂凛。当世界得知其名,赛车将再次迎来一个崭新的时代。

我的评分:
我的评价:前面是毫无疑问的大份,剧情真的非常蠢,但后面回归一般水准。单纯看看美少女赛车服就行,对美少女赛车服不感兴趣的话看这个纯属浪费时间
全13话0 类型 动画 想看 323 在看 304 已看 474 评分 5.6

简介:故事讲述忍者 Kamui为了逃避自己的血腥过往,脱离忍者组织,隐姓埋名地生活着。然而由于他背叛了忍者古老的信条,一天夜晚,他的家人被前组织的暗杀者残忍杀害。为了给亲人复仇,Kamui重新化身为从前的自己。然而,二十一世纪的忍者是一个时代错误,因为他们要以残酷而古老的技巧对抗高科技武器。Kamui要面对训练有素的刺客、战斗机器人以及其它强大的忍者,来推翻他的前组织。

我的评分:
我的评价:老老实实走忍杀路线多好,非要搞个高达战甲,穿上战甲之后的打戏真的是烂完了,慢慢悠悠的毫无感觉。结局也莫名奇妙的,唉,浪费时间。
全14话0 类型 动画 想看 2121 在看 2528 已看 19301 评分 8.3

简介:「能一辈子和我搞乐队吗?」在高一的春季即将结束之时,羽丘女子学园里人人都在参加乐队活动。入学较晚的爱音,也为了尽快融入班级,急忙寻找乐队成员。在寻找时,她发现被称为「羽丘的不可思议少女」的灯还没有参加乐队,于是爱音不由得向她发出邀请…我们的<音乐(呐喊)>,伤痕累累而充满狼狈。迷茫也无所谓,迷茫也要前进。

全16话0 类型 动画 想看 2242 在看 3778 已看 31013 评分 8.4

简介:作为网络吉他手“吉他英雄”而广受好评的后藤一里,在现实中却是个什么都不会的沟通障碍者。一里有着组建乐队的梦想,但因为不敢向人主动搭话而一直没有成功,直到一天在公园中被伊地知虹夏发现并邀请进入缺少吉他手的“结束乐队”。可是,完全没有和他人合作经历的一里,在人前完全发挥不出原本的实力。为了努力克服沟通障碍,一里与“结束乐队”的成员们一同开始努力……

全13话0 类型 动画 想看 267 在看 278 已看 3156 评分 5.6

简介:たったひとりの身内である祖父を亡くした相馬一也は、ある日、突然、異世界に勇者として召喚されてしまう。召喚された先は、まるで中世ヨーロッパのようなエルフリーデン王国であった。勇者どころか、ごくふつうの青年のソーマだが、持ち前の合理的精神と現代知識から、次々と新しい政策を打ち出し、傾きかけていた王国の財政政治体制を立て直していく。ソーマと共に歩むのは、エルフリーデン王国の王女リーシア、王国一の武を誇るダークエルフのアイーシャ、怜悧な頭脳を持つハクヤ、大食いのポンチョ、歌姫のジュナ、動物と意思疎通できる少女トモエなど、多才で個性的な仲間たち。現代知識で窮地の王国を再生する異世界内政 ファンタジー、『現実主義勇者の王国再建記』。いよいよ開幕!

全13话0 类型 动画 想看 2309 在看 538 已看 8868 评分 7.5

简介:我们为什么要这么做……?哈尔希洛回过神来,才发现自己身处在黑暗当中,他完全不知道自己人在何处,也不明白这个地方是哪里。他的身边有一群和他一样失去记忆,只记得自己名字的男女;而离开了地底后,等待着众人的是一个「宛如游戏」的世界。为求生存,哈尔希洛与自己有着相同境遇的伙伴们组成队伍、学习技能,以义勇兵见习者的身份踏入了这个世界--「格林姆迦尔」。没有人知道未来会遇见什么……这一切,就是从灰烬之中所诞生的冒险谭。

全14话0 类型 动画 想看 632 在看 536 已看 6824 评分 4.1

简介:在她成为神的那天,世界开始走向终结——高中最后的暑假,在成神阳太的眼前,突然有一天,自称“知晓一切的神”的少女雏出现了。“30天后,这个世界就会结束。”阳太对告知这样的消息感到困惑,但是看到神一样的预知能力,确信那个力量是真的。与超常的力量相反,天真烂漫、天真无邪的小雏,不知为何决定寄居在阳太的家中,两人共同生活。向着“世界末日”,喧闹的一个夏天开始了。

全12话0 类型 动画 想看 636 在看 302 已看 6168 评分 6.2

简介:厌恶魔术的魔术讲师所展开的叛逆英雄幻想剧!魔术与科学共同发展的世界——卢瓦佛斯。位于魔导大国·阿尔扎诺帝国南部的“阿尔扎诺帝国魔法学院”,是学习世界最先端魔术的最高学舍。拥有约四百年历史的这所学院,是所有有志于魔术之道人们的憧憬,学院的讲师和学生们也因自己的身份而自豪。突然来到这所正统学院赴任的非常勤讲师,格伦·勒达斯。“我来教给你们真正的魔术吧”被称作“不正经”的这个男人的破天荒授课,就此开始。

我的评分:
我的评价:前面半部分不错,妹子可爱爆了。后面实在蠢的有点看不下去。
全15话0 类型 动画 想看 186 在看 83 已看 1763 评分 6.4

简介:无知少女穿越异世界,遭遇裸男围攻。是屈服?是挣扎?是反抗?凸变英雄LEAF之脱变英雄,看春光乍泄,照亮世界!

全4话0 类型 动画 想看 1317 在看 702 已看 6552 评分 7.5

简介:クラシック三冠レース『皐月賞』『日本ダービー』『菊花賞』一生に一度だけ挑戦できるそのレースで勝つことは、すべてのウマ娘、すべてのトレーナーにとっての夢である。『ナリタトップロード』『アドマイヤベガ』『テイエムオペラオー』三強と呼ばれたウマ娘たちが、絶対に負けられない想いを胸に頂点を目指し競い合う、熱い熱いクラシック戦線。――ウマ娘の新シリーズ配信アニメプロジェクト、いよいよ始動です。

\ No newline at end of file diff --git a/categories/index.html b/categories/index.html index d1616a4..d3e90b8 100644 --- a/categories/index.html +++ b/categories/index.html @@ -1,2 +1,2 @@ -分类 - TwoSix的小木屋 +分类 - TwoSix的小木屋
\ No newline at end of file diff --git "a/categories/\345\245\275\350\275\257\346\216\250\350\215\220/index.html" "b/categories/\345\245\275\350\275\257\346\216\250\350\215\220/index.html" index f10afc5..1f9c54f 100644 --- "a/categories/\345\245\275\350\275\257\346\216\250\350\215\220/index.html" +++ "b/categories/\345\245\275\350\275\257\346\216\250\350\215\220/index.html" @@ -1,2 +1,2 @@ -分类: 好软推荐 - TwoSix的小木屋 +分类: 好软推荐 - TwoSix的小木屋
1
\ No newline at end of file diff --git "a/categories/\347\274\226\347\250\213-\350\257\255\350\250\200/index.html" "b/categories/\347\274\226\347\250\213-\350\257\255\350\250\200/index.html" index ef5baca..5c6efd0 100644 --- "a/categories/\347\274\226\347\250\213-\350\257\255\350\250\200/index.html" +++ "b/categories/\347\274\226\347\250\213-\350\257\255\350\250\200/index.html" @@ -1,2 +1,2 @@ -分类: 编程/语言 - TwoSix的小木屋 +分类: 编程/语言 - TwoSix的小木屋
12
\ No newline at end of file diff --git "a/categories/\347\274\226\347\250\213-\350\257\255\350\250\200/page/2/index.html" "b/categories/\347\274\226\347\250\213-\350\257\255\350\250\200/page/2/index.html" index a530c56..ee00043 100644 --- "a/categories/\347\274\226\347\250\213-\350\257\255\350\250\200/page/2/index.html" +++ "b/categories/\347\274\226\347\250\213-\350\257\255\350\250\200/page/2/index.html" @@ -1,2 +1,2 @@ -分类: 编程/语言 - TwoSix的小木屋 +分类: 编程/语言 - TwoSix的小木屋
12
\ No newline at end of file diff --git a/index.html b/index.html index 26e955e..c14348d 100644 --- a/index.html +++ b/index.html @@ -1,2 +1,2 @@ -TwoSix的小木屋 +TwoSix的小木屋
home-banner-background
TwoSix的小木屋

12
\ No newline at end of file diff --git a/page/2/index.html b/page/2/index.html index f4fa388..d46b68f 100644 --- a/page/2/index.html +++ b/page/2/index.html @@ -1,2 +1,2 @@ -TwoSix的小木屋 +TwoSix的小木屋
home-banner-background
12
\ No newline at end of file diff --git a/search.xml b/search.xml index 2d2f124..dc693cd 100644 --- a/search.xml +++ b/search.xml @@ -21,6 +21,177 @@ cmake + + 【Rust 学习记录】11. 编写自动化测试 + /2023/05/09/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%9111-%E7%BC%96%E5%86%99%E8%87%AA%E5%8A%A8%E5%8C%96%E6%B5%8B%E8%AF%95/ + +

这一章讲的就是怎么在Rust编写单元测试代码,这一部分的思想不仅适用于Rust,在绝大多数语言都是有用武之地的

+ +

如何编写测试

测试代码的构成

构成

通用测试代码通常包括三个部分

+
    +
  1. 准备所需的数据或者前置状态
  2. +
  3. 调用需要测试的代码
  4. +
  5. 使用断言,判断运行结果是否和我们期望的一致
  6. +
+

在Rust中,有专门用于编写测试代码的相关功能,包含test属性,测试宏,should_panic属性等等

+

在最简单的情况下,Rust中的测试就是一个标注有test属性的函数。只需要将#[test]添加到函数的关键字fn上,就能使用cargo test命令来运行测试。

+

测试命令会构建一个可执行文件,调用所有标注了test的函数,生成相关报告。

+
+

PS:

+

属性是一种修饰代码的一种元数据,例如之前为了输出结构体时,加入的#[derive(Debug)]就是一个属性,声明属性后,会为下面的代码自动生成一些实现,如#[derive(Debug)]修饰结构体时,就会为结构体生成Debug trait的实现

+
+

初次尝试

接下来我们就试试怎么测试

+

首先新建一个名为adder的项目cargo new adder --lib(–lib指生成lib.rs文件)

+

可能是版本比较新,lib.rs里直接生成有了以下代码

+
pub fn add(left: usize, right: usize) -> usize {
left + right
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
+ +

我们先忽略一些没讲过的关键词,这段代码里我们定义了一个it_works函数,并标注为测试函数,然后使用断言判断add函数的结果是否正确的等于4。在了解了大概功能之后,我们直接运行测试看看

+
cargo test
Compiling adder v0.1.0 (E:\Code\rust\adder)
Finished test [unoptimized + debuginfo] target(s) in 0.29s
Running unittests src\lib.rs (target\debug\deps\adder-0a86cd050490705e.exe)

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
+ +

对应的测试结果如上

+
    +
  • passed: 测试通过的函数数量,我们这里只有一个it_works函数,且测试通过,所以为1
  • +
  • failed: 测试失败的函数数量
  • +
  • ignored: 被标记为忽略的测试函数,后面会提
  • +
  • measured: Rust还提供了衡量函数性能的benchmark方法,不过编写书的时候似乎这部分还不完善,所以不会有讲解,想了解需要自行学习
  • +
  • filtered out:被过滤掉的测试函数
  • +
  • Doc-tests:文档测试,这是个很好用的特性,可以防止你在修改了函数之后,忘记修改自己的文档,保证文档能和实际代码同步。
  • +
+

测试时,每一个测试函数都是运行在独立的线程里的,所以发生panic时并不会影响其他的测试,我们可以写一个错误的函数看看

+
pub fn add(left: usize, right: usize) -> usize {
left + right
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn error() {
let result = add(3, 2);
assert_eq!(result, 4);
}

#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
+ +

输出结果:

+
cargo test
Compiling adder v0.1.0 (E:\Code\rust\adder)
Finished test [unoptimized + debuginfo] target(s) in 0.24s
Running unittests src\lib.rs (target\debug\deps\adder-0a86cd050490705e.exe)

running 2 tests
test tests::it_works ... ok
test tests::error ... FAILED

failures:

---- tests::error stdout ----
thread 'tests::error' panicked at 'assertion failed: `(left == right)`
left: `5`,
right: `4`', src\lib.rs:18:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
tests::error

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`
+ +

可以看见,error的panic并不影响it_works的测试通过。

+

assert!宏

assert!

assert!宏主要的功能是用来确保某个值为true,所以常被用于测试中。如a>b等场景,返回的是一个bool值,就完美的符合assert!的使用场景,可以使用assert!进行测试,例如

+
pub fn cmp(a: i32, b: i32) -> bool {
if a>b{
true
}
else{
false
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn cmp_test(){
let result = cmp(3, 2);
assert!(result);
}
}
+ +

assert_eq!和assert_ne!

那如果返回值不是bool值呢?前面也出现过了,我们可以使用assert_eq!或者assert_ne!来断言两个值是否相等。

+

eq则对应的只有相等才能通过断言,ne则对应的只有不相等才能通过断言,用例见上面的add测试即可。

+

但是注意,assert_eq!和assert_ne!使用了==!=来实现是否相等的判断,也就意味着,传入这两个宏的参数是必须实现了PartialEq这个trait的。同时,我们可见错误的输出中会打印出详细的不相等原因,也就是说它还同时需要实现了Debug宏帮助打印输出。一般绝大部分参数都是满足要求的,自定义的结构体时需要注意。

+
+

之前提到过属性这个概念,会为你自动实现一些功能,实际上PartialEq和Debug作为可派生的宏,也内置了属性的实现,你只需要在自己定义的结构体上加上#[derive(PartialEq, Debug)],就能自动帮你实现这两个宏

+
+

自定义错误提示代码

上面我们说到assert_eq是会有详细输出的,告诉你怎么不相等了,帮助你排除bug,但普通的assert!只判断布尔值,所以没办法有详细的输出,这时候我们可以定制一个输出,使得错误提示更人性化一点。

+

如下:

+
pub fn cmp(a: i32, b: i32) -> bool {
if a>b{
true
}
else{
false
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn cmp_test(){
let a = 2;
let b = 3;
let result = cmp(a, b);
assert!(result, "{} is not bigger than {}", a, b);
}
}
+ +

和一般不一样,我们不需要用什么格式化字符串的方法先格式化一个字符串,再传入这个字符串,断言支持直接使用格式化的语法。这段代码的输出如下

+
running 1 test
thread 'tests::cmp_test' panicked at '2 is not bigger than 3', src\lib.rs:23:9
stack backtrace:
+ +

可见报错提示相对于单纯的panicked at更人性化了一些。

+

当然,自定义输出也支持在assert_eq和assert_ne里使用

+

should_panic

should_panic也是一个属性,用来测试代码是否能正确的在出错时发生panic。用例如下

+
pub fn positive_num(a: i32) -> i32 {
if a > 0 {
a
} else {
panic!("{} is not positive", a)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
#[should_panic]
fn pos_test(){
let a = -1;
positive_num(a);
}
}
+ +

这段代码用来检查一个数是否是正数,在不是时抛出panic,接下来我们使用#[should_panic]来检查程序是否正确的panic,这段代码运行测试通过没问题。

+

接下来我们修改一下a的值,让程序不抛出panic,看看会发生什么

+
running 1 test
test tests::pos_test - should panic ... FAILED

failures:

---- tests::pos_test stdout ----
note: test did not panic as expected

failures:
tests::pos_test

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `-p adder --lib`
+ +

测试失败,告诉你pos_test没有按照预期发生panic。这个特性可以用来检查你的代码是否能正确的处理报错,发生panic以阻止程序进一步运行,产生不可预估的后果。

+

但是,单纯这么使用感觉有点含糊不清,因为程序发生panic的原因可能不是我们所预期的,假如其他一些我们不知道的原因抛出了panic,也会导致测试通过。所以我们可以添加一个可选参数expected,用来检查panic发生报错的输出信息里是否包含指定的文字。

+
#[test]
#[should_panic(expected = "positive")]
fn pos_test(){
let a = -1;
positive_num(a);
}
+ +

这时候,should_panic就会检查发生的panic输出的报错信息是否包含”positive”这个字符串,如果是,才会测试通过,输出如下:

+
running 1 test
thread 'tests::pos_test' panicked at '-1 is not positive', src\lib.rs:9:9
stack backtrace:
+ +

可见,输出中也包含了报错的信息,更人性化了。

+

使用Result编写测试

之前学习Result枚举的时候我们就知道了这东西是用来处理报错的,自然也就可以用来处理测试。使用时也很简单,我们只需要声明测试函数的返回值是Result,test命令就会自动根据Result的枚举结果来判断是否测试成功了。如:

+
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn it_works() -> Result<(), String>{
if 2+2 == 4 {
Ok(())
} else {
Err(String::from("two plus two does not equal four"))
}
}
}
+ +

使用Result编写测试函数的主要优势是可以使用问号表达式进行错误捕获,更方便我们去编写一些复杂的测试函数,可以让函数在任一时刻有错误被捕获到时,就返回报错。

+

问号表达式的使用可见【Rust 学习记录】9. 错误处理的?运算符部分,这里就不再写代码举例了(主要书上没例子,我也懒的写)

+

控制测试的运行方式

这一部分主要是对cargo test命令的讲解,具体的运行方式,参数的使用等。

+

cargo test的参数统一需要写在--后面,也就是说你想要使用--help显示参数文档时,需要使用以下命令

+
cargo test -- --help
+ +

并行或串行的执行代码

默认情况下,测试是多线程并发执行的,这可以使测试更快速的完成,且相互之间不会影响结果。但如果测试间有相互依赖关系,则需要串行执行。例如两个测试用例同时在操作一个文件,一个测试在写内容,一个测试在读内容时,则容易导致测试结果不合预期。

+

我们可以使用--test-threads=1来指定测试的线程数为1,即可实现串行执行,当然,你想执行的更快也可以指定更多的线程

+
cargo test -- --test-threads=1
+ +

显示函数的输出

默认情况下,test命令会捕获所有测试成功时的输出,也就是说,对于测试成功的函数,即使你使用了println!打印输出,你也无法在控制台看见你的输出,因为它被test命令捕获吞掉了。

+

如果你想要在控制台显示你的输出,只需要用--nocapture设置不捕获输出即可

+
cargo test -- --nocapture
+ +

只运行部分特定名称的测试

如果测试的函数越写越多,执行所有的测试可能很花时间,通常我们编写了一个新的功能并想进行测试的时候,我们只需要测试这一个功能就足够了,因此可以向test命令指定函数名称来进行测试。

+

如:

+
fn add_two(a: i32, b: i32) -> i32 {
a + b
}


#[cfg(test)]
mod tests {
use super::*;

#[test]
fn add_test_1() {
assert_eq!(add_two(1, 2), 3);
}

#[test]
fn add_test_2() {
assert_eq!(add_two(2, 2), 4);
}

#[test]
fn one_hundred() {
assert_eq!(100, 100);
}

}
+ +

对于这段代码,我们只想测试one_hundred这个函数,只需要对test命令指定运行one_hundred即可

+
cargo test one_hundred
+ +

输出如下:

+
running 1 test
test tests::one_hundred ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
+ +

这里显示2 filtered out,代表有两个测试用例被我们过滤掉了。

+

当然,这个方法也并不是只能运行一个测试函数,也可以通过部分匹配的方法执行多个名称里包含相同字符串的测试函数,例如:

+
cargo test add        

running 2 tests
test tests::add_test_1 ... ok
test tests::add_test_2 ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
+ +

我们使用cargo test add命令,则可以测试所有名字里带add的测试函数,忽略掉了one_hundred函数。

+

不过需要注意的是,这种方法一次只能使用一个参数进行匹配并测试,如果你想同时用多个规则匹配多类的测试函数,就需要用其他方法了。

+

通过显示指定来忽略某些测试

忽略部分测试函数

当有部分测试函数执行特别耗时时,我们不想每次测试都执行这个函数,我们就可以通过#[ignore]属性来显示指定忽略这个测试函数。如:

+
fn add_two(a: i32, b: i32) -> i32 {
a + b
}


#[cfg(test)]
mod tests {
use super::*;

#[test]
fn add_test_1() {
assert_eq!(add_two(1, 2), 3);
}

#[test]
#[ignore]
fn add_test_2() {
assert_eq!(add_two(2, 2), 4);
}

#[test]
fn one_hundred() {
assert_eq!(100, 100);
}

}
+ +

此时我们直接执行cargo test,输出如下

+
running 3 tests
test tests::add_test_2 ... ignored
test tests::add_test_1 ... ok
test tests::one_hundred ... ok

test result: ok. 2 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s

Doc-tests adder
+ +

可见add_test_2函数被忽略不执行了,提示1 ignored。

+

单独执行被忽略的测试函数

如果我们想单独执行这些被忽略的函数,则可以使用--ignored命令

+
cargo test -- --ignored

running 1 test
test tests::add_test_2 ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
+ +

可见,只有add_test_2被执行了。

+

测试的组织结构

测试通常分为两类,单元测试和集成测试。单元测试小而专注,集中于测试一个私有接口或模块;集成测试则独立于代码库之外,正常的从外部调用公共接口,一次测试可能使用多个模块

+

单元测试

单元测试的目的在于将一小段代码单独隔离开来,快速确定代码结果是否符合预期。一般来说,单元测试的代码和需要测试的代码存放在同一文件中。同时也约定俗成的在每个源代码文件里都会新建一个tests模块来存放测试函数,并使用cfg(test)来标注。

+

测试模块和#[cfg(test)]

#[cfg(test)]旨在让Rust只在执行Cargo test命令的时候编译和运行这段代码,而在cargo build的时候剔除掉它们,只用于测试,节省编译时间与空间,使得我们可以更方便的把测试代码和源代码放在同一个文件里。(集成测试时一般不需要标注,因为集成测试一般是独立的一个文件)

+

我们之前编写的测试模块就使用了这个属性

+
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
+ +

测试私有函数

是不是应该测试私有函数一直有争议,不管你觉得要不要,但Rust提供了方法供你方便的测试。

+
fn add(left: usize, right: usize) -> usize {
left + right
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
+ +

以上代码中的add没有标注pub关键字,也就是私有的,但因为Rust的测试代码本身也属于Rust代码,所以可以通过use的方法把私有的函数引入当前作用域来测试,也就是对应的代码里的use super::*;

+

集成测试

集成测试通常是新建一个tests目录,只调用对外公开的那部分接口。

+

tests目录

tests目录需要和src文件夹并列,Cargo会自动在这个目录下面寻找测试文件。

+

现在,我们新建一个tests/integration_test.rs文件,保留之前的lib.rs代码(add函数如果改了私有记得改回公有),并编写测试代码。

+
use adder;

#[test]
fn add_two() {
assert_eq!(adder::add(2, 2), 4);
}
+ +

集成测试就不需要#[cfg(test)]了,Rust有单独为tests目录做处理,不会build这个目录下的文件。

+

接下来,我们再执行以下Cargo test,看看输出

+
cargo test
Compiling adder v0.1.0 (E:\Code\rust\adder)
Finished test [unoptimized + debuginfo] target(s) in 0.32s
Running unittests src\lib.rs (target\debug\deps\adder-0a86cd050490705e.exe)

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Running tests\integration_test.rs (target\debug\deps\integration_test-57f19c149db40d76.exe)

running 1 test
test add_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
+ +

我们可以看到输出里

+
    +
  1. 先输出了单元测试的结果,test tests::it_works … ok,每行输出一个单元测试结果
  2. +
  3. 再输出集成测试的结果,Running tests\integration_test.rs,表示正在测试哪个文件的测试模块,后续跟着这个文件的测试结果
  4. +
  5. 最后是文档测试
  6. +
+

当编写的测试代码越多,输出也就会越多越杂,所以我们也可以使用--test参数指定集成测试的文件名,单独进行测试。如:cargo test --test integration_test

+

在集成测试中使用子模块

测试模块也和普通模块差不多,可以把函数分解到不同文件不同子目录里,当我们需要测试内容越来越多的时候,就会需要这么做。

+

但因为测试的特殊性,rust会把每个集成测试的文件编译成独立的包来隔离作用域,模拟用户实际的使用环境,这就意味着我们以前在src目录下管理文件的方法并不完全适用于tests目录了。

+

例如,我们需要编写一个common.rs文件,并且编写一个setup函数,这个函数将会用在多个不同的测试文件里使用,如

+
pub fn setup(){
// 一些测试所需要初始化的数据
a = 1;
a
}
+ +

我们执行cargo test时会有以下输出:

+
     Running tests\common.rs (target\debug\deps\common-15055e88a26e37ec.exe)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
+ +

可以发现,即便我们没有在common.rs里写任何测试函数,它依旧会将它作为测试文件执行,并输出无意义的running 0 tests。这明显不是我们所希望的,那如何解决呢?

+

我们可以使用mod.rs文件,把common.rs的文件内容移到tests/common/mod.rs里面,这样的意思是让Rust把common视作一个模块,而不是集成测试文件。

+

于是,我们就可以通过mod关键字引入common模块并使用其中的函数,例如

+
// tests/integration_test.rs
use adder;

mod common;

#[test]
fn add_two() {
common::setup();
assert_eq!(adder::add(2, 2), 4);
}
+ +

此时再运行cargo test,就不会出现common相关的测试输出了。

+

二进制包的集成测试

如果我们的项目只有src/main.rs而没有src/lib.rs的话,是没有办法在tests中进行集成测试的,因为只有把代码用lib.rs文件指定为一个代码包crate,才能把函数暴露给其他包来使用,而main.rs对应的是二进制包,只能单独执行自己。

+

所以Rust的二进制项目通常会把逻辑编写在src/lib.rs里,main.rs只对lib.rs的内容进行简单的调用。

+

总结

没什么好总结的,下一章写项目去了。

+]]>
+ + 编程/语言 + + + Rust + +
【Rust 学习记录】10. 泛型、trait与生命周期 /2023/05/08/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%9110-%E6%B3%9B%E5%9E%8B%E3%80%81trait%E4%B8%8E%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F/ @@ -252,209 +423,38 @@ - 【Rust 学习记录】11. 编写自动化测试 - /2023/05/09/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%9111-%E7%BC%96%E5%86%99%E8%87%AA%E5%8A%A8%E5%8C%96%E6%B5%8B%E8%AF%95/ + 【Rust 学习记录】12. 编写一个命令行程序 + /2023/05/12/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%9112-%E7%BC%96%E5%86%99%E4%B8%80%E4%B8%AA%E5%91%BD%E4%BB%A4%E8%A1%8C%E7%A8%8B%E5%BA%8F/ -

这一章讲的就是怎么在Rust编写单元测试代码,这一部分的思想不仅适用于Rust,在绝大多数语言都是有用武之地的

- -

如何编写测试

测试代码的构成

构成

通用测试代码通常包括三个部分

-
    -
  1. 准备所需的数据或者前置状态
  2. -
  3. 调用需要测试的代码
  4. -
  5. 使用断言,判断运行结果是否和我们期望的一致
  6. -
-

在Rust中,有专门用于编写测试代码的相关功能,包含test属性,测试宏,should_panic属性等等

-

在最简单的情况下,Rust中的测试就是一个标注有test属性的函数。只需要将#[test]添加到函数的关键字fn上,就能使用cargo test命令来运行测试。

-

测试命令会构建一个可执行文件,调用所有标注了test的函数,生成相关报告。

-
-

PS:

-

属性是一种修饰代码的一种元数据,例如之前为了输出结构体时,加入的#[derive(Debug)]就是一个属性,声明属性后,会为下面的代码自动生成一些实现,如#[derive(Debug)]修饰结构体时,就会为结构体生成Debug trait的实现

+

本章节我们将开始学习编写一个小项目——开发一个能够和文件系统交互并处理命令行输入、输出的工具

-

初次尝试

接下来我们就试试怎么测试

-

首先新建一个名为adder的项目cargo new adder --lib(–lib指生成lib.rs文件)

-

可能是版本比较新,lib.rs里直接生成有了以下代码

-
pub fn add(left: usize, right: usize) -> usize {
left + right
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
- -

我们先忽略一些没讲过的关键词,这段代码里我们定义了一个it_works函数,并标注为测试函数,然后使用断言判断add函数的结果是否正确的等于4。在了解了大概功能之后,我们直接运行测试看看

-
cargo test
Compiling adder v0.1.0 (E:\Code\rust\adder)
Finished test [unoptimized + debuginfo] target(s) in 0.29s
Running unittests src\lib.rs (target\debug\deps\adder-0a86cd050490705e.exe)

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
- -

对应的测试结果如上

-
    -
  • passed: 测试通过的函数数量,我们这里只有一个it_works函数,且测试通过,所以为1
  • -
  • failed: 测试失败的函数数量
  • -
  • ignored: 被标记为忽略的测试函数,后面会提
  • -
  • measured: Rust还提供了衡量函数性能的benchmark方法,不过编写书的时候似乎这部分还不完善,所以不会有讲解,想了解需要自行学习
  • -
  • filtered out:被过滤掉的测试函数
  • -
  • Doc-tests:文档测试,这是个很好用的特性,可以防止你在修改了函数之后,忘记修改自己的文档,保证文档能和实际代码同步。
  • -
-

测试时,每一个测试函数都是运行在独立的线程里的,所以发生panic时并不会影响其他的测试,我们可以写一个错误的函数看看

-
pub fn add(left: usize, right: usize) -> usize {
left + right
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn error() {
let result = add(3, 2);
assert_eq!(result, 4);
}

#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
+

基本功能实现

首先自然是新建一个项目,名为minigrep

+
cargo new minigrep
-

输出结果:

-
cargo test
Compiling adder v0.1.0 (E:\Code\rust\adder)
Finished test [unoptimized + debuginfo] target(s) in 0.24s
Running unittests src\lib.rs (target\debug\deps\adder-0a86cd050490705e.exe)

running 2 tests
test tests::it_works ... ok
test tests::error ... FAILED

failures:

---- tests::error stdout ----
thread 'tests::error' panicked at 'assertion failed: `(left == right)`
left: `5`,
right: `4`', src\lib.rs:18:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
tests::error

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`
+

实现这一个工具的首要任务自然是接收命令行的参数,例如我们要实现在一个文件里搜索字符串,就得在运行时接收两个参数,一个待搜索的字符串,一个搜索的文件名,例如

+
cargo run string filename.txt
-

可以看见,error的panic并不影响it_works的测试通过。

-

assert!宏

assert!

assert!宏主要的功能是用来确保某个值为true,所以常被用于测试中。如a>b等场景,返回的是一个bool值,就完美的符合assert!的使用场景,可以使用assert!进行测试,例如

-
pub fn cmp(a: i32, b: i32) -> bool {
if a>b{
true
}
else{
false
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn cmp_test(){
let result = cmp(3, 2);
assert!(result);
}
}
+

这种基础的功能自然是已经有一些现成的crate可以使用来实现这些功能的了,不过因为我们的主要目的是学习,所以接下来我们会从零开始实现这些功能。

+

读取参数值

这一部分我们需要用到标准库里的std::env::args函数,这个函数会返回一个命令行参数的迭代器,使得程序可以读取所有传递给它的命令行参数值,放到一个动态数组里。

+

用例:

+
use std::env;

fn main() {
let args: Vec<String> = env::args().collect();
println!("{:?}", args);
}
-

assert_eq!和assert_ne!

那如果返回值不是bool值呢?前面也出现过了,我们可以使用assert_eq!或者assert_ne!来断言两个值是否相等。

-

eq则对应的只有相等才能通过断言,ne则对应的只有不相等才能通过断言,用例见上面的add测试即可。

-

但是注意,assert_eq!和assert_ne!使用了==!=来实现是否相等的判断,也就意味着,传入这两个宏的参数是必须实现了PartialEq这个trait的。同时,我们可见错误的输出中会打印出详细的不相等原因,也就是说它还同时需要实现了Debug宏帮助打印输出。一般绝大部分参数都是满足要求的,自定义的结构体时需要注意。

-
-

之前提到过属性这个概念,会为你自动实现一些功能,实际上PartialEq和Debug作为可派生的宏,也内置了属性的实现,你只需要在自己定义的结构体上加上#[derive(PartialEq, Debug)],就能自动帮你实现这两个宏

-
-

自定义错误提示代码

上面我们说到assert_eq是会有详细输出的,告诉你怎么不相等了,帮助你排除bug,但普通的assert!只判断布尔值,所以没办法有详细的输出,这时候我们可以定制一个输出,使得错误提示更人性化一点。

-

如下:

-
pub fn cmp(a: i32, b: i32) -> bool {
if a>b{
true
}
else{
false
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn cmp_test(){
let a = 2;
let b = 3;
let result = cmp(a, b);
assert!(result, "{} is not bigger than {}", a, b);
}
}
+

这段代码简单来说就是通过env::args()读取命令行参数的迭代器,再通过collect()函数自动遍历迭代器把参数收集到一个动态数组里并返回。

+

接下来我们用终端运行代码,传入参数试试

+
cargo run hahha arg   
Compiling minigrep v0.1.0 (E:\Code\rust\minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 1.81s
Running `target\debug\minigrep.exe hahha arg`
["target\\debug\\minigrep.exe", "hahha", "arg"]
-

和一般不一样,我们不需要用什么格式化字符串的方法先格式化一个字符串,再传入这个字符串,断言支持直接使用格式化的语法。这段代码的输出如下

-
running 1 test
thread 'tests::cmp_test' panicked at '2 is not bigger than 3', src\lib.rs:23:9
stack backtrace:
+

这里的输出的第一个参数是当前运行的程序的二进制文件入口路径,这一功能主要方便程序员打印程序名称/路径,后续才是我们传入的参数字符串,一般情况下我们忽略第一个参数只处理后面两个即可。

+
use std::env;

fn main() {
let args: Vec<String> = env::args().collect();
println!("{:?}", args);

let query = &args[1];
let filename = &args[2];

println!("Searching for {}", query);
println!("In file {}", filename);
}
-

可见报错提示相对于单纯的panicked at更人性化了一些。

-

当然,自定义输出也支持在assert_eq和assert_ne里使用

-

should_panic

should_panic也是一个属性,用来测试代码是否能正确的在出错时发生panic。用例如下

-
pub fn positive_num(a: i32) -> i32 {
if a > 0 {
a
} else {
panic!("{} is not positive", a)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
#[should_panic]
fn pos_test(){
let a = -1;
positive_num(a);
}
}
+

接下来我们再运行以下这段代码,输出一切正常的话,第一步接收命令行参数的任务我们就完成啦!

+

读取文件

既然是要在文件内容里搜索指定字符串,自然要读取文件的内容了。但在这之前我们要先有一个文件,现在我们在项目的根目录下,新建一个poem.txt文件,内容就选择书上给的这首诗吧:

+
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.

How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!
-

这段代码用来检查一个数是否是正数,在不是时抛出panic,接下来我们使用#[should_panic]来检查程序是否正确的panic,这段代码运行测试通过没问题。

-

接下来我们修改一下a的值,让程序不抛出panic,看看会发生什么

-
running 1 test
test tests::pos_test - should panic ... FAILED

failures:

---- tests::pos_test stdout ----
note: test did not panic as expected

failures:
tests::pos_test

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `-p adder --lib`
+

接下来,我们就可以在代码里读取这个文件了,也很简单,用之前使用过的fs库即可,代码如下。

+
use std::env;
use std::fs;

fn main() {
let args: Vec<String> = env::args().collect();
println!("{:?}", args);

let query = &args[1];
let filename = &args[2];

println!("Searching for {}", query);
println!("In file {}", filename);

let contents = fs::read_to_string(filename).expect("Error in reading file");
println!("With text:\n{}", contents)
}
-

测试失败,告诉你pos_test没有按照预期发生panic。这个特性可以用来检查你的代码是否能正确的处理报错,发生panic以阻止程序进一步运行,产生不可预估的后果。

-

但是,单纯这么使用感觉有点含糊不清,因为程序发生panic的原因可能不是我们所预期的,假如其他一些我们不知道的原因抛出了panic,也会导致测试通过。所以我们可以添加一个可选参数expected,用来检查panic发生报错的输出信息里是否包含指定的文字。

-
#[test]
#[should_panic(expected = "positive")]
fn pos_test(){
let a = -1;
positive_num(a);
}
- -

这时候,should_panic就会检查发生的panic输出的报错信息是否包含”positive”这个字符串,如果是,才会测试通过,输出如下:

-
running 1 test
thread 'tests::pos_test' panicked at '-1 is not positive', src\lib.rs:9:9
stack backtrace:
- -

可见,输出中也包含了报错的信息,更人性化了。

-

使用Result编写测试

之前学习Result枚举的时候我们就知道了这东西是用来处理报错的,自然也就可以用来处理测试。使用时也很简单,我们只需要声明测试函数的返回值是Result,test命令就会自动根据Result的枚举结果来判断是否测试成功了。如:

-
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn it_works() -> Result<(), String>{
if 2+2 == 4 {
Ok(())
} else {
Err(String::from("two plus two does not equal four"))
}
}
}
- -

使用Result编写测试函数的主要优势是可以使用问号表达式进行错误捕获,更方便我们去编写一些复杂的测试函数,可以让函数在任一时刻有错误被捕获到时,就返回报错。

-

问号表达式的使用可见【Rust 学习记录】9. 错误处理的?运算符部分,这里就不再写代码举例了(主要书上没例子,我也懒的写)

-

控制测试的运行方式

这一部分主要是对cargo test命令的讲解,具体的运行方式,参数的使用等。

-

cargo test的参数统一需要写在--后面,也就是说你想要使用--help显示参数文档时,需要使用以下命令

-
cargo test -- --help
- -

并行或串行的执行代码

默认情况下,测试是多线程并发执行的,这可以使测试更快速的完成,且相互之间不会影响结果。但如果测试间有相互依赖关系,则需要串行执行。例如两个测试用例同时在操作一个文件,一个测试在写内容,一个测试在读内容时,则容易导致测试结果不合预期。

-

我们可以使用--test-threads=1来指定测试的线程数为1,即可实现串行执行,当然,你想执行的更快也可以指定更多的线程

-
cargo test -- --test-threads=1
- -

显示函数的输出

默认情况下,test命令会捕获所有测试成功时的输出,也就是说,对于测试成功的函数,即使你使用了println!打印输出,你也无法在控制台看见你的输出,因为它被test命令捕获吞掉了。

-

如果你想要在控制台显示你的输出,只需要用--nocapture设置不捕获输出即可

-
cargo test -- --nocapture
- -

只运行部分特定名称的测试

如果测试的函数越写越多,执行所有的测试可能很花时间,通常我们编写了一个新的功能并想进行测试的时候,我们只需要测试这一个功能就足够了,因此可以向test命令指定函数名称来进行测试。

-

如:

-
fn add_two(a: i32, b: i32) -> i32 {
a + b
}


#[cfg(test)]
mod tests {
use super::*;

#[test]
fn add_test_1() {
assert_eq!(add_two(1, 2), 3);
}

#[test]
fn add_test_2() {
assert_eq!(add_two(2, 2), 4);
}

#[test]
fn one_hundred() {
assert_eq!(100, 100);
}

}
- -

对于这段代码,我们只想测试one_hundred这个函数,只需要对test命令指定运行one_hundred即可

-
cargo test one_hundred
- -

输出如下:

-
running 1 test
test tests::one_hundred ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
- -

这里显示2 filtered out,代表有两个测试用例被我们过滤掉了。

-

当然,这个方法也并不是只能运行一个测试函数,也可以通过部分匹配的方法执行多个名称里包含相同字符串的测试函数,例如:

-
cargo test add        

running 2 tests
test tests::add_test_1 ... ok
test tests::add_test_2 ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
- -

我们使用cargo test add命令,则可以测试所有名字里带add的测试函数,忽略掉了one_hundred函数。

-

不过需要注意的是,这种方法一次只能使用一个参数进行匹配并测试,如果你想同时用多个规则匹配多类的测试函数,就需要用其他方法了。

-

通过显示指定来忽略某些测试

忽略部分测试函数

当有部分测试函数执行特别耗时时,我们不想每次测试都执行这个函数,我们就可以通过#[ignore]属性来显示指定忽略这个测试函数。如:

-
fn add_two(a: i32, b: i32) -> i32 {
a + b
}


#[cfg(test)]
mod tests {
use super::*;

#[test]
fn add_test_1() {
assert_eq!(add_two(1, 2), 3);
}

#[test]
#[ignore]
fn add_test_2() {
assert_eq!(add_two(2, 2), 4);
}

#[test]
fn one_hundred() {
assert_eq!(100, 100);
}

}
- -

此时我们直接执行cargo test,输出如下

-
running 3 tests
test tests::add_test_2 ... ignored
test tests::add_test_1 ... ok
test tests::one_hundred ... ok

test result: ok. 2 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s

Doc-tests adder
- -

可见add_test_2函数被忽略不执行了,提示1 ignored。

-

单独执行被忽略的测试函数

如果我们想单独执行这些被忽略的函数,则可以使用--ignored命令

-
cargo test -- --ignored

running 1 test
test tests::add_test_2 ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
- -

可见,只有add_test_2被执行了。

-

测试的组织结构

测试通常分为两类,单元测试和集成测试。单元测试小而专注,集中于测试一个私有接口或模块;集成测试则独立于代码库之外,正常的从外部调用公共接口,一次测试可能使用多个模块

-

单元测试

单元测试的目的在于将一小段代码单独隔离开来,快速确定代码结果是否符合预期。一般来说,单元测试的代码和需要测试的代码存放在同一文件中。同时也约定俗成的在每个源代码文件里都会新建一个tests模块来存放测试函数,并使用cfg(test)来标注。

-

测试模块和#[cfg(test)]

#[cfg(test)]旨在让Rust只在执行Cargo test命令的时候编译和运行这段代码,而在cargo build的时候剔除掉它们,只用于测试,节省编译时间与空间,使得我们可以更方便的把测试代码和源代码放在同一个文件里。(集成测试时一般不需要标注,因为集成测试一般是独立的一个文件)

-

我们之前编写的测试模块就使用了这个属性

-
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
- -

测试私有函数

是不是应该测试私有函数一直有争议,不管你觉得要不要,但Rust提供了方法供你方便的测试。

-
fn add(left: usize, right: usize) -> usize {
left + right
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
- -

以上代码中的add没有标注pub关键字,也就是私有的,但因为Rust的测试代码本身也属于Rust代码,所以可以通过use的方法把私有的函数引入当前作用域来测试,也就是对应的代码里的use super::*;

-

集成测试

集成测试通常是新建一个tests目录,只调用对外公开的那部分接口。

-

tests目录

tests目录需要和src文件夹并列,Cargo会自动在这个目录下面寻找测试文件。

-

现在,我们新建一个tests/integration_test.rs文件,保留之前的lib.rs代码(add函数如果改了私有记得改回公有),并编写测试代码。

-
use adder;

#[test]
fn add_two() {
assert_eq!(adder::add(2, 2), 4);
}
- -

集成测试就不需要#[cfg(test)]了,Rust有单独为tests目录做处理,不会build这个目录下的文件。

-

接下来,我们再执行以下Cargo test,看看输出

-
cargo test
Compiling adder v0.1.0 (E:\Code\rust\adder)
Finished test [unoptimized + debuginfo] target(s) in 0.32s
Running unittests src\lib.rs (target\debug\deps\adder-0a86cd050490705e.exe)

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Running tests\integration_test.rs (target\debug\deps\integration_test-57f19c149db40d76.exe)

running 1 test
test add_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
- -

我们可以看到输出里

-
    -
  1. 先输出了单元测试的结果,test tests::it_works … ok,每行输出一个单元测试结果
  2. -
  3. 再输出集成测试的结果,Running tests\integration_test.rs,表示正在测试哪个文件的测试模块,后续跟着这个文件的测试结果
  4. -
  5. 最后是文档测试
  6. -
-

当编写的测试代码越多,输出也就会越多越杂,所以我们也可以使用--test参数指定集成测试的文件名,单独进行测试。如:cargo test --test integration_test

-

在集成测试中使用子模块

测试模块也和普通模块差不多,可以把函数分解到不同文件不同子目录里,当我们需要测试内容越来越多的时候,就会需要这么做。

-

但因为测试的特殊性,rust会把每个集成测试的文件编译成独立的包来隔离作用域,模拟用户实际的使用环境,这就意味着我们以前在src目录下管理文件的方法并不完全适用于tests目录了。

-

例如,我们需要编写一个common.rs文件,并且编写一个setup函数,这个函数将会用在多个不同的测试文件里使用,如

-
pub fn setup(){
// 一些测试所需要初始化的数据
a = 1;
a
}
- -

我们执行cargo test时会有以下输出:

-
     Running tests\common.rs (target\debug\deps\common-15055e88a26e37ec.exe)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
- -

可以发现,即便我们没有在common.rs里写任何测试函数,它依旧会将它作为测试文件执行,并输出无意义的running 0 tests。这明显不是我们所希望的,那如何解决呢?

-

我们可以使用mod.rs文件,把common.rs的文件内容移到tests/common/mod.rs里面,这样的意思是让Rust把common视作一个模块,而不是集成测试文件。

-

于是,我们就可以通过mod关键字引入common模块并使用其中的函数,例如

-
// tests/integration_test.rs
use adder;

mod common;

#[test]
fn add_two() {
common::setup();
assert_eq!(adder::add(2, 2), 4);
}
- -

此时再运行cargo test,就不会出现common相关的测试输出了。

-

二进制包的集成测试

如果我们的项目只有src/main.rs而没有src/lib.rs的话,是没有办法在tests中进行集成测试的,因为只有把代码用lib.rs文件指定为一个代码包crate,才能把函数暴露给其他包来使用,而main.rs对应的是二进制包,只能单独执行自己。

-

所以Rust的二进制项目通常会把逻辑编写在src/lib.rs里,main.rs只对lib.rs的内容进行简单的调用。

-

总结

没什么好总结的,下一章写项目去了。

-]]>
- - 编程/语言 - - - Rust - -
- - 【Rust 学习记录】12. 编写一个命令行程序 - /2023/05/12/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%9112-%E7%BC%96%E5%86%99%E4%B8%80%E4%B8%AA%E5%91%BD%E4%BB%A4%E8%A1%8C%E7%A8%8B%E5%BA%8F/ - -

本章节我们将开始学习编写一个小项目——开发一个能够和文件系统交互并处理命令行输入、输出的工具

- -

基本功能实现

首先自然是新建一个项目,名为minigrep

-
cargo new minigrep
- -

实现这一个工具的首要任务自然是接收命令行的参数,例如我们要实现在一个文件里搜索字符串,就得在运行时接收两个参数,一个待搜索的字符串,一个搜索的文件名,例如

-
cargo run string filename.txt
- -

这种基础的功能自然是已经有一些现成的crate可以使用来实现这些功能的了,不过因为我们的主要目的是学习,所以接下来我们会从零开始实现这些功能。

-

读取参数值

这一部分我们需要用到标准库里的std::env::args函数,这个函数会返回一个命令行参数的迭代器,使得程序可以读取所有传递给它的命令行参数值,放到一个动态数组里。

-

用例:

-
use std::env;

fn main() {
let args: Vec<String> = env::args().collect();
println!("{:?}", args);
}
- -

这段代码简单来说就是通过env::args()读取命令行参数的迭代器,再通过collect()函数自动遍历迭代器把参数收集到一个动态数组里并返回。

-

接下来我们用终端运行代码,传入参数试试

-
cargo run hahha arg   
Compiling minigrep v0.1.0 (E:\Code\rust\minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 1.81s
Running `target\debug\minigrep.exe hahha arg`
["target\\debug\\minigrep.exe", "hahha", "arg"]
- -

这里的输出的第一个参数是当前运行的程序的二进制文件入口路径,这一功能主要方便程序员打印程序名称/路径,后续才是我们传入的参数字符串,一般情况下我们忽略第一个参数只处理后面两个即可。

-
use std::env;

fn main() {
let args: Vec<String> = env::args().collect();
println!("{:?}", args);

let query = &args[1];
let filename = &args[2];

println!("Searching for {}", query);
println!("In file {}", filename);
}
- -

接下来我们再运行以下这段代码,输出一切正常的话,第一步接收命令行参数的任务我们就完成啦!

-

读取文件

既然是要在文件内容里搜索指定字符串,自然要读取文件的内容了。但在这之前我们要先有一个文件,现在我们在项目的根目录下,新建一个poem.txt文件,内容就选择书上给的这首诗吧:

-
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.

How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!
- -

接下来,我们就可以在代码里读取这个文件了,也很简单,用之前使用过的fs库即可,代码如下。

-
use std::env;
use std::fs;

fn main() {
let args: Vec<String> = env::args().collect();
println!("{:?}", args);

let query = &args[1];
let filename = &args[2];

println!("Searching for {}", query);
println!("In file {}", filename);

let contents = fs::read_to_string(filename).expect("Error in reading file");
println!("With text:\n{}", contents)
}
- -

接下来,再运行以下这段代码,用命令行传递参数的方法传递我们的文件名poem.txt

-
cargo run bog poem.txt       
Compiling minigrep v0.1.0 (E:\Code\rust\minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.63s
Running `target\debug\minigrep.exe bog poem.txt`
["target\\debug\\minigrep.exe", "bog", "poem.txt"]
Searching for bog
In file poem.txt
With text:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.

How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!
+

接下来,再运行以下这段代码,用命令行传递参数的方法传递我们的文件名poem.txt

+
cargo run bog poem.txt       
Compiling minigrep v0.1.0 (E:\Code\rust\minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.63s
Running `target\debug\minigrep.exe bog poem.txt`
["target\\debug\\minigrep.exe", "bog", "poem.txt"]
Searching for bog
In file poem.txt
With text:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.

How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!

可见,顺利的输出了我们的文件内容,代码没有问题。

到此为止,我们的基本功能就实现完了。但目前为止,我们现在写的代码都一股脑的堆积在main.rs里,非常简单,但也不具备可扩展性以及可维护性,远远称不上一个”项目”。接下来我们就要用到之前学习的模块化管理的知识,重构一下我们的代码,让它变得更规范化。

@@ -914,286 +914,100 @@
- 【Rust 学习记录】5. 结构体 - /2023/03/30/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%915-%E7%BB%93%E6%9E%84%E4%BD%93/ - 定义及实例化方式

定义和创建实例

定义方法和 C++ 是一模一样了,详见代码

-
struct User{
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
+ 【Rust 学习记录】7. 包、单元包和模块 + /2023/04/01/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%917-%E5%8C%85%E3%80%81%E5%8D%95%E5%85%83%E5%8C%85%E5%92%8C%E6%A8%A1%E5%9D%97/ + 包与单元包
    +
  1. 单元包(Crate):单元包可以被用来生成二进制的程序或者库;单元包的入口文件称为单元包的入口节点
  2. +
  3. 包(Package):一个包由一个或多个单元包集合而成,用 Cargo.toml 描述包内的单元包怎么构建;一个包最多也拥有一个库单元包;包可以有多个二进制单元包;包必须至少拥有一个单元包,可以是库单元包,也可以是二进制单元包。
  4. +
+

举个例子,我们一直用的cargo new test命令就是用来生成一个名为 test 包的,其中我们的代码文件 src/main.rs 就是 test 包下的一个单元包,叫 main,代码文件就是这个单元包的根节点,这类代码编译后可以生成一个二进制可执行文件,所以也叫二进制单元包。我们也可以通过不断的在 src 目录下写代码,创建更多的二进制单元包。

+

书上并没有介绍我更关心的库单元包,只是它举了个文件名例子src/lib.rs。后面再看。

+

同时,每个单元包内的代码都有自己的作用域(和 c++ 的命名空间相当),用来避免命名冲突。也就是我们之前 use 了 rand 包后,需要指定rand::Rng才能使用Rng模块一样。这样可以避免 Rng 这个名字和你自己定义的 Rng 结构冲突,你可以更随心所欲的命名。

+

模块

模块可以提供私有/公共的权限管理功能,也就是熟知的 private 和 public

+

模块的定义

我们现在 src 文件夹下创建一个lib.rs文件,新建一个库单元包,然后再编写代码

+
mod front_of_house{
mod hosting{
fn add_to_waitlist(){

}
fn seat_at_table(){

}
}

mod serving{
fn take_order(){

}
fn serve_order(){

}
fn take_payment(){

}
}
}
-

实例化方法稍显不同,方法和定义差不多,指名道姓的赋值,优势是不用对应顺序,可读性强。访问方法就还是传统的点运算符

-
struct User{
username: String,
email: String,
sign_in_count: u64,
active: bool,
}

fn main() {
let user1 = User{
email: String::from("test@1.com"),
username: String::from("haha"),
sign_in_count: 1,
active: true,
};
println!("user1: {}", user1.email);
}
+

代码解释

+
    +
  1. mod关键字:mod 关键字用来定义一个模块,我们定义了一个名为 front_of_house 的模块,用来管理餐厅的前厅部分,并且前厅部分又分为服务客户的服务员,处理订单的前台等,所以我们在模块下又定义了两个模块 hosting, serving
  2. +
  3. 接着我们以模块来对代码进行分组,在不同模块下定义了对应的功能函数
  4. +
+

这段代码就相当于我们构建了一个单元包,单元包里包含了一个模块,模块内有两个模块,而两个模块又有多个函数,构成了一个树的层级结构。

+

包管理架构

+

模块的调用

我们可以通过绝对路径和相对路径的方式来调用这个层级结构的某个函数。

+

Rust 通过 :: 符号来构建访问路径,当我们想调用 hostting模块下的 add_to_waitlist 函数,可以用以下方式(这段代码是暂时编译不通过的)

+
mod front_of_house{
mod hosting{
fn add_to_waitlist(){

}
fn seat_at_table(){

}
}

mod serving{
fn take_order(){

}
fn serve_order(){

}
fn take_payment(){

}
}
}

pub fn eat_at_restaurant() {
crate::front_of_house::hosting::add_to_waitlist();

front_of_house::hosting::add_to_waitlist();
}
-

同理,结构体也有可变与不可变一说,可变结构体,结构体所有变量可变,不可变结构体,结构体所有变量不可变,Rust 没有让结构体部分变量可变,部分不可变的说法

-

一些语法糖

同名参数对应赋值

如果每次都要写一个 email: xxxx, username: xxxx 好像有点麻烦是吗?Rust 提供了一个简单的方法,当变量名和结构体内的字段名完全一样的时候,会对应赋值(有一说一,这个设计挺有想法的,语法糖+1)

-

所以我们可以很轻松的给 User 写一个创建用的函数,这样就可以实现带默认值,轻松的构建结构体实例了

-
struct User{
username: String,
email: String,
sign_in_count: u64,
active: bool,
}

fn create_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
// 不写分号返回 User 变量
}
fn main() {
let user1 = create_user(String::from("hah@haha.com"), String::from("haha"));
println!("user1: {}", user1.email);
}
+
    +
  1. 绝对路径:crate::front_of_house::hosting::add_to_waitlist(); 从指定的入口文件开始访问到函数
  2. +
  3. 相对路径:front_of_house::hosting::add_to_waitlist(); 用相对路径来访问当前函数同级下的函数,这里的eat_at_restaurantfront_of_house同级
  4. +
+

绝对路径和相对路径的优缺点也不用多说了吧,当可能需要同步移动两个模块的时候,相对路径好,单独移动一个模块的时候用绝对路径好。视情况而定就好

+

访问权限

刚说了上面的代码是编译不通过的,我们编译一下代码,看看为什么不通过,

+
error[E0603]: module `hosting` is private
-

用之前的实例构造现在的实例

在一些情况下其实结构体内的实例都不需要怎么变动,就像上面的例子里,sign_in_countactive 参数都是采用同一个默认值来赋给所有实例的,那有没有一些方法能简化这种情况的代码书写呢?有的。

-
fn main() {
let user1 = create_user(String::from("hah@haha.com"), String::from("haha"));
let user2 = User {
email: String::from("user2@haha.com"),
username: String::from("user2"),
..user1
};
println!("user2: {}", user2.active);
}
+

报错,hosting 是私有的

+

也就是说,Rust 里所有的条目,在没有特意声明的情况下,默认都是私有的,权限这块的规则大概是:父级模块无法访问私有的子模块条目,子模块可以访问父级模块的私有条目,同级之间可以互相访问。

+

但是注意,公有的模块不代表模块里的字段公有。例如

+
mod front_of_house{
pub mod hosting{
fn add_to_waitlist(){

}
fn seat_at_table(){

}
}

mod serving{
fn take_order(){

}
fn serve_order(){

}
fn take_payment(){

}
}
}

pub fn eat_at_restaurant() {
crate::front_of_house::hosting::add_to_waitlist();

front_of_house::hosting::add_to_waitlist();
}
-

..user1 就代表了剩下的值都和 user1 一样,把 user1 里对应字段的值赋给 user2 即可。(这个感觉不如函数封装性好吧,但可能也看情况)

-

元组结构体——没有字段名的结构体

其实就相当于给元组命个名字,适用于很多不需要给字段命名的情况下,例如颜色的RGB,大家都懂是吧,就用一个三元组命名 Color 就好了。定义方式如下

-
struct Color(i8, i8, i8);

fn main() {
let black = Color(0, 0, 0);
println!("user2: {}", black.0);
}
+

这里我们用pub关键字声明了hosting模块公有,但实际仍然会编译错误。因为这一次声明,只是声明了父级的 front_of_house 模块可以访问 hosting 模块了,但父级 hosting 模块却依旧不能访问它的私有字段 add_to_waitlist

+

也就是说,父级条目是不是公有的并不影响它内部的条目的公有或私有状态

+

所以要代码能编译通过很简单,我们把 add_to_waitlist 也公开就行。

+

可能有人想问,为什么 front_of_house 没有公开也能访问?因为 front_of_house 和 eat_at_restaurant 是同级的,具有互相访问的权限。

+

super 关键字

super 关键字和python一样,用来查找到父模块的路径,属于相对路径的一种。应该不用多说,用一个代码来当例子吧

+
mod front_of_house{
pub mod hosting{
pub fn add_to_waitlist(){

}
fn seat_at_table(){

}
}

mod serving{
fn take_order(){
super::hosting::add_to_waitlist(); // 通过相对路径访问到add_to_waitlist
}
fn serve_order(){

}
fn take_payment(){

}
}
}
-

值得一提的是,定义成结构体后也是用点运算符访问变量,而不是 [] 运算符了

-

空结构体——没有字段的结构体

当你想创建一个空结构体的时候,也是不会报错的,原理说是和空元组相似,然后在某些方面会有用,后续再介绍。

-

我也不太懂,就不多说了,后面再看吧。

-

结构体的所有权

在上面举的这个例子中

-
struct User{
username: String,
email: String,
sign_in_count: u64,
active: bool,
}

fn main() {
let user1 = User{
email: String::from("test@1.com"),
username: String::from("haha"),
sign_in_count: 1,
active: true,
};
println!("user1: {}", user1.email);
}
+

结构体和枚举类型的公有

结构体

结构体的权限和模块基本相同,也就是父级模块的公有,不影响子字段的公有或私有

+
mod back_of_house{
pub struct Breakfast{
pub toast: String,
seasonal_fruit: String,
}
impl Breakfast{
pub fn summer(toast: &str) -> Breakfast{
Breakfast{
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}

pub fn eat_at_restaurant() {
let mut meal = back_of_house::Breakfast::summer("Rye");
meal.toast = String::from("Wheat");
println!("I'd like {} toast please", meal.toast);
println!("I'd like {} fruit please", meal.seasonal_fruit);
}
-

我们定义的所有字段都是具有值的所有权的,所以结构体实例能具有所有字段数据的所有权,能伴随着自己直到离开作用域,但也有不具有所有权的定义方式

-
struct User{
username: &str,
email: &str,
sign_in_count: u64,
active: bool,
}

fn main() {
let user1 = User{
email: String::from("test@1.com"),
username: String::from("haha"),
sign_in_count: 1,
active: true,
};
println!("user1: {}", user1.email);
}
+

这里,我们首先定义了一个后厨模块 back_of_house,定义了一个早餐结构体 Breakfast,包含吐司和水果两个变量,一个公有一个私有,然后实现了一个结构体内的函数 summer,用来创建一个包含指定吐司和水果 Breakfast 结构体。

+

后续,我们在 eat_at_restaurant 通过相对路径创建了一个可变的 Breakfast 结构体,并试图访问结构体的字段。

+

通过观察报错就可以知道,公有的 meal.toast 可以正常的访问和修改,但私有的 meal.seasonal_fruit 无法访问,也就无从谈起修改了。

+

值得一提的是,因为 Breakfast 有一个私有字段,所以如果我们不定义一个子级的 summer 函数,我们甚至不能创建一个 Breakfast 结构体,因为我们没办法在外部给 seasonal_fruit 赋值。

+

枚举类型

但枚举类型不一样,枚举公有后,所有字段都公有了,因为枚举类型这东西必须全公有才好用,一个公有一个私有,没有什么意义,例如说你 match 总要做个完整性检查吧?字段都不能全部完全访问,何谈完整的处理?所以一半私有一半公有是没有意义的。

+
mod back_of_house{
pub struct Breakfast{
pub toast: String,
seasonal_fruit: String,
}
impl Breakfast{
pub fn summer(toast: &str) -> Breakfast{
Breakfast{
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}

pub enum Appetizer{
Soup,
Salad,
}
}

pub fn eat_at_restaurant() {
let a = back_of_house::Appetizer::Soup;
let b = back_of_house::Appetizer::Salad;
}
-

但这种方式现在是没办法定义通过的,报错提示需要指定生命周期,这个涉及到了生命周期,所以就放到后面介绍了。

-

初试trait——为结构体增加更多有用的功能

说实话我不知道这个trait是什么意思,大概查了一下,是 特性(性状)的意思,用来定义一个类型可能和其他类型共享的功能,或许差不多相当于和 python 里的 xxx 差不多吗?但看样子还能自己定义的样子。先不管吧,大概了解一下概念,先学着。

-

打印结构体

用过 python 的应该都知道 python 类有个 __str__ 函数,可以定义一个类的字符串格式,方便输出成人类能查看的格式,而不是一串地址,Rust 的结构体也有这个功能—— Display

-

我们可以先跑一下这段代码

-
struct rectangle{
w: u32,
h: u32
}

fn main() {
let rect = rectangle{w: 10, h: 20};
println!("rectangle is {}", rect);
}
+

这里我们新加了一个枚举类型前菜 Appetizer,字段没有声明为公有,但依旧可以正常访问。

+

use关键字

use的基本使用

如果我们要多次调用模块里的函数,如果一直要写一大串的路径似乎有点麻烦。use 关键字就是用来简化这个步骤的。原理就叫:引入作用域

+
// 还是前面的代码,前厅部分,为了美观省略了
use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
-

可以看到一个报错

-
= help: the trait `std::fmt::Display` is not implemented for `rectangle`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
+

这里我们就用 use 关键字,用绝对路径的方法把 hosting 这个字段引入了当前作用域,这样 hosting 就可以作为本地的字段,直接使用就行了,当然,这样引入后自然也会和本地的 hosting 字段冲突,你不能再定义一个叫 hosting 的东西了。

+

或许有人想问 use 相对路径行不?可以,可以自己试试。

+
+

PS:在书上的版本还需要在相对路径前加入 self 字段来指定当前所在的作用域,但它提到了有些开发者在视图去掉这个字段。我的版本下没加也编译通过了,看来他们成功了。

+
+

后面书上还提到了一个代码规范,虽然我不会这么写,但似乎 csdn 的博客上有不少人确实会这样喜欢省事,我简单拉出来提一下。

+

有人可能会问,上面的代码为什么只 use 到了 hosting,既然只用 add_to_waitlist 函数为什么不直接 use 到 add_to_waitlist 函数呢?

+

原因很简单,我们需要告诉所有看这段代码的人,当然也包括自己,这个函数并不是在本地定义的,而是引用了哪里的一个包/模块里面的函数,既增加了一定的可读性,也防止了一部分同名。毕竟很多最底层的字段命名,一般都是十分通用的名字。这是一个好习惯。

+

pub use

use 字段通常是以私有的方式,引入当前作用域,也就是说,你引入之后,也只是在当前 use 的作用域生效,在其地方是不生效的,依旧要通过路径访问。pub use 字段就是解决这个问题的,让其他代码也能简单的导入代码。

+

这个的应用场景主要是,你为了自己更方便的管理自己的代码,所以分了很多层级结构,但其实其他代码并不是很在乎你这块代码的很多结构,就比如餐厅的例子,顾客不需要在乎你的前厅,你的后厨,顾客只在乎来餐厅吃饭的几个步骤,坐座位,点单,上菜,吃。所以你就可以使用 pub use,把这几个步骤从前厅后厨里导出来,方便顾客使用。

+

例子到后面跨文件的部分再举吧。

+

嵌套路径use

当我们使用包内的多个模块的时候,每个都写一行 use 会占用很多地方,这时候就可以用嵌套路径的方法。用标准包为例子

+
use std::cmp::Ordering;
use std::io;

// 下面的写法和上面等价
use std::{cmp::Ordering, io};
-

the trait std::fmt::Display is not implemented for rectangle 意思就是这个结构体还没实现 Display 这个方法,也就是说,println! 这个宏在输出的时候,还会调用一下类型的格式化函数,来进行指定的输出,之前我们用的基本类型都是默认实现了 Display 方法的,而这个 rectangle 是我们自己定义的,没有 Display 方法,println! 就不知道怎么格式化了,所以就报错。

-

但除了这个还有一个有意思的提示 in format strings you may be able to use {:?} (or {:#?} for pretty-print) instead 。这句话告诉我们,可以用 {:?}{:#?} 来整一个漂亮的输出?啥意思?写一下吧~

-
= help: the trait `Debug` is not implemented for `rectangle`
= note: add `#[derive(Debug)]` to `rectangle` or manually `impl Debug for rectangle`
+

也就是用花括号把同级的模块括起来一起导入,逗号分开。

+

如果我导入了一个模块,又想导入模块下面的一个函数呢?可以用 self 代表当前路径

+
use std::io;
use std::io::Write;

// 下面的写法和上面等价
use std::{self, Write};
-

被骗了,还是报错。但我们发现了另一个有意思的 trait—— Debug 。也就是说,Rust 对格式化的方法区分了两种,Debug 是专门面向开发者调试时的输出用格式。我超,什么是现代化语法啊(后仰)。这种特性真能派上不少用途。

-

提示里说,要么添加 [derive(Debug)] 要么自己实现一个 Debug 我们可以先添加一下这个试试

-
#[derive(Debug)]
struct rectangle{
w: u32,
h: u32
}

fn main() {
let rect = rectangle{w: 10, h: 20};
println!("rectangle is {:?}", rect);
}
+

如果我想导入一个模块的所有字段的?可以用统配符*,就相当于 import * 吧。是个坏文明,因为这样你就不知道你写的字段是不是和包内的字段重名了,别用。

+
use std::io::*;
-

添加在函数头,进行一个 Debug 注解就可以了,这次运行就不会报错了。我们来看看输出是怎样的

-
rectangle is rectangle { w: 10, h: 20 }
+

将模块拆分为不同文件

之前说过了模块的层级目录对吧,我们可以把这个层级目录转换为对应的文件层级目录,实现不同文件对应不同模块。

+

用之前写的代码 front_of_house->hosting->add_to_waitlist 为例子。

+

首先,我们需要在我们的库单元包声明一个前台模块。

+

lib.rs

mod front_of_house;

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
-

可见 Rust 标准的 Debug 格式化输出就是 结构体名{所有字段名: 对应的值},嗯,挺不错的,不用自己手动一个一个输出了。我们再来看看之前提示里提到的另一个 {:#?}

-
rectangle is rectangle {
w: 10,
h: 20,
}
+

然后,对应的再在 lib.rs 的同级目录下新建一个 front_of_house.rs 文件,对应模块的入口。声明 hosting 模块。

+

front_of_house.rs

pub mod hosting;
-

这个输出会更好看点,有了一定的排版,对于复杂的结构体会更有可读性。

-

好,书上的 trait 介绍就到这里结束了(是不是把一开始的 Format 忘了),它说到第 10 章的时候会更详细的介绍 trait 的时候,可以通过像这种对结构体进行 trait 注解的方式提供很多功能,包括自带的,甚至可以自己自定义,确实期待。

-

方法

结构体,或者说类,当然不能少了函数,书上对普通函数和结构体里的函数区分了一下概念,结构体内的函数叫方法,因为方法的定义有局限性,例如参数要有self,只能定义在结构体或trait之类的地方,属于是一个子集吧。我们也严谨点,区分一下吧。

-

定义

#[derive(Debug)]
struct Rectangle{
w: u32,
h: u32
}
impl Rectangle {
fn area(&self) -> u32 {
self.w * self.h
}
}

fn main() {
let rect = Rectangle{w: 10, h: 20};
println!("rectangle's area is {:#?}", rect.area());
}
- -

(吐槽:这里把 rectangle 的首字母大写了,因为 Rust 的编译器居然会警告我的命名不规范,牛)

-

可以看见,方法和函数定义差不多,也是用 fn 来定义,指定哪个函数里的话倒是比较意外,居然不是写在 Rectangle 定义的花括号里,而是另开一个 impl Rectangle 再来定义方法。另外,Rust 方法和 Python 也有点相似之处,也是通过 self 来指代当前实例,self 可以用三种方式来定义

-
    -
  1. &self :不可变引用,这个是最常见的,我们只要读取数据,什么也不干,所以不需要用到所有权,也最方便
  2. -
  3. self:获取所有权,应该最不常见,有时方法需要用来转换self类型的话,需要用到所有权,获取所有权后再进行返回;如果不返回的话,所有权在调用完方法就被回收了,实例就销毁了。
  4. -
  5. &mul self:可变引用,也没什么好说的,就是有时要改变实例内字段的值时会用。
  6. -
-

然后书上介绍了 Rust 为什么没有 -> 运算符的问题,我没太看懂,就简述一下我的理解,具体感兴趣可以自行看书或查阅资料。

-

C++在对于一个指针类型的结构体变量里,需要对变量进行一次解引用,也就是 ractangle->area()= (*rectangle).area() 所以额外定义了一个 -> 运算符写起来方便些。而 Rust 里,self的类型被显式定义了,所以编译器可以自动的根据你定义的 self 类型,去自动推理出 self 是否需要自动引用还是解引用,所以就不需要 -> 运算符了。

-

关联函数

impl 块里,除了带 self 的方法之外,Rust 还允许在块里定义不含 self 的函数,这些函数因为和结构体有关联,又不太需要 self 所以称为关联函数。和 Python 的 @staticmethod 差不多吧

-

这里用一个定义函数来举例

-
#[derive(Debug)]
struct Rectangle{
w: u32,
h: u32
}
impl Rectangle {
fn area(self) -> u32 {
self.w * self.h
}
fn square(size: u32) -> Rectangle {
Rectangle { w: size, h: size }
}
}

fn main() {
let rect = Rectangle{w: 10, h: 20};
println!("rectangle's area is {:#?}", rect.area());
println!("rectangle is {:#?}", Rectangle::square(3));
}
- -

使用也就是指定命名空间调用就可以了。

-

多个impl块

使用多个 impl 块也是合法的,可以编译通过。书上说后面会有应用场景介绍,那就后面再看吧,目前感觉还派不上用场?

-
#[derive(Debug)]
struct Rectangle{
w: u32,
h: u32
}
impl Rectangle {
fn area(self) -> u32 {
self.w * self.h
}
}
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle{w: size, h: size}
}
}


fn main() {
let rect = Rectangle{w: 10, h: 20};
println!("rectangle's area is {:#?}", rect.area());
println!("rectangle is {:#?}", Rectangle::square(3));
}
- -
-

本章到此结束!没什么好总结的,是比较基础的,也很重要的一部分,下一章继续干。

-]]>
- - 编程/语言 - - - Rust - -
- - 【Rust 学习记录】6. 枚举与模式匹配 - /2023/03/30/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%916-%E6%9E%9A%E4%B8%BE%E4%B8%8E%E6%A8%A1%E5%BC%8F%E5%8C%B9%E9%85%8D/ - 定义枚举

例子:IP地址,只有 IPV4, IPV6 两个模式。

-

所以我们可以通过枚举类型的方式来描述 IP 地址的类型。

-
enum IpAddKind{
IPV4,
IPV6
}

fn main() {
let four = IpAddKind::IPV4;
let six = IpAddKind::IPV6;
}
- -

这里也就两个点:

-
    -
  1. 使用 enum 关键字就可以完成一个枚举类型的定义。
  2. -
  3. 访问枚举类型的值是通过命名空间的方式来实现的,而不是点运算符。
  4. -
-

接下来再谈论一个实际的问题:这里的枚举类型只定义了两个类别,没有办法对应实际的IP地址,怎么办?

-

一般情况下,我们首先想到的肯定是用结构体,一个字段存储类型,一个字段存储地址对吧。但是 Rust 有一个非常方便的特性:关联的数据可以直接嵌入到枚举类型里

-
enum IpAddKind{
IPV4(String),
IPV6(String)
}

fn main() {
let local = IpAddKind::IPV4(String::from("127.0.0.1"));
let loopback = IpAddKind::IPV6(String::from("::1"));
}
- -

厉害吧。不过好像结构体也能做是吧?不完全是,枚举类型有一个结构体做不到的功能。

-

枚举类型可以为每个类型值指定一个类型,例如 IPV4 通常是四个数字来表示,而 IPV6 则不同,那么我们可以使用四个数字来描述 IPV4 ,使用字符串来描述 IPV6

-
enum IpAddKind{
IPV4(u8, u8, u8, u8),
IPV6(String)
}
- -

这个时候,把类型和内容拆分成两个字段来描述的结构体,就没有办法实现了,只能固定一种类型。

-

另外,IPV4和IPV6因为很常用,所以在标准库里其实就有定义好了一套枚举类型。它的定义方法是这样的。

-

img

-

也就是先用两个结构体描述一下具体的 IPV4和IPV6 ,然后再定义一个枚举类型,把这两个结构体嵌入到枚举类型里。

-

另外,枚举类型还有对于结构体另外的优势是,我们可以轻松的定义一个用于处理多个数据类型的函数

-

例如,我们可以像上面官方一样,用两个结构体去描述 IPV4和IPV6,但如果我们要定义一个函数,同时处理IP地址的话,就不知道该指定传入参数的类型是 IPV4还是IPV6了,但我们定义一个枚举类型 IpAddr ,就可以轻松的定义函数的传入类型为 IpAddr,然后可以同时处理两个结构体的数据。

-

Option——一个常用的枚举类型

Option 枚举类型定义了值可能不存在的情况,或者可以说是其他语言的空值 Null 。本来就很常用了,但这个类型在 Rust 里更有用,因为编译器可以自动检查我们是不是妥善的处理了所有应该被处理的情况

-

Rust 并没有像其他语言一样,有空值 Null 或者 None 的概念。书上说这是个错误的设计方法,因为当你定义了一个空值,在后续可能没有给他赋予其他值就进行使用的话,就会触发问题。这种设计理念可以帮助人们更方便的去实现一套系统,但也给系统埋下了更多的隐患。

-

Rust 结合了一下这个理念,觉得空值是有意义的,触发空值问题的本质是实现的措施的问题,所以提供了一个具有类似概念的枚举类型——Option. 标准库里的定义是这样的

-
enum Option<T>{
Some(T),
None,
}
- -

Option 是被预导入的,因为它很有用,所以我们不用 use 来导入它。所以我们可以不用指定命名空间,使用 Some 或者 None 关键词, 是后面学的语法,用来代表任意类型

-

我们可以用这个方法来定义一些变量

-
fn main() {
let some_number = Some(5);
let some_string = Some("hello");
let absent_number = None;
}
- -

这段编译是不通过的,这也体现了 Option 相对于普通空值的优势。

-

我们来简单的通过几个例子来了解一下 Option 的设计理念

-
    -
  1. Option和 T 是两个不同的类型
  2. -
-
fn main() {
let a = Some(5);
let b = 5;
println!("{}", a+b);
}
- -

可以看到这段代码里,虽然 a, b 同样都是 i32 但一个是被Some持有,那它们就是不同的类型,编译器无法理解不同类型相加,所以这就意味着什么。当我们对于一个可能有值,可能没值的变量,我们就要去使用 Option 枚举,一旦使用了Option枚举,我们再要实际使用它的时候,就要显式的把这个 Some 类型的变量转换到 i32 的变量,再去使用。相当于强迫你去对这个变量编写一段代码,这样就避免了你不经过处理就使用,结果使用到空值的情况。

-

当然,因为是枚举类型,我们也可以方便的用 match 来处理不同的情况,例如有值时怎么样,没值时怎么样等等。接下来就开始介绍一下 match

-

match运算符

match 是一个很好用的运算符,它除了提高可读性外,也方便编译器对你的代码进行安全检查,确保你对所有可能的情况都进行了处理。

-
enum Coin{
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}

fn main() {
let my_coin = Coin::Dime;
println!("The value of my coin is {} cents", value_in_cents(my_coin));
}
- -

这就是一个简单的,输入硬币类型,返回硬币价值的代码。相对于 if-else 表达式,match 的第一个优势自然就是可读性。你 if-else 需要想方设法凑一个 bool 值,可能用字符串,可能用字典,可能用数字下标,可能用结构体什么的,但 match 枚举类型就不用,可以为任意你想要的类型定义一个名字,直接用,直接返回任意值,结构还更紧凑好看。

-

另一个优势就是,match 运算符可以匹配枚举变量的部分值。

-

美国的25美分硬币里,很多个州都有不同的设计,也只有25美分的硬币有这个特点,所以我们可以给25美分加一个值:州,对应不同的设计

-
#[derive(Debug)]
enum UsState{
Alabama,
Alaska,
}

enum Coin{
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("The state of coin is {:?}", state);
25
}
}
}

fn main() {
let my_coin = Coin::Quarter(UsState::Alaska);
println!("The value of my coin is {} cents", value_in_cents(my_coin));
}
- -

这里我们用到了之前的 Debug 注解,让编译器自动格式化枚举类型的输出,然后在match匹配里,提取了 Quarter 枚举类型里的值,命名为 state,然后输出。

-

匹配Option

接下来我们就可以综合一下上面学到的东西,用match来处理一下空和非空的情况了。

-
fn pluse_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}

fn main() {
let num = Some(5);
let num2 = pluse_one(None);
}
- -

这就是一个典型的例子:

-
    -
  1. 当为空的时候,什么也不干
  2. -
  3. 当不为空的时候,取出值,进行处理
  4. -
-

这也就是 match 的相对于 if-else 的优势:可以以一个更简单,更紧凑,可读性更高的方式,进行模式匹配,进行对应值的处理。接下来就介绍 match 在安全性方面的优势:让编译器帮你检查是否处理完了所有情况。

-

必须穷举所有可能

我们来试着漏处理为空值的情况。

-
fn pluse_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}

fn main() {
let num = Some(5);
}
- -

编译器马上报错:non-exhaustive patterns: None not covered 你没有覆盖处理空值!

-

这一段代码同时体现了 match 的优势和 Option 实现空值的优势。也就是你必须要处理所有情况,保证没有一点逻辑遗漏的地方,Option也依赖于 match 的这个特性,强迫你处理空值的情况,杜绝了大部分程序员只写一部分 if-else 结果就因为漏了部分情况没处理,导致程序 crash 的问题。

-

_ 通配符

但有时候我明明不用处理所有情况,但每次都要写上很麻烦怎么办?没事,Rust 作为一个现代语言还是准备了一些语法糖,那就是通配符 _ 相当于 if-else 里面的 else。

-
fn pluse_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
_ => None
}
}

fn main() {
let num = Some(5);
}
- -

_ 可以匹配所有类型,所以得放最后,用于处理前面没有被处理过的情况。

-

但有时候我指向对一种情况处理怎么办?还是有点麻烦吧,Rust 还准备了 if let 语句

-

简单控制流 if-let

我们就继续用美分硬币来举例吧,之前写的代码方便些。例如我们只想知道这个硬币是不是 Penny。

-
enum Coin{
Penny,
Nickel,
Dime,
Quarter,
}

fn main() {
let my_coin = Coin::Penny;
match my_coin{
Coin::Penny => println!("Lucky Penny!"),
_ => println!("Not a penny"),
};
}
- -

用 match 的话,就是要定义一个硬币,匹配这个硬币,再写个通配符来检测其他情况,标准流程是吧。但这样写或许繁琐了些,if let 就是这么一个一步到位的语法

-
enum Coin{
Penny,
Nickel,
Dime,
Quarter,
}

fn main() {
let my_coin = Coin::Penny;
if let Coin::Penny = my_coin {
println!("Lucky penny!");
}
}
- -

if let Coin::Penny 就是要匹配的值,my_coin也就是你传入的值,用等号分隔开,如果两者相等的话,执行花括号内的语句。

-

当然,也可以用else

-
enum Coin{
Penny,
Nickel,
Dime,
Quarter,
}

fn main() {
let my_coin = Coin::Penny;
if let Coin::Penny = my_coin {
println!("Lucky penny!");
}else{
println!("Not a lucky penny!");
}
}
- -

这样就和上面的 match 完全匹配了。

-

虽然 if let 让代码写起来更简单了,但也失去了 match 的安全检查,所以是一个取舍吧。个人偏向于使用match,说实话我觉得这 if let 写起来有点别扭,感觉书写逻辑不太舒服。

-

这应该只是一个偶尔可能用到的糖,哪时候你写烦了match,可以想想原来还有 If let 这么个东西

-
-

第六章也就搞定了,大致总结一下,这章主要就是讲了一下1. 枚举类型对于结构体的优势所在,2. match对于if-else的优势所在,3. 独特的空值设计理念

-

今天就到这里吧,后面还有7,8,9章三章的基础通用编程知识,学完就差不多进入进阶阶段了。

-]]>
- - 编程/语言 - - - Rust - -
- - 【Rust 学习记录】7. 包、单元包和模块 - /2023/04/01/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%917-%E5%8C%85%E3%80%81%E5%8D%95%E5%85%83%E5%8C%85%E5%92%8C%E6%A8%A1%E5%9D%97/ - 包与单元包
    -
  1. 单元包(Crate):单元包可以被用来生成二进制的程序或者库;单元包的入口文件称为单元包的入口节点
  2. -
  3. 包(Package):一个包由一个或多个单元包集合而成,用 Cargo.toml 描述包内的单元包怎么构建;一个包最多也拥有一个库单元包;包可以有多个二进制单元包;包必须至少拥有一个单元包,可以是库单元包,也可以是二进制单元包。
  4. -
-

举个例子,我们一直用的cargo new test命令就是用来生成一个名为 test 包的,其中我们的代码文件 src/main.rs 就是 test 包下的一个单元包,叫 main,代码文件就是这个单元包的根节点,这类代码编译后可以生成一个二进制可执行文件,所以也叫二进制单元包。我们也可以通过不断的在 src 目录下写代码,创建更多的二进制单元包。

-

书上并没有介绍我更关心的库单元包,只是它举了个文件名例子src/lib.rs。后面再看。

-

同时,每个单元包内的代码都有自己的作用域(和 c++ 的命名空间相当),用来避免命名冲突。也就是我们之前 use 了 rand 包后,需要指定rand::Rng才能使用Rng模块一样。这样可以避免 Rng 这个名字和你自己定义的 Rng 结构冲突,你可以更随心所欲的命名。

-

模块

模块可以提供私有/公共的权限管理功能,也就是熟知的 private 和 public

-

模块的定义

我们现在 src 文件夹下创建一个lib.rs文件,新建一个库单元包,然后再编写代码

-
mod front_of_house{
mod hosting{
fn add_to_waitlist(){

}
fn seat_at_table(){

}
}

mod serving{
fn take_order(){

}
fn serve_order(){

}
fn take_payment(){

}
}
}
- -

代码解释

-
    -
  1. mod关键字:mod 关键字用来定义一个模块,我们定义了一个名为 front_of_house 的模块,用来管理餐厅的前厅部分,并且前厅部分又分为服务客户的服务员,处理订单的前台等,所以我们在模块下又定义了两个模块 hosting, serving
  2. -
  3. 接着我们以模块来对代码进行分组,在不同模块下定义了对应的功能函数
  4. -
-

这段代码就相当于我们构建了一个单元包,单元包里包含了一个模块,模块内有两个模块,而两个模块又有多个函数,构成了一个树的层级结构。

-

包管理架构

-

模块的调用

我们可以通过绝对路径和相对路径的方式来调用这个层级结构的某个函数。

-

Rust 通过 :: 符号来构建访问路径,当我们想调用 hostting模块下的 add_to_waitlist 函数,可以用以下方式(这段代码是暂时编译不通过的)

-
mod front_of_house{
mod hosting{
fn add_to_waitlist(){

}
fn seat_at_table(){

}
}

mod serving{
fn take_order(){

}
fn serve_order(){

}
fn take_payment(){

}
}
}

pub fn eat_at_restaurant() {
crate::front_of_house::hosting::add_to_waitlist();

front_of_house::hosting::add_to_waitlist();
}
- -
    -
  1. 绝对路径:crate::front_of_house::hosting::add_to_waitlist(); 从指定的入口文件开始访问到函数
  2. -
  3. 相对路径:front_of_house::hosting::add_to_waitlist(); 用相对路径来访问当前函数同级下的函数,这里的eat_at_restaurantfront_of_house同级
  4. -
-

绝对路径和相对路径的优缺点也不用多说了吧,当可能需要同步移动两个模块的时候,相对路径好,单独移动一个模块的时候用绝对路径好。视情况而定就好

-

访问权限

刚说了上面的代码是编译不通过的,我们编译一下代码,看看为什么不通过,

-
error[E0603]: module `hosting` is private
- -

报错,hosting 是私有的

-

也就是说,Rust 里所有的条目,在没有特意声明的情况下,默认都是私有的,权限这块的规则大概是:父级模块无法访问私有的子模块条目,子模块可以访问父级模块的私有条目,同级之间可以互相访问。

-

但是注意,公有的模块不代表模块里的字段公有。例如

-
mod front_of_house{
pub mod hosting{
fn add_to_waitlist(){

}
fn seat_at_table(){

}
}

mod serving{
fn take_order(){

}
fn serve_order(){

}
fn take_payment(){

}
}
}

pub fn eat_at_restaurant() {
crate::front_of_house::hosting::add_to_waitlist();

front_of_house::hosting::add_to_waitlist();
}
- -

这里我们用pub关键字声明了hosting模块公有,但实际仍然会编译错误。因为这一次声明,只是声明了父级的 front_of_house 模块可以访问 hosting 模块了,但父级 hosting 模块却依旧不能访问它的私有字段 add_to_waitlist

-

也就是说,父级条目是不是公有的并不影响它内部的条目的公有或私有状态

-

所以要代码能编译通过很简单,我们把 add_to_waitlist 也公开就行。

-

可能有人想问,为什么 front_of_house 没有公开也能访问?因为 front_of_house 和 eat_at_restaurant 是同级的,具有互相访问的权限。

-

super 关键字

super 关键字和python一样,用来查找到父模块的路径,属于相对路径的一种。应该不用多说,用一个代码来当例子吧

-
mod front_of_house{
pub mod hosting{
pub fn add_to_waitlist(){

}
fn seat_at_table(){

}
}

mod serving{
fn take_order(){
super::hosting::add_to_waitlist(); // 通过相对路径访问到add_to_waitlist
}
fn serve_order(){

}
fn take_payment(){

}
}
}
- -

结构体和枚举类型的公有

结构体

结构体的权限和模块基本相同,也就是父级模块的公有,不影响子字段的公有或私有

-
mod back_of_house{
pub struct Breakfast{
pub toast: String,
seasonal_fruit: String,
}
impl Breakfast{
pub fn summer(toast: &str) -> Breakfast{
Breakfast{
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}

pub fn eat_at_restaurant() {
let mut meal = back_of_house::Breakfast::summer("Rye");
meal.toast = String::from("Wheat");
println!("I'd like {} toast please", meal.toast);
println!("I'd like {} fruit please", meal.seasonal_fruit);
}
- -

这里,我们首先定义了一个后厨模块 back_of_house,定义了一个早餐结构体 Breakfast,包含吐司和水果两个变量,一个公有一个私有,然后实现了一个结构体内的函数 summer,用来创建一个包含指定吐司和水果 Breakfast 结构体。

-

后续,我们在 eat_at_restaurant 通过相对路径创建了一个可变的 Breakfast 结构体,并试图访问结构体的字段。

-

通过观察报错就可以知道,公有的 meal.toast 可以正常的访问和修改,但私有的 meal.seasonal_fruit 无法访问,也就无从谈起修改了。

-

值得一提的是,因为 Breakfast 有一个私有字段,所以如果我们不定义一个子级的 summer 函数,我们甚至不能创建一个 Breakfast 结构体,因为我们没办法在外部给 seasonal_fruit 赋值。

-

枚举类型

但枚举类型不一样,枚举公有后,所有字段都公有了,因为枚举类型这东西必须全公有才好用,一个公有一个私有,没有什么意义,例如说你 match 总要做个完整性检查吧?字段都不能全部完全访问,何谈完整的处理?所以一半私有一半公有是没有意义的。

-
mod back_of_house{
pub struct Breakfast{
pub toast: String,
seasonal_fruit: String,
}
impl Breakfast{
pub fn summer(toast: &str) -> Breakfast{
Breakfast{
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}

pub enum Appetizer{
Soup,
Salad,
}
}

pub fn eat_at_restaurant() {
let a = back_of_house::Appetizer::Soup;
let b = back_of_house::Appetizer::Salad;
}
- -

这里我们新加了一个枚举类型前菜 Appetizer,字段没有声明为公有,但依旧可以正常访问。

-

use关键字

use的基本使用

如果我们要多次调用模块里的函数,如果一直要写一大串的路径似乎有点麻烦。use 关键字就是用来简化这个步骤的。原理就叫:引入作用域

-
// 还是前面的代码,前厅部分,为了美观省略了
use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
- -

这里我们就用 use 关键字,用绝对路径的方法把 hosting 这个字段引入了当前作用域,这样 hosting 就可以作为本地的字段,直接使用就行了,当然,这样引入后自然也会和本地的 hosting 字段冲突,你不能再定义一个叫 hosting 的东西了。

-

或许有人想问 use 相对路径行不?可以,可以自己试试。

-
-

PS:在书上的版本还需要在相对路径前加入 self 字段来指定当前所在的作用域,但它提到了有些开发者在视图去掉这个字段。我的版本下没加也编译通过了,看来他们成功了。

-
-

后面书上还提到了一个代码规范,虽然我不会这么写,但似乎 csdn 的博客上有不少人确实会这样喜欢省事,我简单拉出来提一下。

-

有人可能会问,上面的代码为什么只 use 到了 hosting,既然只用 add_to_waitlist 函数为什么不直接 use 到 add_to_waitlist 函数呢?

-

原因很简单,我们需要告诉所有看这段代码的人,当然也包括自己,这个函数并不是在本地定义的,而是引用了哪里的一个包/模块里面的函数,既增加了一定的可读性,也防止了一部分同名。毕竟很多最底层的字段命名,一般都是十分通用的名字。这是一个好习惯。

-

pub use

use 字段通常是以私有的方式,引入当前作用域,也就是说,你引入之后,也只是在当前 use 的作用域生效,在其地方是不生效的,依旧要通过路径访问。pub use 字段就是解决这个问题的,让其他代码也能简单的导入代码。

-

这个的应用场景主要是,你为了自己更方便的管理自己的代码,所以分了很多层级结构,但其实其他代码并不是很在乎你这块代码的很多结构,就比如餐厅的例子,顾客不需要在乎你的前厅,你的后厨,顾客只在乎来餐厅吃饭的几个步骤,坐座位,点单,上菜,吃。所以你就可以使用 pub use,把这几个步骤从前厅后厨里导出来,方便顾客使用。

-

例子到后面跨文件的部分再举吧。

-

嵌套路径use

当我们使用包内的多个模块的时候,每个都写一行 use 会占用很多地方,这时候就可以用嵌套路径的方法。用标准包为例子

-
use std::cmp::Ordering;
use std::io;

// 下面的写法和上面等价
use std::{cmp::Ordering, io};
- -

也就是用花括号把同级的模块括起来一起导入,逗号分开。

-

如果我导入了一个模块,又想导入模块下面的一个函数呢?可以用 self 代表当前路径

-
use std::io;
use std::io::Write;

// 下面的写法和上面等价
use std::{self, Write};
- -

如果我想导入一个模块的所有字段的?可以用统配符*,就相当于 import * 吧。是个坏文明,因为这样你就不知道你写的字段是不是和包内的字段重名了,别用。

-
use std::io::*;
- -

将模块拆分为不同文件

之前说过了模块的层级目录对吧,我们可以把这个层级目录转换为对应的文件层级目录,实现不同文件对应不同模块。

-

用之前写的代码 front_of_house->hosting->add_to_waitlist 为例子。

-

首先,我们需要在我们的库单元包声明一个前台模块。

-

lib.rs

mod front_of_house;

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
- -

然后,对应的再在 lib.rs 的同级目录下新建一个 front_of_house.rs 文件,对应模块的入口。声明 hosting 模块。

-

front_of_house.rs

pub mod hosting;
- -

然后,我们再在同级下新建一个文件夹,front_of_house,对应front_of_house模块下面的模块代码入口。再在 front_of_house 文件夹下新建一个 hosting.rs。添加进我们的 add_to_waitlist 代码

-

front_of_house/hosting.rs

pub fn add_to_waitlist(){
println!("add_to_waitlist");
}
+

然后,我们再在同级下新建一个文件夹,front_of_house,对应front_of_house模块下面的模块代码入口。再在 front_of_house 文件夹下新建一个 hosting.rs。添加进我们的 add_to_waitlist 代码

+

front_of_house/hosting.rs

pub fn add_to_waitlist(){
println!("add_to_waitlist");
}

修改完后,再 cargo run 一下,编译没有问题,依旧能够通过路径正常访问到 add_to_waitlist 函数~这就是模块层级路径和文件层级路径的对应关系,只需要声明模块后,给出一个模块的对应入口文件,就可以通过这种方式拆分为多个模块文件啦。

好,我们再来试试跨文件使用代码吧。

@@ -1379,65 +1193,82 @@
- 【Rust学习记录】1. 开发环境搭建 - /2023/03/24/%E3%80%90Rust%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%911-%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA/ - -

本系列参考书目:《RUST权威指南》

+ 【Rust 学习记录】5. 结构体 + /2023/03/30/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%915-%E7%BB%93%E6%9E%84%E4%BD%93/ + 定义及实例化方式

定义和创建实例

定义方法和 C++ 是一模一样了,详见代码

+
struct User{
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
- +

实例化方法稍显不同,方法和定义差不多,指名道姓的赋值,优势是不用对应顺序,可读性强。访问方法就还是传统的点运算符

+
struct User{
username: String,
email: String,
sign_in_count: u64,
active: bool,
}

fn main() {
let user1 = User{
email: String::from("test@1.com"),
username: String::from("haha"),
sign_in_count: 1,
active: true,
};
println!("user1: {}", user1.email);
}
-

久闻rust大名,趁着研究生还是学习生涯,抽空出来试试这个所谓的既高效,又安全的语言

-

环境安装与搭建

Rust安装

windows的安装很傻瓜式,只要进入官网 ,下载最新版本的安装包,按照进行安装即可。期间可能会提示让你安装VS的工具,照着安装即可。

-

安装完成后,就可以通过 rustc --version 来测试是否安装成功了。

-

同时,安装后rust也会在本地生成一份文档,可以通过 rustup doc 用浏览器打开。

-

Hello World!

接下来进行一个开启一门全新语言的必备仪式,hello world!

-
    -
  1. 新建一个文件命名为 hello.rs rs就是rust代码文件的后缀
  2. -
  3. 编写代码
  4. -
-
fn main(){
println!("Hello, world!");
}
+

同理,结构体也有可变与不可变一说,可变结构体,结构体所有变量可变,不可变结构体,结构体所有变量不可变,Rust 没有让结构体部分变量可变,部分不可变的说法

+

一些语法糖

同名参数对应赋值

如果每次都要写一个 email: xxxx, username: xxxx 好像有点麻烦是吗?Rust 提供了一个简单的方法,当变量名和结构体内的字段名完全一样的时候,会对应赋值(有一说一,这个设计挺有想法的,语法糖+1)

+

所以我们可以很轻松的给 User 写一个创建用的函数,这样就可以实现带默认值,轻松的构建结构体实例了

+
struct User{
username: String,
email: String,
sign_in_count: u64,
active: bool,
}

fn create_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
// 不写分号返回 User 变量
}
fn main() {
let user1 = create_user(String::from("hah@haha.com"), String::from("haha"));
println!("user1: {}", user1.email);
}
-
    -
  1. 编译程序 rustc hello.rs ,接下来就会看到文件夹内生成了一个可执行文件 hello.exe,执行它,就能看到你的 hello world啦~跨过这一步,我们就是一名rust开发者了
  2. -
-

稍微了解一下这个函数

-

首先fn就是function的缩写,用来定义函数;其他语法都和c++差不多,唯独不一样的就是函数println后带着一个感叹号,这似乎是rust里的宏机制,这个后面再学学吧。

-

Cargo的安装与使用

创建项目

书上写的是,cargo是构建+包管理的工具,或许可以理解成,ubuntu里的apt+cmake的组合?

-

在安装rust时就已经同步安装了cargo,我们可以通过 cargo --version 来检查是否正确安装

-
cargo new hello_cargo
+

用之前的实例构造现在的实例

在一些情况下其实结构体内的实例都不需要怎么变动,就像上面的例子里,sign_in_countactive 参数都是采用同一个默认值来赋给所有实例的,那有没有一些方法能简化这种情况的代码书写呢?有的。

+
fn main() {
let user1 = create_user(String::from("hah@haha.com"), String::from("haha"));
let user2 = User {
email: String::from("user2@haha.com"),
username: String::from("user2"),
..user1
};
println!("user2: {}", user2.active);
}
-

这一个命令会使用cargo来创建新的项目架构,我们进入创建的hello_cargo文件夹,可以看到cargo帮我们初始化了一个Cargo.toml文件,一个src目录,放置了一个main.rs的源文件,还有一个.gitignore文件,说明它还帮我们配置了git版本管理

-

关于toml文件,我们可以用编辑器打开它

-
[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
+

..user1 就代表了剩下的值都和 user1 一样,把 user1 里对应字段的值赋给 user2 即可。(这个感觉不如函数封装性好吧,但可能也看情况)

+

元组结构体——没有字段名的结构体

其实就相当于给元组命个名字,适用于很多不需要给字段命名的情况下,例如颜色的RGB,大家都懂是吧,就用一个三元组命名 Color 就好了。定义方式如下

+
struct Color(i8, i8, i8);

fn main() {
let black = Color(0, 0, 0);
println!("user2: {}", black.0);
}
-

我们可以看到这么些东西,上面自然就是你所创建的代码包的相关信息,而dependencies就是我们代码需要依赖的第三方包信息,不过我们一个hello world不需要什么,所以现在这里是空的。

-

也就是说,这是个rs项目的标准配置文件

-

我们再打开main.rs,就会发现cargo已经帮我们写好了一个hello world程序~

-

编译、运行与发布

之前用rustc来编译单个文件,现在我们来使用cargo构建整个项目,首先回到根目录 hello_cargo,输入命令

-
cargo build
+

值得一提的是,定义成结构体后也是用点运算符访问变量,而不是 [] 运算符了

+

空结构体——没有字段的结构体

当你想创建一个空结构体的时候,也是不会报错的,原理说是和空元组相似,然后在某些方面会有用,后续再介绍。

+

我也不太懂,就不多说了,后面再看吧。

+

结构体的所有权

在上面举的这个例子中

+
struct User{
username: String,
email: String,
sign_in_count: u64,
active: bool,
}

fn main() {
let user1 = User{
email: String::from("test@1.com"),
username: String::from("haha"),
sign_in_count: 1,
active: true,
};
println!("user1: {}", user1.email);
}
+ +

我们定义的所有字段都是具有值的所有权的,所以结构体实例能具有所有字段数据的所有权,能伴随着自己直到离开作用域,但也有不具有所有权的定义方式

+
struct User{
username: &str,
email: &str,
sign_in_count: u64,
active: bool,
}

fn main() {
let user1 = User{
email: String::from("test@1.com"),
username: String::from("haha"),
sign_in_count: 1,
active: true,
};
println!("user1: {}", user1.email);
}
+ +

但这种方式现在是没办法定义通过的,报错提示需要指定生命周期,这个涉及到了生命周期,所以就放到后面介绍了。

+

初试trait——为结构体增加更多有用的功能

说实话我不知道这个trait是什么意思,大概查了一下,是 特性(性状)的意思,用来定义一个类型可能和其他类型共享的功能,或许差不多相当于和 python 里的 xxx 差不多吗?但看样子还能自己定义的样子。先不管吧,大概了解一下概念,先学着。

+

打印结构体

用过 python 的应该都知道 python 类有个 __str__ 函数,可以定义一个类的字符串格式,方便输出成人类能查看的格式,而不是一串地址,Rust 的结构体也有这个功能—— Display

+

我们可以先跑一下这段代码

+
struct rectangle{
w: u32,
h: u32
}

fn main() {
let rect = rectangle{w: 10, h: 20};
println!("rectangle is {}", rect);
}
+ +

可以看到一个报错

+
= help: the trait `std::fmt::Display` is not implemented for `rectangle`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
+ +

the trait std::fmt::Display is not implemented for rectangle 意思就是这个结构体还没实现 Display 这个方法,也就是说,println! 这个宏在输出的时候,还会调用一下类型的格式化函数,来进行指定的输出,之前我们用的基本类型都是默认实现了 Display 方法的,而这个 rectangle 是我们自己定义的,没有 Display 方法,println! 就不知道怎么格式化了,所以就报错。

+

但除了这个还有一个有意思的提示 in format strings you may be able to use {:?} (or {:#?} for pretty-print) instead 。这句话告诉我们,可以用 {:?}{:#?} 来整一个漂亮的输出?啥意思?写一下吧~

+
= help: the trait `Debug` is not implemented for `rectangle`
= note: add `#[derive(Debug)]` to `rectangle` or manually `impl Debug for rectangle`
+ +

被骗了,还是报错。但我们发现了另一个有意思的 trait—— Debug 。也就是说,Rust 对格式化的方法区分了两种,Debug 是专门面向开发者调试时的输出用格式。我超,什么是现代化语法啊(后仰)。这种特性真能派上不少用途。

+

提示里说,要么添加 [derive(Debug)] 要么自己实现一个 Debug 我们可以先添加一下这个试试

+
#[derive(Debug)]
struct rectangle{
w: u32,
h: u32
}

fn main() {
let rect = rectangle{w: 10, h: 20};
println!("rectangle is {:?}", rect);
}
+ +

添加在函数头,进行一个 Debug 注解就可以了,这次运行就不会报错了。我们来看看输出是怎样的

+
rectangle is rectangle { w: 10, h: 20 }
+ +

可见 Rust 标准的 Debug 格式化输出就是 结构体名{所有字段名: 对应的值},嗯,挺不错的,不用自己手动一个一个输出了。我们再来看看之前提示里提到的另一个 {:#?}

+
rectangle is rectangle {
w: 10,
h: 20,
}
+ +

这个输出会更好看点,有了一定的排版,对于复杂的结构体会更有可读性。

+

好,书上的 trait 介绍就到这里结束了(是不是把一开始的 Format 忘了),它说到第 10 章的时候会更详细的介绍 trait 的时候,可以通过像这种对结构体进行 trait 注解的方式提供很多功能,包括自带的,甚至可以自己自定义,确实期待。

+

方法

结构体,或者说类,当然不能少了函数,书上对普通函数和结构体里的函数区分了一下概念,结构体内的函数叫方法,因为方法的定义有局限性,例如参数要有self,只能定义在结构体或trait之类的地方,属于是一个子集吧。我们也严谨点,区分一下吧。

+

定义

#[derive(Debug)]
struct Rectangle{
w: u32,
h: u32
}
impl Rectangle {
fn area(&self) -> u32 {
self.w * self.h
}
}

fn main() {
let rect = Rectangle{w: 10, h: 20};
println!("rectangle's area is {:#?}", rect.area());
}
+ +

(吐槽:这里把 rectangle 的首字母大写了,因为 Rust 的编译器居然会警告我的命名不规范,牛)

+

可以看见,方法和函数定义差不多,也是用 fn 来定义,指定哪个函数里的话倒是比较意外,居然不是写在 Rectangle 定义的花括号里,而是另开一个 impl Rectangle 再来定义方法。另外,Rust 方法和 Python 也有点相似之处,也是通过 self 来指代当前实例,self 可以用三种方式来定义

+
    +
  1. &self :不可变引用,这个是最常见的,我们只要读取数据,什么也不干,所以不需要用到所有权,也最方便
  2. +
  3. self:获取所有权,应该最不常见,有时方法需要用来转换self类型的话,需要用到所有权,获取所有权后再进行返回;如果不返回的话,所有权在调用完方法就被回收了,实例就销毁了。
  4. +
  5. &mul self:可变引用,也没什么好说的,就是有时要改变实例内字段的值时会用。
  6. +
+

然后书上介绍了 Rust 为什么没有 -> 运算符的问题,我没太看懂,就简述一下我的理解,具体感兴趣可以自行看书或查阅资料。

+

C++在对于一个指针类型的结构体变量里,需要对变量进行一次解引用,也就是 ractangle->area()= (*rectangle).area() 所以额外定义了一个 -> 运算符写起来方便些。而 Rust 里,self的类型被显式定义了,所以编译器可以自动的根据你定义的 self 类型,去自动推理出 self 是否需要自动引用还是解引用,所以就不需要 -> 运算符了。

+

关联函数

impl 块里,除了带 self 的方法之外,Rust 还允许在块里定义不含 self 的函数,这些函数因为和结构体有关联,又不太需要 self 所以称为关联函数。和 Python 的 @staticmethod 差不多吧

+

这里用一个定义函数来举例

+
#[derive(Debug)]
struct Rectangle{
w: u32,
h: u32
}
impl Rectangle {
fn area(self) -> u32 {
self.w * self.h
}
fn square(size: u32) -> Rectangle {
Rectangle { w: size, h: size }
}
}

fn main() {
let rect = Rectangle{w: 10, h: 20};
println!("rectangle's area is {:#?}", rect.area());
println!("rectangle is {:#?}", Rectangle::square(3));
}
+ +

使用也就是指定命名空间调用就可以了。

+

多个impl块

使用多个 impl 块也是合法的,可以编译通过。书上说后面会有应用场景介绍,那就后面再看吧,目前感觉还派不上用场?

+
#[derive(Debug)]
struct Rectangle{
w: u32,
h: u32
}
impl Rectangle {
fn area(self) -> u32 {
self.w * self.h
}
}
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle{w: size, h: size}
}
}


fn main() {
let rect = Rectangle{w: 10, h: 20};
println!("rectangle's area is {:#?}", rect.area());
println!("rectangle is {:#?}", Rectangle::square(3));
}
-

img

-

编译完成如上图,我们就可以发现,目录里生成了一个target文件夹,在 ./targe/debug 目录下,就可以找到我们编译成功的可执行文件了,同样运行它,就能看到 hello world了

-

PS:如果想要编译+运行,可以使用 cargo run 命令(若源代码未发生改变,cargo run不会重新进行构建,而是直接运行)

-

另外,cargo还有一个比较好用的命令 cargo check ,这个命令可以让你在编译大型程序的时候,免去漫长的编译等待,快速的知道代码能否完成编译(真的这么神吗,cmake编译一个小时opencv最后环境出错编译失败的我如此问道)


-

如果你已经准备好发布你的程序了,那么就可以用 cargo build --release 来编译代码,它会在 target/release 的目录下生成可执行文件,和debug不同的是,它会花更长的编译时间来优化你的代码,使代码有更好的运行性能。也就是说,普通的build侧重于快速的编译,让你调试程序,release build侧重于可执行文件的运行性能,用来交付给用户。

-

img

-

可以看到编译后有一个optimized的标签,表示是优化过的target,同时也少了debug info。(但编译速度更快了,应该就是我之前debug编译过一次,基于那个的基础上,又花了0.84s进行优化)

-

PS:同理,你想编译完直接测试,可以用 cargo run --release

-

这么看来,cargo应该就是rust构建项目的核心工具了。

-

到此为止,第一章结束。

+

本章到此结束!没什么好总结的,是比较基础的,也很重要的一部分,下一章继续干。

]]>
编程/语言 @@ -1559,6 +1390,175 @@
  • 一些基本的错误处理场景原则
  • 第10章开始就是 trait,泛型和生命周期了。

    +]]>
    + + 编程/语言 + + + Rust + +
    + + 【Rust学习记录】1. 开发环境搭建 + /2023/03/24/%E3%80%90Rust%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%911-%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA/ + +

    本系列参考书目:《RUST权威指南》

    + + + +

    久闻rust大名,趁着研究生还是学习生涯,抽空出来试试这个所谓的既高效,又安全的语言

    +

    环境安装与搭建

    Rust安装

    windows的安装很傻瓜式,只要进入官网 ,下载最新版本的安装包,按照进行安装即可。期间可能会提示让你安装VS的工具,照着安装即可。

    +

    安装完成后,就可以通过 rustc --version 来测试是否安装成功了。

    +

    同时,安装后rust也会在本地生成一份文档,可以通过 rustup doc 用浏览器打开。

    +

    Hello World!

    接下来进行一个开启一门全新语言的必备仪式,hello world!

    +
      +
    1. 新建一个文件命名为 hello.rs rs就是rust代码文件的后缀
    2. +
    3. 编写代码
    4. +
    +
    fn main(){
    println!("Hello, world!");
    }
    + +
      +
    1. 编译程序 rustc hello.rs ,接下来就会看到文件夹内生成了一个可执行文件 hello.exe,执行它,就能看到你的 hello world啦~跨过这一步,我们就是一名rust开发者了
    2. +
    +

    稍微了解一下这个函数

    +

    首先fn就是function的缩写,用来定义函数;其他语法都和c++差不多,唯独不一样的就是函数println后带着一个感叹号,这似乎是rust里的宏机制,这个后面再学学吧。

    +

    Cargo的安装与使用

    创建项目

    书上写的是,cargo是构建+包管理的工具,或许可以理解成,ubuntu里的apt+cmake的组合?

    +

    在安装rust时就已经同步安装了cargo,我们可以通过 cargo --version 来检查是否正确安装

    +
    cargo new hello_cargo
    + +

    这一个命令会使用cargo来创建新的项目架构,我们进入创建的hello_cargo文件夹,可以看到cargo帮我们初始化了一个Cargo.toml文件,一个src目录,放置了一个main.rs的源文件,还有一个.gitignore文件,说明它还帮我们配置了git版本管理

    +

    关于toml文件,我们可以用编辑器打开它

    +
    [package]
    name = "hello_cargo"
    version = "0.1.0"
    edition = "2021"

    # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

    [dependencies]
    + +

    我们可以看到这么些东西,上面自然就是你所创建的代码包的相关信息,而dependencies就是我们代码需要依赖的第三方包信息,不过我们一个hello world不需要什么,所以现在这里是空的。

    +

    也就是说,这是个rs项目的标准配置文件

    +

    我们再打开main.rs,就会发现cargo已经帮我们写好了一个hello world程序~

    +

    编译、运行与发布

    之前用rustc来编译单个文件,现在我们来使用cargo构建整个项目,首先回到根目录 hello_cargo,输入命令

    +
    cargo build
    + +

    img

    +

    编译完成如上图,我们就可以发现,目录里生成了一个target文件夹,在 ./targe/debug 目录下,就可以找到我们编译成功的可执行文件了,同样运行它,就能看到 hello world了

    +

    PS:如果想要编译+运行,可以使用 cargo run 命令(若源代码未发生改变,cargo run不会重新进行构建,而是直接运行)

    +

    另外,cargo还有一个比较好用的命令 cargo check ,这个命令可以让你在编译大型程序的时候,免去漫长的编译等待,快速的知道代码能否完成编译(真的这么神吗,cmake编译一个小时opencv最后环境出错编译失败的我如此问道)

    +
    +

    如果你已经准备好发布你的程序了,那么就可以用 cargo build --release 来编译代码,它会在 target/release 的目录下生成可执行文件,和debug不同的是,它会花更长的编译时间来优化你的代码,使代码有更好的运行性能。也就是说,普通的build侧重于快速的编译,让你调试程序,release build侧重于可执行文件的运行性能,用来交付给用户。

    +

    img

    +

    可以看到编译后有一个optimized的标签,表示是优化过的target,同时也少了debug info。(但编译速度更快了,应该就是我之前debug编译过一次,基于那个的基础上,又花了0.84s进行优化)

    +

    PS:同理,你想编译完直接测试,可以用 cargo run --release

    +

    这么看来,cargo应该就是rust构建项目的核心工具了。

    +

    到此为止,第一章结束。

    +]]>
    + + 编程/语言 + + + Rust + +
    + + 【Rust 学习记录】6. 枚举与模式匹配 + /2023/03/30/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%916-%E6%9E%9A%E4%B8%BE%E4%B8%8E%E6%A8%A1%E5%BC%8F%E5%8C%B9%E9%85%8D/ + 定义枚举

    例子:IP地址,只有 IPV4, IPV6 两个模式。

    +

    所以我们可以通过枚举类型的方式来描述 IP 地址的类型。

    +
    enum IpAddKind{
    IPV4,
    IPV6
    }

    fn main() {
    let four = IpAddKind::IPV4;
    let six = IpAddKind::IPV6;
    }
    + +

    这里也就两个点:

    +
      +
    1. 使用 enum 关键字就可以完成一个枚举类型的定义。
    2. +
    3. 访问枚举类型的值是通过命名空间的方式来实现的,而不是点运算符。
    4. +
    +

    接下来再谈论一个实际的问题:这里的枚举类型只定义了两个类别,没有办法对应实际的IP地址,怎么办?

    +

    一般情况下,我们首先想到的肯定是用结构体,一个字段存储类型,一个字段存储地址对吧。但是 Rust 有一个非常方便的特性:关联的数据可以直接嵌入到枚举类型里

    +
    enum IpAddKind{
    IPV4(String),
    IPV6(String)
    }

    fn main() {
    let local = IpAddKind::IPV4(String::from("127.0.0.1"));
    let loopback = IpAddKind::IPV6(String::from("::1"));
    }
    + +

    厉害吧。不过好像结构体也能做是吧?不完全是,枚举类型有一个结构体做不到的功能。

    +

    枚举类型可以为每个类型值指定一个类型,例如 IPV4 通常是四个数字来表示,而 IPV6 则不同,那么我们可以使用四个数字来描述 IPV4 ,使用字符串来描述 IPV6

    +
    enum IpAddKind{
    IPV4(u8, u8, u8, u8),
    IPV6(String)
    }
    + +

    这个时候,把类型和内容拆分成两个字段来描述的结构体,就没有办法实现了,只能固定一种类型。

    +

    另外,IPV4和IPV6因为很常用,所以在标准库里其实就有定义好了一套枚举类型。它的定义方法是这样的。

    +

    img

    +

    也就是先用两个结构体描述一下具体的 IPV4和IPV6 ,然后再定义一个枚举类型,把这两个结构体嵌入到枚举类型里。

    +

    另外,枚举类型还有对于结构体另外的优势是,我们可以轻松的定义一个用于处理多个数据类型的函数

    +

    例如,我们可以像上面官方一样,用两个结构体去描述 IPV4和IPV6,但如果我们要定义一个函数,同时处理IP地址的话,就不知道该指定传入参数的类型是 IPV4还是IPV6了,但我们定义一个枚举类型 IpAddr ,就可以轻松的定义函数的传入类型为 IpAddr,然后可以同时处理两个结构体的数据。

    +

    Option——一个常用的枚举类型

    Option 枚举类型定义了值可能不存在的情况,或者可以说是其他语言的空值 Null 。本来就很常用了,但这个类型在 Rust 里更有用,因为编译器可以自动检查我们是不是妥善的处理了所有应该被处理的情况

    +

    Rust 并没有像其他语言一样,有空值 Null 或者 None 的概念。书上说这是个错误的设计方法,因为当你定义了一个空值,在后续可能没有给他赋予其他值就进行使用的话,就会触发问题。这种设计理念可以帮助人们更方便的去实现一套系统,但也给系统埋下了更多的隐患。

    +

    Rust 结合了一下这个理念,觉得空值是有意义的,触发空值问题的本质是实现的措施的问题,所以提供了一个具有类似概念的枚举类型——Option. 标准库里的定义是这样的

    +
    enum Option<T>{
    Some(T),
    None,
    }
    + +

    Option 是被预导入的,因为它很有用,所以我们不用 use 来导入它。所以我们可以不用指定命名空间,使用 Some 或者 None 关键词, 是后面学的语法,用来代表任意类型

    +

    我们可以用这个方法来定义一些变量

    +
    fn main() {
    let some_number = Some(5);
    let some_string = Some("hello");
    let absent_number = None;
    }
    + +

    这段编译是不通过的,这也体现了 Option 相对于普通空值的优势。

    +

    我们来简单的通过几个例子来了解一下 Option 的设计理念

    +
      +
    1. Option和 T 是两个不同的类型
    2. +
    +
    fn main() {
    let a = Some(5);
    let b = 5;
    println!("{}", a+b);
    }
    + +

    可以看到这段代码里,虽然 a, b 同样都是 i32 但一个是被Some持有,那它们就是不同的类型,编译器无法理解不同类型相加,所以这就意味着什么。当我们对于一个可能有值,可能没值的变量,我们就要去使用 Option 枚举,一旦使用了Option枚举,我们再要实际使用它的时候,就要显式的把这个 Some 类型的变量转换到 i32 的变量,再去使用。相当于强迫你去对这个变量编写一段代码,这样就避免了你不经过处理就使用,结果使用到空值的情况。

    +

    当然,因为是枚举类型,我们也可以方便的用 match 来处理不同的情况,例如有值时怎么样,没值时怎么样等等。接下来就开始介绍一下 match

    +

    match运算符

    match 是一个很好用的运算符,它除了提高可读性外,也方便编译器对你的代码进行安全检查,确保你对所有可能的情况都进行了处理。

    +
    enum Coin{
    Penny,
    Nickel,
    Dime,
    Quarter,
    }
    fn value_in_cents(coin: Coin) -> u8 {
    match coin {
    Coin::Penny => 1,
    Coin::Nickel => 5,
    Coin::Dime => 10,
    Coin::Quarter => 25,
    }
    }

    fn main() {
    let my_coin = Coin::Dime;
    println!("The value of my coin is {} cents", value_in_cents(my_coin));
    }
    + +

    这就是一个简单的,输入硬币类型,返回硬币价值的代码。相对于 if-else 表达式,match 的第一个优势自然就是可读性。你 if-else 需要想方设法凑一个 bool 值,可能用字符串,可能用字典,可能用数字下标,可能用结构体什么的,但 match 枚举类型就不用,可以为任意你想要的类型定义一个名字,直接用,直接返回任意值,结构还更紧凑好看。

    +

    另一个优势就是,match 运算符可以匹配枚举变量的部分值。

    +

    美国的25美分硬币里,很多个州都有不同的设计,也只有25美分的硬币有这个特点,所以我们可以给25美分加一个值:州,对应不同的设计

    +
    #[derive(Debug)]
    enum UsState{
    Alabama,
    Alaska,
    }

    enum Coin{
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
    }
    fn value_in_cents(coin: Coin) -> u8 {
    match coin {
    Coin::Penny => 1,
    Coin::Nickel => 5,
    Coin::Dime => 10,
    Coin::Quarter(state) => {
    println!("The state of coin is {:?}", state);
    25
    }
    }
    }

    fn main() {
    let my_coin = Coin::Quarter(UsState::Alaska);
    println!("The value of my coin is {} cents", value_in_cents(my_coin));
    }
    + +

    这里我们用到了之前的 Debug 注解,让编译器自动格式化枚举类型的输出,然后在match匹配里,提取了 Quarter 枚举类型里的值,命名为 state,然后输出。

    +

    匹配Option

    接下来我们就可以综合一下上面学到的东西,用match来处理一下空和非空的情况了。

    +
    fn pluse_one(x: Option<i32>) -> Option<i32> {
    match x {
    None => None,
    Some(i) => Some(i + 1),
    }
    }

    fn main() {
    let num = Some(5);
    let num2 = pluse_one(None);
    }
    + +

    这就是一个典型的例子:

    +
      +
    1. 当为空的时候,什么也不干
    2. +
    3. 当不为空的时候,取出值,进行处理
    4. +
    +

    这也就是 match 的相对于 if-else 的优势:可以以一个更简单,更紧凑,可读性更高的方式,进行模式匹配,进行对应值的处理。接下来就介绍 match 在安全性方面的优势:让编译器帮你检查是否处理完了所有情况。

    +

    必须穷举所有可能

    我们来试着漏处理为空值的情况。

    +
    fn pluse_one(x: Option<i32>) -> Option<i32> {
    match x {
    Some(i) => Some(i + 1),
    }
    }

    fn main() {
    let num = Some(5);
    }
    + +

    编译器马上报错:non-exhaustive patterns: None not covered 你没有覆盖处理空值!

    +

    这一段代码同时体现了 match 的优势和 Option 实现空值的优势。也就是你必须要处理所有情况,保证没有一点逻辑遗漏的地方,Option也依赖于 match 的这个特性,强迫你处理空值的情况,杜绝了大部分程序员只写一部分 if-else 结果就因为漏了部分情况没处理,导致程序 crash 的问题。

    +

    _ 通配符

    但有时候我明明不用处理所有情况,但每次都要写上很麻烦怎么办?没事,Rust 作为一个现代语言还是准备了一些语法糖,那就是通配符 _ 相当于 if-else 里面的 else。

    +
    fn pluse_one(x: Option<i32>) -> Option<i32> {
    match x {
    Some(i) => Some(i + 1),
    _ => None
    }
    }

    fn main() {
    let num = Some(5);
    }
    + +

    _ 可以匹配所有类型,所以得放最后,用于处理前面没有被处理过的情况。

    +

    但有时候我指向对一种情况处理怎么办?还是有点麻烦吧,Rust 还准备了 if let 语句

    +

    简单控制流 if-let

    我们就继续用美分硬币来举例吧,之前写的代码方便些。例如我们只想知道这个硬币是不是 Penny。

    +
    enum Coin{
    Penny,
    Nickel,
    Dime,
    Quarter,
    }

    fn main() {
    let my_coin = Coin::Penny;
    match my_coin{
    Coin::Penny => println!("Lucky Penny!"),
    _ => println!("Not a penny"),
    };
    }
    + +

    用 match 的话,就是要定义一个硬币,匹配这个硬币,再写个通配符来检测其他情况,标准流程是吧。但这样写或许繁琐了些,if let 就是这么一个一步到位的语法

    +
    enum Coin{
    Penny,
    Nickel,
    Dime,
    Quarter,
    }

    fn main() {
    let my_coin = Coin::Penny;
    if let Coin::Penny = my_coin {
    println!("Lucky penny!");
    }
    }
    + +

    if let Coin::Penny 就是要匹配的值,my_coin也就是你传入的值,用等号分隔开,如果两者相等的话,执行花括号内的语句。

    +

    当然,也可以用else

    +
    enum Coin{
    Penny,
    Nickel,
    Dime,
    Quarter,
    }

    fn main() {
    let my_coin = Coin::Penny;
    if let Coin::Penny = my_coin {
    println!("Lucky penny!");
    }else{
    println!("Not a lucky penny!");
    }
    }
    + +

    这样就和上面的 match 完全匹配了。

    +

    虽然 if let 让代码写起来更简单了,但也失去了 match 的安全检查,所以是一个取舍吧。个人偏向于使用match,说实话我觉得这 if let 写起来有点别扭,感觉书写逻辑不太舒服。

    +

    这应该只是一个偶尔可能用到的糖,哪时候你写烦了match,可以想想原来还有 If let 这么个东西

    +
    +

    第六章也就搞定了,大致总结一下,这章主要就是讲了一下1. 枚举类型对于结构体的优势所在,2. match对于if-else的优势所在,3. 独特的空值设计理念

    +

    今天就到这里吧,后面还有7,8,9章三章的基础通用编程知识,学完就差不多进入进阶阶段了。

    ]]>
    编程/语言 @@ -1713,82 +1713,98 @@
    - 为你的hexo博客添加一个追番列表 - /2024/06/28/%E4%B8%BA%E4%BD%A0%E7%9A%84hexo%E5%8D%9A%E5%AE%A2%E6%B7%BB%E5%8A%A0%E4%B8%80%E4%B8%AA%E8%BF%BD%E7%95%AA%E5%88%97%E8%A1%A8/ - -

    本文基于插件hexo-bilibili-bangumi 编写,并修改为适配redefine主题的样式,最终结果示例见我的追番列表

    - - - -

    主要是闲来无事逛github的时候发现了hexo-bilibili-bangumi 这么一个插件,可以爬取bili/bangumi的数据并渲染为一个页面展示你的追番列表,整好我前段时间开始有了bangumi记录追番的习惯,所以想着上手用用。

    -

    如何使用

    正常来说,按照官方的readme操作完就可以上手使用了

    -
      -
    1. 安装插件

      -
      $ npm install hexo-bilibili-bangumi --save
      -
    2. -
    3. _config.yml配置文件里添加你的配置(以下配置为了确保一次正常运行,与官方示例不同,完整示例见官方):

      -
      bangumi: # 追番设置
      enable: true
      source: bangumi
      bgmInfoSource: 'bgmApi'
      path:
      vmid: 根据官方readme获取
      title: '追番列表'
      quote: '生命不息,追番不止!'
      show: 1
      lazyload: false
      srcValue: '__image__'
      lazyloadAttrName: 'data-src=__image__'
      loading:
      showMyComment: false
      pagination: false
      metaColor:
      color:
      webp:
      progress:
      extraOrder:
      order: latest
      coverMirror:
      -
    4. -
    5. 编译并生成静态文件

      -
      hexo c
      call hexo bangumi -u # 必须要在hexo g前添加这句,爬取数据
      call hexo g
      call hexo s
      -
    6. -
    7. 然后你就可以在/bangumis后缀的页面下看见你的追番列表页面啦。(如果修改了path的话,以你实际填写path为主)

      -
    8. -
    + 使用Sphinx为你的项目快速构建文档 + /2023/04/15/%E4%BD%BF%E7%94%A8Sphinx%E4%B8%BA%E4%BD%A0%E7%9A%84%E9%A1%B9%E7%9B%AE%E5%BF%AB%E9%80%9F%E6%9E%84%E5%BB%BA%E6%96%87%E6%A1%A3/ + 最近写了个软件,需要写个接口文档,看到别人项目的文档有不少都是托管在 Read the Docs 上的,于是搜了一下,Read the Docs 是一个托管平台,而这个平台的文档是基于 Sphinx 构建的,所以就学了一下,以此记录。

    +

    安装Sphinx

    很简单,用pip安装即可,尽量使用官方的源,国内源听说多少有点问题

    +
    pip install sphinx
    -
    -
    -

    附言

    +

    构建Sphinx项目

    快速构建

    推荐在项目的根目录构建一个文件夹docs来专门存放文档的源码,然后cd docs下构建源码

    +

    构建也非常简单,一行命令即可

    +
    sphinx-quickstart
    -
    -
    -

    实际配置过程中,lazyload选项容易和主题冲突,导致图片一直转圈;pagination选项也会有冲突,导致分页异常。因此上面的配置我都改成了默认关闭。

    +

    输入命令后,会提示是否要创建独立目录,选择y是即可,然后提示你填一些信息,包括项目名,作者名,语言等等,照实填写即可,语言是简体中文的话,填 zh_CN 即可

    +

    构建完毕后,如果之前选择的是y的话,我们就可以在目录下看到buildsource文件夹了,其中source文件夹就是存放项目源码的文件夹

    +

    source中包含了一个conf.py文件,用于填写项目配置,一个index.rst文件,是首页的源码。

    +

    若无特殊需求的话,直接根据rst格式编写你的代码,然后make html即可完成项目的构建。

    +

    但是为了写一个文档,又专门去学一个 rst 语法似乎有些不大合适,所以需要修改一下配置文件,让Sphinx支持大众都熟知的 markdown 语法。

    +

    配置文件

    markdown配置

    安装扩展

    首先,为了支持 markdown 语法,我们需要安装一个扩展插件myst-parser

    +
    pip install --upgrade myst-parser
    -
    -
    +

    添加扩展配置

    安装完成后,我们在conf.py文件内,修改一下extensions字段,引入扩展即可

    +
    extensions = ['myst_parser']
    -

    进阶

    因为默认的样式不太好看,以及其他配置和我当前使用的主题redefine有诸多冲突,因此需要进行一些修改才能正常使用,以下是我做的部分修改分享,也是给自己作一次存档。

    +

    如果你的 markdown 文件可能非 md 结尾,则需要添加一下source_suffix字段

    +
    source_suffix = {
    '.rst': 'restructuredtext',
    '.txt': 'markdown',
    '.md': 'markdown',
    }
    -
    -
    -

    信息

    +

    意思就是,.rst文件,使用restructuredtext进行解析,.txt.md文件则使用 markdown 进行解析(.rst不能删,首页还得用)

    +

    此外,myst-parser默认关闭了很多一些非基本markdown的语法,我们可以通过添加myst_enable_extensions字段来支持这些语法,以下是一个完整的示例:

    +
    myst_enable_extensions = [
    "amsmath",
    "attrs_inline",
    "colon_fence",
    "deflist",
    "dollarmath",
    "fieldlist",
    "html_admonition",
    "html_image",
    "linkify",
    "replacements",
    "smartquotes",
    "strikethrough",
    "substitution",
    "tasklist",
    ]
    -
    -
    -

    本人传统后端出身,对前端一概不通,以下修改基本都是靠堆时间慢慢调试+GPT完成,所以改的不好或有其他方案建议的欢迎批评指出(我们GPT真是太强啦)

    +

    按需开启即可,每个语法扩展具体功能如下:

    +
      +
    • amsmath:LaTeX数学公式的软件包
    • +
    • attrs_inline:属性扩展,这个我不太了解,应该和HTML的写法相关
    • +
    • colon_fence:表格的语法
    • +
    • deflist:列表的语法,也就是我现在在写的这个无序列表
    • +
    • dollarmath:使用美元符号$$包围的数学公式语法
    • +
    • fieldlist:块列表语法,一般用在说明函数及其参数的功能的时候
    • +
    • html_admonition:基于html的提示框语法
    • +
    • html_image:基于html的图片显示语法
    • +
    • linkify:网址链接可点击的语法
    • +
    • replacements:这个我不太懂,看起来是可以支持字符串替换
    • +
    • smartquotes:会帮你自动把直引号转换成弯引号
    • +
    • strikethrough:删除线语法,就是这样
    • +
    • substitution:替换语法,差不多就是你可以定义一个变量,然后在后续的文本里添加占位符,构建时会帮你自动把变量的值填进占位符里
    • +
    • tasklist:todo列表语法
    • +
    +

    因篇幅有限,扩展仅作简单概述,甚至可能不准确,具体每个扩展的使用方法,以及效果示例,请查看官方文档

    +
    +

    linkify 需要额外安装一个插件,pip install linkify-it-py

    +
    +

    主题配置

    默认的主题很丑,所以我们选择使用 read the docs 的主题配置

    +

    首先安装一下主题

    +
    pip install sphinx_rtd_theme
    -
    -
    +

    然后在conf.py文件修改一下html_theme字段即可,改为

    +
    html_theme = 'sphinx_rtd_theme'
    -
      -
    1. 针对获取的番剧封面太小的问题,修改了lib/templates/bgm-template.ejs文件(因为我只用bgm源所以是这个,bili源有另一个templates文件);以及途中感觉他的布局有些奇怪,my-comments明明是在picture和右边的内容下面,但却归到右边内容的div里,用负数的padding来移到左边…所以布局也改了改

      -

      主要是把img的width从110改成了130px,然后新增一个bangumi-block的div和mycomments纵向排列。

      -
      <div class="bangumi-item">
      <div class="bangumi-block">
      <div class="bangumi-picture"><img src="<%= lazyload ? (loading || "https://cdn.jsdelivr.net/npm/hexo-bilibili-bangumi@1.2.0/lib/img/loading.gif") : (srcValue === '__loading__' ? (loading || "https://cdn.jsdelivr.net/npm/hexo-bilibili-bangumi@1.2.0/lib/img/loading.gif") : `https:${item.cover.replace(/^https:/, '')}`) %>" <%- lazyload ? ` data-src="${item.cover}"` : (lazyloadAttrName ? ` ${lazyloadAttrName.split('=')[0]}="${lazyloadAttrName.split('=')[1] === '__loading__' ? (loading || "https://cdn.jsdelivr.net/npm/[email protected]/lib/img/loading.gif") : (lazyloadAttrName.split('=')[1] === '__image__' ? `https:${item.cover.replace(/^https:/, '')}` : (lazyloadAttrName.split('=')[1] || ''))}"` : "") %> referrerPolicy="no-referrer" width="130" style="width:130px;margin:20px auto;" />
      </div>
      <div class="bangumi-info">
      <div class="bangumi-title">
      <a target="_blank" href="https://bangumi.tv/subject/<%= item.id %>"><%= item.title || "Unknown" %></a>
      </div>
      <div class="bangumi-meta">
      <span class="bangumi-info-items" <%- metaColor %>>
      <span class="bangumi-info-item">
      <% if(item.totalCount){ %>
      <span class="bangumi-info-total"><%= item.totalCount %></span><em
      class="bangumi-info-label-em">0</em>
      </span>
      <% } %>
      <span class="bangumi-info-item bangumi-type">
      <span class="bangumi-info-label">类型</span> <em><%= item.type %></em>
      </span>
      <span class="bangumi-info-item bangumi-wish">
      <span class="bangumi-info-label">想看</span> <em><%= item.wish %></em>
      </span>
      <span class="bangumi-info-item bangumi-doing">
      <span class="bangumi-info-label">在看</span> <em><%= item.doing || "-" %></em>
      </span>
      <span class="bangumi-info-item bangumi-collect">
      <span class="bangumi-info-label">已看</span> <em><%= item.collect || "-" %></em>
      </span>
      <span class="bangumi-info-item bangumi-info-item-score">
      <span class="bangumi-info-label">评分</span> <em><%= item.score || "-" %></em>
      </span>
      </span>
      </div>
      <div class="bangumi-comments" <%- color %>>
      <p>简介:<%= item.des || "暂无简介" %></p>
      </div>
      </div>
      </div>
      <% if (showMyComment && item.myComment) { %>
      <div class="bangumi-my-comments">我的评分:
      <% if (item.myStars) { %>
      <span class="bangumi-starstop"><span class="bangumi-starlight stars<%= item.myStars %>"></span></span>
      <% } %>
      <br>
      我的评价:<%= item.myComment %>
      </div>
      <% } %>
      </div>

      -
    2. -
    3. 因插件为对swup没作兼容,而redefine主题推荐开启swup,开启后会导致无法加载bangumi插件的js脚本,因此对隔壁的lib/templates/bangumi.ejs进行了修改

      -

      主要是将<script>标签改成了<script data-swup-reload-script type="text/javascript">

      -
      ···以上省略
      <script data-swup-reload-script type="text/javascript">
      ···以下省略
      </script>
      -
    4. -
    5. 对样式进行美化(自认为的)

      -

      因bangumi插件已经支持针对不同主题的样式表,所以只需要在/src/lib/templates/theme目录下,新增一个redefine.css文件,然后填写自己重新针对该主题设置的样式表即可,以下是我自己修改的样式表:

      -
      .bangumi-tabs {
      margin-bottom: 15px;
      margin-top: 15px;
      display: flex;
      justify-content: center;
      background-color: #f4f4f4;
      padding: 5px 5px;
      border-radius: 10px;
      width: fit-content;
      margin-left: auto;
      margin-right: auto;
      }

      .bangumi-tab {
      padding: 10px 20px;
      justify-content: center;
      text-align: center;
      cursor: pointer;
      border: none;
      background-color: transparent;
      color: #000;
      margin: 5px;
      border-radius: 8px;
      transition: background-color 0.3s, color 0.3s, box-shadow 0.3s;
      font-weight: bold;
      }

      a.bangumi-tab {
      text-decoration: none;
      }

      .bangumi-active {
      background-color: #fafafa;
      color: #005080;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
      border-radius: 8px;
      font-weight: bold;
      }

      .bangumi-tab:hover {
      background-color: #e8e6e6;
      }

      .bangumi-item {
      position: relative;
      clear: both;
      padding: 15px;
      margin: 15px;
      height: fit-content;
      background-color: transparent;
      box-shadow: 0 4px 8px rgba(0,0,0,0.1);
      border: 1px solid #e1e1e1;
      border-radius: 10px;
      transform: translateZ(0);
      transition: transform 0.3s, box-shadow 0.3s;
      }

      @media screen and (max-width: 600px) {
      .bangumi-item {
      width: 100%;
      }
      }

      .bangumi-block {
      min-height: 180px;
      }

      .bangumi-picture {
      display: flex !important;
      justify-content: center;
      align-items: center;
      padding-left: 20px;
      width: 130px;
      height: auto;
      }

      .bangumi-picture img {
      width: 100%;
      height: 100%;
      object-fit: cover;
      box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.3);
      }

      .bangumi-info {
      padding-left: 150px;
      margin-top: 10px;
      }

      .bangumi-title {
      font-size: 1.2rem;
      }

      .bangumi-title a {
      line-height: 1;
      text-decoration: none;
      }

      .bangumi-meta {
      font-size:12px;
      padding-right:10px;
      height:45px
      }

      .bangumi-comments {
      font-size: 0.92rem;
      margin-top:12px
      }

      .bangumi-comments>p {
      word-break: break-all;
      text-overflow: ellipsis;
      overflow: hidden;

      white-space: normal;
      display: -webkit-box;
      -webkit-box-orient: vertical;
      -webkit-line-clamp: 3;
      }

      .bangumi-pagination {
      margin-top: 15px;
      text-align: center;
      margin-bottom: 10px;
      }

      .bangumi-button {
      padding: 5px;
      }

      .bangumi-button:hover {
      background: #657b83;
      color: #fff;
      }

      .bangumi-hide {
      display: none;
      }

      .bangumi-show {
      display: block;
      }

      .bangumi-info-items {
      font-size: 0.92rem;
      color: #005080;
      padding-top: 10px;
      line-height: 1;
      float: left;
      width: 100%;
      }

      .bangumi-item:hover {
      box-shadow: 0 6px 12px rgba(0,0,0,0.25);
      }

      .bangumi-info-item {
      display: inline-block;
      width: 13%;
      border-right: 1px solid #005080;
      text-align: center;
      height: 34px;
      }

      .bangumi-info-label {
      display: block;
      line-height: 12px;
      }

      .bangumi-info-item em {
      display: block;
      padding-top: 6px;
      line-height: 17px;
      font-style: normal;
      font-weight: 700;
      }

      .bangumi-info-total {
      padding-top: 11px;
      display: block;
      line-height: 12px;
      font-weight: bold;
      }

      .bangumi-info-item-score {
      border-right: 1px solid #0000;
      width: 50px;
      }

      .bangumi-info-label-em {
      color: rgba(0, 0, 0, 0);
      opacity: 0;
      visibility: hidden;
      line-height: 6px !important;
      padding: 0 !important;
      }

      @media (max-width:650px) {

      .bangumi-coin,
      .bangumi-type {
      display: none;
      }

      .bangumi-info-item {
      width: 16%;
      }
      }

      @media (max-width:590px) {

      .bangumi-danmaku,
      .bangumi-wish {
      display: none;
      }

      .bangumi-info-item {
      width: 19%;
      }
      }

      @media (max-width:520px) {

      .bangumi-play,
      .bangumi-doing {
      display: none;
      }

      .bangumi-info-item {
      width: 24%;
      }
      }

      @media (max-width:480px) {

      .bangumi-follow,
      .bangumi-collect {
      display: none;
      }

      .bangumi-info-item {
      width: 30%;
      }
      }

      @media (max-width:400px) {
      .bangumi-area {
      display: none;
      }

      .bangumi-info-item {
      width: 45%;
      }
      }

      .bangumi-my-comments {
      border: 1px dashed #8f8f8f;
      padding: 3px;
      border-radius: 5px;
      margin-left: 5px;
      }

      .bangumi-starstop {
      background: transparent url(https://cdn.jsdelivr.net/npm/hexo-bilibili-bangumi@1.7.9/lib/img/rate_star_2x.png);
      height: 10px;
      background-size: 10px 19.5px;
      background-position: 100% 100%;
      background-repeat: repeat-x;
      width: 50px;
      display: inline-block;
      float: none;
      }

      .bangumi-starlight {
      background: transparent url(https://cdn.jsdelivr.net/npm/hexo-bilibili-bangumi@1.7.9/lib/img/rate_star_2x.png);
      height: 10px;
      background-size: 10px 19.5px;
      background-position: 100% 100%;
      background-repeat: repeat-x;
      display: block;
      width: 100%;
      background-position: 0 0;
      }

      .bangumi-starlight.stars1 {
      width: 5px;
      }

      .bangumi-starlight.stars2 {
      width: 10px;
      }

      .bangumi-starlight.stars3 {
      width: 15px;
      }

      .bangumi-starlight.stars4 {
      width: 20px;
      }

      .bangumi-starlight.stars5 {
      width: 25px;
      }

      .bangumi-starlight.stars6 {
      width: 30px;
      }

      .bangumi-starlight.stars7 {
      width: 35px;
      }

      .bangumi-starlight.stars8 {
      width: 40px;
      }

      .bangumi-starlight.stars9 {
      width: 45px;
      }

      .bangumi-starlight.stars10 {
      width: 50px;
      }

    6. -
    -

    但苦于实在不会前端,目前还遗留了一些问题:1. 不知道该怎么获取主题当前是日间还是夜间模式,然后针对夜间模式进行适配,所以夜间模式下会不太好看;2. 其实想把选页的按钮也改成redefine首页那样的模式,但也确实不会修改了,问GPT也只能做到改样式的程度了。

    -
    -

    如果你想使用我修改后的版本,只需要在path/to/your_blot/node_modules/hexo-bilibili-bangumi目录下,完成以下步骤即可

    -
      -
    1. 安装依赖:npm install
    2. -
    3. 作上述同样的修改
    4. -
    5. 编译:npm run build
    6. -
    -

    然后hexo重新生成以下就完事了。

    +

    编写你的文档

    因为我们配置了 markdown 语法,所以我们只需要使用常规的markdown编译器,正常的写每一页文档即可。

    +

    这里我就写了两页,代码结构+如何添加算法,如下,写完放进source的目录即可

    +

    img

    +
    +

    注意,markdown语法需要把最高级的标题留给页面标题,例如用#一级标题写了页面的标题后,文章内容就只能用二级及以下的标题了,不然后面目录显示会有问题,会把文件所有最高级标题都作为目录标题

    +
    +

    修改主页代码

    接下来我们去 index.rst 文件下,修改一下我们的主页内容以及左侧目录内容就差不多了

    +
    .. AlgorithmViewer documentation master file, created by
    sphinx-quickstart on Fri Apr 14 13:50:38 2023.
    You can adapt this file completely to your liking, but it should at least
    contain the root `toctree` directive.

    Welcome to AlgorithmViewer's documentation!
    ===========================================

    .. toctree::
    :maxdepth: 2
    :caption: Contents

    代码结构
    如何添加算法
    + +

    rst 的语法我也不太懂,所以只简单针对这一个文件作简单的分析

    +

    .. 开头的类似于注释,不会被编译到网页上

    +

    ===========================================上的一行就是我们的欢迎页标题,rst的标题等号长度不得小于文字长度

    +

    toctree:: 声明了一个树状结构,也就是我们的目录,maxdepth就是层级的最深深度,2也就是只显示两层

    +

    caption 指定目录的标题,这里的目录标题是 Contents

    +

    然后在后面接上你编写的文档文件即可,按我的写法,最终生成的页面会是这样的

    +

    img

    +

    生成HTML文件

    配置完成后,我们就可以在根目录docs下执行编译命令了,也是一行代码的事

    +
    make html
    + +

    成功 build 后,我们就可以到 build/html 文件夹下,看到我们的HTML文件了,打开 index.html,就可以看到你的文档啦

    ]]>
    - 编程/语言 + 好软推荐 - 前端 + 好软推荐
    @@ -1932,98 +1948,82 @@ - 使用Sphinx为你的项目快速构建文档 - /2023/04/15/%E4%BD%BF%E7%94%A8Sphinx%E4%B8%BA%E4%BD%A0%E7%9A%84%E9%A1%B9%E7%9B%AE%E5%BF%AB%E9%80%9F%E6%9E%84%E5%BB%BA%E6%96%87%E6%A1%A3/ - 最近写了个软件,需要写个接口文档,看到别人项目的文档有不少都是托管在 Read the Docs 上的,于是搜了一下,Read the Docs 是一个托管平台,而这个平台的文档是基于 Sphinx 构建的,所以就学了一下,以此记录。

    -

    安装Sphinx

    很简单,用pip安装即可,尽量使用官方的源,国内源听说多少有点问题

    -
    pip install sphinx
    + 为你的hexo博客添加一个追番列表 + /2024/06/28/%E4%B8%BA%E4%BD%A0%E7%9A%84hexo%E5%8D%9A%E5%AE%A2%E6%B7%BB%E5%8A%A0%E4%B8%80%E4%B8%AA%E8%BF%BD%E7%95%AA%E5%88%97%E8%A1%A8/ + +

    本文基于插件hexo-bilibili-bangumi 编写,并修改为适配redefine主题的样式,最终结果示例见我的追番列表

    -

    构建Sphinx项目

    快速构建

    推荐在项目的根目录构建一个文件夹docs来专门存放文档的源码,然后cd docs下构建源码

    -

    构建也非常简单,一行命令即可

    -
    sphinx-quickstart
    + -

    输入命令后,会提示是否要创建独立目录,选择y是即可,然后提示你填一些信息,包括项目名,作者名,语言等等,照实填写即可,语言是简体中文的话,填 zh_CN 即可

    -

    构建完毕后,如果之前选择的是y的话,我们就可以在目录下看到buildsource文件夹了,其中source文件夹就是存放项目源码的文件夹

    -

    source中包含了一个conf.py文件,用于填写项目配置,一个index.rst文件,是首页的源码。

    -

    若无特殊需求的话,直接根据rst格式编写你的代码,然后make html即可完成项目的构建。

    -

    但是为了写一个文档,又专门去学一个 rst 语法似乎有些不大合适,所以需要修改一下配置文件,让Sphinx支持大众都熟知的 markdown 语法。

    -

    配置文件

    markdown配置

    安装扩展

    首先,为了支持 markdown 语法,我们需要安装一个扩展插件myst-parser

    -
    pip install --upgrade myst-parser
    +

    主要是闲来无事逛github的时候发现了hexo-bilibili-bangumi 这么一个插件,可以爬取bili/bangumi的数据并渲染为一个页面展示你的追番列表,整好我前段时间开始有了bangumi记录追番的习惯,所以想着上手用用。

    +

    如何使用

    正常来说,按照官方的readme操作完就可以上手使用了

    +
      +
    1. 安装插件

      +
      $ npm install hexo-bilibili-bangumi --save
      +
    2. +
    3. _config.yml配置文件里添加你的配置(以下配置为了确保一次正常运行,与官方示例不同,完整示例见官方):

      +
      bangumi: # 追番设置
      enable: true
      source: bangumi
      bgmInfoSource: 'bgmApi'
      path:
      vmid: 根据官方readme获取
      title: '追番列表'
      quote: '生命不息,追番不止!'
      show: 1
      lazyload: false
      srcValue: '__image__'
      lazyloadAttrName: 'data-src=__image__'
      loading:
      showMyComment: false
      pagination: false
      metaColor:
      color:
      webp:
      progress:
      extraOrder:
      order: latest
      coverMirror:
      +
    4. +
    5. 编译并生成静态文件

      +
      hexo c
      call hexo bangumi -u # 必须要在hexo g前添加这句,爬取数据
      call hexo g
      call hexo s
      +
    6. +
    7. 然后你就可以在/bangumis后缀的页面下看见你的追番列表页面啦。(如果修改了path的话,以你实际填写path为主)

      +
    8. +
    -

    添加扩展配置

    安装完成后,我们在conf.py文件内,修改一下extensions字段,引入扩展即可

    -
    extensions = ['myst_parser']
    +
    +
    +

    附言

    -

    如果你的 markdown 文件可能非 md 结尾,则需要添加一下source_suffix字段

    -
    source_suffix = {
    '.rst': 'restructuredtext',
    '.txt': 'markdown',
    '.md': 'markdown',
    }
    +
    +
    +

    实际配置过程中,lazyload选项容易和主题冲突,导致图片一直转圈;pagination选项也会有冲突,导致分页异常。因此上面的配置我都改成了默认关闭。

    -

    意思就是,.rst文件,使用restructuredtext进行解析,.txt.md文件则使用 markdown 进行解析(.rst不能删,首页还得用)

    -

    此外,myst-parser默认关闭了很多一些非基本markdown的语法,我们可以通过添加myst_enable_extensions字段来支持这些语法,以下是一个完整的示例:

    -
    myst_enable_extensions = [
    "amsmath",
    "attrs_inline",
    "colon_fence",
    "deflist",
    "dollarmath",
    "fieldlist",
    "html_admonition",
    "html_image",
    "linkify",
    "replacements",
    "smartquotes",
    "strikethrough",
    "substitution",
    "tasklist",
    ]
    +
    +
    -

    按需开启即可,每个语法扩展具体功能如下:

    -
      -
    • amsmath:LaTeX数学公式的软件包
    • -
    • attrs_inline:属性扩展,这个我不太了解,应该和HTML的写法相关
    • -
    • colon_fence:表格的语法
    • -
    • deflist:列表的语法,也就是我现在在写的这个无序列表
    • -
    • dollarmath:使用美元符号$$包围的数学公式语法
    • -
    • fieldlist:块列表语法,一般用在说明函数及其参数的功能的时候
    • -
    • html_admonition:基于html的提示框语法
    • -
    • html_image:基于html的图片显示语法
    • -
    • linkify:网址链接可点击的语法
    • -
    • replacements:这个我不太懂,看起来是可以支持字符串替换
    • -
    • smartquotes:会帮你自动把直引号转换成弯引号
    • -
    • strikethrough:删除线语法,就是这样
    • -
    • substitution:替换语法,差不多就是你可以定义一个变量,然后在后续的文本里添加占位符,构建时会帮你自动把变量的值填进占位符里
    • -
    • tasklist:todo列表语法
    • -
    -

    因篇幅有限,扩展仅作简单概述,甚至可能不准确,具体每个扩展的使用方法,以及效果示例,请查看官方文档

    -
    -

    linkify 需要额外安装一个插件,pip install linkify-it-py

    -
    -

    主题配置

    默认的主题很丑,所以我们选择使用 read the docs 的主题配置

    -

    首先安装一下主题

    -
    pip install sphinx_rtd_theme
    +

    进阶

    因为默认的样式不太好看,以及其他配置和我当前使用的主题redefine有诸多冲突,因此需要进行一些修改才能正常使用,以下是我做的部分修改分享,也是给自己作一次存档。

    -

    然后在conf.py文件修改一下html_theme字段即可,改为

    -
    html_theme = 'sphinx_rtd_theme'
    +
    +
    +

    信息

    -

    编写你的文档

    因为我们配置了 markdown 语法,所以我们只需要使用常规的markdown编译器,正常的写每一页文档即可。

    -

    这里我就写了两页,代码结构+如何添加算法,如下,写完放进source的目录即可

    -

    img

    -
    -

    注意,markdown语法需要把最高级的标题留给页面标题,例如用#一级标题写了页面的标题后,文章内容就只能用二级及以下的标题了,不然后面目录显示会有问题,会把文件所有最高级标题都作为目录标题

    -
    -

    修改主页代码

    接下来我们去 index.rst 文件下,修改一下我们的主页内容以及左侧目录内容就差不多了

    -
    .. AlgorithmViewer documentation master file, created by
    sphinx-quickstart on Fri Apr 14 13:50:38 2023.
    You can adapt this file completely to your liking, but it should at least
    contain the root `toctree` directive.

    Welcome to AlgorithmViewer's documentation!
    ===========================================

    .. toctree::
    :maxdepth: 2
    :caption: Contents

    代码结构
    如何添加算法
    +
    +
    +

    本人传统后端出身,对前端一概不通,以下修改基本都是靠堆时间慢慢调试+GPT完成,所以改的不好或有其他方案建议的欢迎批评指出(我们GPT真是太强啦)

    -

    rst 的语法我也不太懂,所以只简单针对这一个文件作简单的分析

    -

    .. 开头的类似于注释,不会被编译到网页上

    -

    ===========================================上的一行就是我们的欢迎页标题,rst的标题等号长度不得小于文字长度

    -

    toctree:: 声明了一个树状结构,也就是我们的目录,maxdepth就是层级的最深深度,2也就是只显示两层

    -

    caption 指定目录的标题,这里的目录标题是 Contents

    -

    然后在后面接上你编写的文档文件即可,按我的写法,最终生成的页面会是这样的

    -

    img

    -

    生成HTML文件

    配置完成后,我们就可以在根目录docs下执行编译命令了,也是一行代码的事

    -
    make html
    +
    +
    -

    成功 build 后,我们就可以到 build/html 文件夹下,看到我们的HTML文件了,打开 index.html,就可以看到你的文档啦

    +
      +
    1. 针对获取的番剧封面太小的问题,修改了lib/templates/bgm-template.ejs文件(因为我只用bgm源所以是这个,bili源有另一个templates文件);以及途中感觉他的布局有些奇怪,my-comments明明是在picture和右边的内容下面,但却归到右边内容的div里,用负数的padding来移到左边…所以布局也改了改

      +

      主要是把img的width从110改成了130px,然后新增一个bangumi-block的div和mycomments纵向排列。

      +
      <div class="bangumi-item">
      <div class="bangumi-block">
      <div class="bangumi-picture"><img src="<%= lazyload ? (loading || "https://cdn.jsdelivr.net/npm/hexo-bilibili-bangumi@1.2.0/lib/img/loading.gif") : (srcValue === '__loading__' ? (loading || "https://cdn.jsdelivr.net/npm/hexo-bilibili-bangumi@1.2.0/lib/img/loading.gif") : `https:${item.cover.replace(/^https:/, '')}`) %>" <%- lazyload ? ` data-src="${item.cover}"` : (lazyloadAttrName ? ` ${lazyloadAttrName.split('=')[0]}="${lazyloadAttrName.split('=')[1] === '__loading__' ? (loading || "https://cdn.jsdelivr.net/npm/[email protected]/lib/img/loading.gif") : (lazyloadAttrName.split('=')[1] === '__image__' ? `https:${item.cover.replace(/^https:/, '')}` : (lazyloadAttrName.split('=')[1] || ''))}"` : "") %> referrerPolicy="no-referrer" width="130" style="width:130px;margin:20px auto;" />
      </div>
      <div class="bangumi-info">
      <div class="bangumi-title">
      <a target="_blank" href="https://bangumi.tv/subject/<%= item.id %>"><%= item.title || "Unknown" %></a>
      </div>
      <div class="bangumi-meta">
      <span class="bangumi-info-items" <%- metaColor %>>
      <span class="bangumi-info-item">
      <% if(item.totalCount){ %>
      <span class="bangumi-info-total"><%= item.totalCount %></span><em
      class="bangumi-info-label-em">0</em>
      </span>
      <% } %>
      <span class="bangumi-info-item bangumi-type">
      <span class="bangumi-info-label">类型</span> <em><%= item.type %></em>
      </span>
      <span class="bangumi-info-item bangumi-wish">
      <span class="bangumi-info-label">想看</span> <em><%= item.wish %></em>
      </span>
      <span class="bangumi-info-item bangumi-doing">
      <span class="bangumi-info-label">在看</span> <em><%= item.doing || "-" %></em>
      </span>
      <span class="bangumi-info-item bangumi-collect">
      <span class="bangumi-info-label">已看</span> <em><%= item.collect || "-" %></em>
      </span>
      <span class="bangumi-info-item bangumi-info-item-score">
      <span class="bangumi-info-label">评分</span> <em><%= item.score || "-" %></em>
      </span>
      </span>
      </div>
      <div class="bangumi-comments" <%- color %>>
      <p>简介:<%= item.des || "暂无简介" %></p>
      </div>
      </div>
      </div>
      <% if (showMyComment && item.myComment) { %>
      <div class="bangumi-my-comments">我的评分:
      <% if (item.myStars) { %>
      <span class="bangumi-starstop"><span class="bangumi-starlight stars<%= item.myStars %>"></span></span>
      <% } %>
      <br>
      我的评价:<%= item.myComment %>
      </div>
      <% } %>
      </div>

      +
    2. +
    3. 因插件为对swup没作兼容,而redefine主题推荐开启swup,开启后会导致无法加载bangumi插件的js脚本,因此对隔壁的lib/templates/bangumi.ejs进行了修改

      +

      主要是将<script>标签改成了<script data-swup-reload-script type="text/javascript">

      +
      ···以上省略
      <script data-swup-reload-script type="text/javascript">
      ···以下省略
      </script>
      +
    4. +
    5. 对样式进行美化(自认为的)

      +

      因bangumi插件已经支持针对不同主题的样式表,所以只需要在/src/lib/templates/theme目录下,新增一个redefine.css文件,然后填写自己重新针对该主题设置的样式表即可,以下是我自己修改的样式表:

      +
      .bangumi-tabs {
      margin-bottom: 15px;
      margin-top: 15px;
      display: flex;
      justify-content: center;
      background-color: #f4f4f4;
      padding: 5px 5px;
      border-radius: 10px;
      width: fit-content;
      margin-left: auto;
      margin-right: auto;
      }

      .bangumi-tab {
      padding: 10px 20px;
      justify-content: center;
      text-align: center;
      cursor: pointer;
      border: none;
      background-color: transparent;
      color: #000;
      margin: 5px;
      border-radius: 8px;
      transition: background-color 0.3s, color 0.3s, box-shadow 0.3s;
      font-weight: bold;
      }

      a.bangumi-tab {
      text-decoration: none;
      }

      .bangumi-active {
      background-color: #fafafa;
      color: #005080;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
      border-radius: 8px;
      font-weight: bold;
      }

      .bangumi-tab:hover {
      background-color: #e8e6e6;
      }

      .bangumi-item {
      position: relative;
      clear: both;
      padding: 15px;
      margin: 15px;
      height: fit-content;
      background-color: transparent;
      box-shadow: 0 4px 8px rgba(0,0,0,0.1);
      border: 1px solid #e1e1e1;
      border-radius: 10px;
      transform: translateZ(0);
      transition: transform 0.3s, box-shadow 0.3s;
      }

      @media screen and (max-width: 600px) {
      .bangumi-item {
      width: 100%;
      }
      }

      .bangumi-block {
      min-height: 180px;
      }

      .bangumi-picture {
      display: flex !important;
      justify-content: center;
      align-items: center;
      padding-left: 20px;
      width: 130px;
      height: auto;
      }

      .bangumi-picture img {
      width: 100%;
      height: 100%;
      object-fit: cover;
      box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.3);
      }

      .bangumi-info {
      padding-left: 150px;
      margin-top: 10px;
      }

      .bangumi-title {
      font-size: 1.2rem;
      }

      .bangumi-title a {
      line-height: 1;
      text-decoration: none;
      }

      .bangumi-meta {
      font-size:12px;
      padding-right:10px;
      height:45px
      }

      .bangumi-comments {
      font-size: 0.92rem;
      margin-top:12px
      }

      .bangumi-comments>p {
      word-break: break-all;
      text-overflow: ellipsis;
      overflow: hidden;

      white-space: normal;
      display: -webkit-box;
      -webkit-box-orient: vertical;
      -webkit-line-clamp: 3;
      }

      .bangumi-pagination {
      margin-top: 15px;
      text-align: center;
      margin-bottom: 10px;
      }

      .bangumi-button {
      padding: 5px;
      }

      .bangumi-button:hover {
      background: #657b83;
      color: #fff;
      }

      .bangumi-hide {
      display: none;
      }

      .bangumi-show {
      display: block;
      }

      .bangumi-info-items {
      font-size: 0.92rem;
      color: #005080;
      padding-top: 10px;
      line-height: 1;
      float: left;
      width: 100%;
      }

      .bangumi-item:hover {
      box-shadow: 0 6px 12px rgba(0,0,0,0.25);
      }

      .bangumi-info-item {
      display: inline-block;
      width: 13%;
      border-right: 1px solid #005080;
      text-align: center;
      height: 34px;
      }

      .bangumi-info-label {
      display: block;
      line-height: 12px;
      }

      .bangumi-info-item em {
      display: block;
      padding-top: 6px;
      line-height: 17px;
      font-style: normal;
      font-weight: 700;
      }

      .bangumi-info-total {
      padding-top: 11px;
      display: block;
      line-height: 12px;
      font-weight: bold;
      }

      .bangumi-info-item-score {
      border-right: 1px solid #0000;
      width: 50px;
      }

      .bangumi-info-label-em {
      color: rgba(0, 0, 0, 0);
      opacity: 0;
      visibility: hidden;
      line-height: 6px !important;
      padding: 0 !important;
      }

      @media (max-width:650px) {

      .bangumi-coin,
      .bangumi-type {
      display: none;
      }

      .bangumi-info-item {
      width: 16%;
      }
      }

      @media (max-width:590px) {

      .bangumi-danmaku,
      .bangumi-wish {
      display: none;
      }

      .bangumi-info-item {
      width: 19%;
      }
      }

      @media (max-width:520px) {

      .bangumi-play,
      .bangumi-doing {
      display: none;
      }

      .bangumi-info-item {
      width: 24%;
      }
      }

      @media (max-width:480px) {

      .bangumi-follow,
      .bangumi-collect {
      display: none;
      }

      .bangumi-info-item {
      width: 30%;
      }
      }

      @media (max-width:400px) {
      .bangumi-area {
      display: none;
      }

      .bangumi-info-item {
      width: 45%;
      }
      }

      .bangumi-my-comments {
      border: 1px dashed #8f8f8f;
      padding: 3px;
      border-radius: 5px;
      margin-left: 5px;
      }

      .bangumi-starstop {
      background: transparent url(https://cdn.jsdelivr.net/npm/hexo-bilibili-bangumi@1.7.9/lib/img/rate_star_2x.png);
      height: 10px;
      background-size: 10px 19.5px;
      background-position: 100% 100%;
      background-repeat: repeat-x;
      width: 50px;
      display: inline-block;
      float: none;
      }

      .bangumi-starlight {
      background: transparent url(https://cdn.jsdelivr.net/npm/hexo-bilibili-bangumi@1.7.9/lib/img/rate_star_2x.png);
      height: 10px;
      background-size: 10px 19.5px;
      background-position: 100% 100%;
      background-repeat: repeat-x;
      display: block;
      width: 100%;
      background-position: 0 0;
      }

      .bangumi-starlight.stars1 {
      width: 5px;
      }

      .bangumi-starlight.stars2 {
      width: 10px;
      }

      .bangumi-starlight.stars3 {
      width: 15px;
      }

      .bangumi-starlight.stars4 {
      width: 20px;
      }

      .bangumi-starlight.stars5 {
      width: 25px;
      }

      .bangumi-starlight.stars6 {
      width: 30px;
      }

      .bangumi-starlight.stars7 {
      width: 35px;
      }

      .bangumi-starlight.stars8 {
      width: 40px;
      }

      .bangumi-starlight.stars9 {
      width: 45px;
      }

      .bangumi-starlight.stars10 {
      width: 50px;
      }

    6. +
    +

    但苦于实在不会前端,目前还遗留了一些问题:1. 不知道该怎么获取主题当前是日间还是夜间模式,然后针对夜间模式进行适配,所以夜间模式下会不太好看;2. 其实想把选页的按钮也改成redefine首页那样的模式,但也确实不会修改了,问GPT也只能做到改样式的程度了。

    +
    +

    如果你想使用我修改后的版本,只需要在path/to/your_blot/node_modules/hexo-bilibili-bangumi目录下,完成以下步骤即可

    +
      +
    1. 安装依赖:npm install
    2. +
    3. 作上述同样的修改
    4. +
    5. 编译:npm run build
    6. +
    +

    然后hexo重新生成以下就完事了。

    ]]>
    - 好软推荐 + 编程/语言 - 好软推荐 + 前端
    diff --git a/sitemap.txt b/sitemap.txt index fca778a..33bf258 100644 --- a/sitemap.txt +++ b/sitemap.txt @@ -5,25 +5,25 @@ https://twosix.page/about/index.html https://twosix.page/bangumi/index.html https://twosix.page/2023/03/25/%E3%80%90Rust%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%912-%E7%8C%9C%E6%95%B0%E6%B8%B8%E6%88%8F%E2%80%94%E2%80%94%E5%B0%9D%E8%AF%95%E4%BB%A3%E7%A0%81%E7%BC%96%E5%86%99/ https://twosix.page/2023/03/25/%E3%80%90Rust%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%913-%E9%80%9A%E7%94%A8%E7%BC%96%E7%A8%8B%E6%A6%82%E5%BF%B5/ -https://twosix.page/2024/06/28/%E4%B8%BA%E4%BD%A0%E7%9A%84hexo%E5%8D%9A%E5%AE%A2%E6%B7%BB%E5%8A%A0%E4%B8%80%E4%B8%AA%E8%BF%BD%E7%95%AA%E5%88%97%E8%A1%A8/ -https://twosix.page/2023/03/26/%E3%80%90Rust%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%914-%E6%89%80%E6%9C%89%E6%9D%83/ https://twosix.page/2023/04/15/%E4%BD%BF%E7%94%A8Sphinx%E4%B8%BA%E4%BD%A0%E7%9A%84%E9%A1%B9%E7%9B%AE%E5%BF%AB%E9%80%9F%E6%9E%84%E5%BB%BA%E6%96%87%E6%A1%A3/ +https://twosix.page/2023/03/26/%E3%80%90Rust%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%914-%E6%89%80%E6%9C%89%E6%9D%83/ +https://twosix.page/2024/06/28/%E4%B8%BA%E4%BD%A0%E7%9A%84hexo%E5%8D%9A%E5%AE%A2%E6%B7%BB%E5%8A%A0%E4%B8%80%E4%B8%AA%E8%BF%BD%E7%95%AA%E5%88%97%E8%A1%A8/ https://twosix.page/2023/04/02/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%918-%E9%80%9A%E7%94%A8%E9%9B%86%E5%90%88%E7%B1%BB%E5%9E%8B/ -https://twosix.page/2023/03/24/%E3%80%90Rust%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%911-%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA/ https://twosix.page/2023/04/06/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%919-%E9%94%99%E8%AF%AF%E5%A4%84%E7%90%86/ +https://twosix.page/2023/03/24/%E3%80%90Rust%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%911-%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA/ https://twosix.page/2024/01/18/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%9113-%E9%97%AD%E5%8C%85%E4%B8%8E%E8%BF%AD%E4%BB%A3%E5%99%A8/ https://twosix.page/2024/01/23/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%9114-%E8%BF%9B%E4%B8%80%E6%AD%A5%E8%AE%A4%E8%AF%86Cargo%E5%8F%8Acrates-io/ +https://twosix.page/2023/04/01/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%917-%E5%8C%85%E3%80%81%E5%8D%95%E5%85%83%E5%8C%85%E5%92%8C%E6%A8%A1%E5%9D%97/ https://twosix.page/2023/03/30/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%915-%E7%BB%93%E6%9E%84%E4%BD%93/ https://twosix.page/2023/03/30/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%916-%E6%9E%9A%E4%B8%BE%E4%B8%8E%E6%A8%A1%E5%BC%8F%E5%8C%B9%E9%85%8D/ -https://twosix.page/2023/04/01/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%917-%E5%8C%85%E3%80%81%E5%8D%95%E5%85%83%E5%8C%85%E5%92%8C%E6%A8%A1%E5%9D%97/ https://twosix.page/2023/05/12/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%9112-%E7%BC%96%E5%86%99%E4%B8%80%E4%B8%AA%E5%91%BD%E4%BB%A4%E8%A1%8C%E7%A8%8B%E5%BA%8F/ https://twosix.page/2024/06/27/cmake%E8%B0%83%E7%94%A8windeployqt%E5%AE%9E%E7%8E%B0%E8%87%AA%E5%8A%A8%E6%89%93%E5%8C%85qt%E7%9A%84dll%E6%96%87%E4%BB%B6/ -https://twosix.page/2023/05/08/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%9110-%E6%B3%9B%E5%9E%8B%E3%80%81trait%E4%B8%8E%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F/ https://twosix.page/2023/05/09/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%9111-%E7%BC%96%E5%86%99%E8%87%AA%E5%8A%A8%E5%8C%96%E6%B5%8B%E8%AF%95/ +https://twosix.page/2023/05/08/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%9110-%E6%B3%9B%E5%9E%8B%E3%80%81trait%E4%B8%8E%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F/ https://twosix.page/ https://twosix.page/tags/cmake/ https://twosix.page/tags/Rust/ -https://twosix.page/tags/%E5%89%8D%E7%AB%AF/ https://twosix.page/tags/%E5%A5%BD%E8%BD%AF%E6%8E%A8%E8%8D%90/ +https://twosix.page/tags/%E5%89%8D%E7%AB%AF/ https://twosix.page/categories/%E7%BC%96%E7%A8%8B-%E8%AF%AD%E8%A8%80/ https://twosix.page/categories/%E5%A5%BD%E8%BD%AF%E6%8E%A8%E8%8D%90/ diff --git a/sitemap.xml b/sitemap.xml index f8f4e63..7aa906c 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -65,7 +65,7 @@ - https://twosix.page/2024/06/28/%E4%B8%BA%E4%BD%A0%E7%9A%84hexo%E5%8D%9A%E5%AE%A2%E6%B7%BB%E5%8A%A0%E4%B8%80%E4%B8%AA%E8%BF%BD%E7%95%AA%E5%88%97%E8%A1%A8/ + https://twosix.page/2023/04/15/%E4%BD%BF%E7%94%A8Sphinx%E4%B8%BA%E4%BD%A0%E7%9A%84%E9%A1%B9%E7%9B%AE%E5%BF%AB%E9%80%9F%E6%9E%84%E5%BB%BA%E6%96%87%E6%A1%A3/ 2024-07-04 @@ -83,7 +83,7 @@ - https://twosix.page/2023/04/15/%E4%BD%BF%E7%94%A8Sphinx%E4%B8%BA%E4%BD%A0%E7%9A%84%E9%A1%B9%E7%9B%AE%E5%BF%AB%E9%80%9F%E6%9E%84%E5%BB%BA%E6%96%87%E6%A1%A3/ + https://twosix.page/2024/06/28/%E4%B8%BA%E4%BD%A0%E7%9A%84hexo%E5%8D%9A%E5%AE%A2%E6%B7%BB%E5%8A%A0%E4%B8%80%E4%B8%AA%E8%BF%BD%E7%95%AA%E5%88%97%E8%A1%A8/ 2024-07-04 @@ -101,7 +101,7 @@ - https://twosix.page/2023/03/24/%E3%80%90Rust%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%911-%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA/ + https://twosix.page/2023/04/06/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%919-%E9%94%99%E8%AF%AF%E5%A4%84%E7%90%86/ 2024-07-04 @@ -110,7 +110,7 @@ - https://twosix.page/2023/04/06/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%919-%E9%94%99%E8%AF%AF%E5%A4%84%E7%90%86/ + https://twosix.page/2023/03/24/%E3%80%90Rust%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%911-%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA/ 2024-07-04 @@ -137,7 +137,7 @@ - https://twosix.page/2023/03/30/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%915-%E7%BB%93%E6%9E%84%E4%BD%93/ + https://twosix.page/2023/04/01/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%917-%E5%8C%85%E3%80%81%E5%8D%95%E5%85%83%E5%8C%85%E5%92%8C%E6%A8%A1%E5%9D%97/ 2024-07-04 @@ -146,7 +146,7 @@ - https://twosix.page/2023/03/30/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%916-%E6%9E%9A%E4%B8%BE%E4%B8%8E%E6%A8%A1%E5%BC%8F%E5%8C%B9%E9%85%8D/ + https://twosix.page/2023/03/30/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%915-%E7%BB%93%E6%9E%84%E4%BD%93/ 2024-07-04 @@ -155,7 +155,7 @@ - https://twosix.page/2023/04/01/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%917-%E5%8C%85%E3%80%81%E5%8D%95%E5%85%83%E5%8C%85%E5%92%8C%E6%A8%A1%E5%9D%97/ + https://twosix.page/2023/03/30/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%916-%E6%9E%9A%E4%B8%BE%E4%B8%8E%E6%A8%A1%E5%BC%8F%E5%8C%B9%E9%85%8D/ 2024-07-04 @@ -182,7 +182,7 @@ - https://twosix.page/2023/05/08/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%9110-%E6%B3%9B%E5%9E%8B%E3%80%81trait%E4%B8%8E%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F/ + https://twosix.page/2023/05/09/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%9111-%E7%BC%96%E5%86%99%E8%87%AA%E5%8A%A8%E5%8C%96%E6%B5%8B%E8%AF%95/ 2024-07-04 @@ -191,7 +191,7 @@ - https://twosix.page/2023/05/09/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%9111-%E7%BC%96%E5%86%99%E8%87%AA%E5%8A%A8%E5%8C%96%E6%B5%8B%E8%AF%95/ + https://twosix.page/2023/05/08/%E3%80%90Rust-%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95%E3%80%9110-%E6%B3%9B%E5%9E%8B%E3%80%81trait%E4%B8%8E%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F/ 2024-07-04 @@ -202,7 +202,7 @@ https://twosix.page/ - 2024-07-04 + 2024-10-17 daily 1.0 @@ -210,28 +210,28 @@ https://twosix.page/tags/cmake/ - 2024-07-04 + 2024-10-17 weekly 0.2 https://twosix.page/tags/Rust/ - 2024-07-04 + 2024-10-17 weekly 0.2 - https://twosix.page/tags/%E5%89%8D%E7%AB%AF/ - 2024-07-04 + https://twosix.page/tags/%E5%A5%BD%E8%BD%AF%E6%8E%A8%E8%8D%90/ + 2024-10-17 weekly 0.2 - https://twosix.page/tags/%E5%A5%BD%E8%BD%AF%E6%8E%A8%E8%8D%90/ - 2024-07-04 + https://twosix.page/tags/%E5%89%8D%E7%AB%AF/ + 2024-10-17 weekly 0.2 @@ -240,14 +240,14 @@ https://twosix.page/categories/%E7%BC%96%E7%A8%8B-%E8%AF%AD%E8%A8%80/ - 2024-07-04 + 2024-10-17 weekly 0.2 https://twosix.page/categories/%E5%A5%BD%E8%BD%AF%E6%8E%A8%E8%8D%90/ - 2024-07-04 + 2024-10-17 weekly 0.2 diff --git a/tags/Rust/index.html b/tags/Rust/index.html index 91d50cc..56ac9d7 100644 --- a/tags/Rust/index.html +++ b/tags/Rust/index.html @@ -1,2 +1,2 @@ -标签: Rust - TwoSix的小木屋 +标签: Rust - TwoSix的小木屋
    12
    \ No newline at end of file diff --git a/tags/Rust/page/2/index.html b/tags/Rust/page/2/index.html index 896abc7..faabd0d 100644 --- a/tags/Rust/page/2/index.html +++ b/tags/Rust/page/2/index.html @@ -1,2 +1,2 @@ -标签: Rust - TwoSix的小木屋 +标签: Rust - TwoSix的小木屋
    12
    \ No newline at end of file diff --git a/tags/cmake/index.html b/tags/cmake/index.html index 94a868c..3ac6b49 100644 --- a/tags/cmake/index.html +++ b/tags/cmake/index.html @@ -1,2 +1,2 @@ -标签: cmake - TwoSix的小木屋 +标签: cmake - TwoSix的小木屋
    1
    \ No newline at end of file diff --git a/tags/index.html b/tags/index.html index 0aec2f3..115ac7a 100644 --- a/tags/index.html +++ b/tags/index.html @@ -1,2 +1,2 @@ -标签 - TwoSix的小木屋 -
    \ No newline at end of file +标签 - TwoSix的小木屋 +
    \ No newline at end of file diff --git "a/tags/\345\211\215\347\253\257/index.html" "b/tags/\345\211\215\347\253\257/index.html" index 4711b8e..d7cf919 100644 --- "a/tags/\345\211\215\347\253\257/index.html" +++ "b/tags/\345\211\215\347\253\257/index.html" @@ -1,2 +1,2 @@ -标签: 前端 - TwoSix的小木屋 +标签: 前端 - TwoSix的小木屋
    1
    \ No newline at end of file diff --git "a/tags/\345\245\275\350\275\257\346\216\250\350\215\220/index.html" "b/tags/\345\245\275\350\275\257\346\216\250\350\215\220/index.html" index 688b351..30a98e2 100644 --- "a/tags/\345\245\275\350\275\257\346\216\250\350\215\220/index.html" +++ "b/tags/\345\245\275\350\275\257\346\216\250\350\215\220/index.html" @@ -1,2 +1,2 @@ -标签: 好软推荐 - TwoSix的小木屋 +标签: 好软推荐 - TwoSix的小木屋
    1
    \ No newline at end of file