为什么.NET中没有RAII(资源获取即初始化)?

66
作为一名主要使用C++的开发者,Java和.NET中缺少RAII(资源获取即初始化)一直困扰着我。通过try finally或.NET的using construct将清理责任从类编写者转移到其使用者似乎明显不够好。
我知道Java中为什么不支持RAII,因为所有对象都位于堆上,垃圾回收器本质上不支持确定性销毁,但是在.NET中,随着值类型(struct)的引入,我们有了(看似)完美的RAII候选对象。在堆栈上创建的值类型具有明确定义的作用域,并且可以使用C++析构函数语义。然而,CLR不允许值类型具有析构函数。
我的随机搜索发现一个论点,即如果值类型boxed,则落入垃圾回收器的管辖范围,因此其销毁变得非确定性。我认为这个论点不够强,RAII的好处足以说明具有析构函数的值类型不能被装箱(或用作类成员)。
简而言之,我的问题是:是否有其他原因导致无法使用值类型来引入RAII到.NET?(或者您认为我的关于RAII明显优势的论点存在缺陷吗?)

编辑:由于前四个答案都没有理解问题的重点,我可能没有清楚地表达我的问题。我知道有关Finalize及其非确定性特征的信息,我知道using结构,但我认为这两个选项都不如RAII。 using是类的使用者必须记住的另一件事情(多少人忘记在using块中放置StreamReader?)。我的问题是一种关于语言设计的哲学问题,为什么它是这样的,能否改进?

例如,对于一个通用的确定性可销毁值类型,我可以使usinglock关键字变得冗余(可以通过库类实现):

    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();
        }
    }

我忍不住要以一句相关的引语结束,这是我曾经看过但现在无法找到其来源的引语。

当我的冷酷死亡之手超出范围时,你可以接管我的确定性毁灭。 --匿名

7个回答

15
更好的标题应该是“为什么C#/VB中没有RAII”。C++/CLI(Managed C++的演化)与C++完全相同,都具有RAII。这只不过是CLI语言中其余部分使用的相同终止模式的语法糖而已(C++/CLI中管理对象的析构函数实际上是终止函数),但它确实存在。
您可能会喜欢此链接:http://blogs.msdn.com/hsutter/archive/2004/07/31/203137.aspx

2
很好的一点是,这个问题只涉及到C#/VB,但是C++/CLI的析构函数不是终结器。它们实现了IDisposable接口。这是因为C++/CLI堆栈语义语法是特殊的,因为它允许您编写统一的代码来处理任何有限生命周期的对象,无论它是否实现了IDisposable接口,在泛型代码中特别有用。 - Ben Voigt
链接又坏了 :( - ceztko

14

这是个很好的问题,也是一直困扰着我。看起来RAII的优点被人们所理解的差异很大。就我在.NET方面的经验而言,缺乏确定性(或者至少是可靠的)资源收集是其主要缺点之一。事实上,.NET曾经强迫我多次使用整体结构来处理非托管资源,这些资源可能需要显式收集,但也有可能不需要。当然,这是一个巨大的缺点,因为它使整体架构更加复杂,并将客户的注意力从更核心的方面转移开来。


14
布莱恩·哈里在这篇文章中详细讨论了相关理由,文章链接在此

以下是其中的一部分内容:

确定性终结和值类型(结构体)如何处理?

-------------- 我看到很多关于结构体是否应该有析构函数等问题的提问。 这值得一提。 有许多原因为什么有些语言不支持。

(1) 组合 - 对于相同类型组成的对象,它们不能给你确定性生命周期。任何包含它们的非确定性类都不会调用析构函数,直到由GC最终完成。

(2) 拷贝构造函数 - 真正需要它的地方只有在栈分配的本地变量中。 它们将被限定于方法作用域,所有事情都很棒。 但不幸的是,为了使其真正起作用,您还必须添加拷贝构造函数,并在每次复制实例时调用它们。 这是C ++中最丑陋和最复杂的事情之一。你会发现代码在你意料之外地执行。 它会引起大量的语言问题。 一些语言设计者选择避免这种做法。

假设我们创建了具有析构函数的结构体,但添加了许多限制,以使它们在面对上述问题时表现得合理。 这些限制将是:

(1) 您只能将其声明为本地变量。

(2) 您只能通过引用进行传递

(3) 您不能赋值给它们,只能访问字段并调用它们的方法。

(4) 您不能将其装箱。

(5) 通过尝试使用它们来解决其他语言中类似的问题可能会导致更多的限制和问题。

反射(延迟绑定)因为通常涉及装箱而变得慢。

也许还有更多,但这是个好的开端。

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

.NET中RAII的替代方案是using模式, 一旦你习惯了,它也可以很好地工作。


10
使用“using”语句对于所有东西都使用时可能会变得笨重,这与RAII不同。 - Arafangion
21
布莱恩·哈里是错误的-这些问题中有很多完全是可以解决的;例如,看看boost的指针容器。当然,这需要一些扎实的工作,但完全是可解决的。事实上,几乎每个.NET架构都意识到,您需要所有权的概念才能处理IDisposable-如果您需要它,那么最好将其嵌入语言中。解决方案可能会有些混乱,但与Java + .NET采取的掩耳盗铃的方法相比,混乱的解决方案更好。 - Eamon Nerbonne
这样的结构体在某些情况下非常有用,如果它们可以实现到其他类型的隐式转换操作符,那么它们将更加实用。例如,在流畅接口中的一个问题是,即使其目标的引用不会存活超过其完成,每个 WithXX 方法都必须返回一个新对象。如果 WithXX 方法可以返回一个“不可复制”的结构体,则该结构体的 WithXX 方法可以安全地对其所持有引用的事物进行变异,而无需先进行复制。 - supercat
@Arafangion,有没有静态分析可以让你知道在IDisposable上忘记包含“using”语句? - JoelFan
1
@JoelFan 可能吧。这已经是十多年以来我最后一次看这个了。 :) - Arafangion

1

在我看来,VB.net 和 C# 最需要的是:

  1. 为字段添加“using”声明,编译器会生成代码以处理被标记的所有字段。默认行为应该是:如果类没有实现 IDisposable,则让编译器让其实现,并在任何常见的 IDisposable 实现模式的主要处理程序之前插入处理逻辑,否则使用一个属性指定处理内容应该放在具有特定名称的程序中。
  2. 确定性地处理构造函数和/或字段初始化程序抛出异常的对象的方法,可以通过默认行为(调用默认处理方法)或自定义行为(调用具有特定名称的方法)来实现。
  3. 对于 VB.net,自动生成一个将所有 WithEvent 字段设置为 null 的方法。

在 vb.net 中,所有这些都可以达到不错的效果,在 C# 中则稍微不那么好实现,但是如果能够得到第一类支持,将会改善这两种语言。


1

最接近你所需要的是非常有限的stackalloc运算符。


1

如果你搜索一下,会发现有一些类似的帖子,但基本上归结起来就是,如果你想在.NET上实现RAII,只需实现一个IDisposable类型,并使用“using”语句来获得确定性处理。这样,许多相同的惯用法可以以稍微冗长的方式实现和使用。


-3

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

在 .NET 中,您可以使用 IDisposable 接口和 using 关键字来实现一些确定性操作,但这也有一些限制(当使用构造时需要使用 using 关键字才能实现确定性行为,在类中不自动使用等)。

是的,我认为将 RAII 思想引入 .NET 和其他托管语言是有必要的,尽管确切的机制可能会被无休止地辩论。我唯一能想到的另一种选择是引入一个能够处理任意资源清理(而不仅仅是内存)的 GC,但是当这些资源必须以确定性方式释放时,就会出现问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接