}else {processName(name);if (ad(address) != OK) {retcode = BAD_READ;}else {processAddress(address);if (ad(telNo) != OK) {retcode = BAD_READ;}else {// etc, etc...}}}return retcode; 幸运的是,如果编程语言支持异常,你可以通过更为简洁的方式重写这段代码:retcode = OK;try {ad(name);process(name);ad(address);processAddress(address);ad(telNo);// etc, etc...}catch (IOException e) {retcode = BAD_READ;Lg("Error reading individual: " + tMessage());}return retcode; 现在正常的控制流很清晰,所有的错误处理都移到了一处。什么是异常情况 关于异常的问题之一是知道何时使用它们。我们相信,异常很少应作为程序的正常流程的一部分使用;异常应保留给意外事件。假定某个未被抓住的异常会终止你的程序,问问你自己:“如果我移走所有的异常处理器,这些代码是否仍然能运行?”如果答案是“否”,那么异常也许就正在被用在非异常的情形中。 例如,如果你的代码试图打开一个文件进行读取,而该文件并不存在,应该引发异常吗? 我们的回答是:“这取决于实际情况。”如果文件应该在那里,那么引发异常就有正当理由。某件意外之事发生了——你期望其存在的文件好像消失了。另一方面,如果你不清楚该文件是否应该存在,那么你找不到它看来就不是异常情况,错误返回就是合适的。 让我们看一看第一种情况的一个例子。下面的代码打开文件/etc/passwd,这个文件在所有的UNIX系统上都应该存在。如果它失败了,它会把FileNotFoundException传给它的调用者。public void open_passwd() throws FileNotFoundException {// This may throw FileNotFoundException...ipstream = new FileInputStream("/etc/passwd");// ...} 但是,第二种情况可能涉及打开用户在命令行上指定的文件。这里引发异常没有正当理由,代码看起来也不同:public boolean open_user_file(String name)throws FileNotFoundException {File f = new File(name);if (!ists()) {return false;}ipstream = new FileInputStream(f);return true;} 注意FileInputStream调用仍有可能生成异常,这个例程会把它传递出去。但是,这个异常只在真正异常的情形下才生成;只是试图打开不存在的文件将生成传统的错误返回。提示34Use Exceptions for Exceptional Problems将异常用于异常的问题 我们为何要提出这种使用异常的途径?嗯,异常表示即时的、非局部的控制转移——这是一种级联的(cascading)goto。那些把异常用作其正常处理的一部分的程序,将遭受到经典的意大利面条式代码的所有可读性和可维护性问题的折磨。这些程序破坏了封装:通过异常处理,例程和它们的调用者被更紧密地耦合在一起。错误处理器是另一种选择 错误处理器是检测到错误时调用的例程。你可以登记一个例程处理特定范畴的错误。处理器会在其中一种错误发生时被调用。 有时你可能想要使用错误处理器,或者用于替代异常,或者与异常一起使用。显然,如果你使用像C这样不支持异常的语言,这是你的很少几个选择之一(参见下一页的“挑战”)。但是,有时错误处理器甚至也可用于拥有良好的内建异常处理方案的语言(比如Java)。 考虑一个客户-服务器应用的实现,它使用了Java的Remote Method Invocation(RMI)设施。因为RMI的实现方式,每个对远地例程的调用都必须准备处理RemoteException。增加代码处理这些异常可能会变得让人厌烦,并且意味着我们难以编写既能与本地例程、也能与远地例程一起工作的代码。一种绕开这一问题的可能方法是把你的远地对象包装在非远地的类中。这个类随即实现一个错误处理器接口,允许客户代码登记一个在检测到远地异常时调用的例程。相关内容:l 死程序不说谎,120页挑战l 不支持异常的语言常常拥有一些其他的非局部控制转移机制(例如,C拥有longjmp/setjmp)。考虑一下怎样使用这些设施实现某种仿造的异常机制。其好处和危险是什么?你需要采取什么特殊步骤确保资源不被遗弃?在你编写的所有C代码中使用这种解决方案有意义吗?练习21. 在设计一个新的容器类时,你确定可能有以下错误情况: (解答在292页)(1) add例程中的新元素没有内存可用(2) 在fetch例程中找不到所请求的数据项(3) 传给add例程的是null指针应怎样处理每种情况?应该生成错误、引发异常、还是忽略该情况?25怎样配平资源“我把你带进这个世界,”我的父亲会说:“我也可以把你赶出去。那没有我影响。我要再造另一个你。” ——Bill Cosby,Fatherhood 只要在编程,我们都要管理资源:内存、事务、线程、文件、定时器——所有数量有限的事物。大多数时候,资源使用遵循一种可预测的模式:你分配资源、使用它,然后解除其分配。 但是,对于资源分配和解除分配的处理,许多开发者没有始终如一的计划。所以让我们提出一个简单的提示:提示35Finish What You Start要有始有终 在大多数情况下这条提示都很容易应用。它只是意味着,分配某项资源的例程或对象应该负责解除该资源的分配。让我们通过一个糟糕的代码例子来看一看该提示的应用方式——这是一个打开文件、从中读取消费者信息、更新某个字段、然后写回结果的应用。我们除去了其中的错误处理代码,以让例子更清晰:void readCustomer(const char *fName, Customer *cRec) {cFile = fopen(fName, "r+");fread(cRec, sizeof(*cRec), 1, cFile);}void writeCustomer(Customer *cRec) {rewind(cFile);fwrite (cRec, sizeof(*cRec), 1, cFile);fclose(cFile);}void updateCustomer(const char *fName, double newBalance) {Customer cRec;readCustomer(fName, &cRec);cRlance = newBalance;writeCustomer(&cRec);} 初看上去,例程updateCustomer相当好。它似乎实现了我们所需的逻辑——读取记录,更新余额,写回记录。但是,这样的整洁掩盖了一个重大的问题。例程readCustomer和writeCustomer紧密地耦合在一起[27]——它们共享全局变量cFile。readCustomer打开文件,并把文件指针存储在cFile中,而writeCustomer使用所存储的指针在其结束时关闭文件。这个全局变量甚至没有出现在updateCustomer例程中。 这为什么不好?让我们考虑一下,不走运的维护程序员被告知规范发生了变化——余额只应在新的值不为负时更新。她进入源码,改动updateCustomer:void updateCustomer(const char *fName, double newBalance) {Customer cRec;readCustomer(fName, &cRec);if (newBalance >= 0.0) {cRlance = newBalance;writeCustomer(&cRec);}} 在测试时一切似乎都很好。但是,当代码投入实际工作,若干小时后它就崩溃了,抱怨说打开的文件太多。因为writeCustomer在有些情形下不会被调用,文件也就不会被关闭。 这个问题的一个非常糟糕的解决方案是在updateCustomer中对该特殊情况进行处理:void updateCustomer(const char *fName, double newBalance) {Customer cRec;readCustomer(fName, &cRec);if (newBalance >= 0.0) {cRlance = newBalance;writeCustomer(&cRec);}elsefclose(cFile);} 这可以修正问题——不管新的余额是多少,文件现在都会被关闭——但这样的修正意味着三个例程通过全局的cFile耦合在一起。我们在掉进陷阱,如果我们继续沿着这一方向前进,事情就会开始迅速变糟。 要有始有终这一提示告诉我们,分配资源的例程也应该释放它。通过稍稍重构代码,我们可以在此应用该提示:void readCustomer(FILE *cFile, Customer *cRec) {fread(cRec, sizeof(*cRec), 1, cFile);}void writeCustomer(FILE *cFile, Customer *cRec) {rewind(cFile);fwrite(cRec, sizeof(*cRec), 1, cFile);}void updateCustomer(const char *fName, double newBalance) {FILE *cFile;Customer cRec;cFile = fopen(fName, "r+"); // >---readCustomer(cFile, &cRec); // /if (newBalance >= 0.0) { // /cRlance = newBalance; // /writeCustomer(cFile, &cRec); // /} // /fclose(cFile); // () { return n; }};void doSomething2(void) {NodeResource n;try {// do something}catch (...) {throw;}} 现在包装类NodeResource确保了在其对象被销毁时,相应的节点也会被销毁。为了方便起见,包装提供了解除引用操作符->,这样它的使用者可以直接访问所包含的Node对象中的字段。