C#调用IDisposable.Dispose()和将对象设为null的区别

3
考虑以下代码:
A. 玩具类
class Toy
{
    private string name;
    public string Name
    {
        get { return name; }
        set { name = value; }
    }

    private ToyAddon addOn;
    public ToyAddon AddOn
    {
        get { return addOn; }
        set { addOn = value; }
    }

    public void RemoveAddons()
    {
        /*
         * someone said: this could lead to possible memory leak
         * if this class implements IDisposable,
         * then we are not disposing any of the resource 
         * */
        //this.addOn = null; 

        /* instead use this */
        var disposableToyAddOn = this.addOn as IDisposable;
        if (disposableToyAddOn != null)
            disposableToyAddOn.Dispose();

        this.addOn = null;

    }
    public override string ToString()
    {
        return String.Format("{0}, {1}",
            this.name, 
            (this.AddOn == null) ? "" : addOn.AddOnName);
    }
}

B. Toy Addon

class ToyAddon
{
    private string addOnName;

    public string AddOnName
    {
        get { return addOnName; }
        set { addOnName = value; }
    }

}

C. 主程序

class Program
{
    static void Main(string[] args)
    {
        ToyAddon tAdd = new ToyAddon();
        tAdd.AddOnName = "Amazing AddOn";
        Toy t = new Toy();

        t.Name = "Amazing Toy";
        t.AddOn = tAdd;
        t.RemoveAddons();

        Console.WriteLine(t.ToString());


    }
}

现在有人建议我检查一个拥有“having-a”对象是否实现了IDisposable接口,如果实现了则调用其dispose方法。 (请检查Toy类中的注释)

个人认为,如果我将引用设置为null,则位于堆上的对象将被GC标记为待回收。

如果该类(如ToyAddOn)实现IDisposable接口与否,在堆和栈上发生了什么以及GC的作用,如果能对此进行解释,那将不胜感激。


4
我推荐Stephen Cleary的博客文章"IDisposable: What Your Mother Never Told You About Resource Deallocation",这篇文章非常好,阅读大约需要一个小时,但是你会学到很多东西,更好地理解为什么你的同事告诉你那些内容。 - Scott Chamberlain
这还不完整,也不是那么简单。你需要一个“合约”,声明玩具“拥有”附加组件,并且附加组件不能被重复使用。表达这一点的一种方式是将RemoveAddons重命名为DestroyAddons。 - H H
没有理由就踩一下!!奇怪。 - Bhanu Chhabra
4个回答

2
现在有人建议我检查一个"having-a"对象是否实现了IDisposable接口,然后调用其dispose方法。
IDisposable是一种用于释放需要显式释放的非托管资源的模式,因为GC不知道它们。通常,持有非托管资源的类也会实现终结器。事实上,它实际上延长了对象的生命周期,更多于必要的时间段。在这些情况下,Dispose通常会调用GC SuppressFinalize方法,以将该对象从终结队列中移除。
通常,如果您有一个IDisposable对象,则最好自己实现IDisposable,以确保底层资源得到处理。
如果要"null out"的成员不是静态成员,则通常没有理由这样做。编译器足够聪明,可以将其优化掉,GC也足够聪明,知道不再引用该变量并清理它。
编辑:正如@ScottChamberlain所指出的那样,仍然存在持有对可处置对象的引用的情况,因此GC不会将其计入GC。当处理并将其变为空时,您提示GC它已经"准备好进行垃圾回收"。

将某些对象置空的一个原因是为了减少对象图的大小。有人可能会处理您的类但不释放引用,通过在您被处理时将您的类的可处理资源置空,即使您尚未被处理,也可以让您的子项被收集。 - Scott Chamberlain
@ScottChamberlain 我明白你的意思。虽然那听起来像是一个特殊情况。但我会把它加入到答案中。谢谢。 - Yuval Itzchakov
@downvoter - 请解释一下您对这个答案的不满意之处。 - Yuval Itzchakov
我知道Enigmativity给我点了踩,但他还是留了评论(尽管我已经尝试了两次让他满意,但我现在放弃了,只能接受这个踩了)。 - Scott Chamberlain
@ScottChamberlin 真是让人爱不起来的恶意投票者! :/ - Yuval Itzchakov

1

如果您没有手动处理一个类,它所持有的非托管资源(如果有)只有在GC检测到有足够的内存压力需要进行垃圾回收并且对象被GC和Finalized时才会被清理。

并非所有需要处理的事物都会增加内存压力,它们可能会持有像窗口句柄之类的东西。

如果内存压力从未足够高,那么在释放处理后,您可能永远不会垃圾回收这些值,直到资源用尽。当人们将Bitmap放在紧循环中但不释放它们时,经常看到这种情况,每个bitmap使用一个GDI+句柄,而不计入托管内存压力,也不会触发GC。这经常导致人们在仍然有充足内存的情况下遇到OutOfMemoryExceptions,因为他们真正耗尽的是GDI+句柄。

通过明确释放您的资源,您无需“希望自己运气好”并在非托管资源用尽之前让您的对象被Finalized。

堆栈与此无关。


垃圾收集器不知道 IDisposable 接口,因此它从未调用 .Dispose() 方法。您必须显式地调用它。 - Enigmativity
我认为这并不够清晰。当你说“通过显式处理不需要的资源,你不必‘希望自己很幸运’并且让你的对象被GC回收”时,听起来像是在说GC会调用.Dispose() - Enigmativity
@Enigmativity,我将其更改为“已完成”而不是“GCed”,并进行了一些其他的小文本更改,希望这能澄清我所说的不是Dispose(),而是讨论最终化过程。 - Scott Chamberlain
我知道我可能听起来很烦人,但只有在显式编写了这种方式时,finalizer 才会调用 .Dispose()。即使有一个 finalizer 调用了 .Dispose(),你仍然不能指望它会运行。你不能只是希望侥幸 - 你必须明确地处理可处置的对象。没有其他方法可以保证它被运行。 - Enigmativity

1
尝试将某物转换为IDisposable并在实现该接口时进行处理的模式非常表明设计存在缺陷。如果引用代码持有的类型不实现IDisposable,那么这非常强烈地表明:
  1. 该类型的引用持有者不应该处置它;即使派生类型实现了Disposable,清理的责任也应该由创建这些类型实例(并持有这些类型引用)的代码承担。

  2. 基类型应该实现IDisposable(即使99%的实现都不执行任何操作),因为一些实例将具有超出任何知道它们是否需要清理的人的控制范围的生命周期。其中一个典型的例子是IEnumerator<T>。尽管IEnumerator省略了IDisposable,但很快就意识到了需要它;当Microsoft添加IEnumerator<T>时,他们直接包含了IDisposable。虽然大多数IEnumerator<T>的实现可以被放弃而无需进行清理,但调用集合上的GetEnumerator方法的代码无法知道IEnumerator<T>的实现是否可能是需要清理的罕见实现之一,而且创建IEnumerator<T>IEnumerable<T>无法预知客户端何时完成,因此除客户端外没有其他人可以Dispose IEnumerator<T>

确定是否有实现 Disposable 接口的任何 ToyAddOn 实例可能在其生命周期结束时,仅由没有清理需求的代码持有引用。如果是这样,请让 ToyAddOn 实现 Disposable 接口。如果不是,请留给知道需要进行清理的代码来完成清理。


1
我同意那三个回答。为了简化你的问题的答案:
/* * 有人说:如果这个类实现了IDisposable接口,那么我们没有释放任何资源,这可能会导致内存泄漏 */ //this.addOn = null;
有人说的并不完全适用于你的例子。但是总的来说是正确的。
假设Toy类的生命周期比ToyAddon类长得多。而且ToyAddon类订阅了Toy Class的事件。如果是这样,ToyAddon应该在其Dispose方法中取消订阅。如果你在从Toy实例中移除ToyAddon后没有调用ToyAddon的Dispose方法,那么ToyAddon实例将在内存中存在,就像Toy实例一样长久存在。
而且,即使你在上面的情况下调用了Dispose,仍然通过'addOn'对ToyAddon有一个引用。因此,需要将addOn设置为null才能彻底摆脱它。
在我的观点中,如果我将引用设置为null,那么堆上的对象将被GC标记为可回收。
在你的情况下是正确的,在一般情况下是错误的,正如我上面解释的那样。

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