标题
- 一、功能
- 二 、接受命令行参数
- 三、 读取文件
- 四、重构改进模块性和错误处理
- 4.1 二进制项目的关注分离
- 4.2 提取参数解析器
- 4.3 创建一个Config的构造函数
- 4.4 传参错误处理
- 4.5 从main中提取逻辑
- 4.6 将代码拆分进crate
一、功能
- grep 最简单的使用场景是在特定文件中搜索指定字符串;
- 程序的运行结果是能在文件中寻找目标字符串;
- 使用
cargo new minigrep
创建新项目;
二 、接受命令行参数
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
println!("{:?}",args);
let query = &args[1];
let filename = &args[2];
println!("Search for {}", query);
println!("In file {}", filename);
}
std::env::args
返回一个传递给程序行参数的迭代器;- 调用迭代器的
collect
方法将会得到一个集合; - 集合的第一个值是当前程序的名称,后面的值是传递给程序的参数;
运行结果
- 红框标注的就是所有的参数列表;
- 然后分别输出了
query
和filename
的值; - 程序就是要在文件filename中查找query对应的字符串;
三、 读取文件
- 创建文件
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!
- 为main.rs增加打开文件的代码
use std::fs;
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
// println!("{:?}",args);
let query = &args[1];
let filename = &args[2];
// println!("Search for {}", query);
println!("In file {}", filename);
let contents = fs::read_to_string(filename).expect("Something went wrong reading the file");
println!("With text:\n{}", contents);
}
- 引入
std::fs
,需要用它的read_to_string函数; - 注释不需要的打印
- 使用
cargo run needly poem.txt
运行程序(目前needly参数没有起作用)
下面的运行结果显示实现了文件读取的功能
四、重构改进模块性和错误处理
程序目前有四个问题需要修复:
- 分离解析参数与打开文件功能;
- 添加配置变量的结构;
- 添加更多的文件打开失败原因;
- 添加专用的错误处理函数
4.1 二进制项目的关注分离
- main函数变得庞大时进程二进制分离的指导步骤:
- 将程序拆分成 main.rs 和 lib.rs 并将程序的逻辑放入lib.rs中。
- 当命令行解析逻辑比较小时,可以保留在main.rs中。
- 当命令行解析开始变得复杂时,也同样将其从 main.rs 提取到lib.rs中。
- 经过这些处理之后保留在main函数中的函数功能被限制为:
- 使用参数值调用命令行解析逻辑;
- 设置任何其他的配置;
- 调用 lib.rs 中的 run 函数;
- 如果 run 返回错误,则处理这个错误;
- 总之:main.rs处理程序运行,lib.rs处理所有的真正的任务逻辑;
4.2 提取参数解析器
use std::fs;
use std::env;
struct Config{
query: String,
filename: String,
}
fn parse_config(args: &[String]) -> Config {
let query = args[1].clone();
let filename = args[2].clone();
Config {query, filename}
}
fn main() {
let args: Vec<String> = env::args().collect();
let config = parse_config(&args);
let contents = fs::read_to_string(config.filename)
.expect("Something went wrong reading the file!");
println!("contents: \n{}", contents);
}
- 现在的代码还全都在main.rs里边;
- 将filename和query组合成Config结构体;
- 结构体中使用完整的、拥有所有权的String类型,因此在parse_config函数中进行clone操作;
4.3 创建一个Config的构造函数
- 使用之前学过的方法,为结构体构建一个构造函数;
- 将parse_config的功能移到构建函数里去;
use std::fs;
use std::env;
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 main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
let contents = fs::read_to_string(config.filename)
.expect("Something went wrong reading the file!");
println!("contents: \n{}", contents);
}
4.4 传参错误处理
如果new中的参数个数小于3时,会产生错误,添加代码
fn new(args: &[String]) -> Config{
assert!(!args.len() < 3, "Not enough arguments");
……
}
- assert!宏只有当条件为假时会调用panic!
- 因此习惯写做
”对满足panic!的条件取反"
; panic!
的调用更趋向于程序上的问题而不是使用上的问题;- 可以返回一个可以表明成功或错误的Result;
从 new 中返回 Result
- 返回一个 Result 值,它在成功时会包含一个Config 的实例,错误时会描述问题。
use std::process;
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 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("Something went wrong reading the file!");
println!("contents: \n{}", contents);
}
- new函数返回一个Result,在成功时带有一个Config实例失败时带有一个
&'static str
; &'static str
就代表字符串字面量;unwrap_or_else
定义于Result<T, E> 上,使用它可以进行一些自定义的非 panic! 的错误处理;- 当Result 是Ok时,它返回 Ok 内部封装的值;
- 当Result是Err时,该方法会调用一个闭包(closure),也就是一个我们定义的作为参数传递给
unwrap_or_else
的匿名函数;
4.5 从main中提取逻辑
- 创建run 的函数来存放目前main函数中不属于设置配置或处理错误的所有逻辑;
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.filename)?;
println!("With text:\n{}", contents);
Ok(())
}
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);
}
}
- run函数获取一个Config实例作为参数;
- run函数的返回类型设置为
Result<(), Box<dyn Error>>
; - run函数Ok时返回unit 类型 (),失败时返回了trait对象
Box<dyn Error>
,这无需指定具体将会返回的值的类型,采用动态识别; - run函数里的
read_to_string
后取消了expect而采用?
运算符,这会从函数中返回错误值并由调用者来处理; - run成功时返回一个将unit类型值包装的Ok值,并没有什么实际意义;
- main函数中使用
if let
检查run返回的是否是一个Err;
4.6 将代码拆分进crate
将main.rs中的下述功能都移动到src/lib.rs中
- run 函数定义
- 相关的 use 语句
- Config 的定义
- Config::new 函数定义
src/lib.rs
use std::fs;
use std::error::Error;
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 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);
}
}