与许多其他编程语言不同,Rust 没有异常处理错误。处理错误是什么意思?让我们考虑一个非常简单的 Python 程序,它将字符串转换为整数,有一个成功的情况,一个可能失败的情况
一、Python 中的异常处理
num_str = "10" parsed = int(num_str) print("completed") # output 10
上面从字符串到整数的转换执行成功
name_str = "john" parsed = int(name_str) print("completed") # output Traceback (most recent call last): File "<stdin>", line 1, in <module> ValueError: invalid literal for int() with base 10: 'john'
上述转换失败并出现ValueError
异常。如果我们观察输出,print
则异常之后的语句不会被执行。未正确处理的异常将导致程序停止,想象一下这样的一段代码会停止生产服务器并需要重新启动。下面是 Python 中的一个try-except
块的示例,异常得到了优雅的处理,并且不会停止程序。
name_str = "john" try: parsed = int(name_str) except ValueError as e: print(e) print("completed") # output invalid literal for int() with base 10: 'john' completed
这样,在Python中,我们就可以捕获异常。一旦我们捕获它们,我们就可以将它们转换为不同的异常,记录异常或忽略它。
二、Rust 中的错误处理
回到 Rust,让我们将类似的字符串编码为整数转换程序。
fn main() { let num_str: String = "10".to_owned(); let _parsed_string: i32 = num_str.parse::<i32>().unwrap(); println!("Completed"); }
在这里,我们尝试将一个String
转换为i32
类型,并且程序成功执行。稍后我们将了解为什么需要.unwrap()
。现在,我们只希望代码能够执行。然后 如果我们尝试运行这个程序会发生什么?
fn main() { let num_str: String = "john".to_owned(); let _parsed_string: i32= num_str.parse::<i32>().unwrap(); println!("Completed"); } # output thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit }', src/main.rs:3:47 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
程序在使执行期间会出现ParseIntError
错误,因为它不知道如何将字符john
转换为整数值。出现错误不是出乎意料的,但是我们如何优雅地处理此类错误而不惊慌呢?
1.枚举Result
如果我们看一下类型String
上的方法parse
,我们会发现返回类型是 Result
。
pub fn parse<F: FromStr>(&self) -> Result<F, F::Err> { ... }
这Result
是一个有两个变体的枚举
Ok(T)
-> 代表成功及其数值Err(E)
-> 用错误值表示错误
但是如何使用枚举Result
帮助我们处理错误呢?很简单,每次Result
从函数返回一个类型时,我们都需要处理它的两个变体,无论是成功还是错误。如果我们不这样做,Rust 编译器就会对我们报错. 如果成功,我们将继续使用Ok
变量中的值,如果出现错误,我们可以使用Err
变量进行处理。下面是通过打印消息来处理错误的示例
use std::num::ParseIntError; fn main() { let num_str: String = "john".to_owned(); let parsed_result: Result<i32, ParseIntError> = num_str.parse::<i32>(); match parsed_result { Ok(parsed_string) => println!("Successfully parsed {}", parsed_string), Err(_) => println!("There was an error parsing the input"), } println!("Completed"); } # output There was an error parsing the input Completed
如果我们观察输出,ParseIntError
现在我们的程序已成功处理,并且其余代码也将执行而不会出现恐慌。这就是我们的目标
2.方法unwrap()
那么前面提到的unwrap
是什么事情呢?如果我们再次看一下转换10
为整数的第一个 Rust 示例,
let _parsed_string: i32 = "10".to_owned().parse::<i32>().unwrap();
我们没有使用任何match
语句来处理Result
枚举,而是使用了unwrap
方法。然而代码编译并成功返回输出。
如果函数执行后的输出是变体,则Ok(T)
类型的值T
将被消耗,并且程序将按预期继续。但如果函数的输出是变体,则Err(E)
当前函数将会出现错误。
如果我们看一下第二个代码示例:
let _parsed_string: i32 = "john".to_owned().parse::<i32>().unwrap();
我们尝试将“john”中的字符解析为整数,unwrap
但我们的程序出现了报错。
如果程序仍然会出现报错,我们为什么要使用呢?unwrap
用于快速处理任何Result
类型,仅考虑有效/正确的情况并忽略处理错误。尽管不鼓励大量使用unwrap
,但它在开发过程中非常方便。Result
我们总是可以在稍后阶段返回并正确处理类型。
3.?
运算符
想象一下这样一种情况,我们正在调用一个返回Result
的函数,但我们不想Err
手动处理它的变体(匹配并返回一个新的 Err),相反,我们只想将 Err 转发给调用函数。
让我们考虑下面的示例,其中我们尝试解析 Vec 中的所有元素并将其总和返回到调用的函数parse_vec_and_sum
。
use std::num::ParseIntError; fn main() { let strings_vec: Vec<&str> = vec!("1", "2", "3"); let parsed_result = parse_vec_and_sum(strings_vec); match parsed_result { Ok(parsed_string) => println!("Successfully parsed vec to {}", parsed_string), Err(_) => println!("There was an error parsing the input"), } println!("Completed"); } fn parse_vec_and_sum(v: Vec<&str>) -> Result<i32, ParseIntError> { let mut sum: i32 = 0; for i in v { match parse_int(i.to_owned()) { Ok(parsed_integer) => { // If parsing is successful, we just add it to total sum sum += parsed_integer; }, Err(e) => { // If there is an Error, return it immediately return Err(e); } }; } return Ok(sum); } fn parse_int(str: String) -> Result<i32, ParseIntError> { let x = str.parse::<i32>(); let value: i32; match x{ Ok(v) => {value = v;}, Err(e) => return Err(e) }; // Let's assume, for simplicity, some processing on `value` is needed after `.parse`. // Otherwise we could have just returned value from `.prase` return Ok(value); } # output Successfully parsed vec to 6 Completed
上面的例子运行得很好,但是写起来很冗长。对于这样的操作,我们可以使用简便化的?
运算符。该问号运算符将消耗该值,或者通过返回到调用函数Ok(T)
来传播错误。让我们使用运算符Err(E)
重写上面的程序?
use std::num::ParseIntError; fn main()-> Result<(), ParseIntError> { let strings_vec: Vec<&str> = vec!("1", "2", "3"); let parsed_string = parse_vec_and_sum(strings_vec)?; // operator used here println!("Successfully parsed vec to {}", parsed_string); println!("Completed"); return Ok(()); } fn parse_vec_and_sum(v: Vec<&str>) -> Result<i32, ParseIntError> { let mut sum: i32 = 0; for i in v { sum += parse_int(i.to_owned())?; // operator used here } return Ok(sum); } fn parse_int(str: String) -> Result<i32, ParseIntError> { let x = str.parse::<i32>()?; // operator used here // Let's assume, for simplicity, some processing on `value` is needed after `.parse`. // Otherwise we could have just returned value from `.prase` directly return Ok(x); } # output Successfully parsed vec to 6 Completed
现在,让我们将 vec 更改为可能导致错误的内容
... let strings_vec: Vec<&str> = vec!("john", "2", "3"); ... # output There was an error parsing the input Completed
错误 ParseIntError 是从 parse_int -> parse_vec_and_sum -> main 调用的。这使得我们的程序更加简洁,编写起来更加轻松。
4.结束语
到目前为止,我们已经了解了 Rust 中的基本错误处理。我们已经看过
Result
枚举可以是成功Ok(T)
也可以是失败Err(E)
- 用于
.unwrap
快速处理Result
类型 - 使用
?
运算符通过调用函数处理错误
关于 Rust 中的错误,还有很多东西需要了解。以上面使用运算符?
的示例为例,我们仅返回一种错误类型,如果每个函数返回多个错误怎么办?或者如果 错误Result
是动态的并且我们不确切知道它的类型怎么办?我们该如何处理这样的情况呢?此外,还有std::error::Error
一个表示错误值的基本特征,即表示E
中的任何值Result<T, E>
。我们需要更多地了解这个Error
特征。
但是这些不用担心,我们总能学到更多。但拥有扎实的基础知识将有助于理解更复杂的内容。我希望这篇文章能够帮助到大家了解 Rust 中的异常和异常处理。
相关文章:
- 【Rust 基础篇】Rust 错误处理详解 (自定义错误类型)
- 关于Rust 异常的一些处理方式