当我们编写代码时,错误会在我们调用其他函数的时候在该函数内部发生:

fn f() {
// 当 b() 返回一个错误时,可能会发生错误
a = b()
...
}

这就产生了一个问题:

每种编程语言都找到了解决这三个挑战的不同解决方案。

Java 是第一种将错误管理提升到更高状态的大众语言,它使用了异常。b 可以在错误时抛出一个异常。然后调用函数可以什么都不做,在这种情况下,调用函数 f 会带着异常返回给它的调用者。或者,它可以通过在 try/catch 中包装调用来稍后处理异常。Java 方法的缺点是我们在错误发生后不能有正常的控制流程。我们要么处理它,要么让它冒泡上来。

详细解读

代码 1:异常冒泡

void f() throws Exception {
    b();
}

void b() throws Exception {
    // 当遇到错误时,抛出一个异常
    throw new Exception("An error occurred in function b");
}

在这个例子中,当 b() 遇到错误时,它抛出一个异常。然后,f() 调用了 b(),但是它没有处理这个异常,所以这个异常会继续抛出,也就是“冒泡”到 f() 的调用者。

代码 2:try/catch 处理异常

void f()  {
    try {
         b();
    } catch (Exception e) {
        // 当 function b 抛出异常时,这里会捕获并处理这个异常
        System.out.println("An error occurred: " + e.getMessage());
    }
    // 这行代码会在try/catch块之后执行,即使 function b 抛出了异常
    System.out.println("Continuing execution in function f");
}

void b() throws Exception {
    // 当遇到错误时,抛出一个异常
    throw new Exception("An error occurred in function b");
}

在这个例子中,当 b() 遇到错误时,它抛出一个异常。然后,f() 调用了 b(),并在 try/catch 块中捕获并处理了这个异常。这样,即使 b() 遇到了错误,f() 也可以继续执行。

这就是 try/catch 异常处理机制的一个重要特性,它允许我们在遇到错误时进行恰当的处理,然后继续执行程序的剩余部分。

Java 异常机制的一个缺点是声明了检查异常。如果我们的函数 f() 声明了它的异常,而函数 b() 抛出了不同的异常,我们需要以某种方式处理异常,因为它不能冒泡上来。

详细解读

void f() throws IOException {
    b();
}

void b() throws SQLException {
    // 抛出一个SQLException
    throw new SQLException();
}

当函数 f 调用函数 b 时,如果函数 b 抛出了一个 ,那么这个异常不能直接传递给函数 f,因为函数 f 在声明时只指定了它可能会抛出 ,而没有声明它可能会抛出 。这是 Java 的检查异常机制规定的:如果一个方法可能会抛出某种异常,那么这个方法必须在其声明中指定这种异常。

如果函数 b 抛出的 在函数 f 内部没有被处理,那么编译器会报错,因为这违反了 Java 的检查异常机制。为了解决这个问题,我们可以在函数 f 内部使用 try/catch 语句来捕获并处理这个异常。例如:

void f() throws IOException {
    try {
        b();
    } catch (SQLException e) {
        // 处理SQLException
        System.out.println("Caught SQLException: " + e.getMessage());
    }
}

void b() throws SQLException {
    // 抛出一个SQLException
    throw new SQLException("An error occurred in b");

在这个例子中,当函数 b 抛出 时,这个异常会被函数 f 内部的 catch 语句捕获并处理,所以不会违反 Java 的检查异常机制。

Rust 找到了一个解决方案,它有一个机制可以自动将一个错误—— b() 的错误——转换为另一个错误 - f() 的错误。这样我们又可以让错误冒泡上来而不用处理它。Rust 使用 ? 来实现这一点:

fn f() {
 // 让函数f()返回
 // 错误自动转换并冒泡上来
 a = b()?
 ...
}

详细解读

在Rust编程语言中,? 运算符被用于错误处理。当你在一个返回 或 类型的函数调用后面使用 ?,它会在遇到错误时立即从当前函数返回错误,而不是继续执行后续的代码。这就是所谓的"错误冒泡",因为错误会像冒泡一样从函数调用栈中向上冒出,直到被捕获并处理。

在给出的代码示例中:

fn f() {
 // 让函数f()返回
 // 错误自动转换并冒泡上来
 a = b()?
 ...
}

b() 是一个可能会返回错误的函数。如果 b() 成功执行并返回一个值,那么这个值会被赋给 a,然后程序会继续执行后续的代码。但是,如果 b() 在执行过程中遇到错误并返回一个 Err,那么 ? 运算符会立即触发,导致 f() 函数立即返回这个错误。

这就是 Rust 中的错误冒泡机制。它允许你编写出清晰、简洁的错误处理代码,而不需要在每个可能会出错的地方都写出详细的错误处理逻辑。

一些编程语言通过返回一个错误代码来处理这三个挑战。Go 就是其中之一。

a, err := b()

现在我们可以处理错误

if err != nil { .... }

或者从我们的函数返回。我们可以在错误后有正常的程序流程——在错误的情况下——除非我们想要对 a 进行操作:

a = a + 1

如果有一个错误并且 a 是 nil,这是行不通的。

我们现在可以每次都检查 a 是否存在

if a != nil { .... }

但这很快就会变得繁琐且难以阅读。

详细解读

package main

import (
  "errors"
  "fmt"
)

// 假设这是一个 map
var m = map[string]int{
  "one"1,
  "two"2,
}

// 函数 b() 尝试从 map 中获取一个值
func b(key string) (int, error) {
  value, ok := m[key]
  if !ok {
   return 0, errors.New("key not found")
  }
  return value, nil
}

// 函数 f() 调用 b() 并处理可能出现的错误
func f(key string) {
  a, err := b(key)
  if err != nil {
   fmt.Println("Error:", err)
   return
  }

  // 如果没有错误,对 a 进行操作
  a = a + 1
  fmt.Println("Result:", a)
}

func main() {
  f("two")  // 输出:Result: 3
  f("three"// 输出:Error: key not found
}

假设我们有一个函数 b(),它尝试从一个 map 中获取一个值,如果该值不存在,它将返回一个错误。然后我们有一个函数 f(),它调用 b(),并处理可能出现的错误。

在 Go 语言中,函数通常会返回两个值:一个是函数的预期结果,另一个是可能出现的错误。这是 Go 语言处理错误的一种常见模式。

这段代码的意思是,当你调用函数 b() 时,它会返回两个值:a 和 err。a 是函数的预期结果,err 是可能出现的错误。

a, err := b(key)

然后,你可以检查 err 是否为 nil。如果 err 不为 nil,那么就表示函数 b() 在执行过程中出现了错误。

if err != nil { .... }

在这种情况下,你可以选择处理这个错误(例如,打印错误信息,或者返回给上层函数),或者你可以选择忽略这个错误,让程序继续执行。

然后,你可能想要对 a 进行一些操作,例如:

a = a + 1

但是,如果函数 b() 出现了错误,那么 a 可能是 nil。在这种情况下,尝试对 a 进行操作会导致运行时错误。因此,你需要在对 a 进行操作之前,检查 a 是否为 nil。

if a != nil { .... }

然而,这种检查 a 是否为 nil 的操作,如果在代码中频繁出现,会使代码变得繁琐且难以阅读。这是 Go 语言处理错误的方式的一个缺点。

一些编程语言使用 Monad 来处理错误后的控制流问题。

// a 是 Result 类型
a = b()

有了 Monad,我可以处理错误或从方法返回。如上所述,对于返回,Rust 有一些特殊的语法

a = b()?

带有问号,当 b() 返回错误时,函数将在那一行返回,并且错误会自动转换并冒泡上来。

详细解读

在 Rust 中, 是一个枚举类型,它有两个变体:Ok 和 Err。Ok 表示操作成功,Err 表示操作失败。这是 Rust 处理错误的主要方式。当你看到 时,A 是成功时的返回类型,E 是错误时的返回类型。

让我们来看一个函数 b 的例子,这个函数可能会返回一个错误:

fn b() -> Result<i32, &'static str> {
    // 假设这里有一些可能会失败的代码
    // 如果成功,返回 Ok(value)
    // 如果失败,返回 Err("error message")
    Ok(42// 假设这个函数总是成功,并返回42
}

在这个例子中,b 函数返回 ,这意味着它在成功时返回一个 i32 类型的值,在失败时返回一个静态字符串作为错误消息。

在Rust中,? 运算符用于错误处理。如果你在一个返回 或 的函数中调用了另一个返回 或 的函数,你可以在这个函数调用后面加上 ?。如果函数成功,? 将提取 Ok 变体中的值。如果函数失败,? 将立即从当前函数返回错误。

让我们看一个使用 ? 的例子:

fn a() -> Result<i32, &'static str> {
    let value = b()?; // 如果b成功,value是b返回的值。如果b失败,立即从a返回错误
    // 这里的代码只有在b成功时才会执行
    Ok(value * 2// 假设我们只是简单地将值乘以2然后返回
}

在这个例子中,a 函数调用了 b 函数,并在调用后面加上了 ?。如果 b 成功,value 将是 b 返回的值,然后 a 函数将继续执行。如果 b 失败,? 将立即从 a函数返回错误,后面的代码不会执行。这就是Rust中 ? 的工作方式。

我们也可以在错误的情况下有正常的控制流程,但仍然使用 a 。神奇!

a = b()
c = a.map(|v| v + 1)
...
// 稍后处理错误

如果出现错误,c 也将是一个错误,否则 c 将包含增加了 1 的 a 的值。这样,无论错误是否发生,我们都可以在错误后有相同的控制流程。

这使得对代码的推理变得更容易。

详细解读

这段代码是 Rust 语言的代码,它使用了 Rust 的 或 类型,这两种类型都有一个 map 方法。这段代码的主要目的是在错误发生的情况下仍然保持正常的控制流程。

在 Rust 中, 类型是一种可以表示成功或失败的类型,它有两个变体:Ok(T) 和 Err(E),其中 T 是操作成功时返回的类型,E 是操作失败时返回的错误类型。

map 方法是 类型的一个方法,它接受一个函数作为参数,这个函数会在 是 Ok 的情况下被调用,并对 Ok 的值进行操作。如果 是 Err,那么 map 方法不会做任何事情,只是将错误传递下去。

在你给出的代码中:

a = b()
c = a.map(|v| v + 1)

b() 函数可能返回一个 类型的值,这个值要么是 Ok(v),要么是 Err(e)。然后,map 方法被调用,如果 a 是 Ok(v),那么 map 方法会将 v 增加1,并将结果包装在 Ok 中。如果 a 是 Err(e),那么 map 方法不会做任何事情,只是将错误传递给 c。

这样,无论 b() 函数是否成功,我们都可以在错误发生后继续执行其他代码,这就是所谓的"在错误的情况下仍然有正常的控制流程"。

这种方式使得代码的推理变得更容易,因为我们不需要在每个可能出错的地方都写错误处理代码,只需要在最后处理错误就可以了。

这是一个更完整的函数示例:

fn b() -> Result<i32, &'static str> {
    // 这里只是一个示例,实际上b函数可能会进行一些可能失败的操作
    Ok(5)
}

fn main() {
    let a = b();
    let c = a.map(|v| v + 1);
    match c {
        Ok(v) => println!("The value is: {}", v),
        Err(e) => println!("An error occurred: {}", e),
    }
}

在这个示例中,如果 b 函数成功,那么输出将是 "The value is: 6",如果 b 函数失败,那么输出将是"An error : "加上错误信息。

详细解读

在Rust中,|v| 是一个匿名函数(也称为闭包)的参数列表的表示方式。闭包是一种可以捕获其环境中的值的函数。

在这个特定的例子中,|v| 定义了一个接受一个参数 v 的闭包。闭包的主体跟在 |v| 之后,例如 |v| v + 1。这个闭包接受一个参数 v,并返回 v + 1。

闭包的语法是 |参数列表| -> 返回类型 { 函数体 }。返回类型和大括号可以省略。如果省略了返回类型,Rust会自动推断出返回类型。如果函数体只有一行,大括号也可以省略。

例如,下面的两个闭包是等价的:

let add_one_v1 = |x: i32| -> i32 { x + 1 };
let add_one_v2 = |x| x + 1;

在这两个闭包中, 显式地指定了参数类型和返回类型,而 省略了这些,让Rust自动推断。

Zig 通过使用 ! 来注解类型,简短地表示了 。

// 返回i32
fn f() i32 {
...
}

// 返回i32或一个错误
fn f() !i32 {
...
}

Zig 还通过流分析解决了 Java 中繁琐的异常声明问题。它检查你的函数 f(),找出所有可能返回的错误。然后,如果你在调用代码中检查了一个特定的错误,它会确保这是详尽无遗的。

详细解读

这段话是在讨论 Zig 编程语言如何通过流分析(flow )来改进错误处理的。

在 Java 中,如果一个方法可能会抛出异常,你需要在方法的声明中使用 关键字来指定可能会抛出的异常类型。这被称为“检查异常”( )。这种方式的问题在于,如果你的方法调用了其他可能抛出异常的方法,你需要在你的方法声明中包含所有可能的异常类型,这往往会导致异常声明的列表非常长,而且难以管理。

void f() throws IOException, SQLException{
    b();
}

void b() throws SQLException {
    // 抛出一个SQLException
    throw new SQLException("An error occurred in b");

Zig 采用了一种不同的方式来处理这个问题。在 Zig 中,你不需要在函数声明中指定可能的错误类型。相反,Zig 的编译器会自动分析你的代码,找出所有可能的错误类型。这就是所谓的“流分析”。

更进一步,如果你在调用函数的代码中检查了一个特定的错误,Zig 的编译器会确保你的检查是详尽无遗的。也就是说,编译器会检查你的代码是否处理了所有可能的错误情况。如果没有,编译器会报错。

这种方式的好处是,你不需要在函数声明中列出所有可能的错误类型,这使得代码更简洁,更易于管理。同时,编译器的流分析确保了你的代码处理了所有可能的错误情况,这有助于提高代码的健壮性。

fn f() !i32 {
// 这里可能会抛出错误
...
}

fn g() !i32 {
const result = f() catch |err| {
// 这里处理 f() 抛出的错误
...
};
return result;
}

在这个示例中,函数 `f()` 可能会抛出错误,函数 `g()` 调用了 `f()` 并处理了可能的错误。Zig 的编译器会分析这段代码,确保 `g()` 函数处理了 `f()` 可能抛出的所有错误。

Rust 的 ? 有一个特殊的语法,可以立即返回。Java 有特殊的语法,使用 try/catch 不立即返回,并在我们不编写额外代码的情况下返回给函数的调用者。

问题是:我们更常做什么?返回错误还是继续?我们更常做的事情,应该有更简洁的语法。在Rust的 ? 情况下,我们是否需要一个 ? 来立即返回,或者 ? 来不返回?

a = b()?

? 可以表示“返回错误”。或者行为可能是,如果 b() 返回一个错误,总是立即返回,而 ? 阻止了这个。

这取决于哪种情况更常见。

详细解读

这段话主要在讨论 Rust 和 Java 中错误处理的不同方式,以及如何选择更有效的错误处理策略。

在 Rust 中,? 运算符用于处理 或 类型的值。如果 是 Ok,那么 ? 运算符将提取 Ok 中的值;如果 是 Err,那么 ? 运算符将立即从当前函数返回 Err,这就是所谓的 " on the spot"。

例如:

fn foo() -> Result<i32, &'static str> {
    let a = some_function()?;
    Ok(a + 1)
}

fn some_function() -> Result<i32, &'static str> {
    // ... some code here ...
}

在这个例子中,如果 () 返回一个错误,那么 foo() 函数将立即返回这个错误。

在 Java 中,错误处理通常通过 try/catch 语句进行。如果 try 块中的代码抛出了异常,那么控制流将立即跳转到相应的 catch 块,这就是所谓的 "not on the spot"。

例如:

public void foo() {
    try {
        someFunction();
    } catch (Exception e) {
        // handle exception
    }
}

public void someFunction() throws Exception {
    // ... some code here ...
}

在这个例子中,如果 () 抛出一个异常,那么控制流将立即跳转到 catch 块,而不是立即从 foo() 函数返回。

文章的问题是:我们更常做什么?在错误发生时立即返回,还是继续执行?我们更常做的事情,应该有更简洁的语法。在 Rust 的 ? 情况下,我们是否需要一个 ? 来立即返回,或者 ? 来不返回?这取决于哪种情况更常见。

可能会给我们另一个线索。它有一个特殊的语法,用于在函数返回时进行清理

f := File.open("my.txt")
// 确保我们在退出函数时关闭文件
defer f.close()
a, err = b()
if err != nil {
 // 这里调用了f.close()
 return
}

Java 有一个不太优雅的 。看起来人们认为 error 应该冒泡上来,我们在这种情况下需要一些简单的清理。

从我的经验来看,我也怀疑我们会希望让大多数错误通过自动转换冒泡上来,所以 ? 应该表示我们不希望函数返回,这与 Rust 的做法相反。

看来 Java 在异常上是对的。没有语法意味着冒泡行为。它错过了 Rust 的自动转换和 - 以及 Go 的本地、简单的 defer,而不是 Java 的非本地、冗长的 。而且 Java 没有解释如何正确使用异常,所以每个人都用错了。

详细解读

在 Go 中,defer 关键字用于确保在函数返回时执行某些操作,无论函数是正常返回还是因为错误而返回。这在处理文件、数据库连接等需要显式关闭的资源时非常有用。

例如:

func foo() error {
    f, err := os.Open("my.txt")
    if err != nil {
        return err
    }
    // 确保我们在退出函数时关闭文件
    defer f.Close()

    // ... some code here ...

    return nil
}

在这个例子中,无论 foo() 函数是正常返回还是因为错误而返回/0在c语言中是什么意思?,defer f.Close() 都会确保文件被关闭。

在 Java 中,资源清理通常通过 try/ 语句进行。无论 try 块中的代码是否抛出异常, 块中的代码都会被执行。

例如:

public void foo() {
    FileInputStream fis = null;
    try {
        fis = new FileInputStream("my.txt");
        // ... some code here ...
    } catch (IOException e) {
        // handle exception
    } finally {
        if (fis != null) {
            try {
                fis.close();
            } catch (IOException e) {
                // handle exception
            }
        }
    }
}

在这个例子中,无论 foo() 函数是正常返回还是因为异常而返回, 块中的 fis.close() 都会确保文件被关闭。

文章的观点是,Java 的异常处理机制是正确的,没有语法意味着错误应该冒泡上来。但是,Java 错过了 Rust 的自动转换和 ,以及 Go 的本地、简单的 defer,而不是 Java 的非本地、冗长的 。而且,Java 没有解释如何正确使用异常,所以每个人都用错了。

那么一个假设的语言会是怎样的呢:

fn f() {
// b() 返回 Result 或 Zig 中的 !V,
// 如果 b 是一个错误,f() 返回
// a 是 V 类型
a = b()

// 不在错误上返回,但
// a 是 Result 类型或 !V
a = b()!

// 编译为a = a.map(|v| v + 1)
a = a + 1

// 编译为c = a.map(|v| v.c())
// c是Result类型
c = a.c()
...
}

这具有更高的可读性。

但是,当我们调用另一个方法时,我们应该做什么呢?

// 如果d期望
// C 作为参数类型
// 而不是 Result
// 这不会工作
d(c)

详细解读

在许多编程语言中,当函数遇到错误时,通常的做法是立即返回错误,不再执行后续的代码。这种做法的优点是简单明了,一旦发生错误,函数就会停止执行,这可以防止错误的进一步传播。然而,这种做法的缺点是它可能会导致代码的冗余和复杂性增加,因为我们需要在每个可能产生错误的地方检查错误并返回。

在这个假设的编程语言中,作者提出了一种不同的错误处理方式。当函数遇到错误时,它不会立即返回,而是将错误包装在一个 类型的值中,并继续执行后续的代码。这种做法的优点是它可以使代码更简洁,更容易理解,因为我们不需要在每个可能产生错误的地方检查错误并返回。然而,这种做法的缺点是它可能会导致错误的处理被延迟,或者在某些情况下被忽略。

这是一个示例代码:

fn f() {
  // b() 返回 Result 或者 Zig 中的 !V,
  // 如果 b 是一个错误,f() 返回
  // a 是 V 类型
  a = b()

  // 不在错误上返回,但
  // a 是 Result 类型或者 !V
  a = b()!

  // 编译为 a = a.map(|v| v + 1)
  a = a + 1

  // 编译为 c = a.map(|v| v.c())
  // c 是 Result 类型
  c = a.c()
  ...
}

在这个例子中,a = b()! 这行代码表示,即使 b() 函数返回一个错误,我们也不会立即返回,而是将错误包装在 a 中,并继续执行后续的代码。这使得我们可以在后续的代码中处理错误,或者将错误传递给函数的调用者。

详细解读

在编程中,当错误发生时,我们有两种基本的处理方式:立即处理错误(例如,通过抛出异常或返回错误代码),或者延迟处理错误(例如,通过返回一个可能包含错误的对象,如 Rust 的 或者 Go 的 error)。这两种方式各有优点和缺点。

立即处理错误的优点是可以立即知道错误的存在,并且可以立即停止错误的进一步传播。这可以防止错误的进一步扩大,并且可以使得错误的处理逻辑更加集中和明确。然而,这种方式的缺点是它可能会打断正常的控制流程,使得代码的结构变得复杂。

延迟处理错误的优点是可以保持正常的控制流程,使得代码的结构更加清晰。这种方式允许我们在错误发生时继续执行,然后在适当的时候处理错误。然而,这种方式的缺点是可能会延迟错误的发现和处理,如果我们忘记检查和处理错误,可能会导致错误被忽视。

一些语言有特殊的语言语法来处理问题。例如, 有 do,Scala 有 for。但是,然后你就有了围绕错误的特殊代码和特殊上下文。这使得事情再次变得难以阅读,与意图相反。

所以最好抛出一个编译器错误。并记住,默认的方式是冒泡上去,a 是 V 类型。

我们可以通过控制流分析来减轻痛苦。一些编程语言,如 ,做了类似的事情:

a = b()
a = a + 1 // A仍然是Result
if a instanceof Error {
 return
}
// A现在是V类型
// 因为我们检查了错误
d(a)

看起来每种编程语言都持有优化错误处理谜题的一部分。我看到的,没有一种是成功的。

就是这样,伙计们。

详细解读

这段话主要在讨论一些编程语言如何通过特殊的语法来处理错误,以及如何通过控制流分析来简化错误处理。

在 和 Scala 中,有特殊的语法( 的 do 和 Scala 的 for)来处理可能产生错误的操作。然而,这种方法需要编写特殊的错误处理代码,并且需要在特殊的上下文中进行,这可能使代码更难阅读。

例如/0在c语言中是什么意思?,在 Scala 中,你可能会这样处理可能产生错误的操作:

for {
  a <- someFunction() // someFunction returns an Option or Either
  b <- anotherFunction(a) // anotherFunction returns an Option or Either
yield b

在这个例子中,如果 () 或 (a) 返回一个错误,那么整个 for 表达式将返回一个错误。

文章的观点是,最好抛出一个编译器错误,并记住,默认的方式是错误冒泡上来,a 是 V 类型。

然后,我们可以通过控制流分析来简化错误处理。一些编程语言,如 ,做了类似的事情:

let a = b(); // b returns a value or an error
a = a + 1// a is still a value or an error
if (a instanceof Error) {
  return;
}
// a is now a value, because we checked for an error
d(a);

在这个例子中,如果 b() 返回一个错误,那么 a 将是一个错误,a = a + 1 将不会执行。如果 b() 返回一个值,那么 a 将是一个值,a = a + 1 将正常执行。在检查 a 是否是一个错误后,我们可以确信 a 是一个值,所以我们可以安全地调用 d(a)。

文章的结论是,每种编程语言都持有优化错误处理谜题的一部分,但从我看到的,没有一种是成功的。

WWH 系列文章列表:

[1]

[2]

[3]

[4]

[5]

文章列表:

[1]

[2]

[3]

[4]

[5]

[6]

[7]

最近文章列表:

[1]

[2]

[3]

[4]

[5]

[6]

[7]

[8]

[9]

[10]

喜欢本篇文章,记得动动小手点个在看↓↓

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注