一、错误的两种分类
错误可以分为可恢复错误(如文件不存在、网络超时)和不可恢复错误(如内存耗尽、除零)。不同语言的处理哲学差异巨大:PHP早期混合错误码和警告,现代用异常;Java坚持异常体系;C++倾向于返回值或std::optional(禁用异常时)。近年来Result类型(Rust风格)开始影响Java和PHP。
参考:https://aescc.cn/category/entrance.html
二、PHP:try/catch与错误抑制的混乱
PHP的传统函数常常返回false并触发E_WARNING。例如fopen失败返回false。这导致代码中到处是if(false===$result)。现代PHP开发推荐使用异常:可以自定义继承Exception或RuntimeException。但很多原生函数仍未改为异常,需要手动检查。
错误抑制符@隐藏所有错误,是不良实践。PHP8引入了throw作为表达式,允许在箭头函数中抛异常。PHP还支持set_error_handler将错误转换为异常,许多框架默认这么做了(如Laravel)。
PHP的异常开销较低,但使用应遵循:业务逻辑应抛异常,库函数应文档说明异常类型。
三、Java:受检异常的兴衰
Java的受检异常(CheckedException)是语言独有设计:方法必须声明可能抛出的受检异常,调用者要么捕获要么继续声明。这强迫程序员思考错误处理,但很多开发者选择空的catch块或者笼统的throwsException,从而失去价值。
Spring等框架转而大量使用非受检异常(RuntimeException),使得许多受检异常被包装为运行时异常。JavaI/O和JDBC中的IOException、SQLException仍是受检异常。现代Java微服务中,通常在Controller层统一捕获异常并转换为HTTP状态码。
Java的Optional不能携带错误信息,仅代表值缺失。对于错误上下文,项目倾向于自定义错误类或使用Either类型(如vavr库)。Java没有内置Result类型,但可以用Try(vavr)或Result(arrow)模拟。
参考:https://aescc.cn/category/bedroom.html
四、C++:异常可选与错误码回归
C++支持异常,但许多大型项目(尤其是游戏和嵌入式)禁用异常(-fno-exceptions),转而使用以下方式:
返回错误码(如int,0表示成功)。
使用std::optional返回无值。
使用std::expected(C++23)作为正式的错误传递类型。
输出参数(引用或指针)配合返回bool。
std::expected类似Rust的Result,可以将错误信息与返回值一起传递,强迫调用者检查。由于C++没有?操作符,错误传播仍需手动if判断。
异常在C++中的问题包括:异常安全(需注意资源管理)、二进制大小增大、性能overhead(虽然未抛出时为零)。很多C++编码规范(如GoogleC++Style)禁止使用异常。
五、错误处理的发展趋势
Rust的Result类型启发了跨语言的趋势:Java项目使用Either/Try,PHP项目使用league/result等库,C++23有了std::expected。Result相比异常的优势是:
明确所有可能的错误,异常可能隐藏。
性能好(无栈展开)。
调用者必须处理(编译器警告[[nodiscard]])。
但异常在处理多层调用、复杂清理时更简洁。两种方式可以共存:对于业务可恢复错误使用Result,对于系统级不可恢复错误使用异常。
参考:https://aescc.cn/category/living-room.html
六、最佳实践
PHP:在库代码中抛出特定异常,在边界(Controller)捕获转换为响应。
Java:使用非受检异常为主,受检异常用于强制调用者处理。对于API设计,考虑返回Optional或自定义Result。
C++:若项目启用异常,则构造函数和操作符重载使用异常;否则统一使用std::expected或std::optional。
七、总结
错误处理没有银弹。熟悉多种模式并在项目中保持一致性,比争论哪种更好更重要。建议团队统一规范,并使用静态分析工具强制检查错误处理路径。
参考:https://aescc.cn