12.3.0. 写在正文之前
第12章要做一个实例的项目------一个命令行程序。这个程序是一个grep
(Global Regular Expression Print ),是一个全局正则搜索和输出的工具。它的功能是在指定的文件中搜索出指定的文字。
这个项目分为这么几步:
- 接收命令行参数
- 读取文件
- 重构:改进模块和错误处理(本文)
- 使用TDD(测试驱动开发)开发库功能
- 使用环境变量
- 将错误信息写入标准错误而不是标准输出
喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=^・ω・^=)
12.3.1. 重构的目的
重构的目的是要增进模块化的程度以及改善错误处理能力。
以下是截止到上一篇文章所写出的全部代码:
rust
use std::env;
use std::fs;
fn main() {
let args:Vec<String> = env::args().collect();
let query = &args[1];
let filename = &args[2];
println!("search for {}", query);
println!("In file {}", filename);
let contents = fs::read_to_string(filename)
.expect("Somthing went wrong while reading the file");
println!("With text:\n{}", contents);
}
这个代码存在4个问题:
-
main
函数负责的功能太多,它既负责命令行的功能解析,又负责读取文件。程序代码的编写原则是每一个函数只负责一个功能,所以说最好把函数拆开。 -
query
和filename
这两个变量是用来存储程序配置的,contents
是用来存储文件内容的。随着代码和变量在编写时越来越多,每个变量的实际意义就变得难以追踪。所以最好把这些变量存在结构体里。 -
读取文件时使用
expect
来处理错误,不论读取时出现了什么错误都只会打印出错误信息并恐慌,这并不是最好的处理方式。因为文件读取失败可能是文件找不到,也有可能是权限问题,现在指定的这个恐慌信息"Somthing went wrong while reading the file"并不能帮助用户排查错误。 -
如果程序里到处都使用
expect
方法那么用户得到的报错信息是来自于Rust语言内部的,比如"Index out of bound",不是程序员根本不明白到底是什么引发了错误。最好是将错误的代码集中放置,从而使将来的维护者在需要修改错误处理相关的逻辑时只考虑这一处代码,也能确保向用户打印的错误信息是易于理解的。
12.3.2. 二进制程序关注点分离的指导性原则
很多Rust二进制项目都会面临同样的组织结构问题,它们将过多的功能和过多的任务都放到了main
函数里面。针对这种情况,Rust社区做了一套为二进制程序进行关注点分离的指导性原则:
- 将程序拆分为
main.rs
和lib.rs
,将业务逻辑放入lib.rs
- 当逻辑较少时,将它放在
main.rs
也可以 - 当逻辑变复杂时,需要将它从
main.rs
提取到lib.rs
经过上述拆分之后,这个例子中应该留在main
函数中的功能有:
- 使用参数值调用命令行解析逻辑
- 进行其它配置
- 调用
lib.rs
中的run
函数 - 处理
run
函数可能出现的问题
12.3.3. 分离逻辑
再看一眼代码:
rust
use std::env;
use std::fs;
fn main() {
let args:Vec<String> = env::args().collect();
let query = &args[1];
let filename = &args[2];
println!("search for {}", query);
println!("In file {}", filename);
let contents = fs::read_to_string(filename)
.expect("Somthing went wrong while reading the file");
println!("With text:\n{}", contents);
}
先把获取命令行参数的部分独立出来:
rust
fn parse_config(args: &[String]) -> (&str, &str) {
let query = &args[1];
let filename = &args[2];
(query, filename)
}
&[String]
表示是一个内部元素为String
的Vector
切片- 这里没有打印
query
和filename
的必要了,所以就去掉
然后改一下main
函数,调用parse_config
:
rust
fn main() {
let args:Vec<String> = env::args().collect();
let (query, filename) = parse_config(&args);
let contents = fs::read_to_string(filename)
.expect("Somthing went wrong while reading the file");
println!("With text:\n{}", contents);
}
12.3.4. 使用结构体
parse_config
内把query
和filename
组合成元组返回,在main
函数里又把元组的两个值拆分为两个变量,这种来回拆分合成表明程序中建立的抽象结构有问题。
query
和filename
都是配置的一部分,两者是彼此相关联的,把这两个东西放在元组里不足以表达出这种抽象的关联。最好的办法是放在结构体里:
rust
struct Config {
query: String,
filename: String,
}
fn main() {
let args:Vec<String> = env::args().collect();
let config = parse_config(&args);
let contents = fs::read_to_string(config.filename)
.expect("Somthing went wrong while reading the file");
println!("With text:\n{}", contents);
}
fn parse_config(args: &[String]) -> Config {
let query = args[1].clone();
let filename = args[2].clone();
Config {
query,
filename,
}
}
parse_config
中必须注意query
和filename
的格式:形参args
的类型是&[String]
是一个引用,没有所有权,所以query
和filename
也是引用,但是Config
这个结构体接收的是String
而不是&String
,所以需要通过克隆来获得所有权,把&String
转为String
。
克隆虽然比直接存储引用消耗了更多时间和内存,但它省去了处理生命周期的麻烦,让代码更加直接简单。在某些场景中,放弃一些性能来获取更多的简洁性是非常值得考虑的。
当然,使用String::from
函数来封装也是可以的:
rust
fn parse_config(args: &[String]) -> Config {
let query = &args[1];
let filename = &args[2];
Config {
query: String::from(query),
filename: String::from(filename),
}
}
当然可行的代码可能不止这两种,这里我就采用第一种克隆的方法。
12.3.5. 把函数变为结构体的方法
既然parse_config
会创建一个Config
的实例,也就是说它是一个构造函数。对于构造函数,可以这么写:
rust
impl Config {
fn new(args: &[String]) -> Config {
let query = args[1].clone();
let filename = args[2].clone();
Config {
query,
filename,
}
}
}
只需要把这个函数写在Config
的方法上即可(对于方法的详细解释,详见 5.3. struct的方法(Method))。这里还给parse_config
改了个名叫new
,是因为我把它当作了一个构造函数来处理(构造函数一般都命名为new
)。
这么改,main
函数里面也需要改一下:
rust
let config = Config::new(&args);
12.3.5. 整体代码
以下是截止到本篇文章所写出的所有代码:
rust
use std::env;
use std::fs;
struct Config {
query: String,
filename: String,
}
fn main() {
let args:Vec<String> = env::args().collect();
let config = Config::new(&args);
let contents = fs::read_to_string(config.filename)
.expect("Somthing went wrong while reading the file");
println!("With text:\n{}", contents);
}
impl Config {
fn new(args: &[String]) -> Config {
let query = args[1].clone();
let filename = args[2].clone();
Config {
query,
filename,
}
}
}