[易学易懂系列|rustlang语言|零基础|快速入门|(24)|实战2:命令行工具minigrep(1)]
项目实战
实战2:命令行工具minigrep
有了昨天的基础,我们今天来开始另一个稍微有点复杂的项目。
简单来说,就是开发一个我们自己的grep (globally search a regular expression and print)
首先用命令生成一个工程:
1
2 1cargo new minigrep
2
然后在工程目录minigrep下新建一个文件:poem.txt,文件的内容如下 :
1
2
3
4
5
6
7
8
9
10 1I'm nobody! Who are you?
2Are you nobody, too?
3Then there's a pair of us - don't tell!
4They'd banish us, you know.
5
6How dreary to be somebody!
7How public, like a frog
8To tell your name the livelong day
9To an admiring bog!
10
在工程目录下的src/main.rs文件中,填入以下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 1use std::env;
2use std::fs;
3fn main() {
4 let args: Vec<String> = env::args().collect();//从命令行环境得到用户输入的参数
5
6 let query = &args[1];//参数1
7 let filename = &args[2];//参数2
8
9 println!("Searching for {}", query);//打印参数1
10 println!("In file {}", filename);//打印参数1
11
12 let contents = fs::read_to_string(filename).expect("Something went wrong reading the file");//以参数2为路径,读取文件内容
13
14 println!("With text:\n{}", contents);//打印内容
15}
16
17
我们现在用命令:
1
2 1cargo run the poem.txt
2
结果将打印如下 信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 1Finished dev [unoptimized + debuginfo] target(s) in 0.01s
2 Running `target\debug\minigrep.exe the poem.txt`
3Searching for the
4In file poem.txt
5With text:
6I'm nobody! Who are you?
7Are you nobody, too?
8Then there's a pair of us - don't tell!
9They'd banish us, you know.
10
11How dreary to be somebody!
12How public, like a frog
13To tell your name the livelong day
14To an admiring bog!
15
成功了!
我们成功地从文件poem.txt中读取了相关内容信息。
好现在我们来重构一下代码!
为什么要重构代码?
因为我们的很多逻辑都写在一个main函数上,这是一个不好的现象,如果功能越多,代码也越多,最后可以出现一个超长的main函数。可读性和可维护性都很差!
所以从一开始,我们就要考虑代码的可读性和可维护性,这是最佳实践!
好,我们开始重构吧!
根据单一职责设计原则,一个方法只负责一个职责。
那我们就应该把main方法里的负责处理参数读取和参数搜索的逻辑,把它们分别抽离开来,放在单独的方法中。
我们先把处理参数读取的逻辑抽出来,如下:
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 1use std::env;
2use std::fs;
3fn main() {
4 let args: Vec<String> = env::args().collect();
5
6 let (query, filename) = parseConfig(&args);//抽离参数读取与解析的逻辑
7
8 let contents = fs::read_to_string(filename).expect("Something went wrong reading the file");
9
10 println!("With text:\n{}", contents);
11}
12
13//参数读取与解析的逻辑
14fn parseConfig(args: &[String]) -> (&str, &str) {
15 println!("args len is {}", args.len());
16 println!("args is {:?}", args);
17 let query = &args[1];
18 let filename = &args[2];
19
20 println!("Searching for {}", query);
21 println!("In file {}", filename);
22 (query, filename)
23}
24
25
同样,我们用命令:
1
2 1cargo run the poem.txt
2
运行后的结果为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 1args len is 3
2args is [".\\target\\debug\\minigrep.exe", "the", "poem.txt"]
3Searching for the
4In file poem.txt
5With text:
6I'm nobody! Who are you?
7Are you nobody, too?
8Then there's a pair of us - don't tell!
9They'd banish us, you know.
10
11How dreary to be somebody!
12How public, like a frog
13To tell your name the livelong day
14To an admiring bog!
15
很好,我们的程序正常运行。
我们再来看看把元组用一个结构体来替换,把相关参数属性放在一起,更简洁直观。
开始吧:
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 1use std::env;
2use std::fs;
3fn main() {
4 let args: Vec<String> = env::args().collect();
5
6 let config: Config = parseConfig(&args);
7
8 let contents =
9 fs::read_to_string(config.filename).expect("Something went wrong reading the file");
10
11 println!("With text:\n{}", contents);
12}
13//结构体Config用来封装参数属性
14struct Config {
15 query: String,
16 filename: String,
17}
18//参数读取与解析的逻辑
19fn parseConfig(args: &[String]) -> Config {
20 println!("args len is {}", args.len());
21 println!("args is {:?}", args);
22 let query = args[1].clone();//这里直接用clone方法得到一个参数string的拷贝
23 let filename = args[2].clone();//这里直接用clone方法得到一个参数string的拷贝
24
25 println!("Searching for {}", query);
26 println!("In file {}", filename);
27 Config { query, filename }//返回结构体
28}
29
30
同样,我们用命令:
1
2 1cargo run the poem.txt
2
运行后的结果跟原来一样,重构成功!
有同学会问,这里为什么用clone呢?
不会有性能问题吗?
因为简单!
我们先保持简单,让程序能跑起来,以后再考虑性能的问题。(事实上,这里的性能只损失很少一部分。)
很好!
能否再优化重构一下:参数读取与解析代码?
可以的。
我们看如下 代码:
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 1use std::env;
2use std::fs;
3//主函数,程序入口
4fn main() {
5 let args: Vec<String> = env::args().collect();
6
7 let config: Config = Config::new(&args);//直接调用Config构造函数
8
9 let contents =
10 fs::read_to_string(config.filename).expect("Something went wrong reading the file");
11
12 println!("With text:\n{}", contents);
13}
14//结构体Config用来封装参数属性
15struct Config {
16 query: String,
17 filename: String,
18}
19//为结构体实现一个构造器,其主要功能也是读取和解析参数
20impl Config {
21 fn new(args: &[String]) -> Config {
22 let query = args[1].clone();
23 let filename = args[2].clone();
24
25 Config { query, filename }
26 }
27}
28//参数读取与解析的逻辑,现在可以删除了!
29fn parseConfig(args: &[String]) -> Config {
30 println!("args len is {}", args.len());
31 println!("args is {:?}", args);
32 let query = args[1].clone();//这里直接用clone方法得到一个参数string的拷贝
33 let filename = args[2].clone();//这里直接用clone方法得到一个参数string的拷贝
34
35 println!("Searching for {}", query);
36 println!("In file {}", filename);
37 Config { query, filename }//返回结构体
38}
39
40
同样,我们用命令:
1
2 1cargo run the poem.txt
2
运行后的结果跟原来一样,重构成功!
这时,我们的函数:parseConfig,可以退休了。
好吧,直接把它删除!
同样,我们用命令:
1
2 1cargo run the poem.txt
2
运行后的结果跟原来一样,删除成功!
这里为什么,每执行一次小版本的重构,都要跑一次代码呢?
因为可以保证,每次重构都是很小一步,可以避免错误!如果重构失败,也容易回退代码。
好吧,我们继续重构。
我们现在考虑一下错误处理。
比如,我们现在直接用命令:cargo run 运行代码,会报错:
1
2
3
4
5
6
7
8 1$ cargo run
2 Compiling minigrep v0.1.0 (file:///projects/minigrep)
3 Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
4 Running `target/debug/minigrep`
5thread 'main' panicked at 'index out of bounds: the len is 1
6but the index is 1', src/main.rs:25:21
7note: Run with `RUST_BACKTRACE=1` for a backtrace.
8
好,我们现在在代码上加上错误处理的逻辑,修改Config的构造函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 1//为结构体实现一个构造器,其主要功能也是读取和解析参数
2impl Config {
3 fn new(args: &[String]) -> Config {
4 if args.len() < 3 {
5 panic!("参数个数不够!not enough arguments");//增加错误处理
6 }
7 let query = args[1].clone();
8 let filename = args[2].clone();
9
10 Config { query, filename }
11 }
12}
13
14
直接用命令:cargo run 运行代码,会报错,但错误信息明确多了:
1
2
3
4
5 1Finished dev [unoptimized + debuginfo] target(s) in 0.96s
2 Running `target\debug\minigrep.exe`
3thread 'main' panicked at '参数个数不够!not enough arguments', src\main.rs:23:13
4note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
5
能否更优雅地处理错误信息?
可以,我们可以用Result,代码如下:
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 1use std::env;
2use std::fs;
3use std::process;
4//主函数,程序入口
5fn main() {
6 let args: Vec<String> = env::args().collect();
7
8 // let config: Config = Config::new(&args);
9 let config = Config::new(&args).unwrap_or_else(|err| {
10 println!("Problem parsing arguments: {}", err);
11 process::exit(1);
12 });
13
14 let contents =
15 fs::read_to_string(config.filename).expect("Something went wrong reading the file");
16
17 println!("With text:\n{}", contents);
18}
19//结构体Config用来封装参数属性
20struct Config {
21 query: String,
22 filename: String,
23}
24
25//为结构体实现一个构造器,其主要功能也是读取和解析参数
26impl Config {
27 fn new(args: &[String]) -> Result<Config, &'static str> {
28 if args.len() < 3 {
29 return Err("参数个数不够!not enough arguments");
30 }
31
32 let query = args[1].clone();
33 let filename = args[2].clone();
34
35 Ok(Config { query, filename })
36 }
37}
38
39
直接用命令:cargo run 运行代码,会报错,错误信息一样:
1
2
3
4
5 1Finished dev [unoptimized + debuginfo] target(s) in 0.96s
2 Running `target\debug\minigrep.exe`
3thread 'main' panicked at '参数个数不够!not enough arguments', src\main.rs:23:13
4note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
5
错误处理代码,重构成功!
我们再来跑一下正确的流程。
我们用命令:
1
2 1cargo run the poem.txt
2
运行后的结果跟原来一样,很好!
现在我们再来重构一下main函数的对参数的处理逻辑,代码如下:
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 1use std::env;
2use std::fs;
3use std::process;
4//主函数,程序入口
5fn main() {
6 let args: Vec<String> = env::args().collect();
7
8 // let config: Config = Config::new(&args);
9 let config = Config::new(&args).unwrap_or_else(|err| {
10 println!("Problem parsing arguments: {}", err);
11 process::exit(1);
12 });
13
14 // let contents =
15 // fs::read_to_string(config.filename).expect("Something went wrong reading the file");
16
17 // println!("With text:\n{}", contents)
18 run(config);//重构从文件中读取内容的业务逻辑
19}
20//结构体Config用来封装参数属性
21struct Config {
22 query: String,
23 filename: String,
24}
25
26//为结构体实现一个构造器,其主要功能也是读取和解析参数
27impl Config {
28 fn new(args: &[String]) -> Result<Config, &'static str> {
29 if args.len() < 3 {
30 return Err("参数个数不够!not enough arguments");
31 }
32
33 let query = args[1].clone();
34 let filename = args[2].clone();
35
36 Ok(Config { query, filename })
37 }
38}
39//重构从文件中读取内容的业务逻辑
40fn run(config: Config) {
41 let contents = fs::read_to_string(config.filename)
42 .expect("从文件中读取内容时出错!Something went wrong reading the file");
43
44 println!("With text:\n{}", contents);
45}
46
47
我们用命令:
1
2 1cargo run the poem.txt
2
运行后的结果跟原来一样,重构成功!
我们再来让run方法有返回值,这样,主程序更好处理,看代码:
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 1use std::env;
2use std::error::Error;
3use std::fs;
4use std::process;
5//主函数,程序入口
6fn main() {
7 let args: Vec<String> = env::args().collect();
8
9 // let config: Config = Config::new(&args);
10 let config = Config::new(&args).unwrap_or_else(|err| {
11 println!("Problem parsing arguments: {}", err);
12 process::exit(1);
13 });
14
15 if let Err(e) = run(config) {
16 //根据处理结果返回值 来处理,如果有错误,则打印信息,并直接退出当前程序
17 println!("Application error: {}", e);
18
19 process::exit(1);
20 }
21}
22//结构体Config用来封装参数属性
23struct Config {
24 query: String,
25 filename: String,
26}
27
28//为结构体实现一个构造器,其主要功能也是读取和解析参数
29impl Config {
30 fn new(args: &[String]) -> Result<Config, &'static str> {
31 if args.len() < 3 {
32 return Err("参数个数不够!not enough arguments");
33 }
34
35 let query = args[1].clone();
36 let filename = args[2].clone();
37
38 Ok(Config { query, filename })
39 }
40}
41//重构从文件中读取内容的业务逻辑
42fn run(config: Config) -> Result<(), Box<dyn Error>> {
43 let contents = fs::read_to_string(config.filename)?;
44
45 println!("With text:\n{}", contents);
46
47 Ok(())
48}
49
50
我们用命令:
1
2 1cargo run the poem.txt
2
运行后的结果跟原来一样,重构成功!
非常好!
现在我们再来重构,把main函数,所有业务逻辑迁移到lib.rs文件。
我们来看看怎么重构。
首先,我们先在工程目录下的目录src下创建另一个文件lib.rs,并把main函数相关代码写入进去:
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 1use std::error::Error;
2use std::fs;
3
4//结构体Config用来封装参数属性
5pub struct Config {
6 query: String,
7 filename: String,
8}
9
10//为结构体实现一个构造器,其主要功能也是读取和解析参数
11impl Config {
12 pub fn new(args: &[String]) -> Result<Config, &'static str> {
13 if args.len() < 3 {
14 return Err("参数个数不够!not enough arguments");
15 }
16
17 let query = args[1].clone();
18 let filename = args[2].clone();
19
20 Ok(Config { query, filename })
21 }
22}
23//重构从文件中读取内容的业务逻辑
24pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
25 let contents = fs::read_to_string(config.filename)?;
26
27 println!("With text:\n{}", contents);
28
29 Ok(())
30}
31
32
这时src/main.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 1use minigrep::run;
2use minigrep::Config;
3
4use std::env;
5use std::process;
6//主函数,程序入口
7fn main() {
8 let args: Vec<String> = env::args().collect();
9
10 // let config: Config = Config::new(&args);
11 let config = Config::new(&args).unwrap_or_else(|err| {
12 println!("Problem parsing arguments: {}", err);
13 process::exit(1);
14 });
15
16 if let Err(e) = run(config) {
17 //根据处理结果返回值 来处理,如果有错误,则打印信息,并直接退出当前程序
18 println!("Application error: {}", e);
19
20 process::exit(1);
21 }
22}
23
24
我们用命令:
1
2 1cargo run the poem.txt
2
运行后的结果跟原来一样,重构成功!
完美!
我们已经有一个能运行的基本程序框架。
以上,希望对你有用。
1
2 1如果遇到什么问题,欢迎加入:rust新手群,在这里我可以提供一些简单的帮助,加微信:360369487,注明:博客园+rust
2
参考文章:
https://doc.rust-lang.org/stable/book/ch12-00-an-io-project.html