ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Ruby 异常处理完全指南:raise、rescue、ensure 与回溯机制的深入解析

Ruby 异常处理完全指南:raise、rescue、ensure 与回溯机制的深入解析 Ruby 异常处理完全指南raise、rescue、ensure 与回溯机制的深入解析【免费下载链接】rubyThe Ruby Programming Language项目地址: https://gitcode.com/GitHub_Trending/ru/ruby导读本文以 Ruby 语言官方文档 doc/language/exceptions.md 为核心系统讲解 Ruby 中从抛出异常到捕获异常的完整机制包括 begin/rescue/else/ensure 各子句的语义、$!与$全局变量、cause 链、重抛re-raise、retry 重试、自定义异常类、消息增强detailed_message/full_message与 ANSI 高亮以及回溯backtrace的获取与设置。同时结合本仓库Ruby 官方源码中 error.c、eval.c 与 bootstraptest/test_exception.rb 的实现与测试从源码层面印证文档所述行为帮助你写出健壮、可维护、可诊断的 Ruby 异常处理代码。一、什么是异常Ruby 代码可以抛出异常raise exceptions。大多数情况下抛出异常是为了提醒正在运行的程序出现了一种异常unusual即exceptional的情况可能需要被处理。Ruby 核心库、标准库以及第三方 gem 中的代码都会在特定情况下生成异常。例如File.open(nope.txt) # Raises Errno::ENOENT: No such file or directory这条语句试图打开一个不存在的文件Ruby 会抛出一个Errno::ENOENT异常。在源码层面这对应 error.c 中make_errno_exc与rb_syserr_new等函数的调用路径例如rb_sys_fail_path等系统调用封装会把底层errno转换为对应的SystemCallError子类实例。二、异常的抛出Raised Exceptions抛出异常后程序的执行流无论如何都会被转移transfers program execution。2.1 未被捕获的异常Unrescued Exceptions如果一个异常没有被rescue捕获详见下文已捕获的异常执行权会转移到 Ruby 解释器内部的一段代码——它负责打印消息并退出程序或线程$ ruby -e raise -e:1:in main: unhandled exception从源码看这条路径始于 eval.c 中的rb_exc_raise/rb_raise_jump最终通过rb_longjmp以TAG_RAISE标签跳转到异常处理框架若整条调用链上都没有匹配的处理器解释器会打印unhandled exception并终止程序。2.2 已捕获的异常Rescued Exceptions一个异常处理器exception handler可以决定异常抛出后发生什么处理器可以捕获rescue异常从而阻止程序退出。一个简单的例子begin raise Boom! # Raises an exception, transfers control. puts Will not get here. rescue puts Rescued an exception. # Control transferred to here; program does not exit. end puts Got here.输出Rescued an exception. Got here.2.3 异常处理器的组成元素一个异常处理器由以下几个元素组成元素用途Begin 子句Begin clause开始处理器包含那些一旦抛出异常就可能被捕获的代码。一个或多个 Rescue 子句rescue clauses每个子句包含捕获代码针对特定异常执行。Else 子句可选当 begin 子句中没有异常抛出时执行的代码。Ensure 子句可选无论是否抛出异常、是否被捕获都会执行的代码。end语句结束整个处理器。三、异常处理器的各个子句详解3.1 Begin 子句begin 子句开始一个异常处理器可以以begin语句开头也可以没有 begin参见下文无 begin 的异常处理器。包含其覆盖范围内可能抛出异常的代码。以紧随其后的第一个rescue语句结束。3.2 Rescue 子句一个 rescue 子句以rescue语句开始。包含针对特定已抛出异常执行的代码。以紧随其后的第一个rescue、else、ensure或end语句结束。捕获哪些异常rescue语句可以指定一个或多个要捕获的异常类如果未指定任何类则默认假定为StandardError。rescue 子句会捕获指定的类或未指定时的StandardError及其所有子类begin 1 / 0 # Raises ZeroDivisionError, a subclass of StandardError. rescue puts Rescued #{$!.class} end输出Rescued ZeroDivisionError如果rescue语句指定了异常类则只有该类或其子类会被捕获。下面的例子中程序最终以ZeroDivisionError退出——因为它不是ArgumentError或其子类所以没有被捕获begin 1 / 0 rescue ArgumentError puts Rescued #{$!.class} endrescue语句还可以同时指定多个类此时只要异常属于其中任意一个类或其子类就会被捕获begin 1 / 0 rescue FloatDomainError, ZeroDivisionError puts Rescued #{$!.class} end多个 rescue 子句异常处理器可以包含多个 rescue 子句此时第一个能捕获该异常的子句生效它之前和之后的子句都会被忽略begin Dir.open(nosuch) rescue Errno::ENOTDIR puts Rescued #{$!.class} rescue Errno::ENOENT puts Rescued #{$!.class} end输出Rescued Errno::ENOENT捕获异常对象rescue语句可以指定一个变量其值将被赋为被捕获的异常一个Exception或其子类的实例begin 1 / 0 rescue x puts x.class puts x.message end输出ZeroDivisionError divided by 0全局变量$!与$两个只读全局变量在正常情况下永远为nil只有在 rescue 子句内才有值$!包含被捕获的异常对象。$包含该异常的回溯backtrace数组。begin 1 / 0 rescue p $! p $ end输出#ZeroDivisionError: divided by 0 [t.rb:2:in Integer#/, t.rb:2:in main]异常链Cause在 rescue 子句中Exception#cause方法返回上一个$!的值可能为nil在其它位置调用则始终返回nil。下面的嵌套示例清晰地展示了 cause 的链式行为begin raise(Boom 0) rescue x0 puts Exception: #{x0.inspect}; $!: #{$!.inspect}; cause: #{x0.cause.inspect}. begin raise(Boom 1) rescue x1 puts Exception: #{x1.inspect}; $!: #{$!.inspect}; cause: #{x1.cause.inspect}. begin raise(Boom 2) rescue x2 puts Exception: #{x2.inspect}; $!: #{$!.inspect}; cause: #{x2.cause.inspect}. end end end输出Exception: #RuntimeError: Boom 0; $!: #RuntimeError: Boom 0; cause: nil. Exception: #RuntimeError: Boom 1; $!: #RuntimeError: Boom 1; cause: #RuntimeError: Boom 0. Exception: #RuntimeError: Boom 2; $!: #RuntimeError: Boom 2; cause: #RuntimeError: Boom 1.从源码看cause 链由 eval.c 中rb_f_raise的实现支撑raise会先通过extract_raise_options解析出cause关键字参数随后在rb_raise_jump之前调用rb_ivar_set(exception, id_cause, resolved_cause)把前一个$!写入新异常的cause实例变量若异常对象已冻结则先rb_obj_dup复制一份。3.3 Else 子句else子句以else语句开始。包含在 begin 子句中没有抛出异常时执行的代码。以紧随其后的第一个ensure或end语句结束。begin puts Begin. rescue puts Rescued an exception! else puts No exception raised. end输出Begin. No exception raised.3.4 Ensure 子句ensure子句以ensure语句开始。包含无论如何都会执行的代码——无论是否抛出异常也无论抛出的异常是否被处理。以紧随其后的第一个end语句结束。def foo(boom: false) puts Begin. raise Boom! if boom rescue puts Rescued an exception! else puts No exception raised. ensure puts Always do this. end foo(boom: true) foo(boom: false)输出Begin. Rescued an exception! Always do this. Begin. No exception raised. Always do this.ensure是清理资源的惯用位置例如关闭文件、释放锁、断开连接。本仓库的引导测试 bootstraptest/test_exception.rb 中大量使用了ensure与rescue、raise的组合来验证解释器在这类控制流下的行为正确性。3.5 End 语句end语句结束整个处理器。只有已抛出的异常被捕获时end之后的代码才会被执行。3.6 无 begin 的异常处理器Begin-Less Exception Handlers如上面def foo所示异常处理器不一定需要begin关键字。它还可以是方法体def foo(boom: false) # Serves as beginning of exception handler. puts Begin. raise Boom! if boom rescue puts Rescued an exception! else puts No exception raised. end # Serves as end of exception handler.块blockDir.chdir(.) do |dir| # Serves as beginning of exception handler. raise Boom! rescue puts Rescued an exception! end # Serves as end of exception handler.也就是说任何方法体或块都可以直接充当 begin/end 的载体无需显式写出begin。四、重抛异常Re-Raising an Exception有时捕获异常是有用的但我们希望保留它最终的效果例如程序捕获一个异常、记录其数据然后恢复这个异常。这可以通过raise方法以特殊方式实现rescue 子句可以捕获异常针对该异常做必要的事情比如记录日志调用不带任何参数的raise方法它将重新抛出当前被捕获的异常begin 1 / 0 rescue ZeroDivisionError # Do needful things (like logging). raise # Raised exception will be ZeroDivisionError, not RuntimeError. end输出ruby t.rb t.rb:2:in Integer#/: divided by 0 (ZeroDivisionError) from t.rb:2:in main注意这里重新抛出的是ZeroDivisionError而非RuntimeError。从源码看无参raise的重抛语义在 eval.c 的rb_f_raise中实现当argc 0时函数通过get_errinfo()取出当前线程的$!将其作为要抛出的异常对象如果当前没有待处理的异常$!为 nil则会抛出RuntimeError。此外如果只传了cause:关键字而没有其它参数会抛出ArgumentErroronly cause is given with no arguments。五、重试Retrying有时重试 begin 子句是有用的例如当它必须访问一个可能不稳定的资源如网页时多次尝试访问是有价值的期望它可能变得可用retries 0 begin puts Try ##{retries}. raise Boom rescue puts Rescued retry ##{retries}. if (retries 1) 3 puts Retrying retry else puts Giving up. raise end endTry #0. Rescued retry #0. Retrying Try #1. Rescued retry #1. Retrying Try #2. Rescued retry #2. Giving up. # RuntimeError (Boom) raised.注意retry会重新执行整个 begin 子句而不仅仅是失败点之后的部分。六、抛出异常Kernel#raise 的源码级分析Kernel#raise方法用于抛出异常。上面的重抛、重试示例中使用的raise都是这个方法。从实现角度看Kernel#raise对应 eval.c 中的rb_f_raise其核心流程为用extract_raise_options解析cause:关键字参数无参调用时重抛场景通过get_errinfo()取当前$!用rb_make_exception根据参数构造异常对象可以是异常类的实例、字符串消息或异常对象本身必要时把cause写入异常的cause实例变量调用rb_raise_jump最终通过rb_longjmp(ec, TAG_RAISE, mesg, cause)触发异常跳转见 eval.c。几个与raise相关的要点raise不指定异常类而只给字符串时抛出RuntimeError。这一点在 error.c 的 RuntimeError 文档注释中有明确说明Kernel#raise will raise a RuntimeError if no Exception class is specified.示例raise ouch抛出RuntimeError: ouch。raise可以直接接收一个异常对象也可以接收类 消息等组合形式。正如前文所述raise支持cause:关键字参数来显式指定异常链的上游异常。本仓库的引导测试 bootstraptest/test_exception.rb 对raise/rescue/ensure的组合行为包括在 ensure 中抛出新异常、在 rescue 中重抛等做了系统性验证可作为理解这些语义边界的参考。七、自定义异常Custom Exceptions为了提供额外的或替代性的信息你可以创建自定义异常类。每个自定义异常类都应该继承自某个内置异常类通常是StandardError或RuntimeErrorclass MyException StandardError; end这样MyException及其子类的实例可以被标准的rescue MyException捕获同时也会被不指定类的rescue默认StandardError捕获。内置异常类的完整层次结构可以在源码 error.c 的Init_Exception中找到例如Exception所有异常的基类StandardError←TypeError、ArgumentError、IndexError、KeyError、RangeError、NameError、NoMethodError、RuntimeError、FrozenError、EncodingError等ScriptError←SyntaxError、LoadError、NotImplementedError直接继承Exception不在StandardError之下SystemExit、SignalException←Interrupt、fatal直接继承ExceptionSecurityError、NoMemoryError直接继承ExceptionSystemCallErrorErrno模块下所有系统错误类的基类了解这一层次结构对选择捕获范围至关重要例如rescue Exception会捕获包括SystemExit、NoMemoryError在内的几乎所有异常通常会掩盖严重问题因此实践上默认只 rescueStandardError及其子类。八、异常消息Messages每个Exception对象都有一个消息message它是在对象创建时设置的字符串。消息创建后不能被修改但你可以通过Exception#exception创建一个带不同消息的相似对象。返回消息的方法有Exception#message按原样返回消息。Exception#detailed_message返回增强版消息——附加异常类名并支持可选高亮。Exception#full_message返回增强版消息——附加异常类名和回溯并支持可选高亮。后两个方法都接受关键字参数highlight当highlight: true时返回的字符串会包含加粗和下划线的 ANSI 转义码以增强消息的显示效果见下节。任何异常类Ruby 内置或自定义都可以选择重写这两个方法并可以选择将highlight: true关键字参数解释为返回的消息中应包含指定颜色、加粗和下划线的 ANSI 码。在源码中这两个方法分别实现在 error.c 的exc_detailed_message与exc_full_message并通过rb_define_method(rb_eException, detailed_message, ...)和rb_define_method(rb_eException, full_message, ...)注册到Exception类上见Init_Exception。值得注意的是Ruby 会延迟加载某些 gem 对detailed_message的扩展如 DidYouMean、ErrorHighlight、SyntaxSuggest 的detailed_message装饰器相关机制见 error.c 中rb_eager_load_detailed_message_extension的注释与实现。8.1 ANSI 高亮代码表由于增强后的消息可能被写入非终端设备例如 HTML 页面官方文档建议将 ANSI 码限制为以下广泛支持的代码开始字体颜色颜色ANSI 码红Red\e[31m绿Green\e[32m黄Yellow\e[33m蓝Blue\e[34m品红Magenta\e[35m青Cyan\e[36m开始字体属性属性ANSI 码加粗Bold\e[1m下划线Underline\e[4m结束以上所有效果效果ANSI 码重置Reset\e[0m同时官方文档建议即便 ANSI 码以原样而不是被解释为字体指令的形式出现也应该让消息保持方便人类阅读的形态。九、回溯Backtraces回溯backtrace是对当前调用栈中方法的记录其中每个方法都已被调用但尚未返回。以下方法返回回溯信息Exception#backtrace以字符串数组或nil形式返回回溯。Exception#backtrace_locations以Thread::Backtrace::Location对象数组或nil形式返回回溯。每个Thread::Backtrace::Location对象提供关于某个被调用方法的详细信息。默认情况下Ruby 将异常的回溯设置为抛出异常的位置。开发者可以通过给Kernel#raise提供backtrace参数或使用Exception#set_backtrace来调整。关于backtrace与backtrace_locations的关系需要注意默认情况下backtrace和backtrace_locations表示的是同一个回溯如果开发者通过上述方法把回溯设置为Thread::Backtrace::Location数组它们仍然表示同一个回溯如果开发者把回溯设置为字符串或字符串数组通过Kernel#raise设置时backtrace_locations变为nil通过Exception#set_backtrace设置时backtrace_locations保留原值如果开发者通过Exception#set_backtrace将回溯设置为nilbacktrace_locations保留原值但如果随后该异常被重新抛出reraised则backtrace和backtrace_locations都会变为重抛位置。在源码层面这些方法注册于 error.c 的Init_Exceptionbacktraceexc_backtrace、backtrace_locationsexc_backtrace_locations、set_backtraceexc_set_backtrace同时提供 C APIrb_exc_set_backtrace。其中backtrace方法在异常被raise时可能被调整——源码注释明确指出 The value returned by this method might be adjusted when raising (see Kernel#raise)。而full_message的输出正是异常类名 消息 回溯的组合这也是我们在终端里看到的标准报错格式。十、总结与最佳实践默认捕获StandardErrorrescue不带类时捕获StandardError及其子类若要精确捕获请显式指定异常类可多个。用 x或$!获取异常对象$!/$仅在 rescue 子句内有效推荐使用rescue x显式绑定变量作用域更清晰。善用ensure做清理文件、锁、连接等资源的释放放在ensure中保证异常与否都会执行。用else区分成功分支begin 中无异常时的逻辑放进else避免与异常处理逻辑混在一起。用无参raise重抛在 rescue 中完成日志等处理后调用裸raise保留原始异常类型与位置。用retry做有限次重试注意retry会重跑整个 begin 子句务必设计退出条件防止死循环。自定义异常继承StandardError便于统一捕获也避免意外捕获SystemExit、NoMemoryError等严重异常。善用cause与detailed_message/full_message异常链帮助定位根因增强消息帮助终端可读性自定义类可重写这两个方法以提供更友好的诊断信息。本文所述内容均可在本仓库Ruby 官方源码中找到实现依据语言文档见 doc/language/exceptions.md异常类层次与消息/回溯方法实现在 error.cKernel#raise与 cause 处理在 eval.c语义边界的行为验证可参考引导测试 bootstraptest/test_exception.rb。【免费下载链接】rubyThe Ruby Programming Language项目地址: https://gitcode.com/GitHub_Trending/ru/ruby创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表