rust如何放置篝火_Rust提高模块化和性能关键分析

为了改进我们的程序,我们将修复与程序的结构以及如何处理潜在错误有关的四个问题。

首先,我们的main函数现在执行两项任务:它解析参数并读取文件。对于这么小的函数,这不是主要问题。但是,如果我们继续在内部扩展程序main,则该main函数处理的单独任务的数量 将增加。随着某个职能获得职责,在不破坏其功能之一的情况下,就变得更加难以推理,难以测试并且难以更改。最好将功能分开,以便每个功能负责一项任务。

这个问题也与第二个问题有关:尽管query和filename 是我们程序的配置变量,但是诸如之类contents的变量用于执行程序的逻辑。时间越长main,我们需要将更多变量纳入范围。我们范围内的变量越多,跟踪每个变量的目的就越困难。最好将配置变量分组为一个结构,以明确其用途。

第三个问题是,expect当读取文件失败时,我们通常会打印一条错误消息,但该错误消息只会打印Something went wrong reading the file。读取文件可能会以多种方式失败:例如,文件可能丢失,或者我们可能没有打开文件的权限。现在,无论情况如何,我们都会打印Something went wrong reading the file错误消息,但不会为用户提供任何信息!

第四,我们expect反复使用来处理不同的错误,并且如果用户在未指定足够参数的情况下运行我们的程序,则他们index out of bounds将从Rust中得到一个错误信息,该错误信息无法清楚地解释问题所在。最好将所有错误处理代码都放在一个地方,这样如果需要更改错误处理逻辑,将来的维护者只有一个地方可以参考代码。将所有错误处理代码都放在一个地方还将确保我们打印出对最终用户有意义的消息。

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

二进制项目的关注点分离

将多个任务分配给main功能的组织问题在许多二进制项目中很常见。结果,Rust社区开发了一种流程,用作在二进制程序main开始变大时拆分二进制程序的各个方面的准则。该过程包括以下步骤:

  • 拆分程序到main.rs和lib.rs和移动你的程序的逻辑lib.rs。
  • 只要您的命令行解析逻辑很小,它就可以保留在 main.rs中。
  • 当命令行解析逻辑开始越来越复杂,从提取它main.rs并将其移动到lib.rs。

main在此过程之后,功能中保留的职责应限于以下各项:

  • 使用参数值调用命令行解析逻辑
  • 设置任何其他配置
  • run在lib.rs中调用函数
  • 如果run返回错误,则处理错误

这种模式是关于分离关注点的:main.rs处理运行程序,而lib.rs处理手头任务的所有逻辑。由于无法main直接测试该函数,因此该结构可通过将其移至lib.rs中的函数来测试程序的所有逻辑。保留在main.rs中的唯一代码将足够小,可以通过读取代码来验证其正确性。让我们按照以下过程重做我们的程序。

提取参数解析器

我们将解析参数成函数提取的功能 main将来电的命令行解析逻辑移动准备 的src / lib.rs。清单12-5显示了main调用new函数的新起点parse_config,我们现在将在src / main.rs中定义该函数。

文件名:src / main.rs

fn main() {    let args: Vec = env::args().collect();    let (query, filename) = parse_config(&args);    // --snip--}fn parse_config(args: &[String]) -> (&str, &str) {    let query = &args[1];    let filename = &args[2];    (query, filename)}

清单12-5:parse_config从中提取函数 main

我们还在收集命令行参数到载体,但不是在索引1到可变分配参数值query,并在索引2处的参数值给变量filename的内main 函数,我们通过整个矢量的parse_config函数。parse_config然后,该 函数将保留确定哪个自变量进入哪个变量并将值传递回的逻辑main。我们仍在中创建query和filename变量main,但main不再负责确定命令行参数和变量如何对应。

对于我们的小型程序而言,这种返工似乎有些过头,但是我们正在逐步进行小型重构。进行更改后,再次运行该程序以验证参数解析仍然有效。最好经常检查进度,以帮助确定问题发生的原因。

分组配置值

我们可以采取另一小的步骤来parse_config进一步改善功能。目前,我们正在返回一个元组,但是随后我们立即将该元组再次分解为各个部分。这表明也许我们还没有正确的抽象。

这表明有改进的余地的另一个指标是config的一部分parse_config,这意味着这两个值,我们返回是相关的,是一个配置值的两个部分。除了将两个值分组为一个元组外,我们目前不在数据结构中传达此含义。我们可以将两个值放入一个结构中,并为每个结构字段赋予一个有意义的名称。这样做将使以后的代码维护人员更容易理解不同值之间的关系以及它们的用途。

注意:当复杂类型更合适时使用原始值是一种称为原始痴迷的反模式。

清单12-6显示了对该parse_config功能的改进。

文件名:src / main.rs

fn main() {    let args: Vec = env::args().collect();    let config = parse_config(&args);    println!("Searching for {}", config.query);    println!("In file {}", config.filename);    let contents = fs::read_to_string(config.filename)        .expect("Something went wrong reading the file");    // --snip--}struct Config {    query: String,    filename: String,}fn parse_config(args: &[String]) -> Config {    let query = args[1].clone();    let filename = args[2].clone();    Config { query, filename }}

清单12-6:重构parse_config以返回Config结构的实例

我们添加了一个名为define的结构,以Config具有名为query和的 字段filename。parse_config现在的签名表明它返回一个 Config值。在的正文中parse_config,我们曾经用来返回引用的String值的字符串切片,args现在我们定义Config为包含拥有的String值。该args变量main是参数值的所有者,并且只让parse_config功能借阅,这意味着我们会违反锈病的借贷规则,如果Config试图利用中的值的所有权args。

我们可以String通过多种不同的方式来管理数据,但是最简单的方法(尽管效率低下)是clone在值上调用方法。这将为Config实例拥有一个完整的数据副本,这比存储对字符串数据的引用要花费更多的时间和内存。但是,克隆数据也使我们的代码非常简单,因为我们不必管理引用的生存期。在这种情况下,为了获得简单性而放弃一些性能是一个值得权衡的选择。

使用权衡 clone

clone由于其运行时成本,许多Rustaceans中有一种避免使用它来解决所有权问题的趋势。在 第13章中,您将学习如何在这种情况下使用更有效的方法。但是现在,可以复制几个字符串以继续取得进展是可以的,因为您将只复制一次这些副本,并且文件名和查询字符串非常小。最好有一个效率低下的工作程序,而不是在初次尝试时就对代码进行超优化。随着您对Rust的使用变得越来越丰富,从最有效的解决方案开始将变得更加容易,但是现在,完全可以接受call clone。

我们已经进行了更新,main以便将Configreturn by 的实例放置parse_config到名为的变量中config,并且更新了以前使用splitquery和filenamevariables的代码,因此现在Config改为使用结构上的字段。

现在我们的代码更清晰地传达query和filename是相关的,他们的目的是要配置程序将如何工作。使用这些值的任何代码都知道会在config实例中为它们的目的而命名的字段中找到它们。

创建一个构造函数 Config

到目前为止,我们已经提取了负责解析命令行参数的逻辑main并将其放置在parse_config函数中。这样做有助于我们看到query和filename值是相关的,并且应该在我们的代码中传达这种关系。然后,我们添加了一个Config结构,以命名和的相关用途,query并filename能够从parse_config函数中将值的名称作为结构字段名称返回。

因此,既然该parse_config函数的目的是创建一个Config 实例,我们就可以parse_config从一个普通函数更改new为与该Config结构相关联的命名函数。进行此更改将使代码更加常用。我们可以在标准库中创建类型的实例,例如String通过调用String::new。同样,通过更改parse_config为与new关联的函数Config,我们将能够Config通过调用来创建的实例Config::new。清单12-7显示了我们需要进行的更改。

文件名:src / main.rs

fn main() {    let args: Vec = env::args().collect();    let config = Config::new(&args);    // --snip--}// --snip--impl Config {    fn new(args: &[String]) -> Config {        let query = args[1].clone();        let filename = args[2].clone();        Config { query, filename }    }}

清单12-7:parse_config变成 Config::new

我们已经更新了main呼叫的地方parse_config,而不是call Config::new。我们已经更改了parse_configto的名称,new并将其移至一个impl与new功能关联的代码块中Config。尝试再次编译此代码,以确保它可以工作。

修复错误处理

现在,我们将修复错误处理。回想一下,args如果向量包含少于三个项目,则尝试访问索引1或索引2的向量中的值将导致程序崩溃。尝试运行不带任何参数的程序;它看起来像这样:

$ 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.

该行index out of bounds: the len is 1 but the index is 1是发给程序员的错误消息。它不会帮助我们的最终用户了解发生了什么以及应该怎么做。让我们现在修复它。

改善错误信息

在清单12-8中,我们在new函数中添加了一个检查,该检查将在访问索引1和2之前验证切片是否足够长。如果切片不够长,则程序会出现恐慌并显示比该index out of bounds消息更好的错误消息。

文件名:src / main.rs

    // --snip--    fn new(args: &[String]) -> Config {        if args.len() < 3 {            panic!("not enough arguments");        }        // --snip--

清单12-8:添加对参数数量的检查

这段代码类似于清单9-10中编写的Guess::new函数,panic!当value参数超出有效值范围时调用该 函数。在这里,我们检查的args是至少3的长度,而函数的其余部分可以在满足此条件的假设下运行,而不是在此处检查值的范围。如果args少于三个项目,则此条件为true,我们调用[panic!]宏用以立即结束程序。

在中添加了以下几行代码后new,让我们再次运行不带任何参数的程序,以查看现在的错误情况:

$ 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 'not enough arguments', src/main.rs:26:13note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

输出更好:我们现在有一条合理的错误消息。但是,我们也有不想提供给用户的无关信息。也许使用清单9-10中使用的技术并不是此处的最佳选择:如第9章中所述panic!,对编程的调用比对用法的调用 更适合编程问题 。取而代之的是,我们可以使用您在第9章中学到的另一种技术-返回 表示成功或错误的a。Result

Result从中返回new而不是调用panic!

相反,我们可以返回一个Result值,该值将Config在成功的情况下包含一个实例,并在错误的情况下描述问题。当 Config::new被传达给main,我们可以使用Result类型的信号出现了问题。然后,我们可以进行更改main以将Err 变体转换为对我们的用户来说更实际的错误,而不会引起周围的文本thread 'main'和RUST_BACKTRACE对panic!原因的呼吁。

清单12-9显示了我们需要对的返回值进行的更改 Config::new以及返回a所需的函数主体Result。请注意,只有在我们也进行更新之前,它才会编译main,我们将在下一个清单中进行更新。

文件名:src / main.rs

impl Config {    fn new(args: &[String]) -> Result {        if args.len() < 3 {            return Err("not enough arguments");        }        let query = args[1].clone();        let filename = args[2].clone();        Ok(Config { query, filename })    }}

清单12-9:Result从 返回Config::new

new现在,我们的函数在成功情况下返回a Result,在错误情况下返回Config实例&'static str。回想一下第10章的“静态生命周期”部分 &'static str,它是字符串文字的类型,这是我们目前的错误消息类型。

我们在new函数主体中进行了两项更改:panic!现在,我们返回一个Err 值,而不是 在用户没有传递足够多的参数时调用,而是将Config返回值包装在中Ok。这些更改使函数符合其新类型签名。

Err从中返回值Config::new允许main函数处理Result从new函数返回的值,并在错误情况下更干净地退出进程。

调用Config::new和处理错误

为了处理错误情况并输出一条用户友好的消息,我们需要进行更新 main以处理Result所返回的Config::new,如清单12-10所示。我们还将负责 退出带有非零错误代码的命令行工具,panic!并手动实现它。非零退出状态是一种约定,用于向调用我们的程序的进程发出信号,告知该程序以错误状态退出。

文件名:src / main.rs

use std::process;fn main() {    let args: Vec = env::args().collect();    let config = Config::new(&args).unwrap_or_else(|err| {        println!("Problem parsing arguments: {}", err);        process::exit(1);    });    // --snip--

清单12-10:如果创建新Config失败失败,则返回错误代码

在此清单中,我们使用了以前没有涉及的方法: unwrap_or_else,它是Result由标准库定义的。使用unwrap_or_else允许我们定义一些自定义的,非panic!错误的处理。如果Result是一个Ok值,则此方法的行为类似于unwrap:它返回Ok包装的内部值。但是,如果是一个Err值,则此方法将调用闭包中的代码,该闭包是我们定义的匿名函数,并作为参数传递给unwrap_or_else。我们将在第13章中更详细地介绍闭包。现在,您只需要知道unwrap_or_else将会传递的内部值 Err,在这种情况下,它是静态字符串not enough arguments我们在清单12-9中添加的内容,添加到了在err垂直管道之间出现的参数的闭包中。然后,闭包中的代码可以err 在运行时使用该值。

我们添加了新use行,以将process标准库纳入范围。在错误情况下将运行的闭包

d9b817239c9c0d71fa7139ad641f102a.png

中的代码只有两行:我们打印err值,然后调用process::exit。该 process::exit函数将立即停止程序,并返回作为退出状态代码传递的数字。这类似于panic!清单12-8中使用的 基于-的处理,但是我们不再获得所有多余的输出。让我们尝试一下:

$ cargo run   Compiling minigrep v0.1.0 (file:///projects/minigrep)    Finished dev [unoptimized + debuginfo] target(s) in 0.48s     Running `target/debug/minigrep`Problem parsing arguments: not enough arguments

大!对于我们的用户而言,此输出更加友好。

从中提取逻辑 main

现在,我们已经完成了对配置解析的重构,现在让我们来看程序的逻辑。如我们在“二进制项目的关注点分离”中所述,我们将提取一个名为run的main函数,该函数将保留函数中当前与设置配置或处理错误无关的所有逻辑 。完成后,main将变得简洁明了且易于通过检查进行验证,并且我们将能够为所有其他逻辑编写测试。

清单12-11显示了提取的run函数。目前,我们只是在提取函数方面进行了较小的增量改进。我们仍在src / main.rs中定义函数。

文件名:src / main.rs

fn main() {    // --snip--    println!("Searching for {}", config.query);    println!("In file {}", config.filename);    run(config);}fn run(config: Config) {    let contents = fs::read_to_string(config.filename)        .expect("Something went wrong reading the file");    println!("With text:{}", contents);}// --snip--

清单12-11:提取run包含其余程序逻辑的函数

从读取文件开始,该run函数现在包含中的所有剩余逻辑main。该run函数将Config实例作为参数。

从run函数返回错误

将其余的程序逻辑分离到run函数中,我们可以像Config::new清单12-9中那样改进错误处理。当出现问题时expect,run 函数将返回a ,而不是通过调用使程序惊慌Result。这将使我们进一步main以用户友好的方式整合到处理错误的逻辑中。清单12-12显示了我们需要对的签名和主体进行的更改run。

文件名:src / main.rs

use std::error::Error;// --snip--fn run(config: Config) -> Result> {    let contents = fs::read_to_string(config.filename)?;    println!("With text:{}", contents);    Ok(())}

清单12-12:将run函数更改为返回 Result

我们在这里进行了三项重大更改。首先,我们将run函数的返回类型更改为Result>。该函数先前返回的单位类型为,()我们将其保留为Ok案例中返回的值 。

对于错误类型,我们使用了trait对象 Box(并且我们在std::error::Error范围内use的顶部有一条语句)。我们将在第17章介绍特征对象。现在,只知道这Box意味着函数将返回实现该Error特征的类型,但是我们不必指定返回值将是哪种特定类型。这使我们可以灵活地返回在不同错误情况下可能属于不同类型的错误值。该dyn关键字是短期的“动态的”。

第二,如第9章所述,我们删除了对expect支持?操作符的调用。而不是 出错,将从当前函数返回错误值,以供调用者处理。panic!?

第三,该run函数现在Ok在成功情况下返回一个值。我们已经在签名中声明了run函数的成功类型(),这意味着我们需要将单元类型的值包装在Ok值中。这种Ok(()) 语法起初可能看起来有些奇怪,但是使用()这种用法是惯用的方式,表明我们仅在呼吁run其副作用。它不会返回我们需要的值。

运行此代码时,它将编译但会显示警告:

$ cargo run the poem.txt   Compiling minigrep v0.1.0 (file:///projects/minigrep)warning: unused `std::result::Result` that must be used  --> src/main.rs:19:5   |19 |     run(config);   |     ^^^^^^^^^^^^   |   = note: `#[warn(unused_must_use)]` on by default   = note: this `Result` may be an `Err` variant, which should be handled    Finished dev [unoptimized + debuginfo] target(s) in 0.71s     Running `target/debug/minigrep the poem.txt`Searching for theIn file poem.txtWith 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 frogTo tell your name the livelong dayTo an admiring bog!

Rust告诉我们,我们的代码忽略了该Result值,并且该Result值可能表示发生了错误。但是我们并没有检查是否有错误,编译器提醒我们,我们可能打算在这里添加一些错误处理代码!现在让我们纠正这个问题。

3f9ff92555a0d5d9a62a01b90ca6417e.png

处理错误返回从run在main

我们将检查错误并使用与Config::new清单12-10中所使用的类似的技术来处理它们,但有一点点不同:

文件名:src / main.rs

fn main() {    // --snip--    println!("Searching for {}", config.query);    println!("In file {}", config.filename);    if let Err(e) = run(config) {        println!("Application error: {}", e);        process::exit(1);    }}

我们使用if let而不是unwrap_or_else检查是否run返回Err值,然后调用返回 值process::exit(1)。该run函数不会以 返回实例unwrap的相同方式Config::new返回我们想要的值Config。因为在成功情况下run返回(),所以我们只关心检测错误,因此我们不需要unwrap_or_else返回展开的值,因为它只会是()。

在这两种情况下,if let和unwrap_or_else函数的主体都是相同的:我们输出错误并退出。

将代码拆分为库箱

minigrep到目前为止,我们的项目看起来不错!现在,我们将分割 src / main.rs文件,并将一些代码放入src / lib.rs文件中,以便我们对其进行测试,并拥有一个责任更少的src / main.rs文件。

让我们将不是main函数的所有代码从src / main.rs移到 src / lib.rs:

  • 该run函数定义
  • 相关use声明
  • 的定义 Config
  • 该Config::new函数定义

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 filename: String,}impl Config {    pub fn new(args: &[String]) -> Result {        // --snip--    }}pub fn run(config: Config) -> Result> {    // --snip--}

清单12-13:移动Config和run成 的src / lib.rs

我们在以下字段中广泛使用了pub关键字:on Config,其字段, new方法以及run函数。现在,我们有了一个带有公共API的库箱,我们可以对其进行测试!

现在,我们需要将移至src / lib.rs的代码带入src / main.rs中的二进制板条箱的范围,如清单12-14所示。

文件名:src / main.rs

use std::env;use std::process;use minigrep::Config;fn main() {    // --snip--    if let Err(e) = minigrep::run(config) {        // --snip--    }}

清单12-14:minigrep在src / main.rs中使用库箱

我们添加use minigrep::Config一行以将Config库箱中的类型带入二进制箱的作用域,并在run函数前添加箱名。现在,所有功能都应该已连接并且可以正常工作。使用运行程序,cargo run并确保一切正常。

ew!那是很多工作,但是我们为将来的成功做好了准备。现在,更容易处理错误,并且我们使代码更具模块化。从现在开始,几乎所有工作都将在src / lib.rs中完成。

让我们利用这种新发现的模块性,通过做一些旧代码很难实现但新代码容易实现的事情:我们将编写一些测试!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值