谁处置了IDisposable公共财产?

时间:2009-03-23 19:26:12

标签: c# .net dispose idisposable

如果我有SomeDisposableObject类来实现IDisposable

class SomeDisposableObject : IDisposable
{
    public void Dispose()
    {
        // Do some important disposal work.
    }
}

我还有另一个名为AContainer的类,它有一个SomeDisposableObject个实例作为公共属性:

class AContainer
{
    SomeDisposableObject m_someObject = new SomeDisposableObject();

    public SomeDisposableObject SomeObject
    {
        get { return m_someObject; }
        set { m_someObject = value; }
    }
}

然后FxCop会坚持AContainer也会IDisposable

哪个好,但我无法看到如何安全地从m_someObject.Dispose()拨打AContainer.Dispose(),因为另一个类可能仍然引用m_someObject个实例。

避免这种情况的最佳方法是什么?

(假设其他代码依赖于AContainer.SomeObject总是具有非空值,因此只需将实例的创建移到AContainer之外就不是一个选项。

编辑:我会通过一些示例进行扩展,因为我认为一些评论者错过了这个问题。如果我只在Dispose()上实现一个调用m_someObject.Dispose()的AContainer方法,那么我就会遇到这些情况:

// Example One
AContainer container1 = new AContainer();
SomeDisposableObject obj1 = container1.SomeObject;
container1.Dispose();
obj1.DoSomething(); // BAD because obj1 has been disposed by container1.

// Example Two
AContainer container2 = new AContainer();
SomeObject obj2 = new SomeObject();
container2.SomeObject = obj2; // BAD because the previous value of SomeObject not disposed.
container2.Dispose();
obj2.DoSomething(); // BAD because obj2 has been disposed by container2, which doesn't really "own" it anyway.  

这有帮助吗?

10 个答案:

答案 0 :(得分:24)

没有单一的答案,这取决于您的情况,关键点是属性所代表的可支配资源的所有权,如Jon Skeet points out

查看.NET Framework中的示例有时很有帮助。以下是三个表现不同的示例:

  • 容器始终处置。 System.IO.StreamReader公开一次性属性BaseStream。它被认为拥有底层流,并且处理StreamReader总是处理底层流。

  • 容器永远不会丢弃。 System.DirectoryServices.DirectoryEntry公开Parent属性。它不被认为拥有它的父级,因此处理DirectoryEntry永远不会释放它的父级。

    在这种情况下,每次取消引用Parent属性时都会返回一个新的DirectoryEntry实例,并且可能需要调用者处理它。可以说这打破了属性的准则,也许应该有一个GetParent()方法。

  • 容器有时会处理。 System.Data.SqlClient.SqlDataReader公开一次性Connection属性,但调用者使用SqlCommand.ExecuteReader的CommandBehavior参数决定读者是否拥有(并因此处置)底层连接。

另一个有趣的例子是System.DirectoryServices.DirectorySearcher,它具有读/写一次性属性SearchRoot。如果从外部设置此属性,则假定基础资源不属于该属性,因此容器不会处置该属性。如果它不是从外部设置的,则在内部生成引用,并设置一个标志以确保它将被丢弃。你可以用Lutz Reflector看到这个。

您需要确定您的容器是否拥有该资源,并确保准确记录其行为。

如果您确定拥有该资源,并且该属性是可读/写的,则需要确保您的setter处理它正在替换的任何引用,例如:

public SomeDisposableObject SomeObject    
{        
    get { return m_someObject; }        
    set 
    { 
        if ((m_someObject != null) && 
            (!object.ReferenceEquals(m_someObject, value))
        {
            m_someObject.Dispose();
        }
        m_someObject = value; 
    }    
}
private SomeDisposableObject m_someObject;

更新:GrahamS在评论中正确地指出在处置之前最好在setter中测试m_someObject!=值:我已经更新了上面的例子以考虑到这一点(使用ReferenceEquals而不是比!=要明确)。虽然在许多现实场景中,setter的存在可能意味着该对象不属于容器,因此不会被处置。

答案 1 :(得分:13)

这实际上取决于谁在理论上“拥有”一次性物品。在某些情况下,您可能希望能够传入对象,例如在构造函数中,而不会让您的类负责清理它。其他时候你可能想要自己清理它。如果您正在创建对象(如示例代码中所示),那么清理它几乎肯定是您的责任。

至于财产 - 我不认为拥有财产应该真正转移所有权或类似的东西。如果你的类型负责处理对象,它应该承担这个责任。

答案 2 :(得分:5)

真正的问题可能是您的面向对象设计。如果AContainer是Disposed,则也应该处理其所有成员对象。如果不是这听起来像你可以处置一个身体但想要保持腿部实例生活。听起来不对。

答案 3 :(得分:4)

如果您的课程中有一个一次性物品,则使用IDisposable方法实施Dispose处理包裹的一次性物品。现在调用代码必须确保使用using()或处理对象的等效try / finally代码。

答案 4 :(得分:3)

我会尝试回答我自己的问题:

首先避免它

摆脱这种情况最简单的方法是重构代码以完全避免问题 有两种明显的方法可以做到这一点。

创建外部实例
如果AContainer没有创建SomeDisposableObject实例,而是依赖外部代码来提供它,那么AContainer将不再“拥有”该实例,并且不负责处理它。

可以通过构造函数或通过设置属性来提供外部创建的实例。

public class AContainerClass
{
    SomeDisposableObject m_someObject; // No creation here.

    public AContainerClass(SomeDisposableObject someObject)
    {
        m_someObject = someObject;
    }

    public SomeDisposableObject SomeObject
    {
        get { return m_someObject; }
        set { m_someObject = value; }
    }
}

保持实例私密
发布代码的主要问题是所有权混淆。在Dispose时,AContainer类无法分辨谁拥有该实例。它可以是它创建的实例,也可以是通过属性在外部创建的set的其他实例。

即使它跟踪它并确定它正在处理它创建的实例,那么它仍然无法安全地处理它,因为其他类现在可以引用它们从公共财产获得。

如果代码可以重构以避免公开实例(即完全删除属性),那么问题就会消失。

如果无法避免......

如果出于某种原因,代码无法以这些方式重构(正如我在问题中所规定的那样),那么在我看来,你会留下一些相当困难的设计选择。

始终处置实例
如果您选择此方法,那么您实际上声明AContainer将在设置属性时获得SomeDisposableObject实例的所有权。

在某些情况下这是有道理的,特别是SomeDisposableObject显然是瞬态或从属对象的情况。但是应该仔细记录,因为它要求调用代码知道这种所有权转移。

(使用方法而不是属性可能更合适,因为方法名称可用于进一步提示所有权)。

public class AContainerClass: IDisposable
{
    SomeDisposableObject m_someObject = new SomeDisposableObject();

    public SomeDisposableObject SomeObject
    {
        get { return m_someObject; }
        set 
        {
            if (m_someObject != null && m_someObject != value)
                m_someObject.Dispose();

            m_someObject = value;
        }
    }

    public void Dispose()
    {
        if (m_someObject != null)
            m_someObject.Dispose();

        GC.SuppressFinalize(this);
    }
}

仅在原始实例的情况下处理 在这种方法中,您将跟踪实例是否从最初由AContainer创建的实例更改,并且仅在原始实例时处理它。这里的所有权模式是混合的。 AContainer仍然是其自己的SomeDisposableObject实例的所有者,但是如果提供了外部实例,那么它仍然需要外部代码来处理它。

此方法最能反映此处的实际情况,但可能难以正确实施。客户端代码仍然可以通过执行以下操作导致问题:

AContainerClass aContainer = new AContainerClass();
SomeDisposableObject originalInstance = aContainer.SomeObject;
aContainer.SomeObject = new SomeDisposableObject();
aContainer.DoSomething();
aContainer.SomeObject = originalInstance;

这里交换了一个新实例,一个名为的方法,然后恢复了原始实例。不幸的是,AContainer在替换原始实例时会调用Dispose(),因此它现在无效。

放弃并让GC处理
这显然不太理想。如果SomeDisposableObject类确实包含一些稀缺资源,那么不及时处理它肯定会导致问题。

然而,就客户端代码与AContainer的交互方式而言,它也可能代表最强大的方法,因为它不需要特别了解AContainer如何处理SomeDisposableObject实例的所有权

如果您知道您的系统上的可支配资源实际上并不稀缺,那么这实际上可能是最好的方法。


一些评论者建议,可以使用引用计数来跟踪是否有任何其他类仍然引用SomeDisposableObject实例。这将是非常有用的,因为只有当我们知道这样做是安全的时候我们才能处理它,否则就让GC处理它。

但是我不知道有任何C#/ .NET API来确定对象的引用计数。如果有,请告诉我。

答案 5 :(得分:2)

您无法在Dispose()的{​​{1}}实例上安全地呼叫AContainer的原因是由于缺少封装。公共财产提供对部分内部州的无限制访问。由于内部状态的这一部分必须遵守IDisposable协议的规则,因此确保封装良好非常重要。

问题类似于允许访问用于锁定的实例。如果这样做,则确定获取锁的位置变得更加困难。

如果您可以避免暴露您的一次性实例,那么谁将处理对SomeDisposableObject的呼叫的问题也会消失。

答案 6 :(得分:1)

我遇到的一个有趣的事情是SqlCommand通常拥有一个SqlConnection(都是实现IDisposable)实例。但是,在SqlCommand上调用dispose NOT 也会处理连接。

我在Stackoverflow right here的帮助下也发现了这一点。

所以换句话说,如果“child”(嵌套?)实例可以/将在以后重用,这很重要。

答案 7 :(得分:0)

一般而言,我认为创建该对象的人应该对Disposal负责。在这种情况下,AContainer创建SomeDisposableObject,因此当AContainer为时,它应该是Disposed。

如果出于某种原因,你认为SomeDisposableObject应该比AContainer更长寿 - 我只能想到以下方法:

  • 保留SomeDisposableObject unDisposed,在这种情况下GC将为您处理
  • 为SomeDisposableObject提供对AContainer的引用(请参阅WinForms控件和父属性)。只要SomeDisposableObject可以访问,AContainer也是如此。这将阻止GC处理AContainer,但如果有人手动调用Dispose - 那么,你将Dispose SomeDisposableObject。我会说这是预期的。
  • 将SomeDisposableObject实现为一种方法,比如CreateSomeDisposableObject()。这清楚地表明(呃)客户负责处置。

总而言之 - 我不确定设计是否有意义。毕竟,您似乎期待客户端代码如下:

SomeDisposableObject d;
using (var c = new AContainer()) {
   d = c.SomeObject;
}
// do something with d

这似乎破坏了我的客户端代码。它违反了得墨忒耳法则,对我来说是一种常识。

答案 8 :(得分:0)

你在这里提到的设计不是可以处理这种情况的东西。你说那个类有一个容器然后它应该自己处理它。如果其他对象可能正在使用它,那么它不是容器和类的范围扩大,它需要在该范围的边界处置。

答案 9 :(得分:-1)

你可以在Dispose()中标记Disposal。在所有Disposal都不是析构函数之后 - 对象仍然存在。

这样:

class AContainer : IDisposable
{
    bool _isDisposed=false;

    public void Dispose()
    {
        if (!_isDisposed) 
        {
           // dispose
        }
        _isDisposed=true;
    }
}

也将此添加到您的其他课程中。