重构以提高模块化和错误处理


为了改进我们的程序,我们将修复与程序结构及其处理潜在错误方式有关的四个问题。首先,我们的 函数现在执行两项任务:解析参数和读取文件。作为我们的 程序增长,则 main 函数处理的单独任务的数量将增加。随着功能获得责任,它变得更加难以推理、更难测试,并且更难在不破坏其任何一个部分的情况下进行更改。最好将功能分开,以便每个函数负责一项任务。


这个问题也与第二个问题有关:尽管 queryfile_path 是我们程序的配置变量,像 contents 这样的变量用于执行程序的逻辑。main 越长,我们需要引入范围的变量就越多;我们在范围内的变量越多,跟踪每个变量的用途就越困难。最好将配置变量分组到一个结构中,以明确其用途。


第三个问题是,我们用 expect 在读取文件失败时打印错误消息,但错误消息只是打印。 Should have been able to read the file 读取文件可能以多种方式失败:例如,文件可能丢失,或者我们可能没有打开它的权限。现在,无论情况如何,我们都会为所有内容打印相同的错误消息,这不会给用户任何信息!


第四,我们使用 expect 来处理错误,如果用户在没有指定足够参数的情况下运行我们的程序,他们将从 Rust 得到一个索引越界错误,该错误并不能清楚地解释问题。最好将所有错误处理代码都放在一个地方,这样如果错误处理逻辑需要更改,未来的维护者只有一个地方可以查阅代码。将所有错误处理代码放在一个位置还可以确保我们打印的消息对最终用户有意义。


让我们通过重构我们的项目来解决这四个问题。


二进制项目的关注点分离


将多个任务的责任分配给 main 函数的组织问题是许多二进制项目的常见问题。因此,Rust 社区制定了当 main 开始变大时拆分二进制程序的单独关注点的指南。此过程包括以下步骤:


  • 将程序拆分为 main.rs 文件和 lib.rs 文件,并将程序的逻辑移动到 lib.rs

  • 只要您的命令行解析逻辑很小,它就可以保持在 main.rs

  • 当命令行解析逻辑开始变得复杂时,将其从 main.rs 中提取并移至 lib.rs


此过程后保留在 main 职能中的职责应限于以下内容:


  • 使用参数值调用命令行解析逻辑

  • 设置任何其他配置

  • lib.rs 中调用 run 函数

  • 如果 run 返回错误,则处理错误


此模式是关于分离关注点的:main.rs 处理程序的运行,lib.rs 处理手头任务的所有逻辑。由于您无法直接测试 main 函数,因此此结构允许您通过将程序的所有逻辑移动到 lib.rs 中的函数来测试程序的所有逻辑。保留在 main.rs 中的代码将足够小,以便通过读取来验证其正确性。让我们按照这个过程重新设计我们的程序。


提取 Argument Parser


我们将用于解析参数的功能提取到一个函数中,该函数 main 将调用 main 以准备将命令行解析逻辑移动到 src/lib.rs 的示例 12-5 显示了 main 的新开始,它调用了一个新函数 parse_config,我们现在将在 src/main.rs 中定义它。


文件名: src/main.rs
use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let (query, file_path) = parse_config(&args);

    // --snip--

    println!("Searching for {query}");
    println!("In file {file_path}");

    let contents = fs::read_to_string(file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

fn parse_config(args: &[String]) -> (&str, &str) {
    let query = &args[1];
    let file_path = &args[2];

    (query, file_path)
}

示例 12-5:从 main 中提取 parse_config 函数


我们仍在将命令行参数收集到一个向量中,但不是将索引 1 处的参数值分配给变量 query,将索引 2 处的参数值分配给 main 中的变量 file_path 函数,我们将整个 vector 传递给 parse_config 函数。这 然后,parse_config函数保存确定哪个参数进入哪个变量并将值传递回 main 的逻辑。我们仍然在 main 中创建 queryfile_path 变量,但 main 不再负责确定命令行参数和变量如何对应。


对于我们的小程序来说,这种返工似乎有点矫枉过正,但我们正在以小的增量步骤进行重构。进行此更改后,请再次运行该程序以验证参数解析是否仍然有效。经常检查您的进度是件好事,这有助于在问题发生时确定问题的原因。


对配置值进行分组


我们可以再迈出一小步来进一步改进 parse_config 功能。目前,我们返回了一个元组,但随后我们立即再次将该元组分解为单独的部分。这表明我们可能还没有正确的抽象。


另一个表明有改进空间的指标是 parse_configconfig 部分,这意味着我们返回的两个值是相关的,并且都是一个 configuration 值的一部分。我们目前没有在数据的结构中传达此含义,只是将两个值分组到一个 Tuples 中;相反,我们将这两个值放入一个 struct 中,并为每个 struct 字段指定一个有意义的名称。这样做将使此代码的未来维护者更容易理解不同的值如何相互关联以及它们的用途是什么。


示例 12-6 显示了对 parse_config 函数的改进。


文件名: src/main.rs
use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = parse_config(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    // --snip--

    println!("With text:\n{contents}");
}

struct Config {
    query: String,
    file_path: String,
}

fn parse_config(args: &[String]) -> Config {
    let query = args[1].clone();
    let file_path = args[2].clone();

    Config { query, file_path }
}

示例 12-6:重构 parse_config 以返回 Config 结构体的实例


我们添加了一个名为 Config 的结构体,该结构体被定义为具有名为 queryfile_pathparse_config 的签名现在表示它返回一个 Config 值。在 parse_config 的主体中,我们过去返回引用 argsString 值的字符串切片,现在我们定义了 Config 以包含拥有的 String 值。main 中的 args 变量是参数值的所有者,并且只让 parse_config 函数借用它们,这意味着如果 Config 试图获取 args 中值的所有权,我们将违反 Rust 的借用规则。


我们可以通过多种方式管理 String 数据;最简单但效率较低的路由是对 values 调用 clone 方法。这将为 Config 实例创建数据的完整副本,这比存储对字符串数据的引用需要更多的时间和内存。但是,克隆数据也使我们的代码非常简单,因为我们不必管理引用的生命周期;在这种情况下,为了获得简单性而放弃一点性能是一个值得的权衡。


使用克隆的利弊


许多 Rustacean 都有避免使用 clone 来修复的倾向 所有权问题。在 第 13 章,您将学习如何在这种情况下使用更有效的方法。但就目前而言,可以复制一些字符串以继续进行,因为您只会进行一次复制,并且您的文件路径和查询字符串非常小。拥有一个效率低下的工作程序比尝试在第一次通过时对代码进行超优化要好。随着您对 Rust 的经验越来越丰富,从最有效的解决方案开始会更容易,但就目前而言,调用 clone 是完全可以接受的。


我们更新了 main,因此它将 Config 的实例 parse_config到一个名为 config 的变量中,我们更新了以前使用单独 queryfile_path 变量的代码,因此它现在使用 Config 结构体上的字段。


现在,我们的代码更清楚地传达了 queryfile_path 是相关的,并且它们的目的是配置程序的工作方式。使用这些值的任何代码都知道在 config 实例中为其用途命名的字段中找到它们。


Config 创建构造函数


到目前为止,我们已经从 main 中提取了负责解析命令行参数的逻辑,并将其放在 parse_config 函数中。这样做有助于我们看到 queryfile_path 值是相关的,并且这种关系应该在我们的代码中传达。然后,我们添加了一个 Config 结构体来命名 queryfile_path 的相关目的,并能够将值的名称作为 parse_config 函数的结构体字段名称返回。


所以现在 parse_config 函数的目的是创建一个 Config 实例,我们可以将 parse_config 从普通函数更改为与 Config 结构体关联的名为 new 的函数。进行此更改将使代码更加惯用。我们可以通过调用 String::new 在标准库中创建类型的实例,例如 String。同样,通过将 parse_config 更改为与 Config 关联的函数,我们将能够通过调用 Config::new 来创建 Config 的实例。示例 12-7 显示了我们需要做的更改。


文件名: src/main.rs
use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");

    // --snip--
}

// --snip--

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn new(args: &[String]) -> Config {
        let query = args[1].clone();
        let file_path = args[2].clone();

        Config { query, file_path }
    }
}

示例 12-7:将 parse_config 更改为 Config::new


我们更新了调用 main 的位置parse_config改为调用 Config::new 的 Config::new 中。我们将 parse_config 的名称更改为 new,并将其移动到一个 impl 块中,该块将 new 函数与 Config 相关联。尝试再次编译此代码以确保其正常工作。


修复错误处理


现在,我们将着手修复我们的错误处理。回想一下,如果 vector 包含的项目少于 3 个,则尝试访问索引 1 或索引 2 处的 args vector 中的值将导致程序 panic。尝试在没有任何参数的情况下运行程序;它看起来像这样:

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep`
thread 'main' panicked at src/main.rs:27:21:
index out of bounds: the len is 1 but the index is 1
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


该行 index out of bounds: the len is 1 but the index is 1 是供程序员使用的错误消息。它不会帮助我们的最终用户了解他们应该做什么。现在让我们解决这个问题。


改进错误消息


在示例 12-8 中,我们在 new 函数中添加了一个检查,它将在访问索引 1 和索引 2 之前验证切片是否足够长。如果切片不够长,程序将出现 panic 并显示更好的错误消息。


文件名: src/main.rs
use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    // --snip--
    fn new(args: &[String]) -> Config {
        if args.len() < 3 {
            panic!("not enough arguments");
        }
        // --snip--

        let query = args[1].clone();
        let file_path = args[2].clone();

        Config { query, file_path }
    }
}

示例 12-8:添加参数数量的检查


这段代码类似于我们在 清单 中编写的 Guess::new 函数 9-13 时,我们调用了 panic!value 参数超出有效值的范围。我们不是在这里检查值范围,而是检查 args 的长度是否至少为 3 中,函数的其余部分可以在满足此条件的假设下运行。如果 args 少于 3 项,则此条件为 true,我们调用 panic!宏立即结束程序。


有了这几行额外的代码,让我们再次运行没有任何参数的程序,看看现在错误是什么样子的:

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep`
thread 'main' panicked at src/main.rs:26:13:
not enough arguments
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


这个输出更好:我们现在有一个合理的错误消息。但是,我们也 包含我们不想提供给用户的无关信息。也许 我们在示例 9-13 中使用的技术并不是最好在这里使用的技术:对 恐慌!更适合于编程问题而不是使用问题, 如第 9 章所述。相反,我们将使用您在第 9 章中学到的另一种技术 — 返回一个 结果,这表示成功或错误。


返回 Result 而不是调用 panic!


我们可以改为返回一个 Result 值,该值在成功的情况下将包含一个 Config 实例,并在错误情况下描述问题。我们还会将函数名称从 new 更改为 build,因为许多程序员希望 new 函数永远不会失败。当 Config::buildmain 通信时,我们可以使用 Result 类型来表示有问题。然后我们可以更改 main,将 Err 变体转换为对用户来说更实际的错误,而没有关于线程 'main' 的周围文本,RUST_BACKTRACE调用恐慌会导致。


示例 12-9 显示了我们需要对现在调用 Config::build 的函数的返回值和返回 Result 所需的函数体进行的更改。请注意,在我们更新 main 之前,这不会编译,我们将在下一个清单中这样做。


文件名: src/main.rs
use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

示例 12-9:从 Config::build 返回 Result


我们的 build 函数返回一个 Result,在成功的情况下有一个 Config 实例,在错误的情况下返回一个字符串文字。我们的错误值将始终是具有 'static 生命周期的字符串文本。


我们对函数的主体进行了两处更改:而不是调用 panic! 当用户没有传递足够的参数时,我们现在返回一个 Err 值,并且我们已经将 Config 返回值包装在 Ok 中。这些更改使函数符合其新的类型签名。


Config::build 返回 Err 值允许 main 函数处理从 build 函数返回的 Result 值,并在错误情况下更干净地退出进程。


调用 config::build 并处理错误


要处理错误情况并打印用户友好的消息,我们需要更新 main 来处理 Config::build 返回的 Result,如示例 12-10 所示。我们还将负责从 panic!中退出带有非零错误代码的命令行工具,而是手动实现它。非零退出状态是一种约定,用于向调用程序的进程发出信号,表明程序以错误状态退出。


文件名: src/main.rs
use std::env;
use std::fs;
use std::process;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    // --snip--

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

示例 12-10:如果构建 Config 失败,则退出并显示错误代码


在此清单中,我们使用了一种尚未详细介绍的方法: unwrap_or_else,它由标准库在 Result<T, E> 上定义。使用 unwrap_or_else 允许我们定义一些自定义的、非 panic!的错误处理。如果 ResultOk 值,则此方法的行为类似于 unwrap:它返回 Ok 正在包装的内部值。但是,如果值是 Err 值,则此方法调用闭包中的代码,闭包是我们定义并作为参数传递给 unwrap_or_else 的匿名函数。我们将在第 13 章中更详细地介绍闭包。现在,你只需要知道 unwrap_or_else 将传递 Err 的内部值,在本例中是静态字符串 “not enough arguments” 我们在示例 12-9 中添加到参数 err 的闭包中 显示在垂直管道之间。然后,闭包中的代码可以使用 err 值。


我们添加了一个新的 use 行,用于将 process 从 standard 库引入 scope。在错误情况下运行的闭包中的代码只有两行:我们打印 err 值,然后调用 process::exit。这 process::exit 函数将立即停止程序并返回 作为退出状态代码传递的号码。这类似于 恐慌!-,但我们不再获得所有额外的输出。让我们试一试:

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/minigrep`
Problem parsing arguments: not enough arguments


伟大!此输出对我们的用户更友好。


main 中提取 Logic


现在我们已经完成了配置解析的重构,让我们来看看程序的逻辑。正如我们在“Separation of Concerns for Binary” 中所说 Projects“,我们将提取一个名为 run 的函数,该函数将保存当前在 main 函数,该函数不涉及设置配置或处理错误。完成后,main 将简洁且易于通过检查进行验证,并且我们将能够为所有其他 logic编写测试。


示例 12-11 显示了提取的 run 函数。目前,我们只是对提取函数进行小的增量改进。我们仍在 src/main.rs 中定义函数。


文件名: src/main.rs
use std::env;
use std::fs;
use std::process;

fn main() {
    // --snip--

    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    run(config);
}

fn run(config: Config) {
    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

// --snip--

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

示例 12-11:提取包含其余程序逻辑的 run 函数


run 函数现在包含从 main 开始读取文件的所有剩余 logic。run 函数将 Config 实例作为参数。


run 函数返回错误


将剩余的程序逻辑分离到 run 函数中后,我们可以改进错误处理,就像我们在示例 12-9 中对 Config::build 所做的那样。不是通过调用 expect 来允许程序 panic,而是运行 函数在出现问题时将返回 Result<T, E>。这将让我们以用户友好的方式进一步将处理错误的逻辑整合到 main 中。示例 12-12 显示了我们需要对 run 的签名和主体进行的更改。


文件名: src/main.rs
use std::env;
use std::fs;
use std::process;
use std::error::Error;

// --snip--


fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    run(config);
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    println!("With text:\n{contents}");

    Ok(())
}

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

示例 12-12:将 run 函数更改为返回 Result


我们在此处进行了三项重大更改。首先,我们将 run 函数的返回类型更改为 Result<(),Box<dyn Error>>。此函数之前返回了单元类型 (),我们将其保留为 好的案例。


对于错误类型,我们使用了 trait 对象Box<dyn Error>(并且我们已将 std::error::Error 引入范围,并在顶部添加了 use 语句)。我们将在第 17 章中介绍 trait 对象。现在,只需知道 Box<dyn Error> 意味着该函数将返回一个实现 Error trait 的类型,但我们不必指定返回值将是什么特定类型。这使我们能够灵活地返回在不同错误情况下可能属于不同类型的错误值。dyn 关键字是 dynamic 的缩写。


其次,我们删除了对 expect 的调用,转而使用 运算符,正如我们在 第 9 章 中讨论的那样。而不是 恐慌!发生错误时, 将返回当前函数的 Error 值供调用方处理。


第三,run 函数现在在成功情况下返回 Ok 值。我们在签名中将 run 函数的成功类型声明为 (),这意味着我们需要将 unit type 值包装在 Ok 值中。这 Ok(()) 语法乍一看可能看起来有点奇怪,但像这样使用 () 是表明我们调用 run 只是为了它的副作用的惯用方式;它不会返回我们需要的值。


当您运行此代码时,它将编译,但将显示警告:

$ cargo run -- the poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
warning: unused `Result` that must be used
  --> src/main.rs:19:5
   |
19 |     run(config);
   |     ^^^^^^^^^^^
   |
   = note: this `Result` may be an `Err` variant, which should be handled
   = note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
   |
19 |     let _ = run(config);
   |     +++++++

warning: `minigrep` (bin "minigrep") generated 1 warning
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.71s
     Running `target/debug/minigrep the poem.txt`
Searching for the
In file poem.txt
With text:
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!


Rust 告诉我们,我们的代码忽略了 Result 值,而 Result 值可能表明发生了错误。但是我们没有检查是否有错误,编译器会提醒我们,我们可能打算在这里有一些错误处理代码!现在让我们纠正这个问题。


处理从 run in main 返回的错误


我们将检查错误并使用类似于示例 12-10 中 Config::build 的技术来处理它们,但略有不同:


文件名: src/main.rs

use std::env;
use std::error::Error;
use std::fs;
use std::process;

fn main() {
    // --snip--

    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    if let Err(e) = run(config) {
        println!("Application error: {e}");
        process::exit(1);
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    println!("With text:\n{contents}");

    Ok(())
}

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}


我们使用 if let 而不是 unwrap_or_else 来检查 run 是否返回 Err 值,如果是,则调用 process::exit(1)。run 函数不会返回我们想要解包的值,其方式与 Config::build 返回 Config 实例。因为 run 成功案例中,我们只关心检测一个错误,所以我们不需要 unwrap_or_else返回 unwrapped 值,该值仅为 ()。


在这两种情况下,if letunwrap_or_else 函数的主体是相同的:我们打印 error 并退出。


将代码拆分到库 crate 中


到目前为止,我们的 minigrep 项目看起来不错!现在,我们将拆分 src/main.rs 文件,并将一些代码放入 src/lib.rs 文件中。这样,我们就可以测试代码并拥有一个责任较少的 src/main.rs 文件。


让我们将所有不在 main 函数中的代码从 src/main.rs 移动到 src/lib.rs 中:


  • run 函数定义

  • 相关的使用声明

  • Config 的定义

  • Config::build 函数定义


src/lib.rs 的内容应该具有示例 12-13 中所示的签名(为简洁起见,我们省略了函数的主体)。请注意,在我们修改示例 12-14 中的 src/main.rs 之前,它不会编译。


文件名: src/lib.rs
use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub file_path: String,
}

impl Config {
    pub fn build(args: &[String]) -> Result<Config, &'static str> {
        // --snip--
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    // --snip--
    let contents = fs::read_to_string(config.file_path)?;

    println!("With text:\n{contents}");

    Ok(())
}

示例 12-13:移动 Config运行src/lib.rs


我们自由地使用了 pub 关键字:在 Config 上,在它的字段上,以及它的 build 方法和 run 函数。我们现在有一个库 crate,它有一个我们可以测试的公共 API!


现在我们需要将移动到 src/lib.rs 的代码放入 src/main.rs 中的二进制 crate 的范围内,如示例 12-14 所示。


文件名: src/main.rs
use std::env;
use std::process;

use minigrep::Config;

fn main() {
    // --snip--
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    if let Err(e) = minigrep::run(config) {
        // --snip--
        println!("Application error: {e}");
        process::exit(1);
    }
}

示例 12-14:在 src/main.rs 中使用 minigrep 库 crate


我们添加一行 use minigrep::ConfigConfig 类型从库 crate 引入二进制 crate 的范围,并在 run 函数前加上我们的 crate 名称。现在所有功能都应该已连接并正常工作。使用 cargo run 运行程序并确保一切正常。


呼!这需要做很多工作,但我们已经为未来的成功做好了准备。现在,处理错误变得更加容易,并且我们使代码更加模块化。从现在开始,我们几乎所有的工作都将在 src/lib.rs 中完成。


让我们利用这种新发现的模块化,做一些在旧代码中很难但对新代码来说很容易的事情:我们将编写一些测试!