【Rust】——项目实例:——命令行实例(一)
Y小夜 2024-06-22 08:35:01 阅读 93
💻博主现有专栏:
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
学习推荐:
在当今这个飞速发展的信息时代,人工智能(AI)已经成为了一个不可或缺的技术力量,它正在逐步改变着我们的生活、工作乃至整个社会的运作方式。从智能语音助手到自动驾驶汽车,从精准医疗到智慧城市,人工智能的应用已经渗透到了我们生活的方方面面。因此,学习和掌握人工智能相关的知识和技能,对于任何希望在这个时代保持竞争力的个人来说,都已经变得至关重要。
然而,人工智能是一个涉及数学、计算机科学、数据科学、机器学习、神经网络等多个领域的交叉学科,其学习曲线相对陡峭,对初学者来说可能会有一定的挑战性。幸运的是,随着互联网教育资源的丰富,现在有大量优秀的在线平台和网站提供了丰富的人工智能学习材料,包括视频教程、互动课程、实战项目等,这些资源无疑为学习者打开了一扇通往人工智能世界的大门。
前些天发现了一个巨牛的人工智能学习网站:前言 – 人工智能教程通俗易懂,风趣幽默,忍不住分享一下给大家。
🎯接收命令行程序
使用
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.rs 和 lib.rs 并将程序的逻辑放入 lib.rs 中。当命令行解析逻辑比较小时,可以保留在 main.rs 中。当命令行解析开始变得复杂时,也同样将其从 main.rs 提取到 lib.rs 中。
经过这些过程之后保留在
main
函数中的责任应该被限制为:使用参数值调用命令行解析逻辑设置任何其他的配置调用 lib.rs 中的
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
来确保一切都正确的衔接在一起。
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。