我注意到RAII在Stackoverflow上得到了很多关注,但在我的圈子里(主要是C++),RAII非常明显,就像问什么是类或析构函数一样.
所以我真的很好奇,如果那是因为我每天都被硬核C++程序员所包围,并且RAII一般不是众所周知的(包括C++),或者如果Stackoverflow上的所有这些问题都归因于事实我现在正在与没有使用C++成长的程序员联系,而在其他语言中,人们只是不使用/了解RAII?
有很多原因可以解释为什么不知道RAII.首先,名称不是特别明显.如果我还不知道RAII是什么,我肯定不会从名字中猜出来.(资源获取是初始化?这与析构函数或清理有什么关系,这是RAII的真正特征?)
另一个原因是它在没有确定性清理的语言中不能很好地工作.
在C++中,我们确切地知道析构函数何时被调用,我们知道调用析构函数的顺序,并且我们可以定义它们以执行我们喜欢的任何操作.
在大多数现代语言中,一切都是垃圾收集,这使得RAII实现起来更加棘手.没有理由不能将RAII扩展添加到C#中,但它并不像C++那样明显.但正如其他人所提到的,Perl和其他语言支持RAII尽管被垃圾收集.
也就是说,仍然可以用C#或其他语言创建自己的RAII样式包装器.我刚才用C#做过.我必须写一些东西以确保数据库连接在使用后立即关闭,任何C++程序员都会看到这个任务是RAII的明显候选者.当然,using
每当我们使用数据库连接时,我们都可以将所有内容包装在-statements中,但这只是混乱且容易出错.
我的解决方案是编写一个辅助函数,它将一个委托作为参数,然后在调用时,打开一个数据库连接,并在using语句中,将它传递给委托函数,伪代码:
T RAIIWrapper(Func f){ using (var db = new DbConnection()){ return f(db); } }
仍然不如C++ - RAII那么好或明显,但它实现了大致相同的事情.每当我们需要DbConnection时,我们必须调用这个辅助函数,以保证它之后会被关闭.
我一直使用C++˚RAII,但我也在VB6中开发了很长时间,RAII在那里一直是一个广泛使用的概念(尽管我从来没有听过有人称之为).
事实上,许多VB6程序都非常依赖RAII.我反复看到的一个更奇怪的用途是以下小类:
' WaitCursor.cls ' Private m_OldCursor As MousePointerConstants Public Sub Class_Inititialize() m_OldCursor = Screen.MousePointer Screen.MousePointer = vbHourGlass End Sub Public Sub Class_Terminate() Screen.MousePointer = m_OldCursor End Sub
用法:
Public Sub MyButton_Click() Dim WC As New WaitCursor ' … Time-consuming operation. ' End Sub
一旦耗时的操作终止,原始光标将自动恢复.
RAII代表资源获取是初始化.这根本不是语言无关的.这个咒语在这里是因为C++以它的工作方式工作.在C++中,在构造函数完成之前,不会构造对象.如果尚未成功构造对象,则不会调用析构函数.
翻译成实用语言,构造函数应该确保它涵盖了无法彻底完成其工作的情况.例如,如果在构造期间发生异常,那么构造函数必须正常处理它,因为析构函数不会在那里提供帮助.这通常通过覆盖构造函数中的异常或将此麻烦转发给其他对象来完成.例如:
class OhMy { public: OhMy() { p_ = new int[42]; jump(); } ~OhMy() { delete[] p_; } private: int* p_; void jump(); };
如果jump()
构造函数中的调用抛出我们遇到麻烦,因为p_
会泄漏.我们可以像这样修复:
class Few { public: Few() : v_(42) { jump(); } ~Few(); private: std::vectorv_; void jump(); };
如果人们没有意识到这一点,那么这是因为以下两件事之一:
他们不太了解C++.在这种情况下,他们应该在编写下一个课程之前再次打开TCPPPL.具体来说,本书第三版的第14.4.1节谈到了这种技术.
他们根本不懂C++.没关系.这个成语非常C++ y.要么学习C++,要么忘记这一切,继续你的生活.最好学习C++.;)
对于正在评论RAII(资源获取是初始化)的人来说,这是一个激励性的例子.
class StdioFile { FILE* file_; std::string mode_; static FILE* fcheck(FILE* stream) { if (!stream) throw std::runtime_error("Cannot open file"); return stream; } FILE* fdup() const { int dupfd(dup(fileno(file_))); if (dupfd == -1) throw std::runtime_error("Cannot dup file descriptor"); return fdopen(dupfd, mode_.c_str()); } public: StdioFile(char const* name, char const* mode) : file_(fcheck(fopen(name, mode))), mode_(mode) { } StdioFile(StdioFile const& rhs) : file_(fcheck(rhs.fdup())), mode_(rhs.mode_) { } ~StdioFile() { fclose(file_); } StdioFile& operator=(StdioFile const& rhs) { FILE* dupstr = fcheck(rhs.fdup()); if (fclose(file_) == EOF) { fclose(dupstr); // XXX ignore failed close throw std::runtime_error("Cannot close stream"); } file_ = dupstr; return *this; } int read(std::vector& buffer) { int result(fread(&buffer[0], 1, buffer.size(), file_)); if (ferror(file_)) throw std::runtime_error(strerror(errno)); return result; } int write(std::vector const& buffer) { int result(fwrite(&buffer[0], 1, buffer.size(), file_)); if (ferror(file_)) throw std::runtime_error(strerror(errno)); return result; } }; int main(int argc, char** argv) { StdioFile file(argv[1], "r"); std::vector buffer(1024); while (int hasRead = file.read(buffer)) { // process hasRead bytes, then shift them off the buffer } }
这里,当StdioFile
创建实例时,获取资源(在这种情况下为文件流); 当它被销毁时,资源被释放.没有try
或finally
需要阻止; 如果读取导致异常,fclose
则会自动调用,因为它在析构函数中.
main
无论是正常还是异常,保证在函数离开时调用析构函数.在这种情况下,清理文件流.世界再次安全.:-D
RAII.
它以构造函数和析构函数开头,但它不止于此.
它是关于在存在异常的情况下安全地控制资源.
是什么让RAII最终得到优势,而且这种机制使代码更安全,因为它将对象的使用权从对象的用户正确地移动到对象的设计者.
读这个
使用RAII正确使用StdioFile的示例.
void someFunc() { StdioFile file("Plop","r"); // use file } // File closed automatically even if this function exits via an exception.
最终获得相同的功能.
void someFunc() { // Assuming Java Like syntax; StdioFile file = new StdioFile("Plop","r"); try { // use file } finally { // close file. file.close(); // // Using the finaliser is not enough as we can not garantee when // it will be called. } }
因为您必须显式添加try {} finally {}块,这使得这种编码方法更容易出错(即,对象的用户需要考虑异常).通过使用RAII异常,必须在实现对象时对安全性进行编码.
问题是这个C++具体.
简答:没有.
更长的答案:
它需要具有定义生命周期的构造函数/析构函数/异常和对象.
从技术上讲,它不需要例外.当异常可能被使用时,它变得更有用,因为它使得在异常存在的情况下控制资源变得非常容易.
但是在控制可以提前离开函数并且不执行所有代码的所有情况下都很有用(例如,从函数中提前返回.这就是为什么C中的多个返回点是一个糟糕的代码气味,而C++中的多个返回点不是代码味[因为我们可以使用RAII清理]).
在C++中,受控生命周期是通过堆栈变量或智能指针实现的.但这不是我们唯一能够实现严格控制的生命周期.例如,Perl对象不是基于堆栈的,但由于引用计数而具有非常可控的寿命.
RAII的问题是首字母缩略词.它与概念没有明显的相关性.这与堆栈分配有什么关系?这就是它归结为什么.C++使您能够在堆栈上分配对象,并保证在堆栈展开时调用它们的析构函数.鉴于此,RAII听起来像是一种有意义的封装方式吗?不,我几个星期前来到这里之前从未听说过RAII,当我看到有人发帖说他们永远不会雇用一个不知道RAII是什么的C++程序员时,我甚至不得不笑.当然,大多数所有有能力的专业C++开发人员都知道这个概念.这只是首字母缩略词的构思很差.
修改@ Pierre的回答:
在Python中:
with open("foo.txt", "w") as f: f.write("abc")
f.close()
是否引发异常,自动调用.
一般而言,可以做到用contextlib.closing,从documenation:
closing(thing)
:返回一个上下文管理器,它在块完成时关闭.这基本上相当于:from contextlib import contextmanager @contextmanager def closing(thing): try: yield thing finally: thing.close()并允许您编写如下代码:
from __future__ import with_statement # required for python version < 2.6 from contextlib import closing import urllib with closing(urllib.urlopen('http://www.python.org')) as page: for line in page: print line无需明确关闭页面.即使发生错误,也会在退出with块时调用page.close().