网站建设需要个体营业执照,邯郸市三建建筑公司网址,网站建设与单位干部作风的关系,网站备案的服务器租用错误处理是编程的一个基本要素。除非你写的是“hello world”#xff0c;否则就必须处理代码中的错误。在本文中#xff0c;我将讨论各种编程语言在处理错误时使用的最常见的四种方法#xff0c;并分析它们的优缺点。
关注不同设计方案的语法、代码可读性、演变过程、运行效… 错误处理是编程的一个基本要素。除非你写的是“hello world”否则就必须处理代码中的错误。在本文中我将讨论各种编程语言在处理错误时使用的最常见的四种方法并分析它们的优缺点。
关注不同设计方案的语法、代码可读性、演变过程、运行效率将有助于我们写出更为优雅和健壮的代码。 返回错误代码 这是最古老的策略之一——如果一个函数可能会出错它可以简单地返回一个错误代码——通常是负数或者null。例如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 函数通常加上一个回调函数后者使用两个参数errorresult例如
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 ResultS, 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。 --END--