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

时间:2008-10-03 04:48:45

标签: c++ language-agnostic raii

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

所以我真的很好奇,如果那是因为我每天被硬核C ++程序员所包围,并且RAII一般不是众所周知的(包括C ++),或者如果所有这些问题都在Stackoverflow上到期了事实上,我现在正在接触那些没有使用C ++成长的程序员,而在其他语言中,人们只是不使用/了解RAII?

17 个答案:

答案 0 :(得分:24)

有很多理由说明为什么不知道RAII。首先,名称不是特别明显。如果我还不知道RAII是什么,我肯定不会从名字中猜出来。 (资源获取是初始化?与析构函数或清理有什么关系,真正表征RAII的是什么?)

另一个原因是它在没有确定性清理的语言中不能正常工作。

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

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

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

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

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

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

答案 1 :(得分:20)

我一直使用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

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

答案 2 :(得分: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 ++。 ;)

答案 3 :(得分: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

答案 4 :(得分:9)

RAII。

它以构造函数和析构函数开头,但它不止于此 所有这些都是关于在存在例外的情况下安全地控制资源。

是什么让RAII最终得到优势,这样的机制是它使代码更安全,因为它将对象的用户正确使用对象的设计者负责。

Read this

使用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 ++具体。
简答:没有。

更长的答案:
它需要Constructors / Destructors / Exceptions和具有已定义生命周期的对象。

从技术上讲,它不需要例外。当异常可能被使用时,它变得更加有用,因为它使得在异常情况下控制资源变得非常容易 但它在所有情况下都很有用,其中控制可以提前保留一个函数而不执行所有代码(例如从函数中提前返回。这就是为什么C中的多个返回点是一个错误的代码气味而多个C ++中的返回点不是代码气味[因为我们可以使用RAII进行清理]。

在C ++中,受控生命周期是通过堆栈变量或智能指针实现的。但这不是我们唯一能够实现严格控制的生命周期。例如,Perl对象不是基于堆栈的,但由于引用计数而具有非常可控的生命周期。

答案 5 :(得分:8)

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

答案 6 :(得分:5)

@Pierre's answer的修改:

在Python中:

with open("foo.txt", "w") as f:
    f.write("abc")
无论是否引发异常,都会自动调用

f.close()

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

  

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
     

无需明确关闭   页。即使发生错误,   page.close()将在调用时调用   退出。

答案 7 :(得分: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

答案 8 :(得分:2)

首先,我非常惊讶它不是更为人所知!我完全认为RAI​​I对C ++程序员来说至少是显而易见的。 但是现在我想我能理解为什么人们会真正询问它。我被包围了,我的自我必须是,C ++怪胎......

所以我的秘密......我想那就是,我曾经多年前一直读过Meyers,Sutter [编辑:]和安德烈,直到我对它进行了研究。

答案 9 :(得分:1)

RAII的问题在于它需要确定性的终结,这对于C ++中基于堆栈的对象是有保证的。依赖于垃圾收集的C#和Java等语言没有这种保证,所以它必须以某种方式“闩上”。在C#中,这是通过实现IDisposable和大部分相同的使用模式来完成的,然后基本上是“使用”语句的激励因素之一,它确保了Disposal并且是众所周知和使用的。

基本上这个成语就在那里,它没有一个奇特的名字。

答案 10 :(得分:1)

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

在其他语言中,您可以使用其他机制来实现这一目标。

在Java中,您尝试{} 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的其他例子是:

  • 获取后解锁互斥锁
  • 打开后关闭数据库连接
  • 分配后释放内存
  • 登录代码块的进入和退出
  • ...

答案 11 :(得分:0)

这有点像知道什么时候你的析构函数会被调用?因此,它并不完全与语言无关,因为在许多GC语言中并非如此。

答案 12 :(得分:0)

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

答案 13 :(得分:0)

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

答案 14 :(得分:0)

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

答案 15 :(得分:-1)

CPython(用C语言编写的官方Python)支持RAII,因为它使用引用计数对象并立即进行基于范围的破坏(而不是收集垃圾时)。不幸的是,Jython(Python in Java)和PyPy不支持这个非常有用的RAII习惯用法,它打破了许多遗留的Python代码。因此对于便携式python,您必须像Java一样手动处理所有异常。

答案 16 :(得分:-2)

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