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要求,资源的有效期与持有资源的对象生命周期严格绑定,即由对象的构造函数完成资源的分配英语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就显得没有必要。

参考资料

编辑
  • Stroustrup, Bjarne. The C++ Programming Language [C++程序设计语言]. Addison-Wesley. 2000 [2014-09-29]. ISBN 0-201-70073-5. (原始内容存档于2014-06-30) (英语). 
  • Stroustrup, Bjarne. The Design and Evolution of C++ [C++语言的设计和演化]. Addison-Wesley. 1994 [2014-09-29]. ISBN 0-201-54330-3. (原始内容存档于2023-06-06) (英语). 
  • Wilson, Matthew. ,Imperfect C++ [Imperfect C++中文版]. Addison-Wesley. 2004 [2023-11-10]. ISBN 0321228774. (原始内容存档于2023-06-05) (英语). 

📚 Artikel Terkait di Wikipedia

B語言

note: auto declares a variable with automatic storage (lifetime is function scope), not "automatic typing" as in C++11. */ if (a = n / b) /* assignment, not

LISP

to define dynamic scope to mean indefinite scope and dynamic extent. Thus we speak of “special” variables as having dynamic scope, or being dynamically

Rust (编程语言)

[2011-04-20]. (原始内容存档于2011-07-20). After that last change fixing the logging scope context bug, looks like stage1/rustc builds. Just shy of midnight :)  catamorphism

POSIX线程

的值。缺省为PTHREAD_EXPLICIT_SCHED。 __scope,表示线程间竞争CPU的范围,也就是说线程优先级的有效范围。POSIX的标准中定义了两个值:PTHREAD_SCOPE_SYSTEM和PTHREAD_SCOPE_PROCESS,前者表示与系统中所有线程一起竞争CPU时间,后

C语言

与ALGOL一族的大多数过程式编程语言类似,C語言是一個有結構化程式設計、具有变量作用域(variable scope)以及遞迴功能的程序式語言。其采用的静态类型系统可以防止无意的程序设计操作。C语言中所有的可执行代码都被包含在子程序(函数)裡。其傳遞參數均是以值傳遞(pass

续体

storable. The possibility of assigning an escape object to a variable whose scope extends beyond the CATCH which created the object can be exploited to "return

Lucid语言

Wadge和Edward A. Ashcroft在1976年设计的,并描述于1985年的书籍《Lucid, the Dataflow Programming Language》。 pLucid是Lucid的首个解释器。 Lucid使用针对数据计算的需求驱动模型。每个语句都可以理解为一个方程,它

万人斩

Esther. Hong Kong Neo-Noir. 28 April 2017. ISBN 9781474412681.  A matter of 'Scope. [2022-10-10]. (原始内容存档于2022-10-11).  摘自Stephen Teo所著书籍《中国武术电影——武侠传统》