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

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

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

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

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

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

二进制项目的关注点分离

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

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

保留在main函数 应限于以下内容:

  • 使用参数值调用命令行解析逻辑
  • 设置任何其他配置
  • 调用run函数 lib.rs
  • 在以下情况下处理错误run返回错误

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

提取 Argument Parser

我们将用于解析参数的功能提取到一个函数中,该函数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:提取parse_config函数从main

我们仍然将命令行参数收集到一个 vector 中,但不是 将索引 1 处的参数值分配给变量query和 变量索引 2 处的 argument valuefile_pathmain函数中,我们将整个 vector 传递给parse_config功能。这parse_configfunction 然后保存确定哪个参数的 logic 放入哪个变量中,并将值传递回main.我们仍在创造 这queryfile_path变量mainmain不再具有 确定命令行参数和变量 通信。

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

对配置值进行分组

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

另一个表明有改进空间的指标是config部分 之parse_config,这意味着我们返回的两个值是相关的,并且 都是 Configuration 值的一部分。我们目前没有传达此内容 在数据结构中的含义,而不是通过将两个值分组为 一个元组;我们将这两个值放入一个结构体中,并为每个 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_path.的签名parse_confignow 表示它返回一个Config价值。在 bodyparse_config,我们过去经常返回的地方 string 切片,该String的值args,我们现在定义Config包含 ownedString值。这args变量main是 参数值,并且只让parse_config函数借用 them,这意味着如果Config尝试拿 中值的所有权args.

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

使用clone

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

我们更新了main因此,它将Config返回者parse_config转换为名为config,我们更新了 以前使用单独的queryfile_path变量,因此它现在使用 的Configstruct 代替。

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

为 创建 ConstructorConfig

到目前为止,我们已经提取了负责解析命令行的 logic arguments frommain并将其放置在parse_config功能。这样做 帮助我们看到queryfile_path值是相关的,并且 关系应该在我们的代码中传达。然后,我们添加了一个Configstruct 设置为 name 的相关用途queryfile_path并能够返回 values 的名称作为 struct 字段名称的parse_config功能。

所以现在parse_config函数是创建一个Config实例中,我们可以更改parse_config从普通函数到函数 叫newConfig结构。进行此更改 将使代码更加地道。我们可以在 标准库,例如String,通过调用String::new.同样,通过 改变parse_config转换为new函数关联Config,我们将 能够创建Config通过调用Config::new.示例 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_configConfig::new

我们更新了main我们打电话的地方parse_config改为调用Config::new.我们更改了parse_confignew并移动了它 在impl块,它将new函数替换为Config.尝试 再次编译此代码以确保其正常工作。

修复错误处理

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

$ 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函数验证 slice 在访问索引 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函数的其余部分可以在假设 this 条件已得到满足。如果args的项目少于 3 项,则此条件 将是true,我们调用panic!宏立即结束程序。

有了这额外的几行代码new,让我们运行程序,不要 arguments 再次查看错误现在是什么样子的:

$ 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 中使用的技术并不是最好在这里使用的技术:对panic!更适合于编程问题而不是使用问题,如第 9 章所述。相反 我们将使用您在第 9 章中学到的另一种技术—返回一个Result,这表示成功或错误。

返回Result而不是调用panic!

我们可以改为返回一个Result值,它将包含Config实例 成功案例,并将描述 Error Case 中的问题。我们也是 将函数名称从newbuild因为许多 程序员期望new函数永远不会失败。什么时候Config::build是 通信对象main,我们可以使用Result键入以表示存在 问题。然后我们可以改变main要将Err变体转换为更多 实际错误对于我们的用户来说,没有周围的文本关于thread 'main'RUST_BACKTRACEpanic!原因。

示例 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:返回一个ResultConfig::build

我们build函数返回一个Result替换为Config实例 case 和 Error case 中的 String 文本。我们的 error 值将始终为 字符串文本,这些文本具有'static辈子。

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

返回Err值来自Config::build允许main函数设置为 处理Resultbuild函数并退出 在错误情况下更干净地处理。

Config::build和处理错误

要处理错误情况并打印用户友好的消息,我们需要更新main来处理Result被退回Config::build,如 示例 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>由 Standard 库。 用unwrap_or_else允许我们定义一些自定义的、非panic!错误 处理。如果Result是一个Okvalue 时,此方法的行为类似 自unwrap:它返回Ok正在包装。但是,如果 value 是一个Err值,此方法调用 Closure 中的代码,即 我们定义并作为参数传递给unwrap_or_else. 我们将在第 13 章中更详细地介绍闭包。为 现在,您只需要知道unwrap_or_else将传递 这Err,在本例中为静态字符串"not enough arguments"我们在示例 12-9 中添加到参数中的闭包err那 显示在垂直管道之间。然后,闭包中的代码可以使用err值。

我们添加了一个新的useline 带来process从 Standard 库到 范围。在错误情况下将运行的闭包中的代码只有 2 lines:我们打印err值,然后调用process::exit.这process::exit函数将立即停止程序并返回 作为退出状态代码传递的号码。这类似于panic!-,但我们不再获取所有 extra 输出。让我们试一试:

$ 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开始 从读取文件。这run函数采用Configinstance 作为 论点。

run功能

将剩余的程序逻辑分离到run函数,我们可以 改进错误处理,就像我们对Config::build在示例 12-9 中。 而不是通过调用expectrun函数将返回一个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>>.此函数以前 返回了 Unit 类型 ,并将其保留为()Ok箱。

对于 error 类型,我们使用了 trait 对象 Box<dyn Error>(我们已经 带std::error::Errorinto 范围替换为use声明)。 我们将在第 17 章中介绍 trait 对象。目前,只需 知道Box<dyn Error>表示该函数将返回一个类型,该 实现Errortrait 的 trait 中,但我们不必指定什么特定的类型 返回值将为。这使我们能够灵活地返回 error 值 在不同的错误情况下可能属于不同的类型。这dyn关键字简短 用于动态

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

第三,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 告诉我们,我们的代码忽略了Resultvalue 和Result价值 可能表示发生了错误。但是,我们不会检查是否或 没有出现错误,编译器提醒我们,我们可能是故意的 这里有一些错误处理代码!现在让我们纠正这个问题。

处理从runmain

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

文件名: 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功能 不会返回我们想要的值unwrap以同样的方式Config::build返回Config实例。因为run返回 成功案例中,我们只关心检测一个错误,所以我们不需要()unwrap_or_else返回 unwrapped 值,该值仅为 .()

if letunwrap_or_else函数在 两种情况:我们打印错误并退出。

将代码拆分到库 crate 中

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

让我们移动所有不在mainsrc/main.rssrc/lib.rs 的函数:

  • 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:移动Configrunsrc/lib.rs

我们自由地使用了pub关键词: onConfig、其字段及其build方法,在run功能。我们现在有一个 library crate,它有 我们可以测试的公共 API!

现在我们需要将移动到 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:使用minigrepsrc/main.rs 中的库 crate

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

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

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

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