x-i18n: generated_at: “2026-03-01T14:16:27Z” model: gemini-3-flash-preview provider: google-gemini-cli source_hash: 51b686ec16b974ffef5494fbaaaf84342322f080cfe28764df78f682f13c6a9f source_path: ch12-03-improving-error-handling-and-modularity.md workflow: 16
重构以提高模块化和错误处理能力 (Refactoring to Improve Modularity and Error Handling)
Refactoring to Improve Modularity and Error Handling
为了改进我们的程序,我们将修复四个与程序结构及其处理潜在错误方式有关的问题。首先,我们的 main 函数现在执行两个任务:解析参数和读取文件。随着程序的增长,main 函数处理的独立任务数量将会增加。随着一个函数承担更多职责,它会变得更难以推理,更难以测试,并且更难在不破坏其中一部分的情况下进行更改。最好分离功能,使每个函数只负责一项任务。
To improve our program, we’ll fix four problems that have to do with the
program’s structure and how it’s handling potential errors. First, our main
function now performs two tasks: It parses arguments and reads files. As our
program grows, the number of separate tasks the main function handles will
increase. As a function gains responsibilities, it becomes more difficult to
reason about, harder to test, and harder to change without breaking one of its
parts. It’s best to separate functionality so that each function is responsible
for one task.
这个问题也与第二个问题有关:虽然 query 和 file_path 是程序的配置变量,但像 contents 这样的变量是用来执行程序逻辑的。main 变得越长,我们需要引入作用域的变量就越多;作用域内的变量越多,就越难跟踪每个变量的用途。最好将配置变量组合到一个结构中,使它们的用途清晰明了。
This issue also ties into the second problem: Although query and file_path
are configuration variables to our program, variables like contents are used
to perform the program’s logic. The longer main becomes, the more variables
we’ll need to bring into scope; the more variables we have in scope, the harder
it will be to keep track of the purpose of each. It’s best to group the
configuration variables into one structure to make their purpose clear.
第三个问题是,我们使用了 expect 来在读取文件失败时打印错误消息,但错误消息只打印 Should have been able to read the file。读取文件可能以多种方式失败:例如,文件可能缺失,或者我们可能没有权限打开它。目前,无论情况如何,我们都会为所有事情打印相同的错误消息,这不会给用户任何信息!
The third problem is that we’ve used expect to print an error message when
reading the file fails, but the error message just prints Should have been able to read the file. Reading a file can fail in a number of ways: For
example, the file could be missing, or we might not have permission to open it.
Right now, regardless of the situation, we’d print the same error message for
everything, which wouldn’t give the user any information!
第四,我们使用 expect 来处理错误,如果用户在运行我们的程序时没有指定足够的参数,他们将从 Rust 得到一个 index out of bounds(索引越界)错误,这并不能清晰地解释问题。最好将所有的错误处理代码放在一个地方,这样如果错误处理逻辑需要更改,未来的维护者只需咨询一处代码。将所有的错误处理代码放在一个地方也将确保我们打印的消息对最终用户是有意义的。
Fourth, we use expect to handle an error, and if the user runs our program
without specifying enough arguments, they’ll get an index out of bounds error
from Rust that doesn’t clearly explain the problem. It would be best if all the
error-handling code were in one place so that future maintainers had only one
place to consult the code if the error-handling logic needed to change. Having
all the error-handling code in one place will also ensure that we’re printing
messages that will be meaningful to our end users.
让我们通过重构我们的项目来解决这四个问题。
Let’s address these four problems by refactoring our project.
二进制项目中的关注点分离 (Separating Concerns in Binary Projects)
Separating Concerns in Binary Projects
把多个任务的责任分配给 main 函数的组织问题在许多二进制项目中都很常见。因此,许多 Rust 程序员发现在 main 函数开始变大时拆分二进制程序的独立关注点很有用。这个过程包含以下步骤:
The organizational problem of allocating responsibility for multiple tasks to
the main function is common to many binary projects. As a result, many Rust
programmers find it useful to split up the separate concerns of a binary
program when the main function starts getting large. This process has the
following steps:
-
将你的程序拆分为一个 main.rs 文件和一个 lib.rs 文件,并将程序的逻辑移动到 lib.rs 中。
-
只要你的命令行解析逻辑很小,它就可以留在
main函数中。 -
当命令行解析逻辑开始变得复杂时,将其从
main函数中提取到其他函数或类型中。 -
Split your program into a main.rs file and a lib.rs file and move your program’s logic to lib.rs.
-
As long as your command line parsing logic is small, it can remain in the
mainfunction. -
When the command line parsing logic starts getting complicated, extract it from the
mainfunction into other functions or types.
在此过程之后留在 main 函数中的职责应仅限于以下内容:
The responsibilities that remain in the main function after this process
should be limited to the following:
-
使用参数值调用命令行解析逻辑
-
设置任何其他配置
-
调用 lib.rs 中的
run函数 -
如果
run返回错误,则处理错误 -
Calling the command line parsing logic with the argument values
-
Setting up any other configuration
-
Calling a
runfunction in lib.rs -
Handling the error if
runreturns an error
这种模式是关于关注点分离的:main.rs 处理运行程序,而 lib.rs 处理手头任务的所有逻辑。因为你不能直接测试 main 函数,所以这种结构让你通过将其移出 main 函数来测试程序的所有逻辑。留在 main 函数中的代码将足够小,可以通过阅读来验证其正确性。让我们按照这个过程重做我们的程序。
This pattern is about separating concerns: main.rs handles running the
program and lib.rs handles all the logic of the task at hand. Because you
can’t test the main function directly, this structure lets you test all of
your program’s logic by moving it out of the main function. The code that
remains in the main function will be small enough to verify its correctness
by reading it. Let’s rework our program by following this process.
提取参数解析器 (Extracting the Argument Parser)
Extracting the Argument Parser
我们将把解析参数的功能提取到一个由 main 调用的函数中。示例 12-5 显示了 main 函数的新开头,它调用了一个新函数 parse_config,我们将在 src/main.rs 中定义该函数。
We’ll extract the functionality for parsing arguments into a function that
main will call. Listing 12-5 shows the new start of the main function that
calls a new function parse_config, which we’ll define in src/main.rs.
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-05/src/main.rs:here}}
我们仍然将命令行参数收集到一个向量中,但不再是在 main 函数中将索引 1 处的参数值分配给变量 query 且将索引 2 处的参数值分配给变量 file_path,而是将整个向量传递给 parse_config 函数。parse_config 函数随后持有确定哪个参数对应哪个变量并传回给 main 的逻辑。我们仍在 main 中创建 query 和 file_path 变量,但 main 不再负责确定命令行参数和变量如何对应。
We’re still collecting the command line arguments into a vector, but instead of
assigning the argument value at index 1 to the variable query and the
argument value at index 2 to the variable file_path within the main
function, we pass the whole vector to the parse_config function. The
parse_config function then holds the logic that determines which argument
goes in which variable and passes the values back to main. We still create
the query and file_path variables in main, but main no longer has the
responsibility of determining how the command line arguments and variables
correspond.
这种重做对于我们这个小程序来说可能看起来有些大材小用,但我们是在以小的、增量式的步骤进行重构。做出此更改后,再次运行程序以验证参数解析是否仍然正常工作。经常检查进度很有好处,有助于在问题发生时识别原因。
This rework may seem like overkill for our small program, but we’re refactoring in small, incremental steps. After making this change, run the program again to verify that the argument parsing still works. It’s good to check your progress often, to help identify the cause of problems when they occur.
组合配置值 (Grouping Configuration Values)
Grouping Configuration Values
我们可以再迈出一小步来进一步改进 parse_config 函数。目前,我们返回的是一个元组,但随后我们立即再次将该元组分解为各个部分。这是一个迹象,表明我们可能还没有得到正确的抽象。
We can take another small step to improve the parse_config function further.
At the moment, we’re returning a tuple, but then we immediately break that
tuple into individual parts again. This is a sign that perhaps we don’t have
the right abstraction yet.
另一个显示有改进空间的指标是 parse_config 的 config 部分,它暗示我们返回的两个值是相关的,并且都是一个配置值的一部分。目前,除了通过将两个值组合成一个元组之外,我们并没有在数据结构中传达这种含义;相反,我们将把这两个值放入一个结构体中,并给每个结构体字段起一个有意义的名称。这样做将使未来的代码维护者更容易理解不同值之间的关系以及它们的用途。
Another indicator that shows there’s room for improvement is the config part
of parse_config, which implies that the two values we return are related and
are both part of one configuration value. We’re not currently conveying this
meaning in the structure of the data other than by grouping the two values into
a tuple; we’ll instead put the two values into one struct and give each of the
struct fields a meaningful name. Doing so will make it easier for future
maintainers of this code to understand how the different values relate to each
other and what their purpose is.
示例 12-6 显示了对 parse_config 函数的改进。
Listing 12-6 shows the improvements to the parse_config function.
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-06/src/main.rs:here}}
我们添加了一个名为 Config 的结构体,定义了名为 query 和 file_path 的字段。parse_config 的签名现在表明它返回一个 Config 值。在 parse_config 的函数体中,我们以前返回引用 args 中 String 值的字符串切片,现在我们将 Config 定义为包含拥有的 String 值。main 中的 args 变量是参数值的所有者,并且仅允许 parse_config 函数借用它们,这意味着如果 Config 尝试获取 args 中值的所有权,我们将违反 Rust 的借用规则。
We’ve added a struct named Config defined to have fields named query and
file_path. The signature of parse_config now indicates that it returns a
Config value. In the body of parse_config, where we used to return
string slices that reference String values in args, we now define Config
to contain owned String values. The args variable in main is the owner of
the argument values and is only letting the parse_config function borrow
them, which means we’d violate Rust’s borrowing rules if Config tried to take
ownership of the values in args.
我们可以通过多种方式管理 String 数据;最简单(虽然有些低效)的方法是在值上调用 clone 方法。这将为 Config 实例制作一份完整的数据副本以归其所有,这比存储对字符串数据的引用需要更多的时间和内存。然而,克隆数据也使我们的代码非常直观,因为我们不必管理引用的生命周期;在这种情况下,牺牲一点性能来换取简洁是值得的。
There are a number of ways we could manage the String data; the easiest, though somewhat inefficient, route is to call the clone method on the values. This will make a full copy of the data for the Config instance to own, which takes more time and memory than storing a reference to the string data. However, cloning the data also makes our code very straightforward because we don’t have to manage the lifetimes of the references; in this circumstance, giving up a little performance to gain simplicity is a worthwhile trade-off.
使用
clone的权衡The Trade-Offs of Using
clone许多 Rustaceans 倾向于避免使用
clone来修复所有权问题,因为它有运行时开销。在第 13 章中,你将学习在此类情况下如何使用更高效的方法。但现在,为了继续取得进展,复制几个字符串是可以的,因为你只需复制一次,而且你的文件路径和查询字符串都非常短。与其在第一次尝试时就过度优化代码,不如先得到一个虽然有点低效但可以工作的程序。随着你对 Rust 变得更有经验,开始使用最有效的解决方案会更容易,但目前,调用clone是完全可以接受的。There’s a tendency among many Rustaceans to avoid using
cloneto fix ownership problems because of its runtime cost. In Chapter 13, you’ll learn how to use more efficient methods in this type of situation. But for now, it’s okay to copy a few strings to continue making progress because you’ll make these copies only once and your file path and query string are very small. It’s better to have a working program that’s a bit inefficient than to try to hyperoptimize code on your first pass. As you become more experienced with Rust, it’ll be easier to start with the most efficient solution, but for now, it’s perfectly acceptable to callclone.
我们更新了 main ,使其将 parse_config 返回的 Config 实例放入名为 config 的变量中,并更新了以前使用单独的 query 和 file_path 变量的代码,使其现在改为使用 Config 结构体上的字段。
We’ve updated main so that it places the instance of Config returned by
parse_config into a variable named config, and we updated the code that
previously used the separate query and file_path variables so that it now
uses the fields on the Config struct instead.
现在我们的代码更清晰地传达了 query 和 file_path 是相关的,并且它们的目的是配置程序的工作方式。任何使用这些值的代码都知道在 config 实例中以其用途命名的字段中找到它们。
Now our code more clearly conveys that query and file_path are related and
that their purpose is to configure how the program will work. Any code that
uses these values knows to find them in the config instance in the fields
named for their purpose.
为 Config 创建构造函数 (Creating a Constructor for Config)
Creating a Constructor for Config
到目前为止,我们已经从 main 中提取了负责解析命令行参数的逻辑,并将其放入了 parse_config 函数中。这样做帮助我们看到 query 和 file_path 值是相关的,并且这种关系应该在我们的代码中体现出来。然后,我们添加了一个 Config 结构体来命名 query 和 file_path 的相关用途,并能够从 parse_config 函数中将值的名称作为结构体字段名返回。
So far, we’ve extracted the logic responsible for parsing the command line
arguments from main and placed it in the parse_config function. Doing so
helped us see that the query and file_path values were related, and that
relationship should be conveyed in our code. We then added a Config struct to
name the related purpose of query and file_path and to be able to return the
values’ names as struct field names from the parse_config function.
既然 parse_config 函数的目的是创建一个 Config 实例,我们就可以将 parse_config 从一个普通的函数更改为一个名为 new 的与 Config 结构体关联的函数。做出此更改将使代码更符合惯例。我们可以通过调用 String::new 来创建标准库中类型(如 String)的实例。类似地,通过将 parse_config 更改为与 Config 关联的 new 函数,我们将能够通过调用 Config::new 来创建 Config 的实例。示例 12-7 显示了我们需要做的更改。
So, now that the purpose of the parse_config function is to create a Config
instance, we can change parse_config from a plain function to a function
named new that is associated with the Config struct. Making this change
will make the code more idiomatic. We can create instances of types in the
standard library, such as String, by calling String::new. Similarly, by
changing parse_config into a new function associated with Config, we’ll
be able to create instances of Config by calling Config::new. Listing 12-7
shows the changes we need to make.
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-07/src/main.rs:here}}
我们更新了 main 中以前调用 parse_config 的地方,改为调用 Config::new。我们将 parse_config 的名称更改为 new ,并将其移动到 impl 块中,该块将 new 函数与 Config 关联。尝试再次编译此代码以确保它能正常工作。
We’ve updated main where we were calling parse_config to instead call
Config::new. We’ve changed the name of parse_config to new and moved it
within an impl block, which associates the new function with Config. Try
compiling this code again to make sure it works.
修复错误处理 (Fixing the Error Handling)
Fixing the Error Handling
现在我们将致力于修复我们的错误处理。回想一下,如果向量包含的项少于三项,尝试访问 args 向量中索引 1 或索引 2 处的值将导致程序恐慌。尝试在没有任何参数的情况下运行该程序;它将看起来像这样:
Recall that attempting to access the values in the args vector at index 1 or index 2 will cause the program to panic if the vector contains fewer than three items. Try running the program without any arguments; it will look like this:
{{#include ../listings/ch12-an-io-project/listing-12-07/output.txt}}
行 index out of bounds: the len is 1 but the index is 1 是面向程序员的错误消息。它无法帮助我们的最终用户理解他们应该做些什么。让我们现在修复它。
The line index out of bounds: the len is 1 but the index is 1 is an error
message intended for programmers. It won’t help our end users understand what
they should do instead. Let’s fix that now.
改进错误消息 (Improving the Error Message)
Improving the Error Message
在示例 12-8 中,我们在 new 函数中添加了一个检查,以便在访问索引 1 和索引 2 之前验证切片是否足够长。如果切片不够长,程序会引发恐慌并显示更好的错误消息。
In Listing 12-8, we add a check in the new function that will verify that the
slice is long enough before accessing index 1 and index 2. If the slice isn’t
long enough, the program panics and displays a better error message.
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-08/src/main.rs:here}}
这段代码类似于我们在示例 9-13 中编写的 Guess::new 函数,我们在那里当 value 参数超出有效值范围时调用了 panic!。这里我们不是检查值的范围,而是检查 args 的长度是否至少为 3,并且函数的其余部分可以在此条件已满足的假设下运行。如果 args 少于三项,此条件将为 true,我们调用 panic! 宏立即结束程序。
This code is similar to the Guess::new function we wrote in Listing
9-13, where we called panic! when the
value argument was out of the range of valid values. Instead of checking for
a range of values here, we’re checking that the length of args is at least
3 and the rest of the function can operate under the assumption that this
condition has been met. If args has fewer than three items, this condition
will be true, and we call the panic! macro to end the program immediately.
在 new 中增加了这几行代码后,让我们再次在没有任何参数的情况下运行该程序,看看现在的错误是什么样子的:
With these extra few lines of code in new, let’s run the program without any
arguments again to see what the error looks like now:
{{#include ../listings/ch12-an-io-project/listing-12-08/output.txt}}
这个输出更好:我们现在有了一个合理的错误消息。但是,我们也有一些不希望提供给用户多余信息。也许我们在示例 9-13 中使用的技术并不是这里最好的选择:正如在第 9 章中讨论的,调用 panic! 对于编程问题比对于用法问题更合适。相反,我们将使用你在第 9 章中学到的另一种技术——返回一个 Result,它指示成功或错误。
This output is better: We now have a reasonable error message. However, we also
have extraneous information we don’t want to give to our users. Perhaps the
technique we used in Listing 9-13 isn’t the best one to use here: A call to
panic! is more appropriate for a programming problem than a usage problem,
as discussed in Chapter 9. Instead,
we’ll use the other technique you learned about in Chapter 9—returning a
Result that indicates either success or an error.
返回 Result 而不是调用 panic! (Returning a Result Instead of Calling panic!)
Returning a Result Instead of Calling panic!
我们可以转而返回一个 Result 值,该值在成功的情况下包含一个 Config 实例,在错误的情况下描述问题。我们还要将函数名从 new 更改为 build ,因为许多程序员期望 new 函数永远不会失败。当 Config::build 与 main 通信时,我们可以使用 Result 类型来发出出现问题的信号。然后,我们可以更改 main ,将 Err 变体转换为对我们的用户更实用的错误,而不会有调用 panic! 所导致的关于 thread 'main' 和 RUST_BACKTRACE 的环绕文本。
We can instead return a Result value that will contain a Config instance in
the successful case and will describe the problem in the error case. We’re also
going to change the function name from new to build because many
programmers expect new functions to never fail. When Config::build is
communicating to main, we can use the Result type to signal there was a
problem. Then, we can change main to convert an Err variant into a more
practical error for our users without the surrounding text about thread 'main' and RUST_BACKTRACE that a call to panic! causes.
示例 12-9 显示了我们需要对现在调用的函数 Config::build 的返回值所做的更改,以及返回 Result 所需的函数体。注意,在我们也更新 main 之前(我们将在下一个列表中完成),这段代码将无法编译。
Listing 12-9 shows the changes we need to make to the return value of the
function we’re now calling Config::build and the body of the function needed
to return a Result. Note that this won’t compile until we update main as
well, which we’ll do in the next listing.
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-09/src/main.rs:here}}
我们的 build 函数在成功的情况下返回带有 Config 实例的 Result ,在错误的情况下返回字符串字面量。我们的错误值始终是具有 'static 生命周期的字符串字面量。
Our build function returns a Result with a Config instance in the success
case and a string literal in the error case. Our error values will always be
string literals that have the 'static lifetime.
我们在函数体中做了两处更改:当用户没有传递足够的参数时,我们现在返回一个 Err 值而不是调用 panic!,并且我们将 Config 返回值包裹在了 Ok 中。这些更改使该函数符合其新的类型签名。
We’ve made two changes in the body of the function: Instead of calling panic!
when the user doesn’t pass enough arguments, we now return an Err value, and
we’ve wrapped the Config return value in an Ok. These changes make the
function conform to its new type signature.
从 Config::build 返回 Err 值允许 main 函数处理从 build 函数返回的 Result 值,并在错误的情况下更干净地退出进程。
Returning an Err value from Config::build allows the main function to
handle the Result value returned from the build function and exit the
process more cleanly in the error case.
调用 Config::build 并处理错误 (Calling Config::build and Handling Errors)
Calling Config::build and Handling Errors
为了处理错误情况并打印用户友好的消息,我们需要更新 main 以处理由 Config::build 返回的 Result,如示例 12-10 所示。我们还将承担起使用非零错误代码退出命令行工具的职责,将其从 panic! 中拿走并改为手动实现。非零退出状态是向调用我们程序的进程发出信号的惯例,表明程序以错误状态退出。
To handle the error case and print a user-friendly message, we need to update
main to handle the Result being returned by Config::build, as shown in
Listing 12-10. We’ll also take the responsibility of exiting the command line
tool with a nonzero error code away from panic! and instead implement it by
hand. A nonzero exit status is a convention to signal to the process that
called our program that the program exited with an error state.
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-10/src/main.rs:here}}
在此列表中,我们使用了一个尚未详细介绍的方法:unwrap_or_else,它由标准库在 Result<T, E> 上定义。使用 unwrap_or_else 允许我们定义一些自定义的、非 panic! 的错误处理。如果 Result 是一个 Ok 值,此方法的行为类似于 unwrap:它返回 Ok 包裹的内部值。然而,如果值是一个 Err 值,此方法将调用闭包(closure)中的代码,闭包是我们定义并作为参数传递给 unwrap_or_else 的匿名函数。我们将在第 13 章中更详细地介绍闭包。目前,你只需要知道 unwrap_or_else 会将 Err 的内部值(在此例中是我们在示例 12-9 中添加的静态字符串 "not enough arguments")传递给出现在垂直线之间的参数 err 所代表的闭包。闭包中的代码随后可以在运行时使用 err 值。
In this listing, we’ve used a method we haven’t covered in detail yet:
unwrap_or_else, which is defined on Result<T, E> by the standard library.
Using unwrap_or_else allows us to define some custom, non-panic! error
handling. If the Result is an Ok value, this method’s behavior is similar
to unwrap: It returns the inner value that Ok is wrapping. However, if the
value is an Err value, this method calls the code in the closure, which is
an anonymous function we define and pass as an argument to unwrap_or_else.
We’ll cover closures in more detail in Chapter 13. For
now, you just need to know that unwrap_or_else will pass the inner value of
the Err, which in this case is the static string "not enough arguments"
that we added in Listing 12-9, to our closure in the argument err that
appears between the vertical pipes. The code in the closure can then use the
err value when it runs.
我们添加了一条新的 use 行,将标准库中的 process 引入作用域。在错误情况下将运行的闭包中的代码只有两行:我们打印 err 值,然后调用 process::exit。process::exit 函数将立即停止程序并返回传递给它的数字作为退出状态代码。这类似于我们在示例 12-8 中使用的基于 panic! 的处理,但我们不再获得所有额外的输出。让我们试一试:
We’ve added a new use line to bring process from the standard library into
scope. The code in the closure that will be run in the error case is only two
lines: We print the err value and then call process::exit. The
process::exit function will stop the program immediately and return the
number that was passed as the exit status code. This is similar to the
panic!-based handling we used in Listing 12-8, but we no longer get all the
extra output. Let’s try it:
{{#include ../listings/ch12-an-io-project/listing-12-10/output.txt}}
太棒了!这个输出对我们的用户友好得多。
Great! This output is much friendlier for our users.
从 main 函数中提取逻辑 (Extracting Logic from main)
Extracting Logic from main
既然我们已经完成了对配置解析的重构,现在让我们转向程序的逻辑。正如我们在“二进制项目中的关注点分离”中所述,我们将提取一个名为 run 的函数,该函数将持有目前 main 函数中除配置设置或处理错误之外的所有逻辑。完成后,main 函数将简洁且易于通过检查进行验证,并且我们将能够为所有其他逻辑编写测试。
Now that we’ve finished refactoring the configuration parsing, let’s turn to
the program’s logic. As we stated in “Separating Concerns in Binary
Projects”, we’ll
extract a function named run that will hold all the logic currently in the
main function that isn’t involved with setting up configuration or handling
errors. When we’re done, the main function will be concise and easy to verify
by inspection, and we’ll be able to write tests for all the other logic.
示例 12-11 显示了提取 run 函数这一小的、增量式的改进。
Listing 12-11 shows the small, incremental improvement of extracting a run
function.
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-11/src/main.rs:here}}
run 函数现在包含 main 中从读取文件开始的所有剩余逻辑。run 函数接收 Config 实例作为参数。
The run function now contains all the remaining logic from main, starting
from reading the file. The run function takes the Config instance as an
argument.
从 run 函数返回错误 (Returning Errors from run)
Returning Errors from run
随着剩余的程序逻辑被分离到 run 函数中,我们可以改进错误处理,就像我们在示例 12-9 中对 Config::build 所做的那样。run 函数将在出现问题时返回 Result<T, E>,而不是通过调用 expect 允许程序恐慌。这将让我们能进一步以用户友好的方式将处理错误的逻辑合并到 main 中。示例 12-12 显示了我们需要对 run 的签名和函数体进行的更改。
With the remaining program logic separated into the run function, we can
improve the error handling, as we did with Config::build in Listing 12-9.
Instead of allowing the program to panic by calling expect, the run
function will return a Result<T, E> when something goes wrong. This will let
us further consolidate the logic around handling errors into main in a
user-friendly way. Listing 12-12 shows the changes we need to make to the
signature and body of run.
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-12/src/main.rs:here}}
我们在这里做了三处重大更改。首先,我们将 run 函数的返回类型更改为 Result<(), Box<dyn Error>>。此函数以前返回单元类型 (),我们将其保留为 Ok 情况下返回的值。
We’ve made three significant changes here. First, we changed the return type of
the run function to Result<(), Box<dyn Error>>. This function previously
returned the unit type, (), and we keep that as the value returned in the
Ok case.
对于错误类型,我们使用了特征对象 Box<dyn Error>(并在顶部通过 use 语句将 std::error::Error 引入了作用域)。我们将在第 18 章介绍特征对象。目前,只需知道 Box<dyn Error> 意味着函数将返回一个实现了 Error 特征的类型,但我们不必指定具体的返回值的类型。这使我们能灵活地在不同的错误情况下返回可能属于不同类型的错误值。关键字 dyn 是 dynamic 的缩写。
For the error type, we used the trait object Box<dyn Error> (and we brought
std::error::Error into scope with a use statement at the top). We’ll cover
trait objects in Chapter 18. For now, just know that
Box<dyn Error> means the function will return a type that implements the
Error trait, but we don’t have to specify what particular type the return
value will be. This gives us flexibility to return error values that may be of
different types in different error cases. The dyn keyword is short for
dynamic.
其次,我们删除了对 expect 的调用,转而使用 ? 运算符,正如我们在第 9 章中讨论的那样。? 不会在错误时引发 panic!,而是将错误值从当前函数返回,供调用者处理。
Second, we’ve removed the call to expect in favor of the ? operator, as we
talked about in Chapter 9. Rather than
panic! on an error, ? will return the error value from the current function
for the caller to handle.
第三,run 函数现在在成功的情况下返回一个 Ok 值。我们在签名中将 run 函数的成功类型声明为 (),这意味着我们需要将单元类型值包裹在 Ok 值中。这种 Ok(()) 语法起初看起来可能有点奇怪。但像这样使用 () 是指示我们仅因其副作用而调用 run 的惯用方式;它不会返回我们需要的值。
Third, the run function now returns an Ok value in the success case.
We’ve declared the run function’s success type as () in the signature,
which means we need to wrap the unit type value in the Ok value. This
Ok(()) syntax might look a bit strange at first. But using () like this is
the idiomatic way to indicate that we’re calling run for its side effects
only; it doesn’t return a value we need.
当你运行这段代码时,它将可以编译但会显示一个警告:
When you run this code, it will compile but will display a warning:
{{#include ../listings/ch12-an-io-project/listing-12-12/output.txt}}
Rust 告诉我们,我们的代码忽略了 Result 值,而 Result 值可能表明发生了错误。但我们没有检查是否有错误,编译器提醒我们大概的意思是应该在这里有一些错误处理代码!现在让我们纠正这个问题。
Rust tells us that our code ignored the Result value and the Result value
might indicate that an error occurred. But we’re not checking to see whether or
not there was an error, and the compiler reminds us that we probably meant to
have some error-handling code here! Let’s rectify that problem now.
处理 main 中从 run 返回的错误 (Handling Errors Returned from run in main)
Handling Errors Returned from run in main
我们将检查错误并使用与示例 12-10 中 Config::build 使用的技术类似的方法来处理它们,但有一点细微的区别:
We’ll check for errors and handle them using a technique similar to one we used
with Config::build in Listing 12-10, but with a slight difference:
文件名: src/main.rs
{{#rustdoc_include ../listings/ch12-an-io-project/no-listing-01-handling-errors-in-main/src/main.rs:here}}
我们使用 if let 而不是 unwrap_or_else 来检查 run 是否返回 Err 值,如果是则调用 process::exit(1)。run 函数并不像 Config::build 返回 Config 实例那样返回一个我们想要 unwrap 的值。因为 run 在成功的情况下返回 (),我们只关心检测错误,所以我们不需要 unwrap_or_else 返回解包后的值,因为那只会是 ()。
We use if let rather than unwrap_or_else to check whether run returns an
Err value and to call process::exit(1) if it does. The run function
doesn’t return a value that we want to unwrap in the same way that
Config::build returns the Config instance. Because run returns () in
the success case, we only care about detecting an error, so we don’t need
unwrap_or_else to return the unwrapped value, which would only be ().
if let 函数体和 unwrap_or_else 函数体在两种情况下都是相同的:我们打印错误并退出。
The bodies of the if let and the unwrap_or_else functions are the same in
both cases: We print the error and exit.
将代码拆分为库 crate (Splitting Code into a Library Crate)
Splitting Code into a Library Crate
我们的 minigrep 项目目前看起来不错!现在我们将拆分 src/main.rs 文件并将一些代码放入 src/lib.rs 文件中。这样,我们就可以测试代码,并让 src/main.rs 文件承担更少的责任。
Our minigrep project is looking good so far! Now we’ll split the
src/main.rs file and put some code into the src/lib.rs file. That way, we
can test the code and have a src/main.rs file with fewer responsibilities.
让我们在 src/lib.rs 而不是 src/main.rs 中定义负责搜索文本的代码,这将让我们(或任何其他使用我们的 minigrep 库的人)能够比从我们的 minigrep 二进制文件中在更多上下文中调用搜索函数。
Let’s define the code responsible for searching text in src/lib.rs rather
than in src/main.rs, which will let us (or anyone else using our
minigrep library) call the searching function from more contexts than our
minigrep binary.
首先,让我们在 src/lib.rs 中定义 search 函数签名,如示例 12-13 所示,其函数体调用 unimplemented! 宏。当我们填充实现时,我们将更详细地解释该签名。
First, let’s define the search function signature in src/lib.rs as shown in
Listing 12-13, with a body that calls the unimplemented! macro. We’ll explain
the signature in more detail when we fill in the implementation.
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-13/src/lib.rs}}
我们在函数定义上使用了 pub 关键字,将 search 指定为我们库 crate 公有 API 的一部分。现在我们有了一个可以在二进制 crate 中使用并可以测试的库 crate!
We’ve used the pub keyword on the function definition to designate search
as part of our library crate’s public API. We now have a library crate that we
can use from our binary crate and that we can test!
现在我们需要将在 src/lib.rs 中定义的代码引入 src/main.rs 中二进制 crate 的作用域,并调用它,如示例 12-14 所示。
Now we need to bring the code defined in src/lib.rs into the scope of the binary crate in src/main.rs and call it, as shown in Listing 12-14.
{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-14/src/main.rs:here}}
我们添加了一行 use minigrep::search ,将搜索函数从库 crate 引入二进制 crate 的作用域。然后,在 run 函数中,我们不再打印文件的内容,而是调用 search 函数并将 config.query 值和 contents 作为参数传递。然后,run 将使用 for 循环打印从 search 返回的与查询匹配的每一行。现在也是删除 main 函数中显示查询和文件路径的 println! 调用的好时机,这样我们的程序就只打印搜索结果(如果未发生错误)。
We add a use minigrep::search line to bring the search function from
the library crate into the binary crate’s scope. Then, in the run function,
rather than printing out the contents of the file, we call the search
function and pass the config.query value and contents as arguments. Then,
run will use a for loop to print each line returned from search that
matched the query. This is also a good time to remove the println! calls in
the main function that displayed the query and the file path so that our
program only prints the search results (if no errors occur).
请注意,搜索函数将在进行任何打印之前将其返回的所有结果收集到一个向量中。搜索大文件时,此实现可能导致显示结果的速度很慢,因为结果不是在找到时就打印出来的;我们将在第 13 章讨论使用迭代器修复此问题的可能方法。
Note that the search function will be collecting all the results into a vector it returns before any printing happens. This implementation could be slow to display results when searching large files, because results aren’t printed as they’re found; we’ll discuss a possible way to fix this using iterators in Chapter 13.
呼!做了很多工作,但我们已经为将来的成功奠定了基础。现在处理错误要容易得多,而且我们使代码更具模块化。从现在开始,几乎我们所有的工作都将在 src/lib.rs 中完成。
Whew! That was a lot of work, but we’ve set ourselves up for success in the future. Now it’s much easier to handle errors, and we’ve made the code more modular. Almost all of our work will be done in src/lib.rs from here on out.
让我们利用这种新获得的模块化,做一些在旧代码中很难但在新代码中很容易的事情:我们将编写一些测试!
Let’s take advantage of this newfound modularity by doing something that would have been difficult with the old code but is easy with the new code: We’ll write some tests!