【问题标题】:Why does the Dispose pattern in C# not work more like RAII in C++为什么 C# 中的 Dispose 模式不像 C++ 中的 RAII 那样工作
【发布时间】:2013-08-29 14:38:12
【问题描述】:

所以我刚刚阅读了关于非垃圾收集语言的RAII 模式,而这个section 引起了我的注意:

开发自定义类时通常会遇到此限制。 C# 和 Java 中的自定义类必须显式实现 dispose 方法,以便与客户端代码兼容。 dispose 方法必须包含对属于该类的所有子资源的显式关闭。在具有 RAII 的 C++ 中不存在此限制,其中自定义类的析构函数会自动递归地销毁所有子资源,而无需任何显式代码。

为什么 C++ 可以正确跟踪这些以 RAII 模式分配的资源,但我们却没有使用 C# using 构造获得这个可爱的 Stack Unwinding?

【问题讨论】:

  • 您知道 C++ 需要多少年的 auto_ptr、shared_ptr 和类似的东西才能正确使用 RAII 习语和集合? :-)
  • @R.MartinhoFernandes 我知道,这仍然不是 C++,而是 C++ + 的东西。 C++ 本身具有不完整且不完全兼容的库。我们可以整天用“但你应该自己实现 RAII”来玩语义,C++ 作为语法确实可以,但支持的“标准”库并不相同,幸运的是 C# 不需要 10至少在容器和引用方面拥有连贯的库。
  • @xanatos 是否使用 C#?如果没有正确的工具,你如何处理 C++ 中的指针向量?你delete他们都是手动的吗?你如何在 C# 中处理List<Stream>?您手动关闭(或using)它们?看起来和我很相似。你如何处理一个有List<Stream> 成员的类?
  • 多么好的问题! C# 中有一个 Dispose 模式。让事情变得丑陋,让我想知道 GC 语言的优势。所以更好的相关阅读:msdn.microsoft.com/en-us/library/fs2xkftw.aspxstackoverflow.com/questions/898828/…

标签: c# c++ .net raii


【解决方案1】:

假设一个对象 O 由两个拥有资源的对象 R 和 S 组成。如果 O 被销毁会怎样?

在带有 RAII 的 C++ 中,对象可以拥有其他对象,因此一个对象的破坏必然与另一个对象的破坏相耦合。如果 O 拥有 R 和 S——通过按值存储它们,或者通过拥有反过来拥有 R 和 S 的东西(unique_ptr,按值存储 R 和 S 的容器),那么必然会破坏 O销毁 R 和 S。只要 R 和 S 的析构函数自行清理干净,O 就不需要手动执行任何操作。

相比之下,C# 对象没有决定其生命周期何时结束的所有者。即使 O 会被确定性地破坏(通常不会),R 和 S 也可能被另一个引用访问。更重要的是,O 引用 R 和 S 的方式与任何其他局部变量、对象字段、数组元素等引用 R 和 S 的方式相同。换句话说,没有办法表明所有权,所以计算机可以' t 决定对象何时应该被销毁,何时它只是一个非拥有/借用的引用。您肯定不希望这段代码关闭文件吗?

File f = GetAFile();
return f; // end of method, the reference f disappears

但是就CLR而言,这里从本地f的引用和从O到R/S的引用完全一样。

TL;DR所有权。

【讨论】:

  • 假设您编写代码 sn-p 的语言支持移动语义,return f 语句不应处理文件。事实上,在 C++/CLI 中,这可以正常工作并达到预期效果。如果您不返回 f 或将其传递出函数,它将被正确处理。考虑到 C++ 的语义,它基本上做了你期望它应该做的事情。
  • @KubaOber 但 C# 没有,而且它缺乏添加合理所有权模型的几个先决条件。这正是我回答的重点。
【解决方案2】:

在 C++ 中,对象有明确的生命周期。自动变量的生命周期在它们超出范围时结束,而动态分配的对象的生命周期在它们被删除时结束。

在 C# 中,大多数对象都是动态分配的,并且没有删除。因此,对象在“删除”时没有定义的时间点。那么,您拥有的最接近的东西是using

【讨论】:

  • 这不是问题所在。问题是关于在单个对象中管理资源的对象的组合。在 C++ 中,组合对象无需代码即可自动释放资源,而在 C# 中,您必须在 Dispose 中显式释放它们。
  • @Alex 很公平,这是 C++ 的一大优势。 :-D
【解决方案3】:

简短的回答:您上一次在 Java/C# 析构函数中清理自己的内存分配/资源分配是什么时候?事实上,您记得上一次编写 Java/C# 析构函数是什么时候?

由于您自己负责在 C++ 中进行清理,因此您必须进行清理。因此,当您停止使用某个资源时(如果您编写了高质量的代码),它会立即被清理掉。

在托管语言中,垃圾收集器负责进行清理。您分配的资源在您停止使用后很长时间仍可能存在(如果垃圾收集器实施不当)。当托管对象创建非托管资源(例如数据库连接)时,这是一个问题。这就是存在Dispose 方法的原因——告诉那些非托管资源离开。由于在垃圾收集器清理内存之前不会调用析构函数,因此在那里进行清理仍然会使(有限)资源打开的时间比需要的时间长。

【讨论】:

    【解决方案4】:

    因为要在 C# 中正确实现这一点,您需要以某种方式标记该类拥有哪些对象以及共享哪些对象。也许语法类似于这个:

    // NOT VALID CODE
    public class Manager: IDisposable
    {
         // Tell the runtime to call resource.Dispose when disposing Manager
         using private UnmanagedResource resource;
    }
    

    我的猜测是他们决定不走这条路,因为如果你必须标记你拥有的对象,你必须编写代码。如果你必须写代码,你可以把它写在Dispose方法中,为你拥有的对象调用Dispose:)

    在 C++ 中,对象所有权通常非常明确 - 如果您拥有实例本身,那么您就拥有它。在 C# 中,您永远不会持有实例本身,您始终持有引用,并且引用可以是您拥有或您使用的东西 - 无法分辨哪个是对于特定实例为 true。

    【讨论】:

    • 在设计合理的语言/框架中,标记类拥有的对象将避免编写代码来清理这些对象的需要。字段注释不会消除在所有情况下编写处理清理代码的需要,但会处理其中的很多。不幸的是,如果构造函数抛出异常,要让事情正常工作需要一种方法来请求应该抛出特定的类方法。如果没有这种能力,如果派生类构造函数抛出,基类就无法避免资源泄漏。
    猜你喜欢
    • 2011-05-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-05-12
    • 1970-01-01
    相关资源
    最近更新 更多