14

您对如何在 .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);
    }
}
4

7 回答 7

24

一个经常陈述的原则是“需要设计模式来解决语言缺陷”。这是该原则的一个例子。我们需要一次性模式,因为语言不会给你它。

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

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

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

于 2009-05-28T02:39:49.200 回答
6

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

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方法中跟踪他们自己的标志,这会取消编译器应该为你做的一半工作。

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

于 2009-06-03T17:41:48.593 回答
5

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

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

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

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

于 2009-05-28T00:32:30.017 回答
4

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

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

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

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

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

于 2009-07-26T20:04:26.640 回答
2

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

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

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

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

IDisposableusing()只有在发生异常时才真正有效并确保资源清理。

有设计模式可以做到这一点,但事实并非如此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() 时,正常行为是使整个对象无效,从而阻止它被使用。

于 2011-02-18T17:32:02.190 回答
0

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

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

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

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

于 2010-09-30T23:09:41.337 回答
-1

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

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

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

于 2009-05-28T00:33:05.883 回答