RAII

RAII,全称资源获取即初始化(英語:Resource Acquisition Is Initialization),它是在一些面向对象语言中的一种慣用法英语Programming idiom。RAII源于C++,在JavaC#DAdaValaRust中也有应用。1984-1989年期间,比雅尼·斯特勞斯特魯普安德鲁·柯尼希英语Andrew Koenig (programmer)在设计C++异常时,为解决資源管理英语Resource management (computing)时的異常安全英语Exception safety性而使用了该用法[1],后来比雅尼·斯特勞斯特魯普将其称为RAII[2]

RAII要求,资源的有效期与持有资源的对象的生命期英语Object lifetime严格绑定,即由对象的构造函数完成资源的分配英语Resource allocation (computer)(获取),同时由析构函数完成资源的释放。在这种要求下,只要对象能正确地析构,就不会出现资源泄漏问题。

作用[编辑]

RAII的主要作用是在不失代码简洁性[3]的同时,可以很好地保证代码的异常安全性。

下面的C++实例说明了如何用RAII访问文件和互斥量:

#include <string> #include <mutex> #include <iostream> #include <fstream> #include <stdexcept>   void write_to_file(const std::string & message) {     // 创建关于文件的互斥锁     static std::mutex mutex;       // 在访问文件前进行加锁     std::lock_guard<std::mutex> lock(mutex);       // 尝试打开文件     std::ofstream file("example.txt");     if (!file.is_open())         throw std::runtime_error("unable to open file");       // 输出文件内容     file << message << std::endl;       // 当离开作用域时,文件句柄会被首先析构 (不管是否抛出了异常)     // 互斥锁也会被析构 (同样地,不管是否抛出了异常) } 

C++保证了所有栈对象在生命周期结束时会被销毁(即调用析构函数)[4],所以该代码是异常安全的。无论在write_to_file函数正常返回时,还是在途中抛出异常时,都会引发write_to_file函数的堆栈回退,而此时会自动调用lock和file对象的析构函数。

当一个函数需要通过多个局部变量来管理资源时,RAII就显得非常好用。因为只有被构造成功(构造函数没有抛出异常)的对象才会在返回时调用析构函数[4],同时析构函数的调用顺序恰好是它们构造顺序的反序[5],这样既可以保证多个资源(对象)的正确释放,又能满足多个资源之间的依赖关系。

由于RAII可以极大地简化资源管理,并有效地保证程序的正确和代码的简洁,所以通常会强烈建议在C++中使用它。

典型用法[编辑]

RAII在C++中的应用非常广泛,如C++标准库中的lock_guard[6]便是用RAII方式来控制互斥量:

template <class Mutex> class lock_guard { private:     Mutex& mutex_;  public:     lock_guard(Mutex& mutex) : mutex_(mutex) { mutex_.lock(); }     ~lock_guard() { mutex_.unlock(); }      lock_guard(lock_guard const&) = delete;     lock_guard& operator=(lock_guard const&) = delete; }; 

程序员可以非常方便地使用lock_guard,而不用担心异常安全问题

extern void unsafe_code();  // 可能抛出异常  using std::mutex; using std::lock_guard;  mutex g_mutex;  void access_critical_section() {     lock_guard<mutex> lock(g_mutex);     unsafe_code(); } 

实际上,C++标准库的实现就广泛应用了RAII,典型的如容器智能指针等。

RRID[编辑]

RAII还有另外一种被称为RRID(Resource Release Is Destruction)的特殊用法[7],即在构造时没有“获取”资源,但在析构时释放资源。ScopeGuard[8]和Boost.ScopeExit[9]就是RRID的典型应用:

#include <functional>  class ScopeGuard { private:     typedef std::function<void()> destructor_type;      destructor_type destructor_;     bool dismissed_;  public:     ScopeGuard(destructor_type destructor) : destructor_(destructor), dismissed_(false) {}      ~ScopeGuard()     {         if (!dismissed_) {             destructor_();         }     }      void dismiss() { dismissed_ = true; }      ScopeGuard(ScopeGuard const&) = delete;     ScopeGuard& operator=(ScopeGuard const&) = delete; }; 

ScopeGuard通常用于省去一些不必要的RAII封装,例如

void foo() {     auto fp = fopen("/path/to/file", "w");     ScopeGuard fp_guard([&fp]() { fclose(fp); });      write_to_file(fp);                     // 异常安全 } 

D语言中,scope关键字也是典型的RRID用法,例如

void access_critical_section() {     Mutex m = new Mutex;     lock(m);      scope(exit) unlock(m);      unsafe_code();                  // 异常安全 }  Resource create() {     Resource r = new Resource();     scope(failure) close(f);      preprocess(r);                  // 抛出异常时会自动调用close(r)     return r; } 

與finally的比較[编辑]

虽然RAII和finally都能保证资源管理时的异常安全,但相对来说,使用RAII的代码相对更加简洁。 如比雅尼·斯特劳斯特鲁普所说,“在真实环境中,调用资源释放代码的次数远多于资源类型的个数,所以相对于使用finally来说,使用RAII能减少代码量。”[10]

例如在Java中使用finally来管理Socket资源

void foo() {     Socket socket;     try {         socket = new Socket();         access(socket);     } finally {         socket.close();     } } 

在采用RAII后,代码可以简化为

void foo() {     try (Socket socket = new Socket()) {         access(socket);     } } 

特别是当大量使用Socket时,重复的finally就显得没有必要。

参考资料[编辑]