除了C++之外的其他编程语言的程序员,是否使用、了解或理解RAII?

37

我注意到RAII在Stackoverflow上受到了很多关注,但在我的圈子里(主要是C++),RAII就像问什么是类或析构函数一样显而易见。

因此,我真的很好奇这是因为我每天都被硬核C++程序员包围,RAII在一般情况下(包括C++)并不那么出名,还是因为我现在接触的程序员没有接受过C++培训,在其他语言中人们根本不使用/知道RAII?


2
再次证明SO的价值。我通常会这样编程,但不知道它被正式称为RAII。谢谢。 - slashmais
3
BASIC程序员是否会想到OEG1K(On Error Goto 1000)? - rwong
其他编程语言有时会使用“执行周围模式”来实现类似的行为。 - StackedCrooked
17个回答

26
有很多原因导致RAII并不是很出名。首先,名称不是特别明显。如果我不知道RAII是什么,我肯定无法从名称中猜测出来。(资源获取即初始化?这与析构函数或清理有什么关系,这才是真正的RAII的特点?)
另一个原因是它在没有确定性清除的语言中效果不佳。
在C++中,我们知道何时调用析构函数,我们知道调用析构函数的顺序,并且我们可以定义它们做任何我们想做的事情。
在大多数现代语言中,所有东西都是垃圾收集的,这使得实现RAII变得更加棘手。没有理由不能将RAII扩展添加到C#等语言中,但这并不像在C++中那样明显。但正如其他人所提到的,Perl和其他语言支持RAII,尽管是垃圾收集的。
话虽如此,仍然可以在C#或其他语言中创建自己的RAII风格的包装器。我曾经在C#中这样做过。我不得不编写一些内容,以确保数据库连接在使用后立即关闭,这是任何C++程序员都应该看作是RAII的明显候选任务。当然,每当我们使用db连接时,我们可以将所有内容都包装在using语句中,但这只会使事情变得混乱和容易出错。
我的解决方案是编写一个帮助函数,该函数将委托作为参数,并且在调用时打开数据库连接,并在using语句内部将其传递给委托函数,伪代码:
T RAIIWrapper<T>(Func<DbConnection, T> f){
  using (var db = new DbConnection()){
    return f(db);
  }
}

虽然不如C++-RAII那么好,但它实现了大致相同的功能。每当我们需要一个DbConnection时,我们必须调用这个辅助函数,它保证在之后被关闭。


1
我是一名 .Net 开发者,所以您能否更明确地解释为什么 'using' 仍会导致问题(混乱和容易出错)?是因为在离开作用域后,'using' 范围内的资源并没有立即被处理而是等待垃圾回收吗?如果是这种情况,在什么情况下会出现错误? - wiz_lee

21

我经常使用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

一旦耗时操作结束,原始光标将自动恢复。


14

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::vector<int> v_;

    void jump();
};

如果有人不了解这一点,那可能是以下两种情况之一:

  • 他们对C++不熟悉。如果是这种情况,他们应该在编写下一个类之前重新阅读TCPPPL第三版的第14.4.1节,具体讲解了这个技巧。
  • 他们完全不了解C++。没关系,这个习惯用法非常符合C++的风格。要么学习C++,要么忘掉它,继续过自己的生活。最好是学习C++。 ;)

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


29
不确定为什么这个答案得到了投票。问题不是什么是RAII,而是该概念在非C++程序员中的地位。 - ApplePieIsGood
3
这是一个很好的解释,所以我理解为什么会被点赞。不过选择它作为答案有点奇怪。 :) - jalf
如果没有try/catch块并且抛出异常,析构函数不能保证在main函数之后被调用,这是实现定义的,MSVC不会调用它。 - paulm
这不就是 Python 的 with 语句吗?只不过使用 C++ 对象代替上下文。 - skywalker
1
Python的with与C#的using类似:你必须记得使用它,否则会出现泄漏。C++的RAII默认情况下是被使用的,你必须做一些特殊的事情来关闭它(例如,将一个对象new成一个裸指针(这在现代C++中是不可取的),并且泄漏)。 - C. K. Young
显示剩余3条评论

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

8
RAII的问题在于它的缩写,与概念没有明显的相关性。这与堆栈分配有什么关系?这就是它的关键点。 C++允许您在堆栈上分配对象,并保证在堆栈取消分配时调用其析构函数。鉴于此,RAII听起来像是一种有意义的封装方式吗?不是的。直到我几周前来到这里,我从未听说过RAII,甚至在阅读到有人发布他们永远不会雇用不知道RAII是什么的C++程序员时,我都不禁笑了出来。毫无疑问,这个概念对于大多数胜任的专业C++开发人员来说都是众所周知的。只是这个缩写很糟糕。

5

一个对@Pierre's answer的修改:

在Python中:

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

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

通常可以使用contextlib.closing来完成,文档中如下描述:

closing(thing): return a context manager that closes thing upon completion of the block. This is basically equivalent to:

from contextlib import contextmanager

@contextmanager
def closing(thing):
    try:
        yield thing
    finally:
        thing.close()

And lets you write code like this:

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

without needing to explicitly close page. Even if an error occurs, page.close() will be called when the with block is exited.


3

2

首先,我很惊讶RAII不是更为人所知!我原以为至少对C++程序员来说RAII是显而易见的。然而现在我可以理解为什么人们实际上会问这个问题。我周围,包括我自己,都是C++狂热者...

所以我的秘密...我想那就是,我过去总是阅读Meyers、Sutter [编辑:]和Andrei,直到我完全掌握了它。


我认为很多人知道这个概念,但不知道这个专业术语。 - mattlant
可能是因为他们没有将这两个联系起来。 - Robert Gould
这正是我了解RAII的方式。感谢Meyers、Sutters和Andrei! - nullDev
多年前学习C++时,这个缩写对我来说毫无意义。这就像问“走路时的LBW技巧”是什么一样。LBW?那是什么?左右看。好吧,当然我过马路时会这么做。你为什么不一开始就这么说呢?而且问这个问题有什么意义吗? - Jon Ericson

1
RAII的问题在于它需要确定性终止,这在C++中基于栈的对象是有保证的。而像C#和Java这样依赖垃圾回收的语言则没有这个保证,因此必须以某种方式“附加”上去。在C#中,通过实现IDisposable来完成这一点,然后基本上就会出现许多相同的使用模式,这也是“using”语句的动机之一,它确保了处理并且被广泛知晓和使用。
所以基本上这个习惯用法已经存在了,只是没有一个花哨的名字。

1
注意:'using'仅适用于函数局部变量,或者说生命周期完全限制在一个堆栈帧中的对象。它不适用于类静态成员、函数静态成员或类成员,也不适用于生命周期以其他方式作用域的堆分配对象。 - Aaron
1
IDispose != RAII。作为对象的用户,您必须在各个地方放置使用语句以获得相同的效果,即使这样做,如果一个对象嵌入了其他对象,则仍然无法正常工作。IDispose是调用每个对象上的“close”的语法糖,而不是RAII。 - gbjbaanb
C# 允许您在堆栈上分配内存,但是我认为您失去了类型安全保障,因为您实际上正在使用原始内存,并且在这样做时,在托管的世界中所有打赌都会失败。 - ApplePieIsGood
1
这个习惯用语在 C# 等语言中不存在 - 请暂时使用 C++,你会发现你刚才说的话是胡说八道。 - 1800 INFORMATION

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接