【问题标题】:Do programmers of other languages, besides C++, use, know or understand RAII?除了 C++ 之外,其他语言的程序员是否使用、了解或理解 RAII?
【发布时间】:2010-09-15 00:13:15
【问题描述】:

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

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

【问题讨论】:

  • 再一次证明它是值得的。我通常倾向于以这种方式编程,但不知道它已被正式化并称为 RAII。谢谢。
  • BASIC 程序员会想到 OEG1K(On Error Goto 1000)吗?
  • 其他语言有时使用execute-around idiom 来实现类似的行为。

标签: c++ language-agnostic raii


【解决方案1】:

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

一旦耗时操作终止,就会自动恢复原来的光标。

【讨论】:

    【解决方案2】:

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

    【讨论】:

    • python 的 C 扩展中的内存处理方式与 RAII 无关,当然也不是基于范围的。不过,我希望是这样,因为尽管我讨厌 C++,但 RAII 是使 C++ 在这方面优于 C 的一件事。
    • 我曾经认为 Python 及其引用计数方案支持 RAII ......但事实并非如此。好的,在 一些 情况下它确实有效,但它变得非常复杂,尤其是当解释器正在退出并且模块已被卸载时。
    【解决方案3】:

    RAII。

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

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

    Read this

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

    【讨论】:

      【解决方案4】:

      对于在此线程中评论 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 是什么,更多的是关于这个概念在非 c++ 程序员中的地位。
      • 这仍然是一个很好的解释,所以我可以理解投票。选择它作为答案有点奇怪。 :)
      • @les Python 的with 和C# 的using 类似:你要记得使用它,否则会泄漏。默认情况下使用 C++ 的 RAII,您必须做一些特殊的事情才能将其关闭(例如,new 将对象转换为原始指针(这在现代 C++ 中是一个很大的禁忌),然后泄漏)。跨度>
      【解决方案5】:

      Common Lisp 具有 RAII:

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

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

      【讨论】:

        【解决方案6】:

        @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() 将在 with 块已退出。

        【讨论】:

          【解决方案7】:

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

          【讨论】:

          • 我是一名 .Net 开发人员,因此您能否详细说明为什么“使用”仍会导致问题(混乱且容易出错)?是不是“使用”范围内的资源在离开范围后没有立即处理,而是等待被垃圾收集?如果是这样,在什么情况下容易出错?
          【解决方案8】:

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

          【讨论】:

            【解决方案9】:

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

            【讨论】:

              【解决方案10】:

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

              【讨论】:

                【解决方案11】:

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

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

                【讨论】:

                • C++ 解决方案更好,因为清理代码只编写一次(在析构函数中,或使用智能指针),并且会自动遵守销毁顺序,不像中所示的笨拙模式你的例子。
                • 而且我什至不会谈论某些语言(即 C#)的垃圾收集器在另一个线程中同时执行,而不是你的终结器,这导致了 SO 中另一个问题的有趣结论: 不要使用 RAII 在 C# 中释放托管资源。
                • 在 Python 中使用“with”或在 C# 中使用“using”是劣等的——使用这些关键字给类的用户带来了负担。使用析构函数,负担就落在了类编写者身上,而且只需要执行一次。
                • @Nemanja Trifunovic:看来你不明白with 语句在 Python 中是如何工作的。您只编写一次上下文管理器。比较 Python 中的 with Lock(obj) as l:... 和 C++ 中的 { Lock l(obj); ...},例如,两者都可以在进入块时调用 obj. acquire(),在退出时调用 obj.release()
                • 没有必要为每个单独的清理活动创建一个新类——您可以使用 Alexei Alexandrescu 的 ScopeGuard (google it) 来确保您提供的 any 功能是在范围退出时调用。
                【解决方案12】:

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

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

                【讨论】:

                • 我认为很多人都知道这个概念,但不知道术语。
                • 这可能是,他们可能不会将两者联系起来。
                • 这正是我了解 RAII 的方式。感谢迈耶斯、萨特斯和安德烈!
                • 多年前学过 C++,这个首字母缩写词对我来说毫无意义。这就像在询问“走路时的LBW技术”。低体重?那是什么?两边看。好吧,当我过马路时,我当然会这样做。当初怎么不说。问这个问题有什么意义?
                【解决方案13】:

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

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

                【讨论】:

                • 注意:“使用”只针对函数局部变量——或者更确切地说,生命周期完全限制在一个堆栈帧中的对象。它不涉及类静态、函数静态或类成员,或以其他方式具有生命周期范围的堆分配对象。
                • IDispose != RAII。你,一个对象的用户,必须把那些 using 语句 everywhere 来获得相同的效果,即使这样,如果一个对象中嵌入了其他对象,它也不起作用。 IDispose 是在每个对象上调用“关闭”的语法糖,而不是 RAII。
                • c# 确实允许您在堆栈上进行分配,但是我认为您失去了类型安全保证,因为您实际上是在使用原始内存,并且当您这样做时,所有的赌注都在托管世界中。
                • 这种成语在 C# 等语言中是没有的——请使用 C++ 一段时间,你会发现你刚才说的是废话
                【解决方案14】:

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

                【讨论】:

                • 不正确。它需要控制寿命。这不限于堆栈对象。例如。 Perl 可以做 RAII,它使用引用计数的对象。
                【解决方案15】:

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

                【讨论】:

                • C++ 确实需要,因为 RAII(深思熟虑的决定)。这些其他语言最终需要,因为它们没有 RAII。 finally 不是一个好的控制机制,它是为解决问题而添加的创可贴。
                • 马丁:听,听! (我猜是/会/不会/?)
                • RAII 有其自身的一系列问题,例如大量复杂的析构函数和用于处理各种资源获取的类的爆炸式增长。
                • @John:是的,这是有代价的。但它正在将责任从对象的用户推到对象的设计者身上。
                • @Martin:我同意,“非常复杂的析构函数”只会复制必须剪切并粘贴到谁知道有多少 finally 块中的代码。 RAII 降低了出错的可能性
                【解决方案16】:

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

                【讨论】:

                  【解决方案17】:

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

                  【讨论】:

                  • 当然,它与语言无关。许多习语和模式由于缺乏功能而无法使用或难以在各种语言中实现。
                  猜你喜欢
                  • 1970-01-01
                  • 2012-06-07
                  • 2011-11-14
                  • 2016-04-02
                  • 2010-12-17
                  • 2020-04-02
                  • 2015-12-28
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多