66

作为一名 C++ 开发人员,Java 和 .NET 中缺少RAII(资源获取即初始化)一直困扰着我。清理的责任从类编写者转移到其消费者(通过try finally.NET 的using构造)这一事实似乎明显逊色。

我明白为什么在 Java 中不支持 RAII,因为所有对象都位于堆上,而垃圾收集器本身不支持确定性销毁,但在 .NET 中引入了值类型(struct)我们有(似乎) RAII 的完美候选人。在堆栈上创建的值类型具有明确定义的范围,并且可以使用 C++ 析构函数语义。但是,CLR 不允许值类型具有析构函数。

我的随机搜索发现了一个论点,即如果一个值类型被装箱,它就属于垃圾收集器的管辖范围,因此它的销毁变得不确定。我觉得这个论点不够有力,RAII 的好处大到足以说明带有析构函数的值类型不能被装箱(或用作类成员)。

长话短说,我的问题是:是否有任何其他原因不能使用值类型来将 RAII 引入 .NET?(或者你认为我关于 RAII 明显优势的论点有缺陷吗?)

编辑:我一定没有清楚地表达这个问题,因为前四个答案没有抓住重点。我知道它的非确定性Finalize特征,我知道using构造,我觉得这两个选项不如 RAII。using类的消费者还必须记住一件事(有多少人忘记将 aStreamReader放在一个using块中?)。我的问题是关于语言设计的哲学问题,为什么它是这样的,可以改进吗?

例如,使用通用的确定性可破坏值类型,我可以使usingandlock关键字变得多余(可通过库类实现):

    public struct Disposer<T> where T : IDisposable
    {
        T val;
        public Disposer(T t) { val = t; }
        public T Value { get { return val; } }
        ~Disposer()  // Currently illegal 
        {
            if (val != default(T))
                val.Dispose();
        }
    }

我不禁以我曾经看过但目前无法找到其来源的恰当报价作为结尾。

当我冰冷的死手超出范围时,您可以承担我的确定性破坏。——匿名

4

7 回答 7

15

更好的标题是“为什么 C#/VB 中没有 RAII”。C++/CLI(托管 C++ 的流产演变)具有与 C++ 完全相同的意义上的 RAII。这只是其他 CLI 语言使用的相同终结模式的语法糖(C++/CLI 的托管对象中的析构函数实际上是终结器),但它就在那里。

您可能会喜欢http://blogs.msdn.com/hsutter/archive/2004/07/31/203137.aspx

于 2008-10-06T10:48:14.743 回答
14

很好的问题,一个让我非常困扰的问题。似乎人们对 RAII 的好处的看法截然不同。根据我使用 .NET 的经验,缺乏确定性(或至少是可靠的)资源收集是主要缺点之一。事实上,.NET 已经多次迫使我使用整个架构来处理可能(但可能不需要)需要显式收集的非托管资源。当然,这是一个巨大的缺点,因为它使整体架构更加困难,并将客户的注意力从更中心的方面转移开。

于 2008-10-06T09:44:41.827 回答
14

布赖恩·哈里 (Brian Harry) 有一篇关于这里的基本原理的不错的帖子。

这是一段摘录:

确定性终结和值类型(结构)呢?

-------------- 我已经看到很多关于具有析构函数的结构等问题。这是值得评论的。为什么某些语言没有它们有很多问题。

(1) 组合 - 在一般情况下,由于上述相同的组合原因,它们不会为您提供确定的生命周期。任何包含一个的非确定性类都不会调用析构函数,直到它被 GC 最终确定。

(2) 复制构造函数——一个真正好的地方是在堆栈分配的局部变量中。他们将被限制在方法范围内,一切都会很好。不幸的是,为了让它真正起作用,您还必须添加复制构造函数并在每次复制实例时调用它们。这是关于 C++ 的最丑陋和最复杂的事情之一。您最终会在您不期望的地方执行所有代码。它会导致一堆语言问题。一些语言设计者选择远离这一点。

假设我们创建了带有析构函数的结构,但添加了一系列限制以使它们的行为在面对上述问题时变得合理。限制将类似于:

(1) 只能将它们声明为局部变量。

(2) 你只能通过引用传递它们

(3)您不能分配它们,您只能访问字段并在它们上调用方法。

(4) 你不能把它们装箱。

(5) 通过反射(后期绑定)使用它们的问题,因为这通常涉及拳击。

也许更多,但这是一个好的开始。

这些东西有什么用?你真的会创建一个只能用作局部变量的文件或数据库连接类吗?我不相信有人真的会。相反,您要做的是创建一个通用连接,然后创建一个自动销毁的包装器以用作作用域局部变量。然后,调用者将选择他们想要使用的内容。请注意,调用者做出了决定,并且它并未完全封装在对象本身中。鉴于您可以使用类似于在几个部分中提出的建议之类的东西。

.NET 中 RAII 的替代品是 using-pattern,一旦您习惯了它,它几乎同样有效。

于 2008-10-06T09:57:40.820 回答
1

您最接近的是非常有限的 stackalloc 运算符。

于 2008-10-06T09:27:57.073 回答
1

如果您搜索它们,则有一些类似的线程,但基本上归结为,如果您想在 .NET 上实现 RAII,只需实现 IDisposable 类型并使用“using”语句来获得确定性 Disposal。这样,许多相同的理念就可以以稍微冗长的方式实现和使用。

于 2008-10-06T09:32:45.970 回答
1

恕我直言,VB.net 和 C# 需要的主要内容是:

  1. 字段的“使用”声明,这将导致编译器生成代码以处理所有标记的字段。编译器的默认行为应该是让一个类实现 IDisposable(如果它没有实现),或者在许多常见 IDisposal 实现模式中的任何一个的主处置例程开始之前插入处置逻辑,或者使用属性来指定处置的东西应该以特定名称进入例程。
  2. 一种通过默认行为(调用默认处置方法)或自定义行为(调用具有特定名称的方法)来确定性地处置其构造函数和/或字段初始值设定项引发异常的对象的方法。
  3. 对于 vb.net,一种自动生成的方法来清空所有 WithEvent 字段。

所有这些都可以在 vb.net 中很好地组合在一起,而在 C# 中则不太好,但是对它们的一流支持会改进这两种语言。

于 2010-12-14T04:16:09.583 回答
-3

您可以使用 finalize() 方法在 .net 和 java 中执行某种形式的 RAII。在 GC 清理类之前调用​​ finalize() 重载,因此可用于清理绝对不应由类保留的任何资源(互斥锁、套接字、文件句柄等)。但它仍然不是确定性的。

使用 .NET,您可以使用 IDisposable 接口和 using 关键字确定性地执行其中一些操作,但这确实有限制(在确定性行为需要使用时使用构造,仍然没有确定性内存释放,不会在类中自动使用等)。

是的,我觉得 RAII 的想法可以引入 .NET 和其他托管语言,尽管确切的机制可能会无休止地争论。我能看到的唯一另一种选择是引入一个可以处理任意资源清理(不仅仅是内存)的 GC,但是当绝对必须确定性地释放所述资源时,你就会遇到问题。

于 2008-10-06T09:31:45.537 回答