【Rust自学】12.4. 重构 Pt.2:错误处理

12.4.0. 写在正文之前

第12章要做一个实例的项目------一个命令行程序。这个程序是一个grep(Global Regular Expression Print ),是一个全局正则搜索和输出的工具。它的功能是在指定的文件中搜索出指定的文字。

这个项目分为这么几步:

  • 接收命令行参数
  • 读取文件
  • 重构:改进模块和错误处理(本文)
  • 使用TDD(测试驱动开发)开发库功能
  • 使用环境变量
  • 将错误信息写入标准错误而不是标准输出

喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=^・ω・^=)

12.4.1. 回顾

上一节中为了模块化我们为变量创建了结构体,还把读取指令的函数独立出去改成了结构体的方法。以下是截止到上一篇文章所写出的所有代码:

rust 复制代码
use std::env;  
use std::fs;  
  
struct Config {  
    query: String,  
    filename: String,  
}  
  
fn main() {  
    let args:Vec<String> = env::args().collect();  
    let config = Config::new(&args);  
  
    let contents = fs::read_to_string(config.filename)  
        .expect("Somthing went wrong while reading the file");  
    println!("With text:\n{}", contents);  
}  
  
impl Config {  
    fn new(args: &[String]) -> Config {  
        let query = args[1].clone();  
        let filename = args[2].clone();  
        Config {  
            query,  
            filename,  
        }  
    }  
}

12.4.2. 意料之外的输入

这个程序能正确运行的前提是用户输入的输入无误,那我们试试不带参数的输入会引发什么:

$ 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 bound"索引越界,作为程序编写者的我们明白这是因为参数不够导师程序在使用索引获取参数时越界触发恐慌。但是作为用户就不可能看懂这个报错信息,无法纠正错误。

这一篇文章要做的就是让程序产生的错误信息易于理解。

12.4.3. 指定报错信息

让用户理解报错信息的方式就是自己指定一个报错信息。刚刚的例子是在运行Config::new时索引越界,所以我们就修改这个地方:

rust 复制代码
impl Config {  
    fn new(args: &[String]) -> Config {  
        if args.len() < 3 {  
            panic!("Not enough arguments");  
        }  
        let query = args[1].clone();  
        let filename = args[2].clone();  
        Config {  
            query,  
            filename,  
        }  
    }  
}

如果args的元素数量小于三就发生恐慌打印"Not enough arguments"来提示用户输入的参数太少了。

再试试不带参数的输入:

$ 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

这一次的错误信息比上一次就好很多了。

但是它仍然残留了一些其他的信息,比如"thread 'main' panicked at src/main.rs:26:13:"和"note: run with RUST_BACKTRACE=1 environment variable to display a backtrace",这些内容是给程序员看的不是给用户看的。所以这些信息也得去掉。

12.4.4. 使用Result类型

panic!适用于程序本身出现问题时的恐慌,而这里却少参数的输入是程序使用时的问题,针对这种问题,使用Result类型来传播错误(这部分的内容详见 9.2. Result枚举与可恢复的错误 Pt.1 & Pt.2)才是最优解:

rust 复制代码
impl Config {  
    fn new(args: &[String]) -> Result<Config, &'static str> {  
        if args.len() < 3 {  
            return Err("Not enough arguments");  
        }  
        let query = args[1].clone();  
        let filename = args[2].clone();  
        Ok(Config { query, filename})  
    }  
}
  • 报错的信息需要用Err包裹,成功的返回值需要用Ok包裹
  • Result类型的Ok返回Config实例,Err返回&str字符串字面值,但是编译器不知道这个&str是从哪里来的以及它的生命周期有多长,所以得带生命周期,我们需要它在程序运行时始终保持有效,写成&'static str这个静态生命周期。

new函数的返回值都变了,main函数里接收值的逻辑也得变:

rust 复制代码
let config = Config::new(&args).unwrap_or_else(|err| {  
    println!("Problem parsing arguments: {}", err);  
    process::exit(1);  
});

unwrap_or_else这个方法会接收Result类型,如果是Ok,就会把Ok附带的值直接返回赋给变量,类似于unwrap;如果是Err,那么这个方法会调用一个闭包(closure)。

闭包是我们定义的匿名函数,并将其作为参数传递给unwrap_or_else。其写法是两个管道符||,在中间放变量名,相当于一个参数,这里就放了err,这个err可以在闭包的函数体内被调用,比如在打印错误时就使用了err

然后使用标准库的process::exit这个函数,使用前记得先导入一下:use std::process;,如果调用exit函数,程序的执行就会立即终止,而其参数,也就是示例代码中的1就作为程序退出时的状态码,这样显示到println!("Problem parsing arguments: {}", err);之后程序就会终止,自然就不会有比如"thread 'main' panicked at src/main.rs:26:13:"和"note: run with RUST_BACKTRACE=1 environment variable to display a backtrace"这些内容。

试一下:

$ 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

闭包这个概念在下一章才会讲到,这里没看懂也没关系,只要了解个大概即可。

12.4.5. 整体代码

以下是截止到这篇文章写出的所有代码:

rust 复制代码
use std::env;  
use std::fs;  
use std::process;  
  
struct Config {  
    query: String,  
    filename: String,  
}  
  
fn main() {  
    let args:Vec<String> = env::args().collect();  
    let config = Config::new(&args).unwrap_or_else(|err| {  
        println!("Problem parsing arguments: {}", err);  
        process::exit(1);  
    });  
  
    let contents = fs::read_to_string(config.filename)  
        .expect("Somthing went wrong while reading the file");  
    println!("With text:\n{}", contents);  
}  
  
impl Config {  
    fn new(args: &[String]) -> Result<Config, &'static str> {  
        if args.len() < 3 {  
            return Err("Not enough arguments");  
        }  
        let query = args[1].clone();  
        let filename = args[2].clone();  
        Ok(Config { query, filename})  
    }  
}
相关推荐
YYHYJX1 分钟前
C#学习笔记 --- 简单应用
开发语言·学习·c#
Clockwiseee4 分钟前
JAVA多线程学习
java·开发语言·学习
Nobita Chen5 分钟前
Python实现windows自动关机
开发语言·windows·python
码路刺客6 分钟前
一学就废|Python基础碎片,OS模块
开发语言·python
z千鑫13 分钟前
【Python】Python之Selenium基础教程+实战demo:提升你的测试+测试数据构造的效率!
开发语言·python·selenium
aiee19 分钟前
GO通过SMTP协议发送邮件
开发语言·后端·golang
云浩舟30 分钟前
Golang并发读取json文件数据并写入oracle数据库的项目实践
开发语言·数据库·golang
walkskyer36 分钟前
Golang strconv包详解:高效类型转换实战
android·开发语言·golang
我命由我1234540 分钟前
Android Room 构建问题:There are multiple good constructors
android·开发语言·java-ee·android studio·android jetpack·android-studio·android runtime
JINGWHALE11 小时前
设计模式 行为型 备忘录模式(Memento Pattern)与 常见技术框架应用 解析
前端·人工智能·后端·设计模式·性能优化·系统架构·备忘录模式