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

为了改进我们的程序,我们将修复与 程序的结构以及它如何处理潜在错误。首先,我们的函数现在执行两项任务:解析参数和读取文件。作为我们的 程序增长,函数处理的单独任务的数量将 增加。随着职能部门的责任增加,它变得更加困难 reason about,更难测试,更难在不破坏其 部件。最好将功能分开,以便每个功能都负责 一个任务。mainmain

这个问题也与第二个问题有关:虽然 和 是我们程序的配置变量,但使用了 以执行程序的逻辑。时间越长,变量越多 我们需要纳入范围;我们在 SCOPE 中的变量越多,就越难 这将是跟踪每个的目的。最好将 配置变量合并到一个结构中,以明确其用途。queryfile_pathcontentsmain

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

第四,我们用于处理错误,如果用户运行我们的程序 如果没有指定足够的参数,它们将收到错误 这并没有清楚地解释问题。如果所有 错误处理代码在一个地方,所以未来的维护者只有一个地方 来查阅代码(如果需要更改错误处理逻辑)。拥有所有 错误处理代码也将确保我们打印消息 这对我们的最终用户来说将是有意义的。expectindex out of bounds

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

二进制项目的关注点分离

将多个任务的责任分配给 该函数对于许多二进制项目都是通用的。因此,Rust community 已经制定了用于拆分 a 的单独关注点的指导方针 binary 程序开始变大。此过程包括以下内容 步骤:mainmain

  • 将程序拆分为 main.rs 文件和 lib.rs 文件,并将 程序的 logic 进行 lib.rs
  • 只要您的命令行解析逻辑很小,它就可以保持 main.rs
  • 当命令行解析逻辑开始变得复杂时,将其提取 从 main.rs 并将其移动到 lib.rs

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

  • 使用参数值调用命令行解析逻辑
  • 设置任何其他配置
  • lib.rs 中调用函数run
  • 如果返回错误,则处理错误run

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

提取 Argument Parser

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

文件名: 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:从parse_configmain

我们仍然将命令行参数收集到一个 vector 中,但不是 将索引 1 处的参数值分配给变量,并将 参数值 2 传递给函数中的变量,我们将整个向量传递给函数。然后,该函数保存确定哪个参数的逻辑 放入哪个变量中,并将值传递回 。我们仍在创造 中的 and 变量,但不再具有 确定命令行参数和变量 通信。queryfile_pathmainparse_configparse_configmainqueryfile_pathmainmain

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

对配置值进行分组

我们可以再迈出一小步来进一步改进该功能。 目前,我们返回了一个 Tuples,但随后我们立即中断了它 tuple 转换为单独的部分。这是一个迹象,也许我们没有 这是正确的抽象。parse_config

另一个表明有改进空间的指标是零件 of ,这意味着我们返回的两个值是相关的,而 都是 Configuration 值的一部分。我们目前没有传达此内容 在数据结构中的含义,而不是通过将两个值分组为 一个元组;我们将这两个值放入一个结构体中,并为每个 struct 字段设置有意义的名称。这样做将使未来更容易 此代码的维护者,以了解不同的值与每个值之间的关系 其他以及它们的目的是什么。configparse_config

示例 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_configConfig

我们添加了一个名为 defined 的结构,以将字段命名为 和 。now 的签名表示它返回一个值。在 的正文中,我们过去返回的地方 引用 中的值的字符串切片,我们现在定义为包含拥有的值。变量 in 是 参数值 and 只让函数借用 它们,这意味着如果试图采用 中值的所有权。Configqueryfile_pathparse_configConfigparse_configStringargsConfigStringargsmainparse_configConfigargs

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

使用克隆的利弊

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

我们更新了 return by 的实例,并将其放入名为 的变量中,并且我们更新了 以前使用单独的 and 变量,因此现在使用 结构体上的字段。mainConfigparse_configconfigqueryfile_pathConfig

现在,我们的代码更清楚地传达了 和 是相关的 和 他们的目的是配置程序的工作方式。任何 使用这些值知道在实例的字段中查找它们 以他们的目的命名。queryfile_pathconfig

Config 创建构造函数

到目前为止,我们已经提取了负责解析命令行的 logic 参数并将其放置在函数中。这样做 帮助我们看到 和 值是相关的,并且 关系应该在我们的代码中传达。然后,我们在 name 的相关用途 和 ,以便能够返回 values 的名称作为函数中的结构体字段名称。mainparse_configqueryfile_pathConfigqueryfile_pathparse_config

所以现在函数的目的是创建一个实例,我们可以从普通函数更改为函数 named 的 API进行此更改 将使代码更加地道。我们可以在 标准库(如 ),通过调用 .同样,通过 更改为与 关联的函数,我们将 能够通过调用 来创建 的实例。示例 12-7 显示了我们需要进行的更改。parse_configConfigparse_confignewConfigStringString::newparse_confignewConfigConfigConfig::new

文件名: 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_configConfig::new

我们更新了调用的位置,改为调用 .我们已将 的名称更改为 并将其移动 ,它将函数与 .尝试 再次编译此代码以确保其正常工作。mainparse_configConfig::newparse_confignewimplnewConfig

修复错误处理

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

$ 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 中,我们在函数中添加了一个检查,该检查将验证 slice 在访问索引 1 和索引 2 之前足够长。如果切片不是 足够长的时间,程序会 panic 并显示更好的错误消息。new

文件名: 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 中,我们在参数超出有效值范围时调用。而不是检查 一个值范围,我们检查长度是否至少为 ,并且函数的其余部分可以在假设 this 条件已得到满足。如果项目少于 3 项,则此条件 将是 ,我们调用宏以立即结束程序。panic!valueargs3argstruepanic!

有了这几行额外的代码,让我们在没有任何 arguments 再次查看错误现在是什么样子的:new

$ 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 章所讨论的,调用 to 更适合编程问题而不是使用问题。相反 我们将使用您在第 9 章中学到的另一种技术 — 返回一个指示成功或错误的 Resultpanic!

返回 Result 而不是调用 panic!

我们可以返回一个值,该值将包含 成功案例,并将描述 Error Case 中的问题。我们也是 将函数名称从 更改为 因为 many 程序员希望函数永远不会失败。时间 通信到 ,我们可以使用 type 来表示存在 问题。然后我们可以 change 将 variant 转换为 more 实际错误对于我们的用户来说没有周围的文本 about 和 a call to 导致。ResultConfignewbuildnewConfig::buildmainResultmainErrthread 'main'RUST_BACKTRACEpanic!

示例 12-9 显示了我们需要对 函数和所需的函数体 以返回 .请注意,在我们将 好吧,我们将在下一个列表中进行。Config::buildResultmain

文件名: 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:返回一个 fromResultConfig::build

我们的函数返回一个 success 中有一个实例 case 和 Error case 中的 String 文本。我们的 error 值将始终为 具有 lifetime 的 String 文本。buildResultConfig'static

我们对函数的主体进行了两项更改:我们现在返回一个值,而不是在用户没有传递足够的参数时调用 我们已将返回值包装在 .这些更改使 函数的 JSON JSON 中的调用。panic!ErrConfigOk

返回值 from 允许函数 处理函数返回的值并退出 在错误情况下更干净地处理。ErrConfig::buildmainResultbuild

调用 config::build 并处理错误

要处理错误情况并打印用户友好的消息,我们需要更新以处理 返回的 ,如 所示 示例 12-10.我们还将负责退出命令行 工具中具有非零错误代码,而是通过以下方式实现它 手。非零退出状态是向进程发出信号的约定 调用我们的程序,该程序退出时出现错误状态。mainResultConfig::buildpanic!

文件名: 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

在这个 清单 中,我们使用了一个我们还没有详细介绍的方法:,它由标准库定义。 使用 允许我们定义一些自定义的、无错误的 处理。如果 是 值,则此方法的行为类似 to :它返回正在包装的内部值。但是,如果 value 是一个值,该方法调用闭包中的代码,即 我们定义并作为参数传递给 的匿名函数。 我们将在第 13 章中更详细地介绍闭包。为 现在,你只需要知道它会传递 ,在本例中,它是我们在示例 12-9 中添加的静态字符串,添加到参数 显示在垂直管道之间。然后,闭包中的代码可以在运行时使用该值。unwrap_or_elseResult<T, E>unwrap_or_elsepanic!ResultOkunwrapOkErrunwrap_or_elseunwrap_or_elseErr"not enough arguments"errerr

我们添加了一行新代码,要从标准库引入 范围。在错误情况下将运行的闭包中的代码只有 2 行:我们打印值,然后调用 .该函数将立即停止程序并返回 作为退出状态代码传递的号码。这类似于示例 12-8 中使用的基于 -的处理方式,但我们不再获取所有 extra 输出。让我们试一试:useprocesserrprocess::exitprocess::exitpanic!

$ 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

现在我们已经完成了配置解析的重构,让我们转向 程序的 logic。正如我们在“Separation of Concerns for Binary” 中所说 Projects“,我们将 提取一个名为 function 的函数,该函数将保存函数中当前不涉及设置配置或处理的所有 logic 错误。完成后,将简洁易用 检查,我们将能够为所有其他 logic编写测试。runmainmain

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

文件名: 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

该函数现在包含 从 开始的所有剩余逻辑 从读取文件。该函数将实例作为 论点。runmainrunConfig

run 函数返回错误

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

文件名: 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:将函数更改为 returnrunResult

我们在此处进行了三项重大更改。首先,我们将 函数设置为 .此函数以前 返回了 Unit 类型 ,并将其保留为 case 中返回的值。runResult<(), Box<dyn Error>>()Ok

对于 error 类型,我们使用了 trait 对象(并且我们已将 带入范围,并在顶部显示一个 statement)。 我们将在第 17 章中介绍 trait 对象。目前,只需 知道这意味着该函数将返回一个类型,该类型 实现 trait,但我们不必指定什么特定类型 返回值将为。这使我们能够灵活地返回 error 值 在不同的错误情况下可能属于不同的类型。关键字简短 用于动态Box<dyn Error>std::error::ErroruseBox<dyn Error>Errordyn

其次,我们删除了对 的调用以支持运算符,因为我们 在第 9 章中讨论过。而不是 on 错误,而是从当前函数返回 error 值 供调用方处理。expect?panic!?

第三,该函数现在在成功案例中返回一个值。 我们已经将函数的成功类型声明为签名 这意味着我们需要将 Unit type 值包装在 value 中。此语法乍一看可能有点奇怪,但像这样使用 表示我们正在调用其副作用的惯用方式 只;它不会返回我们需要的值。runOkrun()OkOk(())()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 告诉我们,我们的代码忽略了 value 和 value 可能表示发生了错误。但是,我们不会检查是否或 没有出现错误,编译器提醒我们,我们可能是故意的 这里有一些错误处理代码!现在让我们纠正这个问题。ResultResult

处理从 run in main 返回的错误

我们将检查错误并使用类似于我们使用的技术来处理它们 with 在示例 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 })
    }
}

我们使用 instead 而不是来检查是否返回值,如果返回值,则调用函数 不会以返回实例的方式返回我们想要的值。因为 在成功的情况下,我们只关心检测错误,因此我们不需要返回 unwrapped 值,它只会是 。if letunwrap_or_elserunErrprocess::exit(1)rununwrapConfig::buildConfigrun()unwrap_or_else()

和 函数的主体相同 两种情况:我们打印错误并退出。if letunwrap_or_else

将代码拆分到库 crate 中

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

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

  • 函数定义run
  • 相关声明use
  • 的定义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:移动到 src/lib.rsConfigrun

我们自由地使用了关键字: on 、 在其字段和方法上以及 on 函数。我们现在有一个 library crate,它有 我们可以测试的公共 API!pubConfigbuildrun

现在我们需要将移动到 src/lib.rs 的代码放入 binary crate 的 src/main.rs 中,如示例 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 中使用库 crateminigrep

我们添加一行代码,从 library crate 添加到二进制 crate 的范围内,并将函数作为 替换为我们的 crate 名称。现在所有功能都应该已连接,并且应该 工作。运行程序并确保一切正常。use minigrep::ConfigConfigruncargo run

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

让我们通过做一些可以 使用旧代码很困难,但使用新代码很容易:我们将 编写一些测试!

本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准