Rust vs C++:2024,谁更懂错误处理?
讲动人的故事,写懂人的代码
「席双嘉,听说你的C++项目又因为忘了检查返回值导致内存泄漏,又加班了?」
周五中午,在国内某科技巨头熙熙攘攘的员工餐厅,贾克强半开玩笑地戳了戳坐在隔壁的席双嘉,眼神中满是戏谑。
贾克强,一个热衷于Rust的程序员,总是乐于挑战和探索新技术的边界。
而席双嘉,则是那种深耕于C++领域,有着丰富经验和对性能追求无比执着的老兵。
席双嘉苦笑着摇头,「是啊,这不还是‘常规操作’嘛。我有异常处理,怕啥。」
「‘常规操作’?”贾克强一边调侃,一边用手比划了一个广场舞的动作,“我宁愿跳广场舞,也不想跟着内存泄漏的节奏扭腰。」
席双嘉自嘲地笑了笑,「说得好像你们用Rust能就免疫这些问题似的。」
1 代码对决
这时,贾克强提出了一个小比赛的想法。就是两人午餐后,回到工位,在30分钟内,各自用Rust和C++实现同一个功能——读取文件到字符串中,看看谁的代码不仅稳定还能效率更高。
席双嘉立刻来了精神,眼睛一亮,「好啊,如果我赢了,今晚的加班夜宵你买单!」
贾克强笑着拍胸脯,「行!但如果我赢了,你得穿上我给你准备的‘安全第一,我用Rust’的T恤,还要给团队做个分享。」
两人一拍即合。
午餐后,两人拿着笔记本电脑,步入配备先进设施的会议室。
贾克强把桌上的计时沙漏倒过来,计时开始。
席双嘉不到10分钟就写完了。贾克强大概用了20分钟。
他们各自在大型电视屏幕上展示自己的代码。
贾克强的Rust代码,显示在左边的电视上。
席双嘉的C++代码,则显示在右边。
2 Rust代码讲解
贾克强展示了他写的Rust代码。
use std::fs::File;
use std::io;
use std::io::Read;
// 定义一个函数,尝试读取文件到字符串中
// 这个函数返回 Result 类型,要么是包含文件内容的 String,要么是 io::Error
fn read_file_to_string(path: &str) -> Result<String, io::Error> {
let mut file = match File::open(path) {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut contents = String::new();
match file.read_to_string(&mut contents) {
Ok(_) => Ok(contents),
Err(e) => Err(e),
}
}
// 使用 Option 类型处理可能的空值
fn find_first_line(contents: &str) -> Option<&str> {
contents.lines().next()
}
fn main() {
// 尝试读取一个文件
match read_file_to_string("hello.txt") {
Ok(contents) => {
// 尝试找到文件的第一行
match find_first_line(&contents) {
Some(line) => println!("First line: {}", line),
None => println!("File is empty"),
}
}
Err(e) => println!("Error reading file: {}", e),
}
}
然后他开始给席双嘉讲解。
「这段Rust代码读取一个文件,并打印文件的第一行。」
「它使用Rust的错误处理和Option类型来处理错误和空值。」
2.1 main()
函数
「main
函数是程序的入口点。这个函数调用了 read_file_to_string
函数。使用了硬编码文件名 "hello.txt"。这个函数要打开并读取文件到一个字符串中。」
fn main() {
// 尝试读取一个文件
match read_file_to_string("hello.txt") {
Ok(contents) => {
// ...
}
Err(e) => println!("Error reading file: {}", e),
}
}
「read_file_to_string
函数返回一个 Result
类型,它可以是 Ok
,包含文件内容作为字符串,或者是 Err
,包含一个错误。」
「 match
语句用来处理这两种可能的结果。如果文件读取成功,内容将传递给 find_first_line
函数。」
Ok(contents) => {
match find_first_line(&contents) {
Some(line) => println!("First line: {}", line),
None => println!("File is empty"),
}
}
「find_first_line
函数接收文件内容作为字符串,并返回文件的第一行。它返回一个 Option
类型,可以是 Some
,包含第一行,或者是 None
,如果文件是空的。」
「和上面的match
一样,这个 match
语句也用来处理这两种可能的结果。如果找到一行,就将它打印到控制台。如果没有,则打印一条表示文件为空的信息。」
「如果在读取文件时发生错误,错误将打印到控制台。」
Err(e) => println!("Error reading file: {}", e),
「这段代码展示了Rust处理错误和空值安全的方法,使用 Result
和 Option
类型和 match
语句以一种安全和明确的方式处理不同的结果。」
2.2 Result
类型与Option
类型的区别
席双嘉举起了手说:「稍等。你一会儿说函数返回Result
类型,一会儿说又返回Option
类型。这俩类型有啥区别?」
贾克强解释说:「在Rust中,Result
和Option
类型都是用于错误处理以及表示值的存在或缺失的枚举,但它们在不同的场景中使用并传达不同的含义。」
「Option
类型用于一个值可能有也可能无的情况。也就是代表一个可以是Some(T)
的值,其中T
是值;或者None
,表示值的缺失。这在处理可能无法在所有情况下都返回值的操作时,特别有用。」
「比如从map中通过key来获取值,但key可能就不存在。」
「或者函数的参数或结构的字段本身就是可选的,也就是可有可无。」
「或者对于在正常操作下可能无法产生值的函数的返回值,这不是由于错误,而是因为该值可能在逻辑上就可以不存在。」
「而Result
类型则是在操作可能成功或失败,且想明确处理两种结果时使用。」
「它代表一个值,可以是 Ok(T)
,其中 T
是成功操作的结果,或者是 Err(E)
,其中 E
是发生的错误。」
「Result
类型在 Rust 的错误处理中被广泛使用。」
「比如可能会出错的文件和网络操作,像文件没找到啊,权限被拒绝啊,网络连接错误等等。」
「或者从字符串或其他格式解析数据时,输入可能无效。」
「或者需要处理各种错误的任何操作。」
席双嘉点了点头。贾克强继续讲代码。
2.3 read_file_to_string
函数
「再看 read_file_to_string
函数。这个函数读取一个文件并将其内容转换为字符串。」
fn read_file_to_string(path: &str) -> Result<String, io::Error> {
「这个函数接收一个文件路径path
作为参数,并返回一个 Result
类型。」
「这个Result
类型,要么是包含文件内容的 String
,要么是 io::Error
。」
「这个函数首先使用 File::open
方法打开文件。这个方法返回一个 Result
类型,通过 match
语句来处理。」
let mut file = match File::open(path) {
Ok(file) => file,
Err(e) => return Err(e),
};
「如果文件成功打开,Ok
变体将返回文件对象file
。如果发生错误,Err
变体将返回错误对象,并且这个错误将立即由函数返回。」
「接下来,创建一个名为 contents
的新的空字符串。这个字符串将用于存储文件的内容。」
let mut contents = String::new();
「然后在文件对象上调用 read_to_string
方法。这个方法将文件的内容读取到 contents
字符串中。与之前一样,这个方法返回一个 Result
类型,通过 match
语句来处理。」
match file.read_to_string(&mut contents) {
Ok(_) => Ok(contents),
Err(e) => Err(e),
}
「如果文件读取成功,Ok
变体将返回 contents
字符串。如果发生错误,Err
变体将返回错误对象。」
2.4 find_first_line
函数
「最后这个find_first_line
函数,要查找并返回给定字符串的第一行。」
fn find_first_line(contents: &str) -> Option<&str> {
「这个函数接受一个字符串引用作为参数,并返回一个Option
类型,可以是包含对第一行的引用的Some
,或者如果字符串为空则为None
。」
「这个函数在字符串引用上调用lines
方法。这个方法返回字符串行的迭代器。」
contents.lines()
「然后在此迭代器上调用next
方法。这个方法返回字符串的下一行,或者如果没有更多行则返回None
。」
contents.lines().next()
「在这种情况下,由于在创建迭代器后立即调用next
,它将返回字符串的第一行,或者如果字符串为空则返回None
。」
2.5 运行代码
「咱们运行一下。」
贾克强在他的macOS命令行上,执行了命令echo "hello, Rust" > hello.txt
,来创建一个新文件hello.txt
。这个文件只有一行hello, Rust
。
然后他运行命令cargo run
,来让程序执行。
他们在命令行窗口,看到了程序的输出
First line: hello, Rust
3 C++代码讲解
见贾克强讲完了Rust代码,席双嘉转向自己的C++代码,并开始讲解。
#include <fstream>
#include <iostream>
#include <string>
#include "lib.hpp"
std::string read_file_to_string(const std::string& path)
{
std::ifstream file(path);
if (!file.is_open()) {
throw std::runtime_error("Could not open file");
}
std::string contents((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
return contents;
}
int main()
{
try {
std::string contents = read_file_to_string("hello.txt");
std::cout << "File contents: " << contents << std::endl;
} catch (...)
std::cerr << "An error occurred." << std::endl;
}
return 0;
}
「我写的C++程序,读取一个文件,并将其内容打印到控制台。」
3.1 main()
函数
「主函数可以处理在程序执行过程中可能发生的任何异常。」
int main()
「main()
函数是程序的入口点。」
「main
函数以一个try-catch块开始。这被用来处理在程序执行过程中可能发生的任何异常。try
块包含可能会抛出异常的代码。
try {
std::string contents = read_file_to_string("hello.txt");
std::cout << "File Contents: " << contents << std::endl;
}
「在try
块中,程序调用了参数为"hello.txt"
的函数read_file_to_string
。」
「这个函数用来读取一个文件,并返回其内容作为一个字符串。」
「如果由于某种原因,比如文件不存在,文件无法打开,那么函数将抛出一个std::runtime_error
。」
「然后,返回的字符串,也就是文件的内容,用std::cout
打印到控制台。」
「如果在try
块的任何地方抛出了异常,程序执行将立即跳转到catch
块。」
catch (...) {
std::cerr << "An error occurred." << std::endl;
}
「catch (...)
语句是一个捕获所有类型异常的处理器。当捕获到异常时,它就用std::cerr
向控制台打印异常消息。」
「最后,main
函数返回0,表示程序已经无错误地完成执行。如果捕获了一个异常,这仍然会是返回值,因为异常被处理了,并且没有导致程序提前终止。」
3.2 read_file_to_string
函数
「再看看 read_file_to_string
函数。这个函数用于读取文件并将其内容返回为字符串。」
「它接受一个参数,path
,这是一个引用 std::string
,代表文件的路径。」
std::string read_file_to_string(const std::string& path)
{
std::ifstream file(path);
...
}
「函数开始时创建一个名为 file
的 std::ifstream
对象。这个对象表示一个文件流,用于读取由 path
指定的文件的数据。」
「如果由于某种原因无法打开文件,例如,如果文件不存在或程序没有必要的权限,那么 file
对象的 is_open
方法将返回 false
。在这种情况下,函数抛出 std::runtime_error
异常。」
if (!file.is_open()) {
throw std::runtime_error("Could not open file");
}
「如果文件成功打开,函数将继续读取其内容。它通过创建一个名为 contents
的 std::string
类型的对象来实现。这个对象用两个 std::istreambuf_iterator<char>
迭代器对象进行初始化。」
「第一个迭代器以 file
为参数构造,表明它将从文件的开头开始读取。」
「第二个迭代器是默认构造的,表明它代表流的结束。」
「这个迭代器范围是从 std::istreambuf_iterator<char>(file)
开始,到 std::istreambuf_iterator<char>()
结束,包含了文件中的所有字符。」
std::string contents((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
「最后,函数返回 contents
字符串,该字符串现在包含了文件的内容。」
return contents;
3.3 运行代码
「我们运行一下代码。」
席双嘉创建了一个有两行文字的hello.txt
文件,然后运行程序。他们在屏幕上看到了输出。
File contents: Line 1
Line 2
4 C++的软肋
贾克强盯着C++代码看了一会儿,问:「你在read_file_to_string
函数中抛出的std::runtime_error
异常,在main()
函数中,没有显式捕获,难道C++编译器不报错吗?」
席双嘉略显尴尬地说:「你算是戳到C++的软肋了。我在代码中catch (...) {
的写法,叫泛捕获异常。」
「这种写法虽然省事儿,但其实并不规范。因为这样的泛捕获虽然可以确保程序不会因为未处理的异常而异常终止,但它也隐藏了异常的具体信息,使得问题的调试和解决更加困难。」
「C++规范地捕获特定类型的异常的代码,应该这样写。」
席双嘉一边说,一边改代码。
// 捕获特定类型的异常
try {
std::string contents = read_file_to_string("hello.txt");
std::cout << "File contents: " << contents << std::endl;
} catch (const std::runtime_error& e) { // 改为捕获特定类型的异常
std::cerr << "Error reading file: " << e.what() << std::endl;
} catch (const std::exception& e) { // 捕获所有标准异常的基类
std::cerr << "Standard error: " << e.what() << std::endl;
} catch (...) { // 最后,用泛捕获来处理其他不可预见的异常
std::cerr << "An unknown error occurred." << std::endl;
}
「这样一来,如果程序在运行时找不到hello.txt
文件,那么就能在屏幕上显示更加容易理解的Error reading file: Could not open file
,而不是泛捕获不明就里的An error occurred.
了」
「对于这种不规范的写法,C++编译器确实是不报错的。另外,即便你把main()
函数里的try-catch都去掉,也就是不处理任何异常,C++编译器也不报错。」
贾克强说:「这真是省事儿一时爽,调试火葬场。Rust可不这么干。在 Rust 中,错误处理的机制与 C++ 的异常处理机制是不同的。」
「Rust 使用 Result
和 Option
类型来处理可能出现的错误和空值,而不是抛出异常。」
「这意味着,在 Rust 中不存在“对异常进行泛捕获”这样的问题,因为 Rust 没有传统意义上的异常。」
「Rust 强制要求开发者处理所有可能的错误情况。如果一个函数返回 Result
类型,你必须显式处理 Ok
和 Err
,或者使用 unwrap()
、expect()
等方法明确标明这里可能的错误不会被处理。当然,这将在运行时引发 panic,如果预期的错误发生的话。」
「Rust 编译器会在编译时捕获未处理的 Result
和 Option
类型,要求程序员处理,否则就编译失败。这样就防止它们被无视。」
「换句话说,Rust 的这种设计哲学确保了代码的安全性和可靠性。因为Rust强制开发者在编写代码时就显式考虑错误处理,而不是依赖于运行时的异常捕获机制。」
席双嘉点头说:「确实如此。或许,是时候拓宽我的技术视野了。我得承认,Rust 在帮助避免这类错误上真的做得更棒。我开始考虑,是不是该花点时间深入了解下 Rust 了。」
贾克强微笑着说:「看吧,Rust 的设计就是为了避免这样的事情。不过,关键还是用对工具。输赢其实不重要,我们能从这次比拼中学到东西,才是真正的收获。」
席双嘉说:「确实,我已经开始想象,用 Rust 重写一些模块会是怎样一番景象了。」
贾克强鼓励道:「这就对了!探索新工具总会带来新的启发。而且,不论是 Rust 还是 C++,我们的目标都是写出更好的代码,不是吗?」
5 终曲
席双嘉笑着说:「行行行,我认输。那件“安全第一,我用Rust”T恤我穿定了。不过,我还是对那个夜宵很感兴趣哦。」
贾克强哈哈大笑:「这有什么难的?穿上T恤,夜宵我请。我们下周开个小会,你分享一下今天的“异常”经历如何?」
席双嘉戏谑地说:「好吧,我这就变成了“穿着Rust T恤的C++程序员”。这要是被团队看到,不知道会不会成为新的梗。」
贾克强打趣道:「别担心,明天我会带一件写着“性能至上,信仰C++”的T恤。我们一起穿,看谁的梗更胜一筹。」
如果喜欢我的文章,期待你的点赞、在看和转发。
如果不喜欢,留个言告诉我哪里不喜欢呗~
转载自:https://juejin.cn/post/7353197221741887528