33

我注意到 RAII 在 Stackoverflow 上引起了很多关注,但在我的圈子(主要是 C++)中,RAII 非常明显,就像问什么是类或析构函数一样。

所以我真的很好奇这是因为我每天都被铁杆 C++ 程序员包围,而 RAII 通常并不为人所知(包括 C++),或者是否所有对 Stackoverflow 的质疑都是由于事实我现在接触的程序员不是用 C++ 长大的,而在其他语言中,人们只是不使用/不了解 RAII?

4

17 回答 17

25

RAII 不为人所知的原因有很多。首先,名字不是特别明显。如果我还不知道 RAII 是什么,我肯定永远不会从名字中猜到它。(资源获取是初始化?这与析构函数或清理有什么关系,这才是RAII的真正特征?)

另一个是它在没有确定性清理的语言中效果不佳。

在 C++ 中,我们确切地知道何时调用析构函数,知道调用析构函数的顺序,我们可以定义它们来做任何我们喜欢的事情。

在大多数现代语言中,一切都是垃圾收集的,这使得 RAII 实现起来更加棘手。没有理由不能将 RAII 扩展添加到 C# 中,但它不像 C++ 中那样明显。但正如其他人所提到的,Perl 和其他语言尽管被垃圾收集,但仍支持 RAII。

也就是说,仍然可以使用 C# 或其他语言创建自己的 RAII 样式的包装器。我不久前在 C# 中做过。我必须写一些东西来确保数据库连接在使用后立即关闭,这是任何 C++ 程序员都会认为是 RAII 的明显候选者的任务。当然,using每当我们使用数据库连接时,我们都可以将所有内容都包装在 -statements 中,但这只是混乱且容易出错。

我的解决方案是编写一个以委托为参数的辅助函数,然后在调用时打开数据库连接,并在 using 语句中将其传递给委托函数,伪代码:

T RAIIWrapper<T>(Func<DbConnection, T> f){
  using (var db = new DbConnection()){
    return f(db);
  }
}

仍然不如 C++-RAII 好或明显,但它实现了大致相同的目标。每当我们需要一个 DbConnection 时,我们都必须调用这个帮助函数来保证它之后会被关闭。

于 2008-12-28T16:47:06.867 回答
20

我一直使用 C++ RAII,但我也在 Visual Basic 6 中开发了很长时间,并且 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

一旦耗时的操作终止,原始游标会自动恢复。

于 2008-10-03T11:44:58.343 回答
14

RAII 代表Resource Acquisition Is Initialization。这根本不是语言无关的。这个口头禅在这里是因为 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::vector<int> v_;

    void jump();
};

如果人们没有意识到这一点,那是因为以下两件事之一:

  • 他们不太了解 C++。在这种情况下,他们应该在编写下一个类之前再次打开TCPPPL 。具体来说,本书第三版的 14.4.1 节讨论了这种技术。
  • 他们根本不懂 C++。没关系。这个成语非常 C++y。要么学习 C++,要么忘记这一切,继续你的生活。最好学习C++。;)
于 2008-10-03T05:10:38.880 回答
11

对于在此线程中评论 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<char>& 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<char> 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<char> buffer(1024);
    while (int hasRead = file.read(buffer)) {
        // process hasRead bytes, then shift them off the buffer
    }
}

这里,当StdioFile创建实例时,获取资源(本例中为文件流);当它被销毁时,资源被释放。不需要tryfinally阻止;如果读取导致异常,fclose则自动调用,因为它在析构函数中。

析构函数保证在函数离开时被调用main,无论是正常还是异常。在这种情况下,文件流被清理。世界再次安全。:-D

于 2008-10-03T04:58:38.707 回答
9

RAII。

它从构造函数和析构函数开始,但不仅如此。
这一切都是为了在出现异常的情况下安全地控制资源。

使 RAII 优于 finally 和此类机制的原因在于它使代码使用起来更安全,因为它将正确使用对象的责任从对象的用户转移到了对象的设计者身上。

读这个

使用 RAII 正确使用StdioFile的示例。

void someFunc()
{
    StdioFile    file("Plop","r");

    // use file
}
// File closed automatically even if this function exits via an exception.

使用 finally 获得相同的功能。

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 对象不是基于堆栈的,但由于引用计数而具有非常可控的生命周期。

于 2008-10-03T05:16:22.423 回答
8

RAII 的问题是首字母缩写词。它与概念没有明显的相关性。这与堆栈分配有什么关系?这就是归结为。C++ 使您能够在堆栈上分配对象并保证在堆栈展开时调用它们的析构函数。鉴于此,RAII 听起来像是一种有意义的封装方式吗?不。直到几周前我来到这里之前,我才听说过 RAII,当我读到有人发帖说他们永远不会雇用一个不知道 RAII 是什么的 C++ 程序员时,我什至不得不大笑。当然,大多数有能力的专业 C++ 开发人员都知道这个概念。只是首字母缩写词构思不佳。

于 2008-12-28T16:07:58.863 回答
5

@Pierre 的答案的修改:

在 Python 中:

with open("foo.txt", "w") as f:
    f.write("abc")

f.close()无论是否引发异常,都会自动调用。

一般来说,可以使用来自文档的contextlib.closure来完成:

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()。

于 2008-10-03T17:54:54.633 回答
3

Common Lisp 有 RAII:

(with-open-file (stream "file.ext" :direction :input)
    (do-something-with-stream stream))

见:http ://www.psg.com/~dlamkins/sl/chapter09.html

于 2009-02-27T20:36:26.890 回答
2

首先,我很惊讶它并不为人所知!我完全认为 RAII 至少对 C++ 程序员来说是显而易见的。但是现在我想我可以理解为什么人们实际上会问这个问题。我被包围了,我自己一定是,C++ 怪胎......

所以我的秘密.. 我想那就是,我曾经读过 Meyers、Sutter [EDIT:] 和 Andrei 几年前一直到我只是摸索它。

于 2008-10-03T05:02:31.207 回答
1

RAII 的问题在于它需要确定性的终结,这对于 C++ 中基于堆栈的对象是有保证的。依赖垃圾回收的 C# 和 Java 等语言没有这种保证,因此必须以某种方式“固定”它。在 C# 中,这是通过实现 IDisposable 来完成的,然后基本上会出现许多相同的使用模式,这是“使用”语句的动机之一,它确保了 Disposal 并且非常知名和使用。

所以基本上成语就在那里,只是没有一个花哨的名字。

于 2008-10-03T05:34:01.413 回答
1

RAII 是 C++ 中的一种方法,用于确保在代码块之后执行清理过程,而不管代码中发生了什么:代码正确执行到最后或引发异常。一个已经引用的示例是在处理文件后自动关闭文件,请参阅此处的答案

在其他语言中,您使用其他机制来实现这一点。

在 Java 中,您有 try { } finally {} 构造:

try {
  BufferedReader file = new BufferedReader(new FileReader("infilename"));
  // do something with file
}
finally {
    file.close();
}

在 Ruby 中,您有自动块参数:

File.open("foo.txt") do | file |
  # do something with file
end

在 Lisp 中,你有unwind-protect和预定义的with-XXX

(with-open-file (file "foo.txt")
  ;; do something with file
)

在 Scheme 中,您拥有dynamic-wind和预定义的with-XXXXX

(with-input-from-file "foo.txt"
  (lambda ()
    ;; do something 
)

在 Python 中你终于可以尝试了

try
  file = open("foo.txt")
  # do something with file
finally:
  file.close()

作为 RAII 的 C++ 解决方案相当笨拙,因为它迫使您为必须执行的各种清理创建一个类。这可能会迫使你编写很多愚蠢的小类。

RAII 的其他示例是:

  • 获取后解锁互斥锁
  • 打开后关闭数据库连接
  • 分配后释放内存
  • 登录和退出代码块
  • ...
于 2008-10-03T07:24:34.283 回答
0

这与知道何时调用您的析构函数有关,对吗?所以它并不完全与语言无关,因为这在许多 GC 语言中都没有。

于 2008-10-03T04:52:26.410 回答
0

我认为许多其他语言(delete例如没有 的语言)并没有给程序员对对象生命周期的完全相同的控制,因此必须有其他方法来提供资源的确定性处置。例如,在 C# 中,使用usingwithIDisposable很常见。

于 2008-10-03T04:54:22.570 回答
0

RAII 在 C++ 中很受欢迎,因为它是少数(唯一?)可以分配复杂范围局部变量但没有finally子句的语言之一。C#、Java、Python、Ruby 都有finally或等效的。finally当变量超出范围时,C 没有,但也无法执行代码。

于 2008-10-03T04:58:19.563 回答
0

我的同事是铁杆的,“阅读规范”C++ 类型。他们中的许多人都知道 RAII,但我从未真正听说过在那个场景之外使用它。

于 2008-10-11T18:00:59.800 回答
-1

CPython(用 C 编写的官方 Python)支持 RAII,因为它使用引用计数的对象和基于范围的立即销毁(而不是在收集垃圾时)。不幸的是,Jython(Java 中的 Python)和 PyPy 不支持这个非常有用的 RAII 习惯用法,并且它破坏了很多遗留的 Python 代码。因此,对于可移植的 python,您必须像 Java 一样手动处理所有异常。

于 2008-10-11T17:39:50.340 回答
-2

RAII 特定于 C++。C++ 具有堆栈分配对象、非托管对象生命周期和异常处理的必要组合。

于 2008-10-03T04:58:40.920 回答