?博主现有专栏:
C51单片机(STC89C516),c语言,c++,离散数学,算法设计与分析,数据结构,Python,Java基础,MySQL,linux,基于HTML5的网页设计及应用,Rust(官方文档重点总结),jQuery,前端vue.js,Javaweb开发,Python机器学习等
?主页链接:
Y小夜-CSDN博客
目录
?接收命令行程序
?读取参数
?将参数值保存进变量
?读取文件
?重构
?二进制项目的关注分离
?提取参数解析器
?组合配置值
?修复错误处理
✨改善错误信息
?从new中返回一个Result
?调用config::build并处理错误
?从main提取逻辑
?从run函数中返回错误
?处理main中run返回的错误
?将代码拆分到库crate
?接收命令行程序
使用 cargo new
新建一个项目,我们称之为 minigrep
以便与可能已经安装在系统上的 grep
工具相区别:
$ cargo new minigrep Created binary (application) `minigrep` project$ cd minigrep
第一个任务是让 minigrep
能够接受两个命令行参数:文件路径和要搜索的字符串。也就是说我们希望能够使用 cargo run
、要搜索的字符串和被搜索的文件的路径来运行程序,像这样:
$ cargo run -- searchstring example-filename.txt
?读取参数
为了确保 minigrep
能够获取传递给它的命令行参数的值,我们需要一个 Rust 标准库提供的函数 std::env::args
。这个函数返回一个传递给程序的命令行参数的 迭代器(iterator)。
use std::env;fn main() { let args: Vec<String> = env::args().collect(); dbg!(args);}
首先使用 use
语句来将 std::env
模块引入作用域以便可以使用它的 args
函数。注意 std::env::args
函数被嵌套进了两层模块中。
提醒: std::env::args
在其任何参数包含无效 Unicode 字符时会 panic。如果你需要接受包含无效 Unicode 字符的参数,使用 std::env::args_os
代替。这个函数返回 OsString
值而不是 String
值。这里出于简单考虑使用了 std::env::args
,因为 OsString
值每个平台都不一样而且比 String
值处理起来更为复杂。
?将参数值保存进变量
目前程序可以访问指定为命令行参数的值。现在需要将这两个参数的值保存进变量这样就可以在程序的余下部分使用这些值了。
use std::env;fn main() { let args: Vec<String> = env::args().collect(); let query = &args[1]; let file_path = &args[2]; println!("Searching for {}", query); println!("In file {}", file_path);}
正如之前打印出 vector 时所所看到的,程序的名称占据了 vector 的第一个值 args[0]
,所以我们从索引为 1
的参数开始。minigrep
获取的第一个参数是需要搜索的字符串,所以将其将第一个参数的引用存放在变量 query
中。第二个参数将是文件路径,所以将第二个参数的引用放入变量 file_path
中。
?读取文件
现在我们要增加读取由 file_path
命令行参数指定的文件的功能。首先,需要一个用来测试的示例文件:我们会用一个拥有多行少量文本且有一些重复单词的文件。
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 frogTo tell your name the livelong dayTo an admiring bog!
use std::env;use std::fs;fn main() { // --snip-- 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}");}
首先,我们增加了一个 use
语句来引入标准库中的相关部分:我们需要 std::fs
来处理文件。
在 main
中新增了一行语句:fs::read_to_string
接受 file_path
,打开文件,接着返回包含其内容的 std::io::Result<String>
。
在这些代码之后,我们再次增加了临时的 println!
打印出读取文件之后 contents
的值,这样就可以检查目前为止的程序能否工作。
?重构
为了改善我们的程序这里有四个问题需要修复,而且它们都与程序的组织方式和如何处理潜在错误有关。
第一,main
现在进行了两个任务:它解析了参数并打开了文件。对于一个这样的小函数,这并不是一个大问题。然而如果 main
中的功能持续增加,main
函数处理的独立任务也会增加。当函数承担了更多责任,它就更难以推导,更难以测试,并且更难以在不破坏其他部分的情况下做出修改。最好能分离出功能以便每个函数就负责一个任务。
这同时也关系到第二个问题:query
和 file_path
是程序中的配置变量,而像 contents
则用来执行程序逻辑。随着 main
函数的增长,就需要引入更多的变量到作用域中,而当作用域中有更多的变量时,将更难以追踪每个变量的目的。最好能将配置变量组织进一个结构,这样就能使它们的目的更明确了。
第三个问题是如果打开文件失败我们使用 expect
来打印出错误信息,不过这个错误信息只是说 Should have been able to read the file
。读取文件失败的原因有多种:例如文件不存在,或者没有打开此文件的权限。目前,无论处于何种情况,我们只是打印出“文件读取出现错误”的信息,这并没有给予使用者具体的信息!
第四,我们不停地使用 expect
来处理不同的错误,如果用户没有指定足够的参数来运行程序,他们会从 Rust 得到 index out of bounds
错误,而这并不能明确地解释问题。如果所有的错误处理都位于一处,这样将来的维护者在需要修改错误处理逻辑时就只需要考虑这一处代码。将所有的错误处理都放在一处也有助于确保我们打印的错误信息对终端用户来说是有意义的。
?二进制项目的关注分离
main
函数负责多个任务的组织问题在许多二进制项目中很常见。所以 Rust 社区开发出一类在 main
函数开始变得庞大时进行二进制程序的关注分离的指导。这些过程有如下步骤:
经过这些过程之后保留在 main
函数中的责任应该被限制为:
run
函数如果 run
返回错误,则处理这个错误 这个模式的一切就是为了关注分离:main.rs 处理程序运行,而 lib.rs 处理所有的真正的任务逻辑。因为不能直接测试 main
函数,这个结构通过将所有的程序逻辑移动到 lib.rs 的函数中使得我们可以测试它们。仅仅保留在 main.rs 中的代码将足够小以便阅读就可以验证其正确性。
?提取参数解析器
首先,我们将解析参数的功能提取到一个 main
将会调用的函数中,为将命令行解析逻辑移动到 src/lib.rs 中做准备。示例中展示了新 main
函数的开头,它调用了新函数 parse_config
。目前它仍将定义在 src/main.rs 中:
fn main() { let args: Vec<String> = env::args().collect(); let (query, file_path) = parse_config(&args); // --snip--}fn parse_config(args: &[String]) -> (&str, &str) { let query = &args[1]; let file_path = &args[2]; (query, file_path)}
我们仍然将命令行参数收集进一个 vector,不过不同于在 main
函数中将索引 1 的参数值赋值给变量 query
和将索引 2 的值赋值给变量 file_path
,我们将整个 vector 传递给 parse_config
函数。接着 parse_config
函数将包含决定哪个参数该放入哪个变量的逻辑,并将这些值返回到 main
。仍然在 main
中创建变量 query
和 file_path
,不过 main
不再负责处理命令行参数与变量如何对应。
?组合配置值
我们可以采取另一个小的步骤来进一步改善这个函数。现在函数返回一个元组,不过立刻又将元组拆成了独立的部分。这是一个我们可能没有进行正确抽象的信号。
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--}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 }}
新定义的结构体 Config
中包含字段 query
和 file_path
。 parse_config
的签名表明它现在返回一个 Config
值。在之前的 parse_config
函数体中,我们返回了引用 args
中 String
值的字符串 slice,现在我们定义 Config
来包含拥有所有权的 String
值。main
中的 args
变量是参数值的所有者并只允许 parse_config
函数借用它们,这意味着如果 Config
尝试获取 args
中值的所有权将违反 Rust 的借用规则。
?修复错误处理
现在我们开始修复错误处理。回忆一下之前提到过如果 args
vector 包含少于 3 个项并尝试访问 vector 中索引 1
或索引 2
的值会造成程序 panic。尝试不带任何参数运行程序;这将看起来像这样:
$ cargo run Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep`thread 'main' panicked at 'index out of bounds: the len is 1 but the index is 1', src/main.rs:27:21note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
✨改善错误信息
在 new
函数中增加了一个检查在访问索引 1
和 2
之前检查 slice 是否足够长。如果 slice 不够长,程序会打印一个更好的错误信息并 panic:
// --snip-- fn new(args: &[String]) -> Config { if args.len() < 3 { panic!("not enough arguments"); } // --snip--
这个输出就好多了,现在有了一个合理的错误信息。然而,还是有一堆额外的信息我们不希望提供给用户。
?从new中返回一个Result
我们可以选择返回一个 Result
值,它在成功时会包含一个 Config
的实例,而在错误时会描述问题。我们还将把函数名从new
改为build
,因为许多程序员希望 new
函数永远不会失败。当 Config::new
与 main
交流时,可以使用 Result
类型来表明这里存在问题。接着修改 main
将 Err
成员转换为对用户更友好的错误,而不是 panic!
调用产生的关于 thread 'main'
和 RUST_BACKTRACE
的文本。
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 }) }}
build
函数体中有两处修改:当没有足够参数时不再调用 panic!
,而是返回 Err
值。同时我们将 Config
返回值包装进 Ok
成员中。这些修改使得函数符合其新的类型签名。
通过让 Config::build
返回一个 Err
值,这就允许 main
函数处理 build
函数返回的 Result
值并在出现错误的情况更明确的结束进程。
?调用config::build并处理错误
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--
在上面的示例中,使用了一个之前没有详细说明的方法:unwrap_or_else
,它定义于标准库的 Result<T, E>
上。使用 unwrap_or_else
可以进行一些自定义的非 panic!
的错误处理。当 Result
是 Ok
时,这个方法的行为类似于 unwrap
:它返回 Ok
内部封装的值。然而,当其值是 Err
时,该方法会调用一个 闭包(closure),也就是一个我们定义的作为参数传递给 unwrap_or_else
的匿名函数。
我们新增了一个 use
行来从标准库中导入 process
。在错误的情况闭包中将被运行的代码只有两行:我们打印出了 err
值,接着调用了 std::process::exit
。process::exit
会立即停止程序并将传递给它的数字作为退出状态码。这类似于示例 12-8 中使用的基于 panic!
的错误处理,除了不会再得到所有的额外输出了。
?从main提取逻辑
现在我们完成了配置解析的重构:让我们转向程序的逻辑。
fn main() { // --snip-- 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--
现在 run
函数包含了 main
中从读取文件开始的剩余的所有逻辑。run
函数获取一个 Config
实例作为参数。
?从run函数中返回错误
通过将剩余的逻辑分离进 run
函数而不是留在 main
中,
use std::error::Error;// --snip--fn run(config: Config) -> Result<(), Box<dyn Error>> { let contents = fs::read_to_string(config.file_path)?; println!("With text:\n{contents}"); Ok(())}
这里我们做出了三个明显的修改。首先,将 run
函数的返回类型变为 Result<(), Box<dyn Error>>
。之前这个函数返回 unit 类型 ()
,现在它仍然保持作为 Ok
时的返回值。
对于错误类型,使用了 trait 对象 Box<dyn Error>
(在开头使用了 use
语句将 std::error::Error
引入作用域)。
第二个改变是去掉了 expect
调用并替换为 第九章 讲到的 ?
。不同于遇到错误就 panic!
,?
会从函数中返回错误值并让调用者来处理它。
第三个修改是现在成功时这个函数会返回一个 Ok
值。因为 run
函数签名中声明成功类型返回值是 ()
,这意味着需要将 unit 类型值包装进 Ok
值中。Ok(())
一开始看起来有点奇怪,不过这样使用 ()
是惯用的做法,表明调用 run
函数只是为了它的副作用;函数并没有返回什么有意义的值。
?处理main中run返回的错误
fn main() { // --snip-- println!("Searching for {}", config.query); println!("In file {}", config.file_path); if let Err(e) = run(config) { println!("Application error: {e}"); process::exit(1); }}
我们使用 if let
来检查 run
是否返回一个 Err
值,不同于 unwrap_or_else
,并在出错时调用 process::exit(1)
。run
并不返回像 Config::build
返回的 Config
实例那样需要 unwrap
的值。因为 run
在成功时返回 ()
,而我们只关心检测错误,所以并不需要 unwrap_or_else
来返回未封装的值,因为它只会是 ()
。
?将代码拆分到库crate
现在我们的 minigrep
项目看起来好多了!现在我们将要拆分 src/main.rs 并将一些代码放入 src/lib.rs,这样就能测试它们并拥有一个含有更少功能的 main
函数。
让我们将所有不是 main
函数的代码从 src/main.rs 移动到新文件 src/lib.rs 中:
run
函数定义相关的 use
语句Config
的定义Config::build
函数定义 现在 src/lib.rs 的内容应该看起来像示例 12-13(为了简洁省略了函数体)。注意直到下一个示例修改完 src/main.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-- }}pub fn run(config: Config) -> Result<(), Box<dyn Error>> { // --snip--}
这里使用了公有的 pub
关键字:在 Config
、其字段和其 build
方法,以及 run
函数上。现在我们有了一个拥有可以测试的公有 API 的库 crate 了。
现在需要在 src/main.rs 中将移动到 src/lib.rs 的代码引入二进制 crate 的作用域中
use std::env;use std::process;use minigrep::Config;fn main() { // --snip-- if let Err(e) = minigrep::run(config) { // --snip-- }}
我们添加了一行 use minigrep::Config
,它将 Config
类型引入作用域,并使用 crate 名称作为 run
函数的前缀。通过这些重构,所有功能应该能够联系在一起并运行了。运行 cargo run
来确保一切都正确的衔接在一起。