Upon mastering the foundational syntax of Rust Course, I conscientiously followed the scriptures to script the File Search Tool as my inaugural foray. Through this exercise, I augmented both my comprehension and proficiency in wielding Rust.

1. Overview of the Project

1.1 Tree

.
├── Cargo.lock
├── Cargo.toml
├── poem.txt
├── src
│ ├── lib.rs
│ └── main.rs
└── target
├── CACHEDIR.TAG
└── debug

1.2 Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// in main.rs

// Import necessary modules from the standard library.
use std::{env, process};

// Import the Config struct and run function from the minigrep module.
use minigrep::Config;

// The main function, where the execution of the program begins.
fn main() {
// Collect command-line arguments into a vector of Strings.
let args: Vec<String> = env::args().collect();

// Attempt to create a Config instance from the command-line arguments.
let config = Config::from(&args).unwrap_or_else(|err| {
// If there's an error, print a message and exit the program with an error code.
println!("Problem parsing arguments: {err}");
process::exit(1);
});

// Print the search query and file path to the console.
println!("Search for {}", config.query);
println!("In file {}", config.file_path);

// Attempt to run the minigrep::run function with the provided Config.
if let Err(e) = minigrep::run(config) {
// If there's an error, print a message and exit the program with an error code.
println!("Application error: {e}");
process::exit(1);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// lib.rs

// Import necessary modules from the standard library.
use std::error::Error;
use std::fs;

// Define a public Config struct with query and file_path fields.
pub struct Config {
pub query: String,
pub file_path: String,
}

// Implement methods for the Config struct.
impl Config {
// A public method to create a Config instance from command-line arguments.
pub fn from(args: &[String]) -> Result<Config, &'static str> {
// Check if there are enough command-line arguments.
if args.len() < 3 {
// If not, return an error with a static string message.
return Err("not enough arguments");
}
// Extract the query and file_path from command-line arguments.
let query = args[1].clone();
let file_path = args[2].clone();

// Return a Result containing a new Config instance.
Ok(Config { query, file_path })
}
}

// A public function to perform the main functionality of the program.
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
// Read the contents of the file specified in the Config.
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");

// Print the contents of the file to the console.
println!("With text:\n{contents}");

// Return a Result indicating success.
Ok(())
}

1.3 Elaboration

  • collect 方法其实并不是std::env包提供的,而是迭代器自带的方法(env::args() 会返回一个迭代器),它会将迭代器消费后转换成我们想要的集合类型,关于迭代器和 collect 的具体介绍,请参考这里

  • 关注点分离(Separation of Concerns)

    • 将程序分割为 main.rslib.rs,并将程序的逻辑代码移动到后者内

    • 命令行解析属于非常基础的功能,严格来说不算是逻辑代码的一部分,因此还可以放在 main.rs

  • clone 的得与失

    • Config 中存储的并不是 &str 这样的引用类型,而是一个 String 字符串,也就是 Config 并没有去借用外部的字符串,而是拥有内部字符串的所有权
    • clone 直接完整的复制目标数据,无需被所有权、借用等问题所困扰,但是它也有其缺点,那就是有一定的性能损耗
  • Config::from的错误处理

    • Result 包含错误时,我们不再调用 panic 让程序崩溃,而是通过 process::exit(1) 来终结进程,其中 1 是一个信号值(事实上非 0 值都可以),通知调用我们程序的进程,程序是因为错误而退出的
    • unwrap_or_else 是定义在 Result<T,E> 上的常用方法,如果 ResultOk,那该方法就类似 unwrap:返回 Ok 内部的值;如果是 Err,就调用闭包中的自定义代码对错误进行进一步处理
    • config 变量的值是一个 Config 实例,而 unwrap_or_else 闭包中的 err 参数,它的类型是 'static str,值是 “not enough arguments” 那个字符串字面量
  • run的错误处理

    • 值得注意的是这里的 Result<(), Box<dyn Error>> 返回类型,首先我们的程序无需返回任何值,但是为了满足 Result<T,E> 的要求,因此使用了 Ok(()) 返回一个单元类型 ()
    • 最重要的是 Box<dyn Error>,这是一个Error 的特征对象(为了使用 Error,我们通过 use std::error::Error; 进行了引入),它表示函数返回一个类型,该类型实现了 Error 特征,就无需指定具体的错误类型
    • 否则还需要查看 fs::read_to_string 返回的错误类型,然后复制到 run 函数返回中,这么做一个是麻烦,最主要的是,一旦这么做,意味着我们无法在上层调用时统一处理错误,但是 Box<dyn Error> 不同,其它函数也可以返回这个特征对象,然后调用者就可以使用统一的方式来处理不同函数返回的 Box<dyn Error>
  • if let 的使用让代码变得更简洁,可读性更好。原因这里并不关注 run 返回的 Ok 值,因此只需要用 if let 去匹配是否存在错误即可