12.5.0. 写在正文之前
第12章要做一个实例的项目——一个命令行程序。这个程序是一个grep
(Global Regular Expression Print),是一个全局正则搜索和输出的工具。它的功能是在指定的文件中搜索出指定的文字。
这个项目分为这么几步:
- 接收命令行参数
- 读取文件
- 重构:改进模块和错误处理(本文)
- 使用TDD(测试驱动开发)开发库功能
- 使用环境变量
- 将错误信息写入标准错误而不是标准输出
喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)
12.5.1. 回顾
之前两节分别做了模块化的优化和错误处理,这节在此基础上还要做进一步的优化。
以下是截止到上一篇文章所写出的全部代码:
rust">use std::env;
use std::fs;
use std::process; struct Config { query: String, filename: String,
} fn main() { let args:Vec<String> = env::args().collect(); 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("Somthing went wrong while reading the file"); println!("With text:\n{}", contents);
} 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}) }
}
12.5.2. 从main
函数中提取逻辑
在 12.3. 重构 Pt.1 中说过二进制程序关注点分离的指导性原则:
- 将程序拆分为
main.rs
和lib.rs
,将业务逻辑放入lib.rs
- 当逻辑较少时,将它放在
main.rs
也可以 - 当逻辑变复杂时,需要将它从
main.rs
提取到lib.rs
根据上述拆分原则,我们应该把main
函数里所有除了配置解析和错误处理之外的所有逻辑单独提取到一个run
函数里。把main
函数精简到足以通过阅读代码来检查正确性,而其他的逻辑就可以通过测试验证了(对于测试这部分的内容,详见第11章)。
对于这个截止到目前的代码,run
函数应该是:
rust">fn run(config: Config) { let contents = fs::read_to_string(config.filename) .expect("Somthing went wrong while reading the file"); println!("With text:\n{}", contents);
}
main
函数里也改为通过调用run
函数来读取:
rust">fn main() { let args:Vec<String> = env::args().collect(); let config = Config::new(&args).unwrap_or_else(|err| { println!("Problem parsing arguments: {}", err); process::exit(1); }); run(config);
}
12.5.3. 改善run
函数的错误处理
现在的run
函数对于读取错误的情况采用的是expect
。而这种错误处理会调用panic!
,我们需要的是像config::new
这样使用Result
类型来传播错误,就应该这么写:
rust">fn run(config: Config) -> Result<(), Box<dyn Error>> { let contents = fs::read_to_string(config.filename)?; println!("With text:\n{}", contents); Ok(())
}
-
Result
类型的Ok
对应的是()
类型(单元类型),这种类型表示什么也不返回,什么也没有,因为run
函数正确执行确实什么都不需要返回。这个函数体的最后一行Ok()
里加了()
就代表返回Ok
变体,并且包裹了一个单元类型。 -
Result
的Err
对应的是Box<dyn Error>
,这个东西你暂且不需要深入了解,之需要知道它代表所有实现了std::error::Error
这个trait的类型(这里只写了Error
是因为我在代码开头写了use std::error::Error;
,把它引入了作用域),但是不需要指定具体的类型。这意味着在不同的场景下可以返回不同的错误类型。dyn
是dynamic动态一词的简写。 -
?
这个符号在 9.3. Result枚举与可恢复的错误 Pt.2 中有详细讲过,这里就再简单讲一下:read_to_string
的返回值是Result
类型。加了?
表示如果read_to_string
的返回值是Ok
,就把Ok
所关联的值返回赋值给变量;如果是Err
,那么会直接终止这个函数的运行,把Err
及其所附带的错误信息返回。也就是说,加?
的效果等同于:
rust">let contents = match fs::read_to_string(config.filename){Ok(contents) => contents,Err(e) => return Err(e),
};
这么改之后就会把错误传播给调用者,也就是main
函数,所以在main
函数里得处理可能出现的错误:
rust">fn main() { let args:Vec<String> = env::args().collect(); 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); }
}
这里使用到的if let
是match
的一个语法糖,把它理解为只处理一种分支的match
即可,详细可见 6.4. 简单的控制流-if let。需要强调,if let
和if
不是同一回事,不要把它们相提并论。
12.5.4. 迁移业务逻辑
现在我们完成了所有函数的独立和错误处理,接下来要做的就是把它们移到lib.rs
里。
迁移的对象就是这些函数、结构体和相关的引用。
迁移后的成果(lib.rs
):
rust">use std::error::Error;
use std::fs; pub struct Config { pub query: String, pub 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(())
}
注意:所有的被main.rs
使用的结构体、结构体上的方法和函数都得在声明时加pub
关键字来声明为公共的才能被调用。
再看看main.rs
:
rust">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 arguments: {}", err); process::exit(1); }); if let Err(e) = minigrep::run(config) { println!("Application error: {}", e); process::exit(1); }
}
所有的重构任务已经完成,下一步就是编写测试(下一篇文章)。