Rust基础拾遗--错误处理、create与模块、宏

本文以笔记形式汇总Rust相关知识。介绍了Rust的错误处理,包括panic和Result两种方式,以及如何捕捉、传播和报告错误等。还阐述了宏的使用,如宏基础、内置宏、调试宏等,通过构建json!宏展示开发过程,并给出避免匹配语法错误的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


前言

   通过Rust程序设计-第二版笔记的形式对Rust相关重点知识进行汇总,读者通读此系列文章就可以轻松的把该语言基础捡起来。


1.错误处理

Rust 中的两类错误处理:panic 和 Result。

  • 普通错误使用 Result 类型来处理。Result 通常用以表示由程序外部的事物引发的错误,比如错误的输入、网络中断或权限问题。
  • panic 针对的是另一种错误,即那种永远不应该发生的错误。

1.1 panic

当程序遇到下列问题的时候,就可以断定程序自身存在 bug,故而会引发 panic:

  • 数组越界访问;
  • 整数除以 0;
  • 在恰好为 Err 的 Result 上调用 .expect();
  • 断言失败。

panic!() 是一种宏,用于处理程序中出现错误的情况。

如果panic真的发生了,那么该怎么办呢?
Rust 为你提供了一种选择。Rust 既可以在发生 panic 时展开调用栈,也可以中止进程。

展开调用栈

panic 是安全的,没有违反 Rust 的任何安全规则,即使你故意在标准库方法的中间引发 panic,它也永远不会在内存中留下悬空指针或半初始化的值。Rust 的设计理念是要在出现任何意外之前捕获诸如无效数组访问之类的错误。继续往下执行显然是不安全的,所以 Rust 会展开这个调用栈。但是进程的其余部分可以继续运行。

panic 是基于线程的。一个线程 panic 时,其他线程可以继续做自己的事。

为了使程序更加健壮,可以使用线程和 catch_unwind() 来处理 panic。

中止

如果 Rust 在试图清理第一个 panic 时,.drop() 方法触发了第二个 panic,那么这个 panic 就是致命的。Rust 会停止展开调用栈并中止整个进程。

Result

Rust 中没有异常。相反,函数执行失败时会有像下面这样的返回类型:

fn get_weather(location: LatLng) -> Result<WeatherReport, io::Error>

Result 类型会指示出可能的失败。当我们调用 get_weather() 函数时,它要么返回一个成功结果 Ok(weather),其中的 weather 是一个新的 WeatherReport 值;要么返回一个错误结果 Err(error_value),其中的 error_value 是一个 io::Error,用来解释出了什么问题。

每当调用此函数时,Rust 都会要求我们编写某种错误处理代码。如果不对 Result 执行某些操作,就无法获取 WeatherReport;如果未使用 Result 值,就会收到编译器警告。

本章将采用类似“食谱”的方式并专注于使用 Result 来实现你期望的错误处理行为。你将了解如何捕获错误、传播错误和报告错误,以及关于组织和使用 Result 类型的常见模式。

捕捉错误

Result 最彻底的处理方式:使用 match 表达式。

match get_weather(hometown) {
   
   
    Ok(report) => {
   
   
        display_weather(hometown, &report);
    }
    Err(err) => {
   
   
        println!("error querying the weather: {}", err);
        schedule_weather_retry();
    }
}

这相当于其他语言中的 try/catch。如果想直接处理错误而不是将错误传给调用者,就可以使用这种方式。

match 有点儿冗长,因此 Result 针对一些常见的特定场景提供了多个有用的方法,每个方法在其实现中都有一个 match 表达式。

返回一个 bool,告知此结果是成功了还是出错了。result.ok()(成功值)以 Option 类型返回成功值(如果有的话)。如果 result 是成功的结果,就返回 Some(success_value);否则,返回 None,并丢弃错误值。result.err()(错误值)以 Option 类型返回错误值(如果有的话)。result.unwrap_or(fallback)(解包或回退值)

如果 result 为成功结果,就返回成功值;否则,返回 fallback,丢弃错误值。

// 对南加州而言,这是一则十拿九稳的天气预报
const THE_USUAL: WeatherReport = WeatherReport::Sunny(72);

// 如果可能,就获取真实的天气预报;如果不行,就回退到常见状态
let report = get_weather(los_angeles).unwrap_or(THE_USUAL);
display_weather(los_angeles, &report);

这是 .ok() 的一个很好的替代方法,因为返回类型是 T,而不是 Option。当然,只有存在合适的回退值时,才能用这个方法。result.unwrap_or_else(fallback_fn)(解包,否则调用)

这个方法也一样,但不会直接传入回退值,而是传入一个函数或闭包。它针对的是大概率不会用到回退值且计算回退值会造成浪费的情况。只有在得到错误结果时才会调用 fallback_fn。

let report = get_weather(hometown) .unwrap_or_else(|_err| vague_prediction(hometown));

最后这两个方法之所以有用,是因为前面列出的所有其他方法,除了 .is_ok() 和 .is_err(),都在消耗 result。也就是说,它们会按值接受 self 参数。有时在不破坏 result 的情况下访问 result 中的数据是非常方便的,这就是 .as_ref() 和 .as_mut() 的用武之地。假设你想调用 result.ok(),但要让 result 保持不可变状态,那么就可以写成 result.as_ref().ok(),它只会借用 result,返回 Option<&T> 而非 Option。

Result错误别名

打印错误

传播错误

处理多种Error类型

处理“不可能发生”的错误

有时我们明确知道某个错误不可能发生。假设我们正在编写代码来解析配置文件,并且确信文件中接下来的内容肯定是一串数字:

if next_char.is_digit(10) {
   
   
    let start = current_index;
    current_index = skip_digits( &line, current_index);
    let digits = & line[start..current_index];
    ...
}

我们想将这个数字串转换为实际的数值。有一个标准方法可以做到这一点:

let num = digits.parse::();

现在的问题是:str.parse:😦) 方法不返回 u64,而是返回了一个 Result。转换可能会失败,因为某些字符串不是数值:

"bleen".parse::() // ParseIntError: 无效的数字

但我们碰巧知道,在这种情况下,digits 一定完全由数字组成。那么应该怎么办呢?如果我们正在编写的代码已经返回了 GenericResult,那么就可以添加一个 ?,并且忽略这个错误。否则,我们将不得不为处理不可能发生的错误而烦恼。最好的选择是使用 Result 的 .unwrap() 方法。如果结果是 Err,就会 panic;但如果成功了,则会直接返回 Ok 中的成功值:

let num = digits.parse::().unwrap();

这和 ? 的用法很相似,但如果我们对这个错误有没有可能发生的理解是错误的,也就是说如果它其实有可能发生,那么这种情况就会报 panic。

事实上,对于刚才这个例子,我们确实理解错了。如果输入中包含足够长的数字串,则这个数值会因为太大而无法放入 u64 中:

"99999999999999999999".parse::() // 溢出错误

因此,在这种特殊情况下使用 .unwrap() 存在 bug。这种有 bug 的输入本不应该引发 panic。

话又说回来,确实会出现 Result 值不可能是错误的情况。例如,在第 18 章中,你会看到 Write 特型为文本和二进制输出定义了一组泛型方法(.write() 等)。所有这些方法都会返回 io::Result,但如果你碰巧正在写入 Vec,那么它们就不可能失败。在这种情况下,可以使用 .unwrap() 或 .expect(message) 来简化 Result 的处理。

当错误表明情况相当严重或异乎寻常,理当用 panic 对它进行处理时,这些方法也很有用:

fn print_file_age(filename: &Path, last_modified: SystemTime) {
   
   
    let age = last_modified.elapsed().expect("system clock drift");
    ...
}

在这里,仅当系统时间早于文件创建时间时,.elapsed() 方法才会失败。如果文件是最近创建的,并且在程序运行期间系统时钟往回调整过,就会发生这种情况。根据这段代码的使用方式,在这种情况下,调用 panic 是一个合理的选择,而不必处理该错误或将该错误传播给调用者。

处理main() 中的错误

在大多数生成 Result 的地方,让错误冒泡到调用者通常是正确的行为。这就是为什么 ? 在 Rust 中会设计成单字符语法。正如我们所见,在某些程序中,它曾连续用于多行代码。但是,如果你传播错误的距离足够远,那么最终它就会抵达 main(),后者必须对其进行处理。通常来说,main() 不能使用 ?,因为它的返回类型不是 Result:处理 main() 中错误的最简单方式是使用 .expect():

fn main() {
   
   
    calculate_tides().expect("error"); // 责任止于此
}

如果 calculate_tides() 返回错误结果,那么 .expect() 方法就会 panic。主线程中的 panic 会打印出一条错误消息,然后以非零的退出码退出,大体上,这就是我们期望的行为。

声明自定义错误类型

编写一个新的 JSON 解析器,并且希望它有自己的错误类型。

// json/src/error.rs

#[derive(Debug, Clone)]
pub struct JsonError {
   
   
    pub message: String,
    pub line: usize,
    pub column: usize,
}

这个结构体叫作 json::error::JsonError。当你想引发这种类型的错误时,可以像下面这样写:

return Err(JsonError {
   
   
    message: "expected ']' at end of array".to_string(),
    line: current_line,
    column: current_column
});

但是,如果你希望达到你的库用户的预期,确保这个错误类型像标准错误类型一样工作,那么还有一点儿额外的工作要做:

use std::fmt;

// 错误应该是可打印的
impl fmt::Display for JsonError {
   
   
    fn fmt(&self, f: &mut fmt::Formatter) -> Result<
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值