简而言之就是,不管当前作用域以何种方式退出,某某操作(通常是资源释放)都一定要被执行。
这个问题的答案其实C++程序员们应该耳熟能详了:RAII。RAII是C++最为强大的特性之一。在C++里面,局部变量的析构函数刚好满足这个语意:无论当前作用域以何种方式退出,所有局部变量的析构函数都必然会被倒着调用一遍。所以只要将有待释放的资源包装在析构函数里面,就能够保证它们即便在异常发生的情况下也会被释放掉了。为此C++提供了一系列的智能指针:auto_ptr、scoped_ptr、scoped_array… 此外所有的STL容器也都是RAII的。在C++里面模拟D的scope(exit)也是利用的RAII。
RAII相较于java的finally的好处和C#的scoped using的好处是非常明显的。只要一段代码就高下立判:
// in Java
String ReadFirstLineFromFile( String path )
{
StreamReader r = null;
String s = null;
try {
r = new StreamReader(path);
s = r.ReadLine();
} finally {
if ( r != null ) r.Dispose();
}
return s;
}
// in C#
String ReadFirstLineFromFile( String path )
{
using ( StreamReader r = new StreamReader(path) ) {
return r.ReadLine();
}
}
显然,Java版本的(try-finally)最臃肿。C#版本(scoped using)稍微好一些,但using毕竟也不属于程序员关心的代码逻辑,仍然属于代码噪音;况且如果不连续地申请N个资源的话,使用using就会造成层层嵌套结构。
如果使用RAII手法来封装StreamReader类的话(std::fstream就是RAII类的一个范例),代码就简化为:
// in C++, using RAII
String ReadFirstLineFromFile(String path)
{
StreamReader r(path);
return r.ReadLine();
}
好处是显而易见的。完全不用担心资源的释放问题,代码也变得“as simple as possible”。此外,值得注意的是,以上代码只是演示了最简单的情况,其中需要释放的资源只有一个。其实这个例子并不能最明显地展现出RAII强大的地方,当需要释放的资源有多个的时候,RAII的真正强大之处才被展现出来,一般地说,如果一个函数依次申请N个资源:
void f()
{
acquire resource1;
…
acquire resource2;
…
acquire resourceN;
…
}
那么,使用RAII的话,代码就像上面这样简单。无论何时退出当前作用域,所有已经构造初始化了的资源都会被析构函数自动释放掉。然而如果使用try-finally的话,f()就变成了:
void f()
{
try {
acquire resource1;
… // #1
acquire resource2;
… // #2
acquire resourceN;
… // #N
} finally {
if(resource1 is acquired) release resource1;
if(resource2 is acquired) release resource2;
…
if(resourceN is acquired) release resourceN;
}
}
为什么会这么麻烦呢,本质上就是因为当执行流因异常跳到finally块中时,你并不知道执行流是从#1处、#2处…还是#N处跳过来的,所以你不知道应该释放哪些资源,只能挨个检查各个资源是否已经被申请了,如果已申请了便将其释放;要能够检查每个资源是否已经被申请了,往往就要求你要在函数一开始将各个资源的句柄全都初始化为null,这样才可以通过if(hResN==null)来检查第N个资源是否已经申请。
最后,RAII其实是scope(exit)的特殊形式。但在资源释放方面,RAII有其特有的优势:如果使用scope(exit)的话,每个资源分配之后都需要用一个scope(exit)跟在后面保护起来;而如果用RAII的话,一个资源申请就对应于一个RAII对象的构造,释放工作则被隐藏在对象的析构函数中,从而使代码主干保持了清爽。