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

时间:2020-03-06 15:03:42  来源:igfitidea点击:

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

所以我真的很好奇,是否是因为我每天都被核心C ++程序员所包围,而RAII并没有广为人知(包括C ++),或者是否所有关于Stackoverflow的质疑都是由于事实我现在正与没有随C ++一起成长的程序员联系,而在其他语言中,人们只是不使用/不了解RAII?

解决方案

这与知道何时调用析构函数有关,这是对的吗?因此,这并不是完全与语言无关的,因为在许多GC语言中都没有做到这一点。

我认为许多其他语言(例如,没有delete的语言)不能给予程序员对对象生命周期的完全相同的控制,因此,必须有其他手段来提供确定性的资源处理方式。例如,在C#中,将usingIDisposable结合使用是很常见的。

RAII在C ++中很流行,因为它是可以分配复杂的作用域局部变量,但没有" finally"子句的几种(唯一的)语言之一。 C#,Java,Python,Ruby都具有" finally"或者同等功能。 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"实例时,将获取资源(在这种情况下为文件流)。销毁资源后,资源将被释放。不需要" try"或者" finally"块;如果读取导致异常,则会自动调用fclose,因为它位于析构函数中。

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

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

首先,我很惊讶它并不为人所知!我完全认为RAI​​I至少对C ++程序员是显而易见的。
但是现在我想我可以理解人们为什么真正问到它了。我被包围了,我的自我也一定是C ++怪胎...

所以我的秘密..我想那是几年前我一直读梅耶斯,萨特[编辑:]和安德烈的书,直到我读完它。

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 ++ y。学习C ++或者忘记所有这些,继续生活。最好学习C ++。 ;)

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

问题在于这是C ++特有的。
简短答案:不可以。

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

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

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

RAII的问题在于,它需要确定性的终结处理,这对于C ++中基于堆栈的对象是有保证的。像Cand Java这样的依赖垃圾回收的语言没有这种保证,因此必须以某种方式"取消"。在C中,这是通过实现IDisposable和许多相同的使用模式来完成的,然后从根本上出现,这是" using"语句的推动因素之一,它确保了Disposal,并且众所周知并已被使用。

因此,从根本上讲,这个成语在那里,只是没有一个好听的名字。

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

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

在Java中,我们可以尝试{}最终{}构造:

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的其他示例是:

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

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

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

@Pierre答案的修改:

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

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

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

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

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

另一个问题是,如果没有确定性的清理,它在语言中就无法正常工作。

在C ++中,我们确切知道析构函数的调用时间,知道析构函数的调用顺序,并且我们可以定义析构函数以执行我们喜欢的任何事情。

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

也就是说,仍然可以用Cor其他语言创建自己的RAII样式的包装器。我前一段时间在Ca做到了。
我必须写些什么来确保在使用后立即关闭数据库连接,任何C ++程序员都认为这是RAII的明显候选对象。
当然,每当使用数据库连接时,我们都可以将所有内容包装在"使用"语句中,但这只是一团糟且容易出错。

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

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

仍然不如C ++-RAII好或者不明显,但它实现了大致相同的功能。每当我们需要DbConnection时,我们都必须调用此辅助函数,以确保以后将其关闭。

普通Lisp具有RAII:

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

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