ChatGPT解决这个技术问题 Extra ChatGPT

为什么 .NET 中没有 RAII?

作为主要的 C++ 开发人员,Java 和 .NET 中缺少 RAII (Resource Acquisition Is Initialization) 一直困扰着我。清理的责任从类编写者转移到其消费者(通过 try finally 或 .NET 的 using construct)这一事实似乎明显逊色。

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

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

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

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

例如,使用通用的确定性可破坏值类型,我可以使 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();
        }
    }

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

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


C
CharlesB

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

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


很好的一点是,问题只是关于 C#/VB,但 C++/CLI 析构函数不是终结器。他们实现了 IDisposable。这是 C++/CLI 堆栈语义语法的特殊之处,因为它允许您编写统一代码来处理任何生命周期有限的对象,无论它是否实现 IDisposable,这在通用代码中特别有用。
断开的链接,再次:(
K
Konrad Rudolph

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


R
Rasmus Faber

Brian Harry 有一篇关于基本原理的好帖子 here

这是一段摘录:

确定性终结和值类型(结构)呢? -------------- 我已经看到很多关于具有析构函数的结构等问题。这是值得评论的。为什么某些语言没有它们有很多问题。 (1) 组合 - 在一般情况下,由于上述相同的组合原因,它们不会为您提供确定的生命周期。任何包含一个的非确定性类都不会调用析构函数,直到它被 GC 最终确定。 (2) 复制构造函数——一个真正好的地方是在堆栈分配的局部变量中。他们将被限制在方法范围内,一切都会很好。不幸的是,为了让它真正起作用,您还必须添加复制构造函数并在每次复制实例时调用它们。这是关于 C++ 的最丑陋和最复杂的事情之一。您最终会在您不期望的地方执行所有代码。它会导致一堆语言问题。一些语言设计者选择远离这一点。假设我们创建了带有析构函数的结构,但添加了一系列限制以使它们的行为在面对上述问题时变得合理。限制类似于: (1) 您只能将它们声明为局部变量。 (2) 你只能通过引用传递它们 (3) 你不能分配它们,你只能访问字段和调用它们的方法。 (4) 你不能把它们装箱。 (5) 通过反射(后期绑定)使用它们的问题,因为这通常涉及拳击。也许更多,但这是一个好的开始。这些东西有什么用?你真的会创建一个只能用作局部变量的文件或数据库连接类吗?我不相信有人真的会。相反,您要做的是创建一个通用连接,然后创建一个自动销毁的包装器以用作作用域局部变量。然后,调用者将选择他们想要使用的内容。请注意,调用者做出了决定,并且它并未完全封装在对象本身中。鉴于您可以使用类似于在几个部分中提出的建议之类的东西。

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


与 RAII 不同,如果将“using”语句用于 /everything/,则它会变得笨拙。
布赖恩·哈里错了——这些问题中的许多都是可以解决的;例如,参见 boost 的指针容器。当然,这需要一些扎实的锻炼,但它是完全可以解决的。就此而言,几乎每个 .NET 架构都意识到您需要所有权概念才能与 IDisposable 一起使用 - 如果您需要它,您不妨将它嵌入到语言中。解决方案可能很混乱,但比 Java + .NET 采用的这种把你的脑袋埋在沙子里然后让它走开的方法更好。
这样的结构在某些情况下可能非常有用,如果它们可以实现到其他类型的隐式转换运算符则更是如此。例如,Fluent 接口中的一个问题是,每个 WithXX 方法都必须返回一个新对象,即使对其目标的引用在其完成后不会继续存在。如果 WithXX 方法可以返回“不可复制”结构,则该结构的 WithXX 方法可以安全地改变它们持有引用的事物,而无需先复制它。
@Arafangion,是否有任何静态分析让您知道您忘记在 IDisposable 上包含“使用”语句?
@JoelFan 可能。距离我上次看这个已经十多年了。 :)
l
leppie

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


T
Torbjörn Gyllebring

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


s
supercat

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

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

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


w
workmad3

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

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

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