Exceptions, Catch, and Throw

2024-06-09 09:38
文章标签 catch exceptions throw

本文主要是介绍Exceptions, Catch, and Throw,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

我们已经开发了一些代码,比较完美的是暂时还没有出现错误。每个库都可以成功调用,用户从不输入无效的数据,并且资源丰富且易获得。但事事无常。欢迎来到真实的世界。

在真实的世界中错误时常发生。好的程序和程序会预计到它们的出现并且合理地处理它们。不过要做到这点并是如同想像的那么简单。通常一段发现错误的代码是没有相应的上下文指导其如何进行下一步的。比如,当尝试打开一个不存在的文件时,有些环境是可以接受的,对另外一些环境却是重大的错误。你的文件处理模块是如何做的呢?

传统方式都是返回错误码。open
方法会返回指定值用于表达当前操作的失败。这个值通过调用过程传递回去,直到某人想对其做出反应为止。

这种处理方式的问题在于管理过多的错误代码会十分痛苦。如果一个函数按顺序分别调用了
openreadclose,每个方法都可能返回错误信息,这个函数的调用者应该如何从返回值中区分这些错误代码?

异常最大程度地解决了这个问题。异常可以让你将错误信息打包成一个对象。异常对象会自动传回调用栈,直到运行时系统发现它,并且能明确这种类型的异常如何处理时为止。

异常类

Exception 类及其子类的对象都可以将异常信息包装起来。Ruby
预定义了一个整洁的异常体系,详情可见在 91 页的表
8.1。如同稍后我们要了解的一样,这个体系让我们更容易考虑对异常的处理。

当你需要抛出异常时,你可以用预定义的 Exception 类,也可以用自己定义的。如果想自定义异常,需要将其作为 SandardError 的子类,或继承 SandardError 的子类。如果不这样做,自定义的异常默认是不会被捕捉的。

每个 Exception
都关联着信息字符串和栈内回溯信息。如果是你自定义的异常,你也可以添加额外信息。

处理异常

我们的点唱机通过 TCP
接口从网络下载歌曲。最简单的代码可以这样写:

opFile = File.open(opName, "w")
while data = socket.read(512)opFile.write(data)
end

如果在下载中途遇到重大的报错应该怎么办?可以确认的是,我们不希望在歌单中存储未传输完成的歌曲。

让我们添加一些异常处理的代码并看看对我们是否有帮助。我们用 begin/end
将可能会抛出异常的代码包围起来,并用 rescue 语法告诉 Ruby
我们需要处理的异常类型。在这个例子中,我们主要关注
SystemCallError 异常(当然,这也暗含了 SystemCallError
的异常子类),这就是需要通过 rescue
处理的事情。在错误处理的代码块中,我们不仅要报告错误,关闭和删除输出文件,还需要将异常抛出。

opFile = File.open(opName, "w")
begin# Exceptions raised by this code will# be caught by the following rescue clausewhile data = socket.read(512)opFile.write(data)endrescue SystemCallError$stderr.print "IO failed: " + $!opFile.closeFile.delete(opName)raise
end

当异常被抛出时,包括随后处理的异常,Ruby 都会将它们的 Exception
对象引用关联至全局变量 $!
(这个感叹号反应出我们面对代码出现的错误时表示的惊讶)。在之前的例子中,我们通过变量对错误信息进行格式化。

在关闭及删除文件之后,我们直接调用 raise
并且没有传递参数,它会通过 $!
抛出异常。这是一个有用的技巧,它允许你写的代码对异常进行过滤,将当前无法处理的异常抛出至更高层级。不过对于错误流程它也喜欢实现一个继承体系。

begin 代码块中你也会有多个 rescue 句式,每个 rescue
都可以指定多个异常抛出。在每个 rescue 语句的结尾你都会给
Ruby 一个局部变量名以接收匹配的异常。许多人发现这样比 $!
全局替换的方式可读性更高。

begineval string
rescue SyntaxError, NameError => boomprint "String doesn't compile: " + boom
rescue StandardError => bangprint "Error running script: " + bang
end"'"

Ruby 是如何决定哪个 rescue 语句应该执行的?它的执行方式和
case 声明十分相似。对 begin 代码块中的每个 rescue 语句而言,Ruby
依次将引发的异常与每个参数进行比较。如果引发的异常与参数匹配,Ruby
将执行 rescue 体的代码并停止查找。匹配操作是通过
$!.kind_of?(parameter)
方法实现,因此无论异常与参数是相同的类还是参数是异常的祖先类都将匹配成功。如果 rescue 是无参语句,会默认参数是 StandardError

如果没有任何 rescue 语句可以匹配,或异常是在 begin/end
代码块外被引发,Ruby
将移动堆栈并在调用者中寻找异常处理器,如果没有找到将延着调用链一直查找。

尽管 rescue 语句的参数代表 Exception
类的名字,但实际上这些参数可以是任意返回 Exception
类的表达式(也包括方法调用)。

打扫房间

有时你需要保证无论是否有异常被引起,某些流程都在代码块结束后再运行。比如,你可能需要在进入代码时开启文件,并且退出代码块时它会被关闭。

ensure 就可以实现这个功能。ensure 在最后一个 rescue
和代码块调用当前代码块完成之后调用。是否正常退出代码块并不影响它,无论是引发异常并被捕捉,还是被未捕捉的异常停掉,ensure
代码块都将继续运行。

f = File.open("testfile")
begin# .. process
rescue# .. handle error
ensuref.close unless f.nil?
end

else 语句也是类似地使用,尽管很少用到。如果使用了
else,它将会在 rescue 之后,在 ensure
之前运行。else
体中的代码只会在主体代码没有引起异常的情况下执行。

f = File.open("testfile")
begin# .. process
rescue# .. handle error
elseputs "Congratulations-- no errors!"
ensuref.close unless f.nil?
end

再做一次

有时你也许需要确认异常的原因。在这些例子中,你能够在 rescue 中使用
retry 声明重复整个 begin/end
代码块。我们清楚的是,当前这是一块巨大区域的无限循环,所以使用这个功能时需要当心(要将手指轻置于停止键)。

作为重试异常的样例,可以看看接下来这段改编至 Minero Aoki net/smtp.rb
库的代码。

@esmtp = truebegin# First try an extended login. If it fails because the# server doesn't support it, fall back to a normal loginif @esmtp then@command.ehlo(helodom)else@command.helo(helodom)endrescue ProtocolErrorif @esmtp then@esmtp = falseretryelseraiseend
end'

例子中的代码首先尝试使用 HELO 命令连接 SMTP
服务器,HELO
命令一般情况机器都是支持的。如果连接失败,代码将把 @estmp
变量设置为 false
并且再次尝试连接服务器。如果再次失将会向调用者抛出异常。

抛出异常

我们已经在其他地方抛出的异常处理上了解了许多。也是时候扭转局势主动进攻了。

你可以使用 Kernel::raise 方法抛出异常。

raise
raise "bad mp3 encoding"
raise InterfaceException, "Keyboard failure", caller

例子中的第一种形式是简单地将异常抛出(如果抛出时没有将异常作为参数默认是
RuntimeError)。这种方式通常在传递异常前拦截异常的处理器中使用。

第二种形式会创建新 RuntimeError
异常,通过传递的字符串设置它的信息。并且异常会被抛出到调用栈。

第三种形式会使用第一个参数创建异常,将第二个参数设置为异常信息,并根据堆栈追踪返回抛出给第三个参数。一般情况下第一个参数都是
Exception
体系中一个类的名字,也可以是这些类对象的引用。堆栈追踪一般通过
Kernel::caller 方法产生。

这有一些 raise 实际的例子。

raiseraise "Missing name" if name.nil?if i >= myNames.sizeraise IndexError, "#{i} >= size (#{myNames.size})"
endraise ArgumentError, "Name too big", caller

在最后的例子中,我们将当前例程从栈回溯中移除了,栈回溯通常在库模块中有用。我们可以更进一步,下面的代码会将两个例程从回溯中移除。

raise ArgumentError, "Name too big", caller[1..-1]

向异常中添加信息

你也可以定义自己的异常,它可以存储你需要从错误端暴露的信息。例如,确切的网络错误类型在环境中转瞬即逝。如果类似错误出现时环境没有问题,你便可以通过异常中的标识告知处理器,它应该再次尝试相同的操作。

class RetryException < RuntimeErrorattr :okToRetrydef initialize(okToRetry)@okToRetry = okToRetryend
end

可能有某处代码如下一样,一个转瞬即逝的错误发生了。

def readData(socket)data = socket.read(512)if data.nil?raise RetryException.new(true), "transient read error"end# .. normal processing
end

在调用栈的更高层级中我们将对异常进行处理。

beginstuff = readData(socket)# .. process stuff
rescue RetryException => detailretry if detail.okToRetryraise
end

捕捉和抛出

当程序运行出错时通过 raiserescue
机制放弃执行是种良好的方式,它可以很好地从深层次嵌套结构从跳出。这也是
catchthrow 迟早用到的地方。

catch (:done)  dowhile getsthrow :done unless fields = split(/\t/)songList.add(Song.new(*fields))endsongList.play
end

catch 通过预设的名字定义了一个代码块(也许是一个 Symbol
也可以是一个字符串)。除非有 throw
的发生,否则这个代码块将正常运行。

当 Ruby 运行至 throw
时,它将把调用栈压缩,以寻找有相同匹配符号的 catch
代码块。当寻找到后,Ruby
会将调用栈解压至目标点并暂停代码块。如果调用 throw
时选填了第二个参数,此参数将作为 catch
的值返回。因此,在之前的例子中如果输入不包含指定格式的内容,throw
将跳转至相同的 catch 结尾,不止是结束掉 while
循环同时也会跳过歌单的播放。

接下来的例子中,如果用户输入「!」我们将通过 throw
停止与用户的互动。

def promptAndGet(prompt)print promptres = readline.chompthrow :quitRequested if res == "!"return res
endcatch :quitRequested doname = promptAndGet("Name: ")age  = promptAndGet("Age:  ")sex  = promptAndGet("Sex:  ")# ..# process information
end

如同这个例子表明的一样,throw 不一定需要出现在 catch 的静态域中。


本文翻译自《Programming Ruby》,主要目的是自己学习使用,文中翻译不到位之处烦请指正,如需转载请注明出处

本章原文为 Exceptions, Catch, and
Throw

这篇关于Exceptions, Catch, and Throw的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1044816

相关文章

面试题之final,finally和finalize的区别以及如果catch里面有return语句,请问finally里面的代码还会执行吗?

/*  * 面试题:  * 1:final,finally和finalize的区别  * final:最终的意思,可以修饰类,成员变量,成员方法  *         修饰类,类不能被继承  *         修饰变量,变量是常量  *         修饰方法,方法不能被重写  * finally:是异常处理的一部分,用于释放资源。  *         一般来

面试题:throws和throw的区别

throw:如果出现了异常情况,我们可以把该异常抛出,这个时候的抛出的应该是异常的对象。     throws和throw的区别(面试题)     throws         用在方法声明后面,跟的是异常类名         可以跟多个异常类名,用逗号隔开         表示抛出异常,由该方法的调用者来处理         throws表示出现异常的一种可能性,并不一定会发生这些异常

c++中 throw try catch的浅显应用

#include <iostream>#include <stdlib.h>using namespace std;void func(int a,int b){if(b==0)throw b;cout<<a/b<<endl;}int main(){int a,b;while (cin>>a>>b){try{func(a,b);}catch(...){cout<<"是否需要重新

Java中try,catch,finally的用法

1。可以在方法签名上加上异常 public T method1() throws AException { try{ }catch(BExcption e) { throw new AException(); } } 2。可以再次捕获异常 public T method1(){ try{ }catch(BExcption e) { try{   doSomething();//这个语句可能

5、catch中发生了未知异常,finally代码块如何应对?

catch中发生了未知异常,finally还会执行么? catch发生了异常,finally还是会执行的,并且是finally执行完成后,才会抛出catch中的异常。 不过catch会吃掉try中抛出的异常,为了避免这种情况,在一些可以预见catch中会发生异常的地方,先把try抛出的异常打印出来,这样从日志中就可以看到完整的异常信息。

try...catch...finally块嵌入return

不论C++还是Java中,try...catch...finally语句块都是用来控制程序异常的处理,而finally块是最后一定执行的,那么现在在try...catch...块中加入了return语句,finally仍会执行吗? public class Try_Return_Final {int test(){int x = 1;try{return x;}finally{++x;}}p

SP2010开发和VS2010专家食谱--第七章节--使用客户端对象模型(2)--Handling exceptions

之前文章中,我们创建了带有自定义域和一些数据的列表。如果这些调用成批发送给服务器,我们如何处理异常呢?本文中,我们将创建控制台应用程序处理异常。

【面试干货】throw 和 throws 的区别

【面试干货】throw 和 throws 的区别 1、throw1.1 示例 2、throws2.1 示例 3、总结 💖The Begin💖点点关注,收藏不迷路💖 在Java中,throw和throws都与异常处理紧密相关,但它们在使用和含义上有明显的区别。 1、throw throw 语句用于在方法体内明确地抛出一个异常。 当throw语句被执行时,它会立

C#学习使用try-catch-finally错误处理表达式

查了下catch语句异常的类型,做了总结。 public class TryCatchFinally_Test : MonoBehaviour {void Start () {//try-catch错误处理表达式try{//执行的代码,其中可能有异常。一旦发现异常,则立即跳到catch执行。否则不会执行catch里面的内容}//catch可以有多个,也可以没有,每个catch可以处理一个