?操作
就像调用.unwrap()
是与panic!
在错误分支中的匹配的快捷方式一样,我们还有另一个与在错误分支返回的匹配的快捷方式:?
。
你没有看错,就是一个问号。我们可以将此操作符附加到Result
类型的值上,Rust将在内部将其扩展为与我们刚刚编写的match非常相似的东西。
可以将对应的代码部分改成如下格式:
fn main() -> Result<(), Box<dyn std::error::Error>> { let content = std::fs::read_to_string("test.txt")?; println!("文件内容:{}", content); Ok(()) }
难道这就是传说中,从天而降的掌法嘛。这也太丝滑了。
这里有一些
Rust
开发中的潜规则。例如,我们main
函数中的错误类型是Box<dyn std::error::Error>
。但是我们已经看到read_to_string
返回的是std::io::Error
。这是因为?
扩展为转换错误类型的代码。同时,
Box<dyn std::error::Error>
也是一个有趣的类型。它是一个Box
,可以包含任何实现标准Error trait
的类型。这意味着基本上所有错误都可以放入这个Box
中,因此我们可以在所有通常返回Result
的函数上使用?
。
有关Box
的使用原理和介绍可以参考Rust智能指针
为错误提供合适的语境提示
使用?
在主函数中时,得到的错误是可以接受的,但不是很好。例如:当我们运行std::fs::read_to_string("test.txt")?
但文件test.txt
不存在时,我们会得到以下输出:
Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }
在代码中不包含文件名的情况下,很难确定哪个文件是NotFound
。有多种处理方式。
创建自己的错误类型
我们可以创建自己的错误类型,然后使用它来构建自定义错误消息:
#[derive(Debug)] struct CustomError(String); fn main() -> Result<(), CustomError> { let path = "test.txt"; let content = std::fs::read_to_string(path) .map_err(|err| CustomError(format!("在读取`{}`时: {}", path, err)))?; println!("文件内容:{}", content); Ok(()) }
我们来简单解释一下上面的代码
#[derive(Debug)] struct CustomError(String);
: 这个代码定义了一个自定义的错误类型CustomError
,它包含一个字符串字段用于存储错误消息。#[derive(Debug)]
属性宏为这个结构体自动生成了Debug
trait 的实现,以便在打印错误时更容易调试。fn main() -> Result<(), CustomError> { ... }
: 这是程序的入口点main
函数的签名。与之前的代码不同,它返回一个Result
,其中成功值是()
,表示成功执行而没有返回值,错误值是自定义错误类型CustomError
。let content = std::fs::read_to_string(path) ... ?;
:与之前的代码不同,这里使用了map_err
方法来处理可能的错误情况。
.map_err(|err| CustomError(format!("在读取{}时: {}", path, err)))
: 这部分使用map_err
方法来处理可能的错误情况。map_err
方法接受一个闭包(匿名函数),该闭包接受一个错误对象err
,并返回一个新的错误对象。在这个闭包中,它将原始的std::io::Error
错误转换为自定义的CustomError
错误类型,并添加了一条包含错误信息的自定义错误消息。?
: 这个问号?
是Rust
中的错误处理操作符。它用于处理 Result 类型的返回值。如果Result
是一个Ok
,则?
不会执行任何操作,它会将成功的值提取出来。如果Result
是一个Err
,则?
会立即将错误返回给调用者,作为整个函数的返回值,就好像使用return Err(...)
一样。
现在,运行这个程序将会得到我们自定义的错误消息:
Error: CustomError("在读取`test.txt`时: No such file or directory (os error 2)")
虽然不太美观,但我们可以稍后轻松调整我们类型的调试输出。
使用anyhow库
上面的模式非常常见。但它有一个问题:我们没有存储原始错误,只有它的字符串表示。我们可以使用anyhow库对此有一个巧妙的解决方案:与我们的CustomError
类型类似,它的Context trait
可以用来添加描述。此外,它还保留了原始错误,因此我们得到一个指出根本原因的错误消息“链”。
首先,通过在Cargo.toml
文件的[dependencies]
部分添加anyhow = "1.0.75"
来导入anyhow crate
。
然后,完整的示例将如下所示:
use anyhow::{Context, Result}; fn main() -> Result<()> { let path = "test.txt"; let content = std::fs::read_to_string(path) .with_context(|| format!("无法读取文件 `{}`", path))?; println!("文件内容:{}", content); Ok(()) }
这将打印一个错误:
Error: 无法读取文件 `test.txt` Caused by: No such file or directory (os error 2)
6. 信息输出处理
使用 println!
我们几乎可以使用println!宏
打印所有我们喜欢的内容。这个宏具有一些非常惊人的功能,但也有特殊的语法。它希望我们将一个字符串字面量作为第一个参数,该字符串包含占位符,这些占位符将由后面的参数的值作为进一步的参数填充。
例如:
let x = 789; println!("我的幸运数字是 {}。", x);
将打印:
我的幸运数字是 789。
上述字符串中的花括号({}
)是其中的一个占位符。这是默认的占位符类型,它尝试以人机友好的方式打印给定的值。对于数字和字符串,这个方法非常有效,但并不是所有类型都可以这样做。这就是为什么还有一种调试模式(debug representation
) --{:?}
。
例如:
let xs = vec![1, 2, 3]; println!("列表是:{:?}", xs);
将打印:
列表是:[1, 2, 3]
如果希望我们自己的数据类型能够用于调试和记录,大多数情况下可以在它们的定义之上添加
#[derive(Debug)]
。用户友好(
User-friendly
)打印使用Display trait
,调试输出(面向开发人员的输出)使用Debug trait
。我们可以在std::fmt模块的文档中找到有关可以在println!
中使用的语法的更多信息。
打印错误信息
通过stderr
来打印错误,以使用户和其他工具更容易将其输出重定向到文件或其他工具。
在大多数操作系统上,程序可以写入两个输出流,
stdout
和stderr
。
stdout
用于程序的实际输出stderr
允许将错误和其他消息与stdout
分开
这样,可以将输出存储到文件或将其管道传输到另一个程序,而错误将显示给用户。
在
Rust
中,可以通过println!
和eprintln!
来实现这一点,前者打印到stdout
,后者打印到stderr
。
println!("这是正常信息"); eprintln!("这是一个错误! :(");
在打印转义代码
时,会使用户的终端处于奇怪现象,所以,当处理原始转义代码时,应该使用像ansi_term
这样的crate
来使我们的输出更加顺畅。
打印优化
向终端打印的速度出奇地慢!如果在循环中调用类似println!
的函数,它可能成为程序运行的瓶颈。为了加快速度,有两件事情可以做。
1. 减少写入次数
首先,我们可能希望减少实际刷新
到终端的写入次数。
println!
在每次调用时都会告诉系统刷新到终端,因为通常会打印每一行。
如果我们不需要这样做,可以将stdout句柄
包装在默认情况下缓冲最多8 KB的BufWriter
中。(当我们想立即打印时,仍然可以在此BufWriter
上调用.flush()
。)
use std::io::{self, Write}; let stdout = io::stdout(); // 获取全局stdout实体 let mut handle = io::BufWriter::new(stdout); // 可选:将该句柄包装在缓冲区中 writeln!(handle, "front: {}", 789); // 如果我们关心此处的错误,请添加`?`
2.使用锁
其次,可以获取stdout
(或stderr
)的锁,并使用writeln!
直接打印到它。这可以防止系统一遍又一遍地锁定和解锁stdout
。
use std::io::{self, Write}; let stdout = io::stdout(); // 获取全局stdout实体 let mut handle = stdout.lock(); // 获取它的锁 writeln!(handle, "front: {}", 789); // 如果我们关心此处的错误,请添加`?`
我们还可以结合两种方法。
具体代码如下:
use std::io::{self, Write}; fn main() -> io::Result<()> { let stdout = io::stdout(); // 获取全局stdout实体 let stdout_lock = stdout.lock(); // 获取stdout的锁 // 将锁包装在BufWriter中 let mut handle = io::BufWriter::new(stdout_lock); writeln!(handle, "front: {}", 789)?; // 如果我们关心此处的错误,请添加`?` Ok(()) }
在这个示例中,首先获取了 stdout
的锁,然后将锁传递给 io::BufWriter
,最后使用 writeln!
向 handle
写入数据。
显示一个进度条
某些CLI
运行时间不到一秒,而其他一些可能需要几分钟或几小时。如果我们正在编写后者类型的程序,我们可能希望向用户显示正在发生的事情。为此,我们可以尝试打印有用的状态更新,最好以易于消耗的形式呈现。
使用indicatif crate
,我们可以向我们的程序添加进度条和小的旋转器。
在使用之前,我们需要在Cargo.toml
中引入对应的库。
[dependencies] indicatif = { version = "*", features = ["rayon"] }
下面是使用indicatif
的一个小示例。
fn main() { let pb = indicatif::ProgressBar::new(100); for i in 0..100 { do_hard_work(); pb.println(format!("[+] 完成了第 #{}项", i)); pb.inc(1); } pb.finish_with_message("任务完成"); } fn do_hard_work() { use std::thread; use std::time::Duration; thread::sleep(Duration::from_millis(250)); }
有关更多信息,请参阅indicatif文档和示例。
日志
为了更容易理解程序中发生的情况,我们可能想要添加一些日志语句。通常在编写应用程序时这很容易。但在半年后再次运行此程序时,日志将变得非常有帮助。在某种程度上,日志记录与使用 println!
相同,只是你可以指定消息的重要性。
通常可以使用的日志级别有
error
、warn
、info
、debug
和trace
(error
优先级最高,trace
优先级最低)。
要向应用程序添加日志记录,你需要两样东西:
log crate
(其中包含了根据日志级别命名的宏)- 一个实际将日志输出写到有用位置的适配器
由于我们现在只关心编写一个 CLI
,一个易于使用的适配器是 env_logger。它被称为env
logger
,因为你可以使用环境变量来指定你想要记录的应用程序部分(以及你想要记录它们的级别)。它将在日志消息前加上时间戳和消息来源的模块。由于库也可以使用 log
,因此我们可以轻松配置它们的日志输出。
以下是简单示例:
配置Cargo.toml
[dependencies] log = "0.4.20" env_logger = "0.10.0"
use log::{info, warn}; fn main() { env_logger::init(); info!("项目启动"); warn!("这是一个警告信息"); }
假设你将此文件保存为 src/bin/output-log.rs
,在 Linux
和 macOS
上,你可以这样运行它:
$ env RUST_LOG=info cargo run --bin output-log
在 Windows PowerShell
中,你可以这样运行:
$ $env:RUST_LOG="info" $ cargo run --bin output-log
在 Windows CMD
中,你可以这样运行:
$ set RUST_LOG=info $ cargo run --bin output-log
上面的代码是在运行 Rust
项目中的二进制文件(通过指定 --bin
标志)并设置日志级别(通过 RUST_LOG
环境变量)。
针对主要的代码,做一下解释:
env RUST_LOG=info
: 这部分设置了一个环境变量RUST_LOG
,用于控制Rust
项目中的日志记录级别。具体来说,它将日志级别设置为info
。
Rust
项目通常使用日志库(例如log
和env_logger
)来记录不同级别的日志消息。info
是一个中等详细的级别,它会记录一些有用的信息,但不会过于冗长。你可以根据需要将日志级别设置为不同的值,如debug
、warn
、error
等。
--bin output-log
: 这部分告诉cargo
运行项目中名为output-log
的二进制文件。Rust 项目通常包含多个二进制文件,这个选项指定要运行的二进制文件的名称。output-log
应该是你的 Rust 项目中一个二进制文件的名称。
综合起来,这行代码的作用是设置日志级别为 info
,然后运行 Rust 项目中名为 output-log
的二进制文件。这有助于控制日志记录的详细程度,并查看项目中的输出日志。如果你的 Rust 项目使用了日志库,并且在代码中有相应的日志记录语句,那么设置日志级别为 info
会让你看到 info
级别的日志消息。
代码展示
我们上面通过几节的内容,从项目配置
/参数获取
/解析文件内容
/处理错误信息
/信息输出处理
等方面。可以构建出在本地,兼容错误提示,并且有很好的输出形式的本地搜索工具。
让我们就上面的内容,从代码上做一次梳理和汇总。
use anyhow::{Context, Result}; use clap::Parser; use indicatif::ProgressBar; use std::fs::File; use std::io::{self, BufRead, Write}; use std::path::PathBuf; use std::thread; use std::time::Duration; /// 在文件中搜索模式并显示包含它的行。 #[derive(Parser)] struct Cli { /// 要查找的模式 pattern: String, /// 要读取的文件的路径 path: PathBuf, } fn main() -> Result<()> { let args = Cli::parse(); // 打开文件并创建一个 BufReader 来逐行读取 let file = File::open(&args.path).with_context(|| format!("无法打开文件 {:?}", &args.path))?; let reader = io::BufReader::new(file); let stdout = io::stdout(); let stdout_lock = stdout.lock(); let mut handle = io::BufWriter::new(stdout_lock); let pb = ProgressBar::new(100); for line in reader.lines() { do_hard_work(); pb.println(format!("[+] 查找到了 #{:?}项", line)); pb.inc(1); let line = line.with_context(|| "无法读取行")?; if line.contains(&args.pattern) { writeln!(handle, "{}", line)?; } } Ok(()) } fn do_hard_work() { thread::sleep(Duration::from_millis(250)); }
对应的Cargo.toml如下
[package] name = "f789" version = "0.1.0" edition = "2021" [dependencies] clap = { version = "4.4.2", features = ["derive"] } anyhow = "1.0.75" indicatif = { version = "0.17.6", features = ["rayon"] } log = "0.4.20" env_logger = "0.10.0"
对应的运行结果如下:
在上文中我们手动创建了一个text.txt
文件。我们只是创建了,没告诉它放置的位置。我们将与src
目录同级。
使用erd -L 1 -y inverted
命令查看目录信息
Cargo
会默认把所有的源代码文件保存到src
目录下,而项目根目录只被用来存储诸如README文档
/许可声明/配置文件等与源代码无关的文件。
如果,我们想看针对大文件的处理方式,我们可以新建一个更大的项目。用于做代码实验。
后记
分享是一种态度。
参考资料:
全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。