[易学易懂系列|rustlang语言|零基础|快速入门|(24)|实战2:命令行工具minigrep(1)]

项目实战

实战2:命令行工具minigrep

有了昨天的基础,我们今天来开始另一个稍微有点复杂的项目。

简单来说,就是开发一个我们自己的grep (globally search a regular expression and print)

首先用命令生成一个工程:

cargo new minigrep

然后在工程目录minigrep下新建一个文件: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!

在工程目录下的src/main.rs文件中,填入以下代码:

use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();//从命令行环境得到用户输入的参数 let query = &args[1];//参数1
let filename = &args[2];//参数2 println!("Searching for {}", query);//打印参数1
println!("In file {}", filename);//打印参数1 let contents = fs::read_to_string(filename).expect("Something went wrong reading the file");//以参数2为路径,读取文件内容 println!("With text:\n{}", contents);//打印内容
}

我们现在用命令:

cargo run the poem.txt

结果将打印如下 信息:

Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target\debug\minigrep.exe the poem.txt`
Searching for the
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中读取了相关内容信息。

好现在我们来重构一下代码!

为什么要重构代码?

因为我们的很多逻辑都写在一个main函数上,这是一个不好的现象,如果功能越多,代码也越多,最后可以出现一个超长的main函数。可读性和可维护性都很差!

所以从一开始,我们就要考虑代码的可读性和可维护性,这是最佳实践!

好,我们开始重构吧!

根据单一职责设计原则,一个方法只负责一个职责。

那我们就应该把main方法里的负责处理参数读取和参数搜索的逻辑,把它们分别抽离开来,放在单独的方法中。

我们先把处理参数读取的逻辑抽出来,如下:

use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect(); let (query, filename) = parseConfig(&args);//抽离参数读取与解析的逻辑 let contents = fs::read_to_string(filename).expect("Something went wrong reading the file"); println!("With text:\n{}", contents);
} //参数读取与解析的逻辑
fn parseConfig(args: &[String]) -> (&str, &str) {
println!("args len is {}", args.len());
println!("args is {:?}", args);
let query = &args[1];
let filename = &args[2]; println!("Searching for {}", query);
println!("In file {}", filename);
(query, filename)
}

同样,我们用命令:

cargo run the poem.txt

运行后的结果为:

args len is  3
args is [".\\target\\debug\\minigrep.exe", "the", "poem.txt"]
Searching for the
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!

很好,我们的程序正常运行。

我们再来看看把元组用一个结构体来替换,把相关参数属性放在一起,更简洁直观。

开始吧:

use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect(); let config: Config = parseConfig(&args); let contents =
fs::read_to_string(config.filename).expect("Something went wrong reading the file"); println!("With text:\n{}", contents);
}
//结构体Config用来封装参数属性
struct Config {
query: String,
filename: String,
}
//参数读取与解析的逻辑
fn parseConfig(args: &[String]) -> Config {
println!("args len is {}", args.len());
println!("args is {:?}", args);
let query = args[1].clone();//这里直接用clone方法得到一个参数string的拷贝
let filename = args[2].clone();//这里直接用clone方法得到一个参数string的拷贝 println!("Searching for {}", query);
println!("In file {}", filename);
Config { query, filename }//返回结构体
}

同样,我们用命令:

cargo run the poem.txt

运行后的结果跟原来一样,重构成功!

有同学会问,这里为什么用clone呢?

不会有性能问题吗?

因为简单!

我们先保持简单,让程序能跑起来,以后再考虑性能的问题。(事实上,这里的性能只损失很少一部分。)

很好!

能否再优化重构一下:参数读取与解析代码?

可以的。

我们看如下 代码:

use std::env;
use std::fs;
//主函数,程序入口
fn main() {
let args: Vec<String> = env::args().collect(); let config: Config = Config::new(&args);//直接调用Config构造函数 let contents =
fs::read_to_string(config.filename).expect("Something went wrong reading the file"); println!("With text:\n{}", contents);
}
//结构体Config用来封装参数属性
struct Config {
query: String,
filename: String,
}
//为结构体实现一个构造器,其主要功能也是读取和解析参数
impl Config {
fn new(args: &[String]) -> Config {
let query = args[1].clone();
let filename = args[2].clone(); Config { query, filename }
}
}
//参数读取与解析的逻辑,现在可以删除了!
fn parseConfig(args: &[String]) -> Config {
println!("args len is {}", args.len());
println!("args is {:?}", args);
let query = args[1].clone();//这里直接用clone方法得到一个参数string的拷贝
let filename = args[2].clone();//这里直接用clone方法得到一个参数string的拷贝 println!("Searching for {}", query);
println!("In file {}", filename);
Config { query, filename }//返回结构体
}

同样,我们用命令:

cargo run the poem.txt

运行后的结果跟原来一样,重构成功!

这时,我们的函数:parseConfig,可以退休了。

好吧,直接把它删除!

同样,我们用命令:

cargo run the poem.txt

运行后的结果跟原来一样,删除成功!

这里为什么,每执行一次小版本的重构,都要跑一次代码呢?

因为可以保证,每次重构都是很小一步,可以避免错误!如果重构失败,也容易回退代码。

好吧,我们继续重构。

我们现在考虑一下错误处理。

比如,我们现在直接用命令:cargo run 运行代码,会报错:

$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/minigrep`
thread 'main' panicked at 'index out of bounds: the len is 1
but the index is 1', src/main.rs:25:21
note: Run with `RUST_BACKTRACE=1` for a backtrace.

好,我们现在在代码上加上错误处理的逻辑,修改Config的构造函数:

//为结构体实现一个构造器,其主要功能也是读取和解析参数
impl Config {
fn new(args: &[String]) -> Config {
if args.len() < 3 {
panic!("参数个数不够!not enough arguments");//增加错误处理
}
let query = args[1].clone();
let filename = args[2].clone(); Config { query, filename }
}
}

直接用命令:cargo run 运行代码,会报错,但错误信息明确多了:

Finished dev [unoptimized + debuginfo] target(s) in 0.96s
Running `target\debug\minigrep.exe`
thread 'main' panicked at '参数个数不够!not enough arguments', src\main.rs:23:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

能否更优雅地处理错误信息?

可以,我们可以用Result,代码如下:

use std::env;
use std::fs;
use std::process;
//主函数,程序入口
fn main() {
let args: Vec<String> = env::args().collect(); // let config: Config = Config::new(&args);
let config = Config::new(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {}", err);
process::exit(1);
}); let contents =
fs::read_to_string(config.filename).expect("Something went wrong reading the file"); println!("With text:\n{}", contents);
}
//结构体Config用来封装参数属性
struct Config {
query: String,
filename: String,
} //为结构体实现一个构造器,其主要功能也是读取和解析参数
impl Config {
fn new(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("参数个数不够!not enough arguments");
} let query = args[1].clone();
let filename = args[2].clone(); Ok(Config { query, filename })
}
}

直接用命令:cargo run 运行代码,会报错,错误信息一样:

Finished dev [unoptimized + debuginfo] target(s) in 0.96s
Running `target\debug\minigrep.exe`
thread 'main' panicked at '参数个数不够!not enough arguments', src\main.rs:23:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

错误处理代码,重构成功!

我们再来跑一下正确的流程。

我们用命令:

cargo run the poem.txt

运行后的结果跟原来一样,很好!

现在我们再来重构一下main函数的对参数的处理逻辑,代码如下:

use std::env;
use std::fs;
use std::process;
//主函数,程序入口
fn main() {
let args: Vec<String> = env::args().collect(); // let config: Config = Config::new(&args);
let config = Config::new(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {}", err);
process::exit(1);
}); // let contents =
// fs::read_to_string(config.filename).expect("Something went wrong reading the file"); // println!("With text:\n{}", contents)
run(config);//重构从文件中读取内容的业务逻辑
}
//结构体Config用来封装参数属性
struct Config {
query: String,
filename: String,
} //为结构体实现一个构造器,其主要功能也是读取和解析参数
impl Config {
fn new(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("参数个数不够!not enough arguments");
} let query = args[1].clone();
let filename = args[2].clone(); Ok(Config { query, filename })
}
}
//重构从文件中读取内容的业务逻辑
fn run(config: Config) {
let contents = fs::read_to_string(config.filename)
.expect("从文件中读取内容时出错!Something went wrong reading the file"); println!("With text:\n{}", contents);
}

我们用命令:

cargo run the poem.txt

运行后的结果跟原来一样,重构成功!

我们再来让run方法有返回值,这样,主程序更好处理,看代码:

use std::env;
use std::error::Error;
use std::fs;
use std::process;
//主函数,程序入口
fn main() {
let args: Vec<String> = env::args().collect(); // let config: Config = Config::new(&args);
let config = Config::new(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {}", err);
process::exit(1);
}); if let Err(e) = run(config) {
//根据处理结果返回值 来处理,如果有错误,则打印信息,并直接退出当前程序
println!("Application error: {}", e); process::exit(1);
}
}
//结构体Config用来封装参数属性
struct Config {
query: String,
filename: String,
} //为结构体实现一个构造器,其主要功能也是读取和解析参数
impl Config {
fn new(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("参数个数不够!not enough arguments");
} let query = args[1].clone();
let filename = args[2].clone(); Ok(Config { query, filename })
}
}
//重构从文件中读取内容的业务逻辑
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.filename)?; println!("With text:\n{}", contents); Ok(())
}

我们用命令:

cargo run the poem.txt

运行后的结果跟原来一样,重构成功!

非常好!

现在我们再来重构,把main函数,所有业务逻辑迁移到lib.rs文件。

我们来看看怎么重构。

首先,我们先在工程目录下的目录src下创建另一个文件lib.rs,并把main函数相关代码写入进去:

use std::error::Error;
use std::fs; //结构体Config用来封装参数属性
pub struct Config {
query: String,
filename: String,
} //为结构体实现一个构造器,其主要功能也是读取和解析参数
impl Config {
pub fn new(args: &[String]) -> Result<Config, &'static 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(())
}

这时src/main.rs的代码更新为如下:

use minigrep::run;
use minigrep::Config; use std::env;
use std::process;
//主函数,程序入口
fn main() {
let args: Vec<String> = env::args().collect(); // let config: Config = Config::new(&args);
let config = Config::new(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {}", err);
process::exit(1);
}); if let Err(e) = run(config) {
//根据处理结果返回值 来处理,如果有错误,则打印信息,并直接退出当前程序
println!("Application error: {}", e); process::exit(1);
}
}

我们用命令:

cargo run the poem.txt

运行后的结果跟原来一样,重构成功!

完美!

我们已经有一个能运行的基本程序框架。

以上,希望对你有用。

如果遇到什么问题,欢迎加入:rust新手群,在这里我可以提供一些简单的帮助,加微信:360369487,注明:博客园+rust

参考文章:

https://doc.rust-lang.org/stable/book/ch12-00-an-io-project.html

[易学易懂系列|rustlang语言|零基础|快速入门|(24)|实战2:命令行工具minigrep(1)]的更多相关文章

  1. [易学易懂系列|rustlang语言|零基础|快速入门|(28)|实战5:实现BTC价格转换工具]

    [易学易懂系列|rustlang语言|零基础|快速入门|(28)|实战5:实现BTC价格转换工具] 项目实战 实战5:实现BTC价格转换工具 今天我们来开发一个简单的BTC实时价格转换工具. 我们首先 ...

  2. [易学易懂系列|rustlang语言|零基础|快速入门|(27)|实战4:从零实现BTC区块链]

    [易学易懂系列|rustlang语言|零基础|快速入门|(27)|实战4:从零实现BTC区块链] 项目实战 实战4:从零实现BTC区块链 我们今天来开发我们的BTC区块链系统. 简单来说,从数据结构的 ...

  3. [易学易懂系列|rustlang语言|零基础|快速入门|(26)|实战3:Http服务器(多线程版本)]

    [易学易懂系列|rustlang语言|零基础|快速入门|(26)|实战3:Http服务器(多线程版本)] 项目实战 实战3:Http服务器 我们今天来进一步开发我们的Http服务器,用多线程实现. 我 ...

  4. [易学易懂系列|rustlang语言|零基础|快速入门|(25)|实战2:命令行工具minigrep(2)]

    [易学易懂系列|rustlang语言|零基础|快速入门|(25)|实战2:命令行工具minigrep(2)] 项目实战 实战2:命令行工具minigrep 我们继续开发我们的minigrep. 我们现 ...

  5. [易学易懂系列|rustlang语言|零基础|快速入门|(23)|实战1:猜数字游戏]

    [易学易懂系列|rustlang语言|零基础|快速入门|(23)|实战1:猜数字游戏] 项目实战 实战1:猜数字游戏 我们今天来来开始简单的项目实战. 第一个简单项目是猜数字游戏. 简单来说,系统给了 ...

  6. [易学易懂系列|rustlang语言|零基础|快速入门|(5)|生命周期Lifetime]

    [易学易懂系列|rustlang语言|零基础|快速入门|(5)] Lifetimes 我们继续谈谈生命周期(lifttime),我们还是拿代码来说话: fn main() { let mut a = ...

  7. [易学易懂系列|rustlang语言|零基础|快速入门|(22)|宏Macro]

    [易学易懂系列|rustlang语言|零基础|快速入门|(22)|宏Macro] 实用知识 宏Macro 我们今天来讲讲Rust中强大的宏Macro. Rust的宏macro是实现元编程的强大工具. ...

  8. [易学易懂系列|rustlang语言|零基础|快速入门|(21)|智能指针]

    [易学易懂系列|rustlang语言|零基础|快速入门|(21)|智能指针] 实用知识 智能指针 我们今天来讲讲Rust中的智能指针. 什么是指针? 在Rust,指针(普通指针),就是保存内存地址的值 ...

  9. [易学易懂系列|rustlang语言|零基础|快速入门|(20)|错误处理]

    [易学易懂系列|rustlang语言|零基础|快速入门|(20)|错误处理] 实用知识 错误处理 我们今天来讲讲Rust中的错误处理. 很多语言都有自己的错误处理方式,比如,java是异常处理机制. ...

随机推荐

  1. POJ2411 Mondriaan's Dream 【状压dp】

    没错,这道题又是我从LZL里的博客里剽过来的,他的题真不错,真香. 题目链接:http://poj.org/problem?id=2411 题目大意:给一个n * m的矩形, 要求用 1 * 2的小方 ...

  2. Min swaps to sort array

    Given an array with distinct numbers, return an integer indicating the minimum number of swap operat ...

  3. inxi 查看国产服务器的硬件配置信息

    下载inxi 查看国产服务器的信息 https://codeload.github.com/smxi/inxi/tar.gz/3.0.35-1 注意查看命令是 ./inxi -F 2. 直接是二进制包 ...

  4. CENTOS 6-7的本地YUM源配置

    本文档适合CENTOS 6-7的本地YUM源配置 cd /media cd CentOS_6.8_Final/ cd Packages 创建目录拷贝文件 mkdir /yum cp * /yum 配置 ...

  5. SQLite基础-5.数据操作语言

    目录 一.添加数据(insert) 二.查询数据(select) 三.更新数据(update) 三. 删除数据(delete) 一.添加数据(insert) INSERT INTO 用于向数据库的某个 ...

  6. C++类的对象和类的指针的区别

    #include <iostream> #include <string> using namespace std; class Student { public: stati ...

  7. Linux就该这么学——新手必须掌握的命令之文件目录管理命令组

    touch命令 用途 : 用于创建空白文件或设置文件的时间 格式 : touch [选项] [文件] 参数 作用 -a 仅修改”读取时间”(atime) -m 仅修改”修改时间”(mtime) -d ...

  8. django进阶版4

    目录 1 Auth模块是什么 2 auth模块常用方法 authenticate() login(HttpRequest, user) logout(request) is_authenticated ...

  9. MySQL各大存储引擎

    MySQL各大存储引擎: 最好先看下你下的MySQL支持什么数据库引擎 存储引擎主要有: 1. MyIsam , 2. InnoDB, 3. Memory, 4. Blackhole, 5. CSV, ...

  10. Ruby Rails学习中:添加安全密码

    接上篇 一. 添加安全密码 我们已经为 name 和 email 字段添加了验证规则, 现在要加入用户所需的最后一个常规属性: 安全密码.每个用户都要设置一个密码(还要二次确认), 数据库中则存储经过 ...