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

Do programmers of other languages, besides C++, use, know or understand RAII?

本文关键字:RAII 是否 之外 C++ 其他 语言 程序员 除了      更新时间:2023-10-16

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

所以我真的很好奇,是因为我每天都被铁杆C++程序员包围,而 RAII 一般来说并不那么出名(包括C++(,还是所有这些关于 Stackoverflow 的质疑都是因为我现在接触到的程序员不是和C++一起长大的,而在其他语言中人们只是不使用/了解 RAII?

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 时,我们都必须调用这个帮助程序函数,以保证它之后会被关闭。

我一直

使用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

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

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++。 ;)

对于在此线程中评论 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

RAII.

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

RAII优于最终和这种机制的原因在于,它使代码使用起来更安全,因为它将正确使用对象的责任从对象的用户转移到对象的设计者。

阅读此内容

使用

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{} final{} 块,这使得这种编码方法更容易出错(,需要考虑异常的是对象的用户(。通过使用 RAII 异常,必须在实现对象时对安全进行一次编码。

问题是C++具体的。
简短回答:不可以。

更长的答案:
它需要构造函数/析构函数/异常和具有已定义生存期的对象。

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

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

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

@Pierre答案的修改:

在 Python 中:

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

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

一般来说,它可以使用contextlib.closesing来完成,从文档:

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(( 将在 与块退出。

Common Lisp 有 RAII:

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

请参阅:http://www.psg.com/~dlamkins/sl/chapter09.html

首先,

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

所以我的秘密..我想那是,几年前我曾经读过迈耶斯,萨特[编辑:]和安德烈,直到我只是摸索它。

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

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

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

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

在 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
)

在方案中,您有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的其他例子是:

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

这与知道何时调用析构函数有关,对吗?因此,它并不完全与语言无关,因为在许多GC'd语言中这不是给定的。

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

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

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

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

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