错误处理与异常

错误处理与异常


  • https://www.kancloud.cn/imxieke/ruby-base/107297


关于错误处理

在程序执行的过程中,通常会有以下错误发生:

  • 数据错误

    在计算家庭收支的时候,若在应该写金额的一栏上填上了商品名,那么就无法计算。此外,HTML 这种格式的数据的情况下,如果存在没有关闭标签等语法错误,也会导致无法处理数据。

  • 系统错误

    硬盘故障等明显的故障,或者没把 CD 插入到驱动器等程序无法恢复的问题。

  • 程序错误

    因调用了不存在的方法、弄错参数值或算法错误而导致错误结果等,像这样,程序本身的缺陷也可能会导致错误。

    程序在运行时可能会遇到各种各样的错误。如果对这些错误放任不管,大部分程序都无法正常运行,因此我们需要对这些错误做相应的处理。

  • 排除错误的原因

    在文件夹中创建文件时,如果文件夹不存在,则由程序本身创建文件夹。如果程序无法创建文件夹,则需要再考虑其他解决方法。

  • 忽略错误

    程序有时候也会有一些无伤大雅的错误。例如,假设运行程序时需要读取某个配置文件,如果我们事前已经在程序中准备好了相应配置的默认值,那么即使无法读取该设定文件,程序也可以忽略这个错误。

  • 恢复错误发生前的状态

    向用户提示程序发生错误,指导用户该如何进行下一步处理。

  • 重试一次

    曾经执行失败的程序,过一段时间后再重新执行可能就会成功。

  • 终止程序

    只是自己一个人用的小程序,也许本来就没必要做错误处理。

    而至于实际应该采取何种处理,则要根据程序代码的规模、应用程序的性质来决定,不能一概而论。但是,对于可预期的错误,我们需要留意以下两点:

  • 是否破坏了输入的数据,特别是人工制作的数据。

  • 是否可以对错误的内容及其原因做出相应的提示。

    覆盖了原有文件、删除了花费大量时间输入的数据等,像这样的重要数据的丢失、破坏可以说是灾难性的错误。另外,如果错误是由用户造成的,或者程序自身不能修复的话,给用户简明易懂的错误提示,会大大提升程序的用户体验。

异常处理

在程序执行的过程中,如果程序出现了错误就会发生异常。异常发生后,程序会暂时停止运行,并寻找是否有对应的异常处理程序。如果有则执行,如果没有,程序就会显示类似以下信息并终止运行。

该信息的格式如下:

from 开头的行表示发生错误的位置。

没有异常处理的编程语言的情况下,编程时就需要逐个确认每个处理是否已经处理完毕。在这类编程语言中,大部分程序代码都被花费在错误处理上,因此往往会使程序变得繁杂。

异常处理有以下优点:

  • 程序不需要逐个确认处理结果,也能自动检查出程序错误

  • 会同时报告发生错误的位置,便于排查错误

  • 正常处理与错误处理的程序可以分开书写,使程序便于阅读

异常处理的写法

Ruby 中使用 begin ~ rescue ~ end 语句描述异常处理。

在 Ruby 中,异常及其相关信息都是被作为对象来处理的。在 rescue 后指定变量名,可以获得异常对象。

即使不指定变量名,Ruby 也会把异常对象赋值给变量 $!。不过,把变量名明确地写出来会使程序更加易懂。

异常发生时被自动赋值的变量

变量 | 意义

  • | - $! | 最后发生的异常(异常对象) $@ | 最后发生的异常的位置信息

异常对象的方法

方法名 | 意义

  • | - class | 异常的种类 message | 异常信息 backtrace | 异常发生的位置信息($@ 与 $!.backtrace 是等价的)

下面是 Unix 的 wc 命令的简易版。结果会输出参数中指定的各文件的行数、单词数、字数(字节数),最后输出全部文件的统计结果。

执行示例

在(A)处无法打开文件时,程序会跳到 rescue 部分。这时,异常对象被赋值给变量 ex,(B)部分的处理被执行。

如果程序中指定了不存在的文件,则会提示发生错误,如下所示。提示发生错误后,并不会马上终止程序,而是继续处理下一个文件。

如果发生异常的方法中没有 rescue 处理,程序就会逆向查找调用者中是否定义了异常处理。下面来看看下图这个例子。调用 foo 方法,尝试打开一个不存在的文件。若 File.open 方法发生异常,那么该异常就会跳过 foo 方法以及 bar 方法,被更上一层的 rescue 捕捉。

然而,并不是说每个方法都需要做异常处理,只需根据实际情况在需要留意的地方做就可以了。在并不特别需要解决错误的情况下,也可以不捕捉异常。当然,不捕捉异常就意味着如果有问题发生程序就会马上终止。

后处理

不管是否发生异常都希望执行的处理,在 Ruby 中可以用 ensure 关键字来定义。

现在,假设我们要实现一个拷贝文件的方法,如下所示。下面的 copy 方法是把文件从 from 拷贝到 to

在(A)部分,如果程序不能打开原文件,那么就会发生异常并把异常返回给调用者。这时,不管接下来的处理是否能正常执行,src 都必须得关闭。关闭 src 的处理在(C)部分执行。ensure 中的处理,在程序跳出 begin ~ end 部分时一定会被执行。即使(B)中的目标文件无法打开,(C)部分的处理也同样会被执行。

重试

rescue 中使用 retry 后,begin 以下的处理会再重做一遍。

在下面的例子中,程序每隔 10 秒执行一次 File.open,直到能成功打开文件为止,打开文件后再读取其内容。

不过需要注意的是,如果指定了无论如何都不能打开的文件,程序就会陷入死循环中。

rescue 修饰符

if 修饰符、unless 修饰符一样,rescue 也有对应的修饰符。

如果表达式 1 中发生异常,表达式 2 的值就会成为整体表达式的值。也就是说,上面的式子与下面的写法是等价的:

我们再来看看下面的例子:

Integer 方法当接收到 "123" 这种数值形式的字符串参数时,会返回该字符串表示的整数值,而当接收到 "abc" 这种非数值形式的字符串参数时,则会抛出异常(在判断字符串是否为数值形式时经常用到此方法)。在本例中,如果 val 是不正确的数值格式,就会抛出异常,而 0 则作为 = 右侧整体表达式的返回值。像这样,这个小技巧经常被用在不需要过于复杂的处理,只是希望简单地对变量赋予默认值的时候。

异常处理语法的补充

如果异常处理的范围是整个方法体,也就是说整个方法内的程序都用 begin ~ end 包含的话,我们就可以省略 begin 以及 end,直接书写 rescueensure 部分的程序。

同样,我们在类定义中也可以使用 rescue 以及 ensure。但是,如果类定义途中发生异常,那么异常发生部分后的方法定义就不会再执行了,因此一般我们不会在类定义中使用它们。

指定需要捕捉的异常

当存在多个种类的异常,且需要按异常的种类分别进行处理时,我们可以用多个 rescue 来分开处理。

通过直接指定异常类,可以只捕捉我们希望处理的异常。

在本例中,程序如果无法打开 file1 就会打开 file2。程序中捕捉的 Errno::ENOENT 以及 Errno::EACCES,分别是文件不存在以及没权限打开文件时发生的异常。

异常类

之前我们提到过异常也是对象。Ruby 中所有的异常都是 Exception 类的子类,并根据程序错误的种类来定义相应的异常。下图为 Ruby 标准库中的异常类的继承关系。

rescue 中指定的异常的种类实际上就是异常类的类名。rescue 中不指定异常类时,程序会默认捕捉 StandardError 类及其子类的异常。

rescue 不只会捕捉指定的异常类,同时还会捕捉其子类。因此,我们在自己定义异常时,一般会先定义继承 StandardError 类的新类,然后再继承这个新类。

这样定义后,通过以下方式捕捉异常的话,同时就会捕捉 MyError 类的子类 MyError1MyError2MyError3 等。

在本例中,

上述写法的作用是定义一个继承 StandardError 类的新类,并将其赋值给 MyError 常量。这与 class 语句定义类的效果是一样的。

使用 class 语句,我们可以进行定义方法等操作,但在本例中,由于我们只需要生成继承 StandardError 类的新类就可以了,所以就向大家介绍了这个只需 1 行代码就能实现类的定义的简洁写法。

主动抛出异常

使用 raise 方法,可以使程序主动抛出异常。在基于自己判定的条件抛出异常,或者把刚捕捉到的异常再次抛出并通知异常的调用者等情况下,我们会使用 raise 方法。

raise 方法有以下 4 种调用方式:

  • raise message

    抛出 RuntimeError 异常,并把字符串作为 message 设置给新生成的异常对象。

  • raise 异常类

    抛出指定的异常。

  • raise 异常类,message

    抛出指定的异常,并把字符串作为 message 设置给新生成的异常对象。

  • raise

    rescue 外抛出 RuntimeError。在 rescue 中调用时,会再次抛出最后一次发生的异常($!)。