【问题标题】:Consider a "disposable" keyword in C#考虑 C# 中的“一次性”关键字
【发布时间】:2009-05-28 00:22:15
【问题描述】:

您对如何在 .Net 中实现一次性对象有何看法?以及如何解决实现 IDisposable 类的重复性问题?

我觉得 IDisposable 类型并不是他们应该成为的一等公民。太多事情留给开发者摆布了。

具体来说,我想知道是否应该在语言和工具方面提供更好的支持,以确保一次性物品得到正确实施和妥善处理。

例如,在 C# 中,如果我的类需要实现一次性语义可以这样声明:

public class disposable MyDisposableThing
{
    ~MyDisposableThing()
    {
        // Dispose managed resources
    }
}

在这种情况下,编译器可以轻松生成 IDisposable 接口的实现。析构函数 ~MyDisposableThing 可以转换为应该释放托管资源的实际 Dispose 方法。

中间的 C# 代码如下所示:

public class MyDisposableThing : IDisposable
{
    private void MyDisposableThingDestructor()
    {
        // Dispose my managed resources
    }

    ~MyDisposableThing()
    {
        DisposeMe(false);
    }

    public void Dispose()
    {
        DisposeMe(true);
        GC.SuppressFinalize(this);
    }

    private bool _disposed;
    private void DisposeMe(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                // Call the userdefined "destructor" 
                MyDisposableThingDestructor();
            }
        }
        _disposed = true;
    }
}

这将使代码更简洁,更少的样板代码处置代码,以及处置托管资源的一致方式。对于边缘情况和非托管资源,仍然支持手动实现 IDisposable。

确保正确处理实例是另一个挑战。考虑以下代码:

private string ReadFile(string filename)
{
    var reader = new StreamReader();
    return reader.ReadToEnd(filename);
}

reader 变量永远不会超出方法的范围,但必须等待 GC 处理它。在这种情况下,编译器可能会引发 StreamReader 对象未显式释放的错误。此错误会提示开发人员将其包装在 using 语句中:

private string ReadFile(string filename)
{
    using (var reader = new StreamReader())
    {
        return reader.ReadToEnd(filename);
    }
}

【问题讨论】:

  • 嗯,前两句是……
  • 也许太多了......我想我只是需要把我的挫败感发泄出来:)
  • 它的社区 wiki ......那么它是否有问题有什么关系呢? Wiki 是知识/信息等。
  • 是的,这是一个问题。问题是,大家怎么看?他们同意吗?有更好的方法吗?
  • @jrista:好点,只有在您发表评论后才注意到

标签: c# design-patterns language-features idisposable


【解决方案1】:

一个经常被提及的原则是“需要设计模式来解决语言缺陷”。这是该原则的一个例子。我们需要一次性模式,因为语言没有提供给您。

我同意可处置性可以从“模式”世界提升到 C# 语言本身,就像我们对属性 getter 和 setter(它们是模式的标准化)所做的那样具有 getter 和 setter 方法)或事件(标准化存储委托并在发生有趣事情时调用它的想法。)

但语言设计成本高昂,而且可以投入的精力有限。因此,我们试图找到最有用、最引人注目的模式来正确地放入语言中。我们试图找到一种方式,不仅方便,而且实际上为语言增加了更多的表达能力。例如,LINQ 将过滤、投影、连接、分组和排序数据的概念转移到了适当的语言中,这为语言增加了很多表达能力。

虽然这当然是个好主意,但我认为它不符合标准。我同意,这将是一个很好的便利,但它不会支持任何真正丰富的新场景。

【讨论】:

  • 再一次,自动属性的例子只是 IMO 的方便。然而它的有用之处在于它解决了不便和代码可读性的问题。提升一次性模式不仅可以解决不便和代码混乱,还可以帮助开发人员做正确的事情。当然,它不会带来任何新的丰富场景,它会让代码更具表现力和安全性,这至少是一个有价值的场景。
  • 确实,自动道具只是一种便利。我很惊讶他们将它纳入 C# 3.0。事实证明,autoprops 的设计、实施、测试和文档成本低到足以证明其合理性,但仍然令人惊讶。
  • 我很惊讶 Eric 对 autoprops 感到惊讶 :) 他们所做的不同之处在于,现在实现属性(而不是字段)实际上很容易,即使是懒惰的程序员也会在应该使用它们的地方使用它们.这是一个巨大的差异,IMO 值得所有的实施努力。这个提议将使懒惰的程序员编写正确的一次性类并正确处理它们。
  • 哦,我同意它们是一个可爱的功能,完全值得。但是在调度 C# 3 时,我们强烈主张任何可能滑倒 LINQ 的东西都将被积极削减。自动道具和部分方法几乎没有吱吱作响。
  • @Camilo:增加语言复杂性是许多人抱怨的事情。每次添加新功能时,我们都会收到很多投诉,因为每个新功能都意味着要花更多的钱来培训人们如何使用新功能。通过元编程增加可扩展性使问题更加复杂;现在你不仅要训练人们如何读写 C#,还要训练人们如何使用元编程来读写 C#。也就是说,我对元编程非常感兴趣,并会考虑将它的某种形式添加到假设的 C# 未来版本中。
【解决方案2】:

除了其他答案之外,还有一个问题是这个应该实现多少以及人们应该从中得到什么?假设我这样声明我的班级:

public disposable class MyClass
{
    readonly AnotherDisposableObject resource = new AnotherDisposableObject();

    ~MyClass()
    {
        this.resource.Dispose();
    }

    public void DoStuff()
    {
        this.resource.SomeMethod();
    }
}

那么,如果调用者调用DoStuff实例被释放后会发生什么?编译器是否应该自动插入类似的东西

if (this.disposed) throw new ObjectDisposedException();

在每个方法的开头,因为您已将类声明为一次性的?

如果是这样,那么在释放对象后显式允许调用方法的情况(例如MemoryStream.GetBuffer)呢?您是否必须向编译器引入一个表明这一点的新关键字,例如public useafterdispose void ...?

如果不是,那么你如何向人们解释 new 关键字为你实现了一些样板代码,但他们仍然需要编写代码来检查对象是否在每个方法中被释放?此外,他们怎么检查这个,因为所有关于对象是否已被释放的状态信息都是自动生成的!现在他们需要在 ~MyClass 方法中跟踪他们自己的标志,这会取消编译器应该为你做的一半工作。

我认为作为一种特定的语言模式,这个概念有太多的漏洞,它只试图解决一个特定的问题。现在可以以通用方式解决这一类问题的是 mixins(即 Disposable mixin),并且这种语言功能通常可用于不同的概念(例如 Equatable mixin、Comparable mixin 等) .)。这就是我的钱的去向。

【讨论】:

  • 这里的有效积分。自动生成守卫是不可行的,因为编译器必须“知道”哪些变量应该被保护。我可以看到这变得过于复杂和太多的魔法发生了。不过,disposable 关键字应该生成一个 IsDisposed 属性,以便轻松创建守卫。是的,这应该是交易的一部分,这样用户就不必自己跟踪处置状态。
【解决方案3】:

就我个人而言,我认为在当前版本的 .NET 中对 IDisposable 的支持相当不错。 using 关键字的存在几乎使它成为我的一流构造。

我承认其中涉及一定数量的样板代码,但不足以保证新的语言功能。 (自动实现的属性是一个很好的例子,就是一个很好的例子。)你在你的帖子中错过了一个重要的点,这个“样板”代码并不是你需要的总是 .主要是,您需要在 if (disposing) 块之外处理 非托管 资源。

当然,析构函数 (~MyDisposableThing) 和无参数 Dispose() 方法是真正的样板文件,可以由语言关键字的用户消除,正如您所建议的那样 - 但我再次不确定是否引入了实际的new 关键字是几行代码所必需的。

我当然明白你在这里提出的观点,并且在某种程度上对此表示同情。 (如果您的建议成为语言规范的一部分,我敢肯定没有编码人员会抱怨。)但是,无论如何,当代码行数相当有限时,它不太可能说服 .NET 开发团队,其中一些可以说是相当特定于上下文(因此不是样板文件)。

【讨论】:

  • 我声明“手动 IDisposable...用于非托管资源”,但我希望看到这种场景也以更具声明性的方式得到支持。也许通过 ~~MyDisposableThing 析构函数 :)
  • 可以说,当您有很多属性时,自动属性删除了更多样板代码,但每个属性仍然只有三行代码。 dispose 模式需要每个类更多的代码。因此,如果您有许多需要 IDisposable 的类,新关键字的影响会非常大。
【解决方案4】:

我完全同意IDisposable 需要更好的语言支持。这是my variant of it from a while ago。细节可能是错误的,但 C++/CLI 是一个很好的模型。不幸的是,当我向他们展示 C++/CLI 中的示例时,这让 C# 程序员感到困惑。但它在实施方面已经做了“正确的事情”;我们只需要 C# 中的新语法。

即使是最简单的 Windows 窗体应用程序也有一个Dispose 方法,它是由向导生成的,面对不熟练的更改是脆弱的。将组件组合在一起以使一个组件可以“拥有”多个其他组件的想法是如此基本,以至于IDisposable 实际上是不可避免的,不幸的是,大多数书籍似乎需要几页来解释如何正确实现它。

现有的using 语句负责客户端。我们需要更多语言支持的地方是在实施方面。

类的某些字段是对类“拥有”的事物的引用,而有些则不拥有。所以我们必须能够将字段标记为已拥有。

此外,自动生成终结器也是一个非常糟糕的主意。大多数情况下,一个类将拥有实现IDisposable 的其他对象。并非所有的类都是线程安全的,它们也不应该是线程安全的。如果它们是从终结器调用的,那会发生在另一个线程上,迫使它们是线程安全的。这可能是 IDisposable 周围最容易引起混淆的一个领域 - 很多人读了这些书,并产生了一种错误的印象,即您必须在支持 @ 的对象上编写终结器987654328@.

【讨论】:

  • 我喜欢你的提议,尽管我会添加对在 try-finally 块中包装构造函数的支持(在“try”末尾设置一个标志,表示成功完成)。目前不可能捕获在构造函数中的“用户代码”开始之前或结束之后发生的异常,即使在这些时间可能发生异常(字段初始化或派生类构造)。
【解决方案5】:

我意识到这是一个旧线程,但有些东西被忽略了。

C# 和 Java 中的 Dispose 模式打破了没有确定性析构函数的根本原因。

MyObject A = new MyObject()
MyObject B = A;
A.Dispose();

现在B的状态是什么?如果 B 的所有者真的不想处理它怎么办。您现在在 C++ 中遇到了同样的问题,您必须跟踪您所持有的对象的所有引用。

IDisposable 仅在 using() 的上下文中真正有效,并确保在发生异常时清理资源。

有一些设计模式可以做到这一点,但不是IDisposable

@Peter 是的,我认为 Dipsosable 模式存在缺陷。当实现 Disposable 模式时,通常并不意味着仅仅为了能够继续使用已处置的对象而处置操作系统资源。通过在 Java 中的 try{} finally{} 或 .NET 中的 using() 之外使用 Disposable 模式,您可以打破使用 GC 的原因之一。我并不是说内存会泄漏。我是说您现在可以拥有对已处置对象的引用的代码的其他部分。现在,开发人员有责任在每次调用之前检查对象是否已被释放,或者至少捕获 ObjectDisposedException。

让我们看一个愚蠢的例子:

FileStream stream = new FileStream (@"c:\mylog.txt");
logger.setStream (stream);

谁应该调用 .Dispose()?记录器现在正在获取文件流的所有权可能并不明显。假设流是在开发人员之外的其他地方创建的,知道它将被设置为日志流。

如果我们要添加一行,我们会破坏记录器

using (FileStream stream = new FileStream (@"c:\mylog.txt"))
    { logger.setStream (stream); }

FileStream stream = new FileStream (@"c:\mylog.txt");
logger.setStream (stream);
stream.Dispose();

Disposable 模式不引用资源的计数。开发人员现在必须清楚谁拥有该对象以及谁负责清理它。真正的问题是,当调用 Dispose() 时,正常行为是使整个对象无效,从而阻止它被使用。

【讨论】:

  • 首先,GC 已经为我们进行了引用计数,它必须这样做才能知道何时可以收集对象。当保存非托管数据时,问题就会出现,而 GC 无法知道何时适合释放它们。因此需要特殊的 IDisposable。因此线程中的论点不是我们是否需要这个接口(显然我们需要)。问题是我们能否从语言支持中受益以实现 IDisposable 接口。考虑到这一点,您的回答似乎有点不对劲:您是否认为一次性模式存在某种错误?
  • 您在SetStream 中提到的问题有一个简单的解决方案:向SetStream 添加一个参数,该参数明确指示流是否应该取得传入对象的所有权。请注意,如果对 SetStream 的调用为其赋予了对象的所有权,则下一次调用应在 那个 对象上调用 Dispose,而不管它是否被赋予了新对象的所有权。
  • 绝大多数与IDisposable 相关的问题都可以解决,如果人们认识到在任何给定时间IDisposable 对象应该只有一个“所有者”;可能存在其他引用,但它们在语义上不应被视为持有对象,而只是标识其他人持有的对象。
  • @supercat 您的“解决方案”正是 C++ 程序员使用的,不仅用于昂贵的系统资源,还用于内存分配。然而,我们都看到 C++ 程序员必须非常注意防止所有权管理错误。这就是这个答案作者试图弄清楚的。使用 C++11 shared_ptr 和确定性析构函数,我相信这个问题(大部分)已经解决。但是我认为这在 C# 或 Java 中仍然很痛苦。
  • @EarthEngine:一般来说,可变状态需要所有权(无论是否需要资源);不可变状态没有。跟踪资源所有权的唯一问题是当一个封装了不可变状态的资源被共享,但占用了原本可以用于其他目的的可替代资源(例如,存储在显示卡缓存中的不可变位图)。否则,最好的模型是封装资源以实现 RAII 和不封装资源以使用 GC 的事物。我希望系统设计人员能够将两者结合起来。
【解决方案6】:

恕我直言,.net 语言在处理 iDisposable 时有一个主要缺点,那就是没有很好的方法来处理引发异常的初始化程序。除非“泄漏”正在构造的对象的副本或其中的一次性对象,否则无法清理在初始化程序抛出之前创建的任何 iDisposable 对象(在初始化程序中或基级构造函数中)。

为此我希望看到两个功能:

  1. 如果异常从其构造函数中抛出,将导致调用特定方法的类声明。
  2. 如果在对象上使用了某些特殊的私有方法或关键字,则表明该字段应调用 .Dispose 的字段声明。

顺便说一句,我还希望看到一个可用于结构方法的声明,该声明表明该方法会改变底层结构。在结构右值上使用此类方法将被禁止,并且在结构属性上使用此类方法会产生读-修改-写序列。

【讨论】:

    【解决方案7】:

    好的,您需要了解托管内存和非托管内存之间的区别。 简而言之,c++ 风格的析构函数在 c# 的托管世界中不起作用,因为无法保证对象何时会被垃圾回收,因此您永远不会知道析构函数何时会被调用,这会使事情变得非常不可预测。

    与 c++ 不同,当类超出范围后立即调用析构函数,因此您可以保证何时调用它。

    这就是c#不能有析构函数的原因。

    【讨论】:

    • 我不认为这就是 OP 想说的。他只是在说明所需的样板级别。在他的示例中,IDisposable 类将使用关键字明确划分(因此与普通类不同)。
    • @Chris:这就是为什么主题是 Dispose 方法的原因,它确实有精确的执行时间。
    猜你喜欢
    • 1970-01-01
    • 2011-08-05
    • 2012-07-25
    • 1970-01-01
    • 2014-08-18
    • 2011-01-14
    • 2012-06-04
    • 2011-01-01
    • 1970-01-01
    相关资源
    最近更新 更多