重构以提高模块化和错误处理
为了改进我们的程序,我们将修复与
程序的结构以及它如何处理潜在错误。首先,我们的函数现在执行两项任务:解析参数和读取文件。作为我们的
程序增长,函数处理的单独任务的数量将
增加。随着职能部门的责任增加,它变得更加困难
reason about,更难测试,更难在不破坏其
部件。最好将功能分开,以便每个功能都负责
一个任务。main
main
这个问题也与第二个问题有关:虽然 和 是我们程序的配置变量,但使用了
以执行程序的逻辑。时间越长,变量越多
我们需要纳入范围;我们在 SCOPE 中的变量越多,就越难
这将是跟踪每个的目的。最好将
配置变量合并到一个结构中,以明确其用途。query
file_path
contents
main
第三个问题是,我们过去常常在
读取文件失败,但错误消息只打印 。读取文件可能以多种方式失败:对于
例如,文件可能丢失,或者我们可能没有打开它的权限。
现在,无论情况如何,我们都会为
everything,这不会给用户任何信息!expect
Should have been able to read the file
第四,我们用于处理错误,如果用户运行我们的程序
如果没有指定足够的参数,它们将收到错误
这并没有清楚地解释问题。如果所有
错误处理代码在一个地方,所以未来的维护者只有一个地方
来查阅代码(如果需要更改错误处理逻辑)。拥有所有
错误处理代码也将确保我们打印消息
这对我们的最终用户来说将是有意义的。expect
index out of bounds
让我们通过重构我们的项目来解决这四个问题。
二进制项目的关注点分离
将多个任务的责任分配给
该函数对于许多二进制项目都是通用的。因此,Rust
community 已经制定了用于拆分 a 的单独关注点的指导方针
binary 程序开始变大。此过程包括以下内容
步骤:main
main
- 将程序拆分为 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 中定义它。main
main
parse_config
我们仍然将命令行参数收集到一个 vector 中,但不是
将索引 1 处的参数值分配给变量,并将
参数值 2 传递给函数中的变量,我们将整个向量传递给函数。然后,该函数保存确定哪个参数的逻辑
放入哪个变量中,并将值传递回 。我们仍在创造
中的 and 变量,但不再具有
确定命令行参数和变量
通信。query
file_path
main
parse_config
parse_config
main
query
file_path
main
main
对于我们的小程序来说,这种返工似乎有点矫枉过正,但我们正在重构 以小的、渐进的步骤进行。进行此更改后,再次运行程序以 验证参数解析是否仍然有效。检查您的进度是件好事 通常,在问题发生时帮助确定问题的原因。
对配置值进行分组
我们可以再迈出一小步来进一步改进该功能。
目前,我们返回了一个 Tuples,但随后我们立即中断了它
tuple 转换为单独的部分。这是一个迹象,也许我们没有
这是正确的抽象。parse_config
另一个表明有改进空间的指标是零件
of ,这意味着我们返回的两个值是相关的,而
都是 Configuration 值的一部分。我们目前没有传达此内容
在数据结构中的含义,而不是通过将两个值分组为
一个元组;我们将这两个值放入一个结构体中,并为每个
struct 字段设置有意义的名称。这样做将使未来更容易
此代码的维护者,以了解不同的值与每个值之间的关系
其他以及它们的目的是什么。config
parse_config
示例 12-6 显示了对该函数的改进。parse_config
我们添加了一个名为 defined 的结构,以将字段命名为 和 。now 的签名表示它返回一个值。在 的正文中,我们过去返回的地方
引用 中的值的字符串切片,我们现在定义为包含拥有的值。变量 in 是
参数值 and 只让函数借用
它们,这意味着如果试图采用
中值的所有权。Config
query
file_path
parse_config
Config
parse_config
String
args
Config
String
args
main
parse_config
Config
args
我们可以通过多种方式管理数据;最容易的,
虽然效率有些低下,但 route 是对 values 调用 method。
这将为实例提供数据的完整副本,以便
比存储对字符串数据的引用需要更多的时间和内存。
但是,克隆数据也使我们的代码非常简单,因为我们
不必管理引用的生命周期;在这种情况下,
为了获得简单性而放弃一点性能是一个值得的权衡。String
clone
Config
使用克隆
的利弊
许多 Rustacean 有一种避免使用 fix 的倾向
所有权问题。在第 13 章中,您将学习如何更高效地使用
方法。但就目前而言,复制一些是可以的
strings 继续进行,因为您只会制作这些副本
once,并且您的文件路径和查询字符串非常小。最好有
一个比尝试超优化代码效率低下的工作程序
在你的第一次通过时。随着您对 Rust 的经验越来越丰富,它会
从最有效的解决方案开始更容易,但就目前而言,它是
完全可以接受调用 .clone
clone
我们更新了 return by 的实例,并将其放入名为 的变量中,并且我们更新了
以前使用单独的 and 变量,因此现在使用
结构体上的字段。main
Config
parse_config
config
query
file_path
Config
现在,我们的代码更清楚地传达了 和 是相关的 和
他们的目的是配置程序的工作方式。任何
使用这些值知道在实例的字段中查找它们
以他们的目的命名。query
file_path
config
为 Config
创建构造函数
到目前为止,我们已经提取了负责解析命令行的 logic
参数并将其放置在函数中。这样做
帮助我们看到 和 值是相关的,并且
关系应该在我们的代码中传达。然后,我们在
name 的相关用途 和 ,以便能够返回
values 的名称作为函数中的结构体字段名称。main
parse_config
query
file_path
Config
query
file_path
parse_config
所以现在函数的目的是创建一个实例,我们可以从普通函数更改为函数
named 的 API进行此更改
将使代码更加地道。我们可以在
标准库(如 ),通过调用 .同样,通过
更改为与 关联的函数,我们将
能够通过调用 来创建 的实例。示例 12-7
显示了我们需要进行的更改。parse_config
Config
parse_config
new
Config
String
String::new
parse_config
new
Config
Config
Config::new
我们更新了调用的位置,改为调用 .我们已将 的名称更改为 并将其移动
,它将函数与 .尝试
再次编译此代码以确保其正常工作。main
parse_config
Config::new
parse_config
new
impl
new
Config
修复错误处理
现在,我们将着手修复我们的错误处理。回想一下,尝试访问
向量中索引 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
这段代码类似于我们在 清单 中编写的 Guess::new
函数
9-13 中,我们在参数超出有效值范围时调用。而不是检查
一个值范围,我们检查长度是否至少为 ,并且函数的其余部分可以在假设 this
条件已得到满足。如果项目少于 3 项,则此条件
将是 ,我们调用宏以立即结束程序。panic!
value
args
3
args
true
panic!
有了这几行额外的代码,让我们在没有任何
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 章中学到的另一种技术 — 返回一个指示成功或错误的 Result
。panic!
返回 Result
而不是调用 panic!
我们可以返回一个值,该值将包含
成功案例,并将描述 Error Case 中的问题。我们也是
将函数名称从 更改为 因为 many
程序员希望函数永远不会失败。时间
通信到 ,我们可以使用 type 来表示存在
问题。然后我们可以 change 将 variant 转换为 more
实际错误对于我们的用户来说没有周围的文本 about 和 a call to 导致。Result
Config
new
build
new
Config::build
main
Result
main
Err
thread 'main'
RUST_BACKTRACE
panic!
示例 12-9 显示了我们需要对
函数和所需的函数体
以返回 .请注意,在我们将
好吧,我们将在下一个列表中进行。Config::build
Result
main
我们的函数返回一个 success 中有一个实例
case 和 Error case 中的 String 文本。我们的 error 值将始终为
具有 lifetime 的 String 文本。build
Result
Config
'static
我们对函数的主体进行了两项更改:我们现在返回一个值,而不是在用户没有传递足够的参数时调用
我们已将返回值包装在 .这些更改使
函数的 JSON JSON 中的调用。panic!
Err
Config
Ok
返回值 from 允许函数
处理函数返回的值并退出
在错误情况下更干净地处理。Err
Config::build
main
Result
build
调用 config::build
并处理错误
要处理错误情况并打印用户友好的消息,我们需要更新以处理 返回的 ,如 所示
示例 12-10.我们还将负责退出命令行
工具中具有非零错误代码,而是通过以下方式实现它
手。非零退出状态是向进程发出信号的约定
调用我们的程序,该程序退出时出现错误状态。main
Result
Config::build
panic!
在这个 清单 中,我们使用了一个我们还没有详细介绍的方法:,它由标准库定义。
使用 允许我们定义一些自定义的、无错误的
处理。如果 是 值,则此方法的行为类似
to :它返回正在包装的内部值。但是,如果
value 是一个值,该方法调用闭包中的代码,即
我们定义并作为参数传递给 的匿名函数。
我们将在第 13 章中更详细地介绍闭包。为
现在,你只需要知道它会传递
,在本例中,它是我们在示例 12-9 中添加的静态字符串,添加到参数
显示在垂直管道之间。然后,闭包中的代码可以在运行时使用该值。unwrap_or_else
Result<T, E>
unwrap_or_else
panic!
Result
Ok
unwrap
Ok
Err
unwrap_or_else
unwrap_or_else
Err
"not enough arguments"
err
err
我们添加了一行新代码,要从标准库引入
范围。在错误情况下将运行的闭包中的代码只有 2
行:我们打印值,然后调用 .该函数将立即停止程序并返回
作为退出状态代码传递的号码。这类似于示例 12-8 中使用的基于 -的处理方式,但我们不再获取所有
extra 输出。让我们试一试:use
process
err
process::exit
process::exit
panic!
$ 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编写测试。run
main
main
示例 12-11 显示了提取的函数。目前,我们只是在制作
提取函数的小型增量改进。我们仍然
在 src/main.rs 中定义函数。run
该函数现在包含 从 开始的所有剩余逻辑
从读取文件。该函数将实例作为
论点。run
main
run
Config
从 run
函数返回错误
将剩余的程序逻辑分离到函数中后,我们可以
改进错误处理,就像我们在示例 12-9 中所做的那样。
当出现问题时,该函数将返回 a,而不是通过调用 来允许程序 panic。这将允许
我们进一步将处理错误的逻辑整合到
用户友好的方式。示例 12-12 显示了我们需要对
签名和正文。run
Config::build
expect
run
Result<T, E>
main
run
我们在此处进行了三项重大更改。首先,我们将
函数设置为 .此函数以前
返回了 Unit 类型 ,并将其保留为 case 中返回的值。run
Result<(), Box<dyn Error>>
()
Ok
对于 error 类型,我们使用了 trait 对象(并且我们已将
带入范围,并在顶部显示一个 statement)。
我们将在第 17 章中介绍 trait 对象。目前,只需
知道这意味着该函数将返回一个类型,该类型
实现 trait,但我们不必指定什么特定类型
返回值将为。这使我们能够灵活地返回 error 值
在不同的错误情况下可能属于不同的类型。关键字简短
用于动态。Box<dyn Error>
std::error::Error
use
Box<dyn Error>
Error
dyn
其次,我们删除了对 的调用以支持运算符,因为我们
在第 9 章中讨论过。而不是 on 错误,而是从当前函数返回 error 值
供调用方处理。expect
?
panic!
?
第三,该函数现在在成功案例中返回一个值。
我们已经将函数的成功类型声明为签名
这意味着我们需要将 Unit type 值包装在 value 中。此语法乍一看可能有点奇怪,但像这样使用
表示我们正在调用其副作用的惯用方式
只;它不会返回我们需要的值。run
Ok
run
()
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 告诉我们,我们的代码忽略了 value 和 value
可能表示发生了错误。但是,我们不会检查是否或
没有出现错误,编译器提醒我们,我们可能是故意的
这里有一些错误处理代码!现在让我们纠正这个问题。Result
Result
处理从 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 let
unwrap_or_else
run
Err
process::exit(1)
run
unwrap
Config::build
Config
run
()
unwrap_or_else
()
和 函数的主体相同
两种情况:我们打印错误并退出。if let
unwrap_or_else
将代码拆分到库 crate 中
到目前为止,我们的项目看起来不错!现在我们将拆分 src/main.rs 文件并将一些代码放入 src/lib.rs 文件中。这样,我们
可以测试代码并拥有职责较少的 src/main.rs 文件。minigrep
让我们将所有不在函数中的代码从 src/main.rs 移动到 src/lib.rs:main
- 函数定义
run
- 相关声明
use
- 的定义
Config
- 函数定义
Config::build
src/lib.rs 的内容应该有示例 12-13 中所示的签名 (为简洁起见,我们省略了函数的主体)。请注意,这不会 编译,直到我们修改示例 12-14 中的 src/main.rs。
我们自由地使用了关键字: on 、 在其字段和方法上以及 on 函数。我们现在有一个 library crate,它有
我们可以测试的公共 API!pub
Config
build
run
现在我们需要将移动到 src/lib.rs 的代码放入 binary crate 的 src/main.rs 中,如示例 12-14 所示。
我们添加一行代码,从
library crate 添加到二进制 crate 的范围内,并将函数作为
替换为我们的 crate 名称。现在所有功能都应该已连接,并且应该
工作。运行程序并确保一切正常。use minigrep::Config
Config
run
cargo run
呼!这需要做很多工作,但我们已经为 前途。现在,处理错误要容易得多,并且我们使代码更加 模块 化。从现在开始,我们几乎所有的工作都将在 src/lib.rs 中完成。
让我们通过做一些可以 使用旧代码很困难,但使用新代码很容易:我们将 编写一些测试!
本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准