Deterministic Destruction & RAII —— 资源管理的利器
正如每一个熟悉标准C++的程序员所清楚的:由C++构造及析构函数的语义保证所支持的RAII(“资源获取即初始化”[2])技术是资源自动和安全管理的利器,这里的资源可以包括内存,文件句柄,mutex,lock等。通过正确的使用RAII,管理资源的代码可以变得惊人的优雅和简单。相信有经验的C++程序员都熟悉应该类似下面的语句:
{
ofstream outf(“ out .txt”);
out << ”...”;
...
} // outf在这里析构!
这里,程序员根本不用手动清理outf,在函数结束(outf超出作用域)时,outf会自动析构,并释放其所有资源。即使后续的代码抛出了异常,C++语言也能保证析构函数会被调用。事实上,在异常抛出后,栈开解(stack unwind)的过程中,所有已经正确构造起来的局部对象都会被析构。这就为异常环境中资源的管理提供了一种强大而优雅的方式。
而对于C#或Java,代码就没有这么优雅了(特别是java)——C#虽然有using关键字,但是代码仍然显得臃肿,而Java为了保证在异常情况下资源能够正常释放,不得不用了丑陋冗长的try-finally块,在情况变得复杂化时,C#的和Java的代码都会变得越发臃肿。
那么,在C++/CLI中,原来的那种优雅的,靠析构函数来确保资源正确释放的手段还存在吗?答案正如你所期望和熟悉的,RAII仍然可以使用,仍然和标准C++中的能力一样强大:
ref关键字表示该类是Managed类。所有的ref类都继承自一个公共基类System::Object。至于struct和class的区别仍然和标准C++中的一样。如你所见,对于ref类,你同样可以像在标准C++中那样定义析构函数,该析构函数会在确定的时候被调用——也就是D超出作用域时。一切都与你以前的经验相符。
值得注意的是,对于了解Java或C#的程序员,ref类的析构函数就是Dispose(),你不必也不应该另外手动定义一个Dispose()成员函数。那么,Finalize函数到那里去了?既然ref类创建在托管堆上,那么迟早要被GC回收,这时候,应该被调用的Finalize函数在哪儿呢?C++/CLI为此引入了一个新的语法符号“!D()”,这就是D的Finalize函数,这个“!D”函数被调用的时机是不确定的,要看GC什么时候决定回收该类占用的空间。
~D()析构函数和标准C++里的用法完全相同,释放以前获取的资源。而对!D()的用法则和Finalize函数一样,由于其调用时机是不确定的,所以千万不要依赖于它来释放关键资源(如文件句柄,Lock等)。
为ref类引入~D()和!D()极大的方便了资源管理,也符合了标准C++程序员所熟悉的方式。Herb Sutter[3]把这个能力看成C++/CLI在Managed环境下最为强大的能力之一。
... {
D()...{System::Console::WriteLine(“in D::D() ”);}
~D()...{System::Console::WriteLine(“in D::~D() ”);}
!D()...{System::Console::WriteLine(“Finalized! ”);}
} ;
int main()
... {
D d; // in D::D()
...
} // d在这里析构!in D::~()