不灭的焱

革命尚未成功,同志仍须努力下载JDK17

作者:Albert.Wen  添加时间:2024-02-14 13:18:10  修改时间:2024-05-03 18:28:48  分类:C/C++/Rust  编辑

与许多其他编程语言不同,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 中的异常和异常处理。

 

 

相关文章:

  1. 【Rust 基础篇】Rust 错误处理详解 (自定义错误类型)
  2. 关于Rust 异常的一些处理方式