对比编程语言的四种错误处理方法,哪种才是最优方案?

简介: 对比编程语言的四种错误处理方法,哪种才是最优方案?

返回错误代码


这是最古老的策略之一——如果一个函数可能会出错,它可以简单地返回一个错误代码——通常是负数或者null。例如,C 语言中经常使用:

C

复制代码

FILE* fp = fopen("file.txt" , "w");if (!fp) {
  // 发生了错误}

这种方法非常简单,既易于实现,也易于理解。它的执行效率也非常高,因为它只需要进行标准的函数调用,并返回一个值,不需要有运行时支持或分配内存。但是,它也有一些缺点:

  • 用户很容易忘记处理函数的错误。例如,在 C 中,printf 可能会出错,但我几乎没有见过程序检查它的返回值!
  • 如果代码必须处理多个不同的错误(打开文件,写入文件,从另一个文件读取等),那么传递错误到调用堆栈会很麻烦。
  • 除非你的编程语言支持多个返回值,否则如果必须返回一个有效值或一个错误,就很麻烦。这导致 C 和 C++ 中的许多函数必须通过指针来传递存储了“成功”返回值的地址空间,再由函数填充,类似于:

my_struct *success_result;
int error_code = my_function(&success_result);
if (!error_code) {
  // can use success_result
}


众所周知,Go 选择了这种方法来处理错误,而且,由于它允许一个函数返回多个值,因此这种模式变得更加人性化,并且非常常见:


user, err = FindUser(username)
if err != nil {
    return err
}

Go 采用的方式简单而有效,会将错误传递到调用方。但是,我觉得它会造成很多重复,而且影响到了实际的业务逻辑。不过,我写的 Go 还不够多,不知道这种印象以后会不会改观!😅


异常


异常可能是最常用的错误处理模式。try/catch/finally 方法相当有效,而且使用简单。异常在上世纪 90 年代到 2000 年间非常流行,被许多语言所采用(例如 Java、C# 和 Python)。

与错误处理相比,异常具有以下优点:

  • 它们自然地区分了“快乐路径”和错误处理路径
  • 它们会自动从调用堆栈中冒泡出来
  • 你不会忘记处理错误!

然而,它们也有一些缺点:需要一些特定的运行时支持,通常会带来相当大的性能开销。

此外,更重要的是,它们具有“深远”的影响——某些代码可能会抛出异常,但被调用堆栈中非常远的异常处理程序捕获,这会影响代码的可读性。

此外,仅凭查看函数的签名,无法确定它是否会抛出异常。

C++ 试图通过throws 关键字来解决这个问题,但它很少被使用,因此在 C++ 17 中已被弃用 ,并在 C++ 20 中被删除。此后,它一直试图引入noexcept 关键字,但我较少写现代 C++,不知道它的流行程度。

(译者注:throws 关键字很少使用,因为使用过于繁琐,需要在函数签名中指定抛出的异常类型,并且这种方法不能处理运行时发生的异常,有因为“未知异常”而导致程序退出的风险)

Java 曾试图使用“受检的异常(checked exceptions)”,即你必须将异常声明为函数签名的一部分——但是这种方法被认为是失败的,因此像 Spring 这种现代框架只使用“运行时异常”,而有些 JVM 语言(如 Kotlin)则完全抛弃了这个概念。这造成的结果是,你根本无法确定一个函数是否会抛出什么异常,最终只得到了一片混乱。

(译者注:Spring 不使用“受检的异常”,因为这需要在函数签名及调用函数中显式处理,会使得代码过于冗长而且造成不必要的耦合。使用“运行时异常”,代码间的依赖性降低了,也便于重构,但也造成了“异常源头”的混乱)


回调函数


另一种方法是在 JavaScript 领域非常常见的方法——使用回调,回调函数会在一个函数成功或失败时调用。这通常会与异步编程结合使用,其中 I/O 操作在后台进行,不会阻塞执行流。

例如,Node.JS 的 I/O 函数通常加上一个回调函数,后者使用两个参数(error,result),例如:


const fs = require('fs');
fs.readFile('some_file.txt', (err, result) => {
  if (err) {
    console.error(err);
    return;
  }
  console.log(result);
});

但是,这种方法经常会导致所谓的“回调地狱”问题,因为一个回调可能需要调用其它的异步 I/O,这可能又需要更多的回调,最终导致混乱且难以跟踪的代码。

现代的 JavaScript 版本试图通过引入promise 来提升代码的可读性:


fetch("https://example.com/profile", {
      method: "POST", // or 'PUT'
})
  .then(response => response.json())
  .then(data => data['some_key'])
  .catch(error => console.error("Error:", error));


promise 模式并不是最终方案,JavaScript 最后采用了由 C#推广开的 async/await 模式,它使异步 I/O 看起来非常像带有经典异常的同步代码:


async function fetchData() {
  try {
    const response = await fetch("my-url");
    if (!response.ok) {
      throw new Error("Network response was not OK");
    }
    return response.json()['some_property'];
  } catch (error) {
    console.error("There has been a problem with your fetch operation:", error);
  }
}

使用回调进行错误处理是一种值得了解的重要模式,不仅仅在 JavaScript 中如此,人们在 C 语言中也使用了很多年。但是,它现在已经不太常见了,你很可能会用的是某种形式的async/await。


函数式语言的 Result


我最后想要讨论的一种模式起源于函数式语言,比如 Haskell,但是由于 Rust 的流行,它已经变得非常主流了。

它的创意是提供一个Result类型,例如:



enum Result<S, E> {
  Ok(S),
  Err(E)
}

这是一个具有两种结果的类型,一种表示成功,另一种表示失败。返回结果的函数要么返回一个Ok 对象(可能包含有一些数据),要么返回一个Err 对象(包含一些错误详情)。函数的调用者通常会使用模式匹配来处理这两种情况。

为了在调用堆栈中抛出错误,通常会编写如下的代码:



let result = match my_fallible_function() {
  Err(e) => return Err(e),
  Ok(some_data) => some_data,
};


由于这种模式非常常见,Rust 专门引入了一个操作符(即问号 ?) 来简化上面的代码:


let result = my_fallible_function()?;   // 注意有个"?"号

这种方法的优点是它使错误处理既明显又类型安全,因为编译器会确保处理每个可能的结果。

在支持这种模式的编程语言中,Result 通常是一个 monad,它允许将可能失败的函数组合起来,而无需使用 try/catch 块或嵌套的 if 语句。

(译者注:函数式编程认为函数的输入和输出应该是纯粹的,不应该有任何副作用或状态变化。monad 是一个函数式编程的概念,它通过隔离副作用和状态来提高代码的可读性和可维护性,并允许组合多个操作来构建更复杂的操作)

根据你使用的编程语言和项目,你可能主要或仅仅使用其中一种错误处理的模式。

不过,我最喜欢的还是 Result 模式。当然,不仅是函数式语言采用了它,例如,在我的雇主 lastminute.com 中,我们在 Kotlin 中使用了 Arrow 库,它包含一个受 Haskell 强烈影响的类型Either。我有计划写一篇关于它的文章,最后感谢你阅读这篇文章,敬请保持关注😊。

目录
相关文章
|
6月前
|
自然语言处理 API 开发工具
PAI如何处理不同编程语言的混合任务?
【7月更文挑战第1天】PAI如何处理不同编程语言的混合任务?
119 57
|
6月前
|
存储 缓存 编译器
编程语言性能优化:黑盒法和数字处理的支持
【7月更文挑战第7天】该文主要讨论了编程中的性能优化技术,特别是针对哈希表查找中模运算的优化。性能优化在不同场合方式不一样,文章强调了分析器在定位性能问题中的重要性,并指出优化应基于对底层架构的理解。
79 3
编程语言性能优化:黑盒法和数字处理的支持
|
6月前
|
开发者
编程问题之逻辑编程有什么缺点
编程问题之逻辑编程有什么缺点
|
6月前
|
存储
代码优化设计问题之当方法体只有一行时,独立存在的方法的必要性开始存疑问题如何解决
代码优化设计问题之当方法体只有一行时,独立存在的方法的必要性开始存疑问题如何解决
|
6月前
|
设计模式 存储
代码优化设计问题之优化枚举的getByName方法以提高效率问题如何解决
代码优化设计问题之优化枚举的getByName方法以提高效率问题如何解决
|
7月前
|
编译器 测试技术 Linux
技术洞察:循环语句细微差异下的性能探索(测试while(u--);和while(u)u--;的区别)
该文探讨了两种循环语句(`while(u--);` vs. `while(u) u--;`)在性能上的微妙差异。通过实验发现,后者比前者平均执行速度快约20%,原因在于循环条件检查的顺序影响了指令数量。尽管差异可能在多数情况下不显著,但在性能关键的代码中,选择合适的循环结构能优化执行效率。建议开发者在编写循环时考虑编译器优化和效率。未来研究可扩展到不同编译器、优化级别及硬件架构的影响。
|
8月前
|
存储 Java API
掌握8条方法设计规则,设计优雅健壮的Java方法
掌握8条方法设计规则,设计优雅健壮的Java方法
|
8月前
|
IDE Java 开发工具
讨论 Python 中泛型(或类似泛型的功能)的优点和缺点
【5月更文挑战第8天】Python虽无显式泛型系统,但可通过类型注解和工具实现类似功能。优点包括提升代码可读性、静态类型检查、更好的IDE支持、灵活性和可逐渐引入。缺点涉及运行时性能开销、学习成本、非强制性及与旧代码集成问题。适当使用工具和实践可管理这些挑战。
49 2
|
8月前
|
存储 算法 C语言
【编程陷阱】编写出色C++代码:遵循的注意事项和最佳实践
【编程陷阱】编写出色C++代码:遵循的注意事项和最佳实践
70 0
|
8月前
|
缓存 Java 应用服务中间件
后端接口性能优化分析-程序结构优化(下)
后端接口性能优化分析-程序结构优化
85 0