异常处理

在 .NET 中,异常是一种派生自 System.Exception 的类。如果一段代码发生了问题,那么异常就会被抛出。抛出的异常会沿着栈传递,直到应用程序处理了它,或者程序终止。

Rust 没有异常,但是区分了 可恢复不可恢复 的错误。一个可恢复的错误代表着一个应当被报告的问题,但是程序为此继续运行。对于可能会因可恢复的错误而失败的操作,它的的结果是类型 Result<T,E>,其中 E 是错误变体的类型。panic! 宏会在程序遇到不可恢复的错误时终止程序。不可恢复的错误总是意味着漏洞的存在。

自定义错误类型

在 .NET 中, 自定义异常从 Exception 类派生。关于 如何创建用户定义的异常 的文档提到了这个例子:

public class EmployeeListNotFoundException : Exception
{
    public EmployeeListNotFoundException() { }

    public EmployeeListNotFoundException(string message)
        : base(message) { }

    public EmployeeListNotFoundException(string message, Exception inner)
        : base(message, inner) { }
}

在 Rust 中,可以通过实现 Error trait 来实现错误值的基本要求。Rust 中最简短的用户定义错误实现是:

#[derive(Debug)]
pub struct EmployeeListNotFound;

impl std::fmt::Display for EmployeeListNotFound {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("Could not find employee list.")
    }
}

impl std::error::Error for EmployeeListNotFound {}

.NET 中 Exception.InnerException 属性的等价是 Rust 中的 Error::source() 方法。然而,对 Error::source() 的实现并非必要的,通用(默认的)实现返回一个 None

引发异常

要在 C# 中引发异常,抛出一个异常的实例:

void ThrowIfNegative(int value)
{
    if (value < 0)
    {
        throw new ArgumentOutOfRangeException(nameof(value));
    }
}

对于 Rust 中的可恢复错误,方法应当返回一个 OkErr 变体:

fn error_if_negative(value: i32) -> Result<(), &'static str> {
    if value < 0 {
        Err("Specified argument was out of the range of valid values. (Parameter 'value')")
    } else {
        Ok(())
    }
}

panic! 宏创建不可恢复的错误:

fn panic_if_negative(value: i32) {
    if value < 0 {
        panic!("Specified argument was out of the range of valid values. (Parameter 'value')")
    }
}

错误传播

在 .NET 中,异常会沿着调用栈传递直到它们被处理或者程序终止。在 Rust 中,不可恢复的错误表现类似,不过通常不会处理它们。

然而,可恢复的错误需要被显式地传播和处理。它们的存在总是由 Rust 函数或方法的签名表示。在 C# 中,捕获异常允许你对错误的存在与否进行处理:

void Write()
{
    try
    {
        File.WriteAllText("file.txt", "content");
    }
    catch (IOException)
    {
        Console.WriteLine("Writing to file failed.");
    }
}

在 Rust 中,这大致相当于:

fn write() {
    match std::fs::File::create("temp.txt")
        .and_then(|mut file| std::io::Write::write_all(&mut file, b"content"))
    {
        Ok(_) => {}
        Err(_) => println!("Writing to file failed."),
    };
}

大多数情况下,可恢复错误更需要被传播而不是处理。这种情况下,方法签名需要与要传播的错误类型兼容。? 运算符 以一种符合人体工学的方式传播错误。

fn write() -> Result<(), std::io::Error> {
    let mut file = std::fs::File::create("file.txt")?;
    std::io::Write::write_all(&mut file, b"content")?;
    Ok(())
}

备注:若要使用问号运算符来传播错误,错误实现必须 兼容,就像传播错误的简写:? 运算符中提到的。最一般的错误类型是错误的 trait 对象 Box<dyn Error>

堆栈跟踪

在 .NET 中抛出异常会导致运行时打印堆栈跟踪信息,这样可以借助额外的上下文来调试问题。

对于 Rust 中的可恢复错误,panic backtrace 提供了类似行为。

stable Rust 中的可恢复错误还不支持 backtrace,不过它当前已有实验性支持,通过使用provide 方法