前言#
昨天我们把minigrep基本实现了
但是它是相当简陋不优雅的,所以我们来优化下。
未优化前的代码#
use std::{env, fs};
#[derive(Debug)]
struct Config<'a> {
query: &'a String,
file_path: &'a String,
}
impl<'a> Config<'a> {
fn new (args: &'a Vec<String>) -> Config {
let query = &args[1];
let file_path = &args[2];
Config {
query,
file_path
}
}
}
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
let poem = fs::read_to_string(&config.file_path).expect("read_file_error");
let poem = poem.to_lowercase();
let mut res = vec![];
for line in poem.lines() {
if line.to_lowercase().contains(config.query.as_str()) {
res.push(line);
}
}
dbg!(res);
}优化1:调整项目组织结构(以下纯个人想法,不认同请直接看官网)#
这个点我们得先处理,为什么呢?
因为如果等项目大了、掺杂了各种代码之后要调整就有较高的成本了,所以一般我们在开发之前就要想要结构是怎么样的,不过万一开发过程中发现还是需要再调整,这个时候也不晚,尽量避免开发完之后再去调整。
迁移struct#
目前我们的项目把代码都堆在main.rs里,main文件作为一个入口文件,不应该堆这么多代码的。
我们需要把我们的代码迁移出来放到lib.rs。
首先需要迁移的理所当然的是我们的struct 。
#[derive(Debug)]
pub struct Config<'a> {
query: &'a String,
file_path: &'a String,
}
pub enum ConfigField {
Query,
FilePath,
}
impl<'a> Config<'a> {
pub fn new (args: &'a Vec<String>) -> Config {
let query = &args[1];
let file_path = &args[2];
Config {
query,
file_path
}
}
pub fn get (&self, field: ConfigField) -> &String {
match field {
ConfigField::Query => self.query,
ConfigField::FilePath => self.file_path,
}
}
}这里你应该注意到代码里多出了个enum: ConfigField,以及一个Config.get的方法。因为struct里的字段是私有的,所以这里使用get方法将它暴露出去。当然,你也可以直接给field设置为pub,这样就不用匹配了。
还记得生命周期里的第三板斧吗?
method是不需要注释生命周期的,所以这里我们不注释也不会报错。
然后我们回到main.rs中
use std::{env, fs};
use minigrep::{Config, ConfigField};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
let poem = fs::read_to_string(config.get(ConfigField::FilePath)).expect("read_file_error");
let poem = poem.to_lowercase();
let mut res = vec![];
for line in poem.lines() {
if line.to_lowercase().contains(config.get(ConfigField::Query).as_str()) {
res.push(line);
}
}
dbg!(res);
}接下来轮到main函数了
我们的main函数
关注点分离#
这是一种开发思想,比如我们前端的fetch[2]就是基于关注点分离的思想架构的。
把一个复杂的问题根据不同的点(侧面)拆分,然后再整合。
目前我们的main函数中做了四件事
- 获取命令行参数
- 创建
config实例 - 读
poem.txt - 匹配行
其中第二点由于我们之前在实现过程中就调整了,所以不需要动。
而第三点第四点实际上是可以合并的,读取poem.txt这个操作是和匹配行这个行为关联的。我们可以把他们放到同一个函数中。
use std::{env, fs};
use minigrep::{Config, ConfigField};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
let res = run(config);
dbg!(res);
}
fn run (config: Config) -> Vec<String> {
let poem = fs::read_to_string(config.get(ConfigField::FilePath)).expect("read_file_error");
let poem = poem.to_lowercase();
let mut res = vec![];
for line in poem.lines() {
if line.to_lowercase().contains(config.get(ConfigField::Query).as_str()) {
res.push(line.to_owned());
}
}
res
}可以看到我将第三点和第四点合并到一起,我还调用了to_owned这个method,因为我们的vector不能带着引用的数据出run这个作用域,所以只能把值带走了。
现在main函数就简洁了。
然后我们还可以把run方法也放到lib.rs中,为什么呢?因为这样就可以写测试代码了。
当前改动后的代码#
lib.rs
use std::{fs};
#[derive(Debug)]
pub struct Config<'a> {
query: &'a String,
file_path: &'a String,
}
pub enum ConfigField {
Query,
FilePath,
}
impl<'a> Config<'a> {
pub fn new (args: &'a Vec<String>) -> Config {
let query = &args[1];
let file_path = &args[2];
Config {
query,
file_path
}
}
pub fn get (&self, field: ConfigField) -> &String {
match field {
ConfigField::Query => self.query,
ConfigField::FilePath => self.file_path,
}
}
}
pub fn run (config: Config) -> Vec<String> {
let poem = fs::read_to_string(config.get(ConfigField::FilePath)).expect("read_file_error");
let poem = poem.to_lowercase();
let mut res = vec![];
for line in poem.lines() {
if line.to_lowercase().contains(config.get(ConfigField::Query).as_str()) {
res.push(line.to_owned());
}
}
res
}
main.rs
use std::{env};
use minigrep::{Config, run};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
let res = run(config);
dbg!(res);
}优化点2:完善错误处理#
目前我们处理错误都是直接expect(...),这样未免有些太过暴力了。毕竟有些我们并不想panic或者想根据不同错误类型输出不同的错误信息。
我们先来收集下可能会出错的地方
- 获取命令行参数,不用想,这块出问题最多,毕竟是提供给用户的(永远永远不要相信你的用户并且一定要把他们想的越笨越好,最好就是能做到饭菜直接炫他们嘴里,并且是已经嚼碎过的,没有硬物的,就差帮他们吃了)。
- 创建实例失败,这个可以归纳到第一点中
- 读取文件失败
所以存在两个会引发错误的地方,针对这两点我们来改下代码。
错误处理点1#
第一点其实还能引申出几个小问题点:
- 参数个数,少了
panic,多了不理。 - 参数反了(这个点其实可以不处理,如果不处理,会读取文件失败,交由文件读取的错误处理)
- 参数字符是否有效(这个问题我们昨天说过,暂不考虑)
所以我们第一点暂时只需要判断参数的个数
那么在哪里加上呢?args.collect目前是不会报错的,所以我们得考虑其他地方,比如我们创建实例的地方。
pub fn new (args: &'a Vec<String>) -> Config {
let args_len = args.len();
if args_len < 3 {
panic!("not enough arguments, current arguments length: {args_len}");
}
let query = &args[1];
let file_path = &args[2];
Config {
query,
file_path
}
}这里直接用了panic,因为是用户输入的,对得起这个规格的待遇(开玩笑的),实际上如果这里不panic后面也同样会在其他地方panic,我们也没办法推测或者模拟用户数据去输出一个值。
其实我觉得这样写已经差不多了,毕竟这只是个demo,怎奈人官方讲究,得用Result。
官方为啥要这样做呢?
因为官方推荐的是把错误处理抛出来在main函数中处理,毕竟这样更方便阅读。
我们来改下代码
pub fn new(args: &'a Vec<String>) -> Result<Config, &'static str> {
let args_len = args.len();
if args_len < 3 {
return Err("not enough arguments");
}
let query = &args[1];
let file_path = &args[2];
Ok(Config { query, file_path })
}其中&'static str之前学可恢复错误的时候也有提到过。
在这里是因为我们的错误提示语是在当前作用域的,引用如果被抛出去了就会变成悬浮引用,那就有问题了。
然后改下main函数中的调用
use std::{env, process};
use minigrep::{Config, run};
fn main() {
let args: Vec<String> = env::args().collect();
dbg!(&args);
let config = Config::new(&args).unwrap_or_else(|err| {
println!("something wrong when new config: {err}");
process::exit(1);
});
let res = run(config);
dbg!(res);
}是不是优雅了很多~
错误处理2#
第二点错误有好几种场景,比如找不到文件或者没有权限等。
我们来改下代码
use std::{fs, error::Error};
pub fn run(config: Config) -> Result<Vec<String>, Box<dyn Error>> {
let poem = fs::read_to_string(config.get(ConfigField::FilePath))?;
let poem = poem.to_lowercase();
let mut res = vec![];
for line in poem.lines() {
if line
.to_lowercase()
.contains(config.get(ConfigField::Query).as_str())
{
res.push(line.to_owned());
}
}
Ok(res)
}这里有几个点需要说下:
read_to_string之前有说过返回一个Result的类型,所以可以直接调用?操作符,直接把错误抛出去。

当然我们现在还看不懂这个inner是个啥。
Box: 这个我们之前有遇到过,就是标明这个函数返回的错误类型现在暂时未知,但是一定是一个实现了std::Error这个trait的类型。dyn也就是dynamic。
ok然后我们改下main函数
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err| {
println!("something wrong when new config: {err}");
process::exit(1);
});
let res = run(config).unwrap_or_else(|err| {
println!("something wrong when read file: {err}");
process::exit(1);
});
dbg!(res);
}这样错误也被抛出来了。
实际上我们还可以优化下代码,比如run函数其实没必要把匹配的结果返回,毕竟我们的项目只是查找并输出,没有后续操作了。
不过这里就不处理了。
完善错误处理后的代码#
lib.rs
use std::{fs, error::Error};
#[derive(Debug)]
pub struct Config<'a> {
query: &'a String,
file_path: &'a String,
}
pub enum ConfigField {
Query,
FilePath,
}
impl<'a> Config<'a> {
pub fn new(args: &'a Vec<String>) -> Result<Config, &'static str> {
let args_len = args.len();
if args_len < 3 {
return Err("not enough arguments");
}
let query = &args[1];
let file_path = &args[2];
Ok(Config { query, file_path })
}
pub fn get(&self, field: ConfigField) -> &String {
match field {
ConfigField::Query => self.query,
ConfigField::FilePath => self.file_path,
}
}
}
pub fn run(config: Config) -> Result<Vec<String>, Box<dyn Error>> {
let poem = fs::read_to_string(config.get(ConfigField::FilePath))?;
let poem = poem.to_lowercase();
let mut res = vec![];
for line in poem.lines() {
if line
.to_lowercase()
.contains(config.get(ConfigField::Query).as_str())
{
res.push(line.to_owned());
}
}
Ok(res)
}
main.rs
use std::{env, process};
use minigrep::{Config, run};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err| {
println!("something wrong when new config: {err}");
process::exit(1);
});
let res = run(config).unwrap_or_else(|err| {
println!("something wrong when read file: {err}");
process::exit(1);
});
dbg!(res);
}优化点3:测试驱动开发(TDD)#
之前学test的时候有说到过,binary crate是没有办法test的,所以这也就是我们把run方法迁移到lib.rs中的原因之一。
另外我们前面完善错误处理也是为了更方便编写测试用例。
那么啥叫测试驱动开发呢(Test-Driven Development)?
它有如下四个步骤
- Write a test that fails and run it to make sure it fails for the reason you expect.
- Write or modify just enough code to make the new test pass.
- Refactor the code you just added or changed and make sure the tests continue to pass.
- Repeat from step 1!
简单地说就是通过测试用例来提高代码质量。
由于我的步骤和官方的并不一样,所以我现在写完了再去加上测试代码就挺鸡肋的。。。。
我这里就不按照官方给的顺序一步一步把代码实现把测试用例喂肥了,直接一步到位。
lib.rs中
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn search () {
let query = String::from("how");
let file_path = String::from("src/poem.txt");
let config = Config {
query: &query,
file_path: &file_path,
};
let res = run(config).unwrap_or_else(|err| {
println!("something wrong when read file: {err}");
process::exit(1);
});
let res = res.join("");
let target = String::from("How dreary to be somebody!How public, like a frog");
assert_eq!(res, target)
}
} 多亏了这个search的测试用例,我发现了我的代码中存在错误的地方

最终输出的内容被字母小写了,这是不对的。我们不能去改动原数据。

在实现的过程中,这段代码一直没删除。。。。。

这就成功了~
环境变量#
我们先来写两个测试用例,一个是对大小写不敏感,一个是敏感的
#[test]
fn sensitive () {
let query = String::from("how");
let file_path = String::from("src/poem.txt");
let config = Config {
query: &query,
file_path: &file_path,
};
let res = run(config).unwrap_or_else(|err| {
println!("something wrong when read file: {err}");
process::exit(1);
});
let res = res.join("");
let target = String::from("");
assert_eq!(res, target)
}
#[test]
fn insensitive () {
let query = String::from("how");
let file_path = String::from("src/poem.txt");
let config = Config {
query: &query,
file_path: &file_path,
};
let res = run(config).unwrap_or_else(|err| {
println!("something wrong when read file: {err}");
process::exit(1);
});
let res = res.join("");
let target = String::from("How dreary to be somebody!How public, like a frog");
assert_eq!(res, target)
}
由于我们的代码中对line做了to_lowercase,所以只有sensitive不能通过是正常的。
不使用环境变量#
如果要让sensitive也通过的话,那就得有个开关控制是否需要to_lowercase。
那么这个开关放到哪里呢?放到Config里,因为它也算是用户的输入内容之一。
pub struct Config<'a> {
query: &'a String,
file_path: &'a String,
pub ignore: bool,
}
impl<'a> Config<'a> {
pub fn new(args: &'a Vec<String>) -> Result<Config, &'static str> {
let args_len = args.len();
if args_len < 4 {
return Err("not enough arguments");
}
let query = &args[1];
let file_path = &args[2];
let ignore = &args[3];
let ignore: bool = match ignore.parse() {
Ok(v) => v,
_ => return Err("parse field ignore error"),
};
Ok(Config { query, file_path, ignore })
}
}
pub fn run(config: Config) -> Result<Vec<String>, Box<dyn Error>> {
let poem = fs::read_to_string(config.get(ConfigField::FilePath))?;
let mut res = vec![];
for line in poem.lines() {
if config.ignore {
if line.to_lowercase().contains(config.get(ConfigField::Query).as_str()) {
res.push(line.to_owned());
}
} else {
if line.contains(config.get(ConfigField::Query).as_str()) {
res.push(line.to_owned());
}
}
}
Ok(res)
}可以看到我们的代码中多了很多东西,都是对第三个参数的兼容场景。

这回sensitive也正常了。

可以看到通过命令行传入的方式也是正常的。
但是这么做我们每次都要手动去传入ignore参数,就很烦,我们来给它个默认值
pub fn new(args: &'a Vec<String>) -> Result<Config, &'static str> {
let args_len = args.len();
let mut ignore = true;
if args_len < 3 {
return Err("not enough arguments");
} else if args_len > 3 {
ignore = match (&args[3]).parse() {
Ok(v) => v,
_ => return Err("parse field ignore error"),
};
}
let query = &args[1];
let file_path = &args[2];
Ok(Config { query, file_path, ignore })
}这样就比较舒服了。
但是这么写未免有些乱,尤其是对参数个数的兼容,很不优雅。
这个时候就轮到环境变量登场了,既能保证不改动太多代码,又能有个默认值的效果。
使用环境变量#
我们直接来看下代码
pub fn new(args: &'a Vec<String>) -> Result<Config, &'static str> {
let args_len = args.len();
if args_len < 3 {
return Err("not enough arguments");
}
let query = &args[1];
let file_path = &args[2];
let ignore = env::var("IGNORE_CASE").is_ok();
Ok(Config { query, file_path, ignore })
}是的,这就完事了,在原来的基础上多了一行。
我们来试下
我这里用的powserShell
$Env:IGNORE_CASE=1; cargo run -- to poem.txt 
可以看到返回的是一个Result的变体Ok。
is_ok表示如果有Ok,那就是true,也就是说只要你set了IGNORE_CASE这个环境变量,那就一定会是true,只有删除之后才会变回false。
Remove-Item Env:IGNORE_CASE
可以看到使用环境变量比传统方式方便非常多。
最终的代码#
lib.rs
use std::{error::Error, fs, env};
#[derive(Debug)]
pub struct Config<'a> {
query: &'a String,
file_path: &'a String,
pub ignore: bool,
}
pub enum ConfigField {
Query,
FilePath,
}
impl<'a> Config<'a> {
pub fn new(args: &'a Vec<String>) -> Result<Config, &'static str> {
let args_len = args.len();
if args_len < 3 {
return Err("not enough arguments");
}
let query = &args[1];
let file_path = &args[2];
let ignore = env::var("IGNORE_CASE").is_ok();
Ok(Config { query, file_path, ignore })
}
pub fn get(&self, field: ConfigField) -> &String {
match field {
ConfigField::Query => self.query,
ConfigField::FilePath => self.file_path,
}
}
}
pub fn run(config: Config) -> Result<Vec<String>, Box<dyn Error>> {
dbg!(&config);
let poem = fs::read_to_string(config.get(ConfigField::FilePath))?;
let mut res = vec![];
for line in poem.lines() {
if config.ignore {
if line.to_lowercase().contains(config.get(ConfigField::Query).as_str()) {
res.push(line.to_owned());
}
} else {
if line.contains(config.get(ConfigField::Query).as_str()) {
res.push(line.to_owned());
}
}
}
Ok(res)
}
#[cfg(test)]
mod tests {
use super::*;
use std::{process};
#[test]
fn search() {
let query = String::from("how");
let file_path = String::from("src/poem.txt");
let config = Config {
query: &query,
file_path: &file_path,
ignore: true
};
let res = run(config).unwrap_or_else(|err| {
println!("something wrong when read file: {err}");
process::exit(1);
});
let res = res.join("");
let target = String::from("How dreary to be somebody!How public, like a frog");
assert_eq!(res, target)
}
#[test]
fn sensitive () {
let query = String::from("how");
let file_path = String::from("src/poem.txt");
let config = Config {
query: &query,
file_path: &file_path,
ignore: false
};
let res = run(config).unwrap_or_else(|err| {
println!("something wrong when read file: {err}");
process::exit(1);
});
let res = res.join("");
let target = String::from("");
assert_eq!(res, target)
}
#[test]
fn insensitive () {
let query = String::from("how");
let file_path = String::from("src/poem.txt");
let config = Config {
query: &query,
file_path: &file_path,
ignore: true
};
let res = run(config).unwrap_or_else(|err| {
println!("something wrong when read file: {err}");
process::exit(1);
});
let res = res.join("");
let target = String::from("How dreary to be somebody!How public, like a frog");
assert_eq!(res, target)
}
}
main.rs
use std::{env, process};
use minigrep::{Config, run};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err| {
println!("something wrong when new config: {err}");
process::exit(1);
});
let res = run(config).unwrap_or_else(|err| {
println!("something wrong when read file: {err}");
process::exit(1);
});
dbg!(res);
}调整错误输出#
rust中有两种输出(output): standard output(stdout)和standard error。
当前我们所学的内容,错误输出都是通过println!这个宏,而println!只能用于标准输出。
那要怎么做才能让错误输出到standard error呢?
非常简单,输入的终端加上 > output.txt,就能让错误信息输出到output.txt文件中。
这种一般交错错误日志,是给开发人员看的。
总结#
这一大章我没有跟着官方的节奏来,我是自己先实现一遍之后再优化的。
如果觉得我的代码有问题请自行看官方文档。
参考#
- ^rust-12.3 https://doc.rust-lang.org/book/ch12-03-improving-error-handling-and-modularity.html#refactoring-to-improve-modularity-and-error-handling
- ^web-fetch https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
- ^rust-12,4-TDD https://doc.rust-lang.org/book/ch12-04-testing-the-librarys-functionality.html
- ^rust-12.5-work-with-environment-variable https://doc.rust-lang.org/book/ch12-05-working-with-environment-variables.html#working-with-environment-variables
- ^rust-12.6 https://doc.rust-lang.org/book/ch12-06-writing-to-stderr-instead-of-stdout.html#writing-error-messages-to-standard-error-instead-of-standard-output
编辑于 2022-12-25 14:16・IP 属地广东
