6.4 出口只要一个

    为什么引入 finally

    微软公司为什么会引入 finally 呢?是为了解决什么样的问题呢?他们是这样回答的。

    采用结构化异常处理可以提高代码的可靠性。比如,程序在程序员预料之外结束时,也可以正确地释放锁定的内存和文件等资源。另外,针对内存不足等特定问题时,不需要使用 goto 语句或细致地检查返回值,使用简洁的结构化代码就可以应对。
    ——“异常处理(SEH)”
    http://msdn.microsoft.com/ja-jp/library/aa984822(v=vs.71).aspx

    成对操作的无遗漏执行

    在程序设计中有很多成对的操作,比如内存锁定后要释放,文件打开后要关闭,上锁之后要解锁等。成对的操作只要执行了其中一个,就要保证另一个也能确实执行。微软公司前面提到的“正确地释放资源”,指的就是无遗漏地执行成对操作。对错误处理,则要能够不使用返回值检查和 goto 语句,简洁地实现。

    拿美术馆来打个比方吧。在入口借出的语音导航仪随后需要全部回收。只有一个出口的话,全部回收并非难事。但是如果有多个出口,不在每一个出口都配备专人的话,就不一定能做到无遗漏全部回收。如果连墙壁都没有,随处都可以出去的话,要无遗漏地全部回收就更困难了。

    程序设计也一样。如果在入口处执行了 lock,随后就需要执行 unlock。如果函数出口(return)只有一个,只要在出口前执行 unlock 即可。但如果有好几个 return,就需要在每一个 return 前面执行 unlock。一个被调用的函数,如果在多处都有抛出异常的可能性,那么在很多个地方都有可能跳出这个函数,此时要无遗漏地执行 unlock 就变得非常困难了。

    C语言
    lock(m);
    / 需要上锁的处理 /
    if(…){
    unlock(m); / ←在出口前解锁 /
    return;
    }
    / 需要上锁的处理 /
    unlock(m); / ←在出口前解锁 /
    return;

    这个问题的解决办法有三种。

    使用 finally 的解决方案

    finally 代码块在 try 代码块执行结束后一定会被执行到,而不管 try 代码块中是否发生异常。1990 年左右,微软公司开始使用 finally,1995 年发布的 Java 语言也引入了 finally。现今,Python 语言和 Ruby 语言也支持同样的语句结构 16。

    16顺便说一下,Python 语言中还有 else 小节,这些代码在 try 小节成功执行后能继续被执行并且不需要捕捉可能的异常。
    Java
    try{
    / 可能出错的代码 /
    } catch(…) {
    / 异常处理代码 /
    } finally {
    / 必将执行的代码 /
    }

    Ruby
    begin
    # 可能出错的代码
    rescue
    # 异常处理代码
    ensure
    # 必将执行的代码
    end

    Python
    try:
    # 可能出错的代码
    except:
    # 异常处理代码
    finally:
    # 必将执行的代码

    没有 finally 的 C++ 语言的解决方案

    C++ 语言中没有 finally。那它是如何表现不管异常是否发生都要执行的代码的呢?

    C++ 语言中使用了一种名叫 RAII(Resource Acquisition Is Initialization,资源获取即初始化)的技术。比如,在操作打开了就要关闭的文件对象时,定义来操作该对象的类,用构造函数打开,用析构函数关闭 17。

    17不习惯 C++ 语言的读者可以这样理解:简单来讲,构造函数和析构函数就是对象在被创建时调用的初始化处理和在消除时调用的事后处理。
    C++
    class SampleRAII {
    public:
    // 构造函数
    SampleRAII()
    : resource(lock()) {
    }

    // 析构函数
    ~SampleRAII() {
    unlock();
    }
    ……
    }

    这种技术在函数结束时,针对函数的局部变量,程序可以自动地调用析构函数 18。C++ 语言设计者斯特劳斯特卢普认为,比起使用 finally,这种方法更为优雅。

    18严格来讲,不是函数而是空间范围,为避免 C++ 语言里复杂的空间范围的话题,此处做了简单化处理。另外,实际上 lock 和 unlock 是带有参数的,代码会比这更复杂,这一点也进行了简单化处理。

    D 语言中 scope (exit) 的解决方案

    2001 年出现的 D 语言以改良 C++ 语言为目标,反对 RAII 是优雅的这一意见。

    打开了就要关闭这样紧密关联的操作,反映在代码上时,如果能放在相近的位置就容易理解多了。基于这一考虑,D 语言中引入了作用域守护(scope guard)的概念。通过使用作用域守护,可以事先定义从某一作用域(如函数)跳出时执行的操作。

    D语言
    void abc()
    {
    Mutex m = new Mutex;

    lock(m); // 锁住mutex
    scope(exit) unlock(m); // 定义作用域结束时的解锁操作

    foo(); // 执行操作
    }