32

如果我有一个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()from AContainer.Dispose(),因为另一个类可能仍然有对该m_someObject实例的引用。

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

(假设其他代码依赖于AContainer.SomeObject始终具有非空值,因此简单地将实例的创建移到外部AContainer不是一种选择)

编辑:我将用一些例子进行扩展,因为我认为一些评论者错过了这个问题。如果我只是实现一个调用 m_someObject.Dispose() 的方法,那么我会遇到以下情况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.  

这有帮助吗?

4

10 回答 10

25

没有单一的答案,这取决于您的情况,关键是财产所代表的一次性资源的所有权,正如Jon Skeet 指出的那样

查看 .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 的存在可能意味着该对象不属于容器,因此不会被释放。

于 2009-03-23T20:31:42.223 回答
15

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

至于房产——我认为拥有房产不应该真正转让所有权或类似的东西。如果你的类型负责处理对象,它应该保持这个责任。

于 2009-03-23T19:32:45.610 回答
5

真正的问题可能是您的面向对象设计。如果 AContainer 是 Disposed,那么它的所有成员对象也应该被释放。如果不是,听起来你可以处理一个身体,但想让腿实例保持活力。听起来不对。

于 2009-03-23T19:57:01.293 回答
4

如果您的类上有一个一次性对象,您可以使用处理包装的一次性用品IDisposable的方法来实现。Dispose现在调用代码必须确保使用它或处理对象using()的等效try/代码。finally

于 2009-03-23T19:27:48.413 回答
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通过属性在外部创建的其他实例。

即使它跟踪它并确定它正在处理它创建的实例,它仍然无法安全地处理它,因为其他类现在可能具有它们从公共属性获得的对它的引用。

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

如果无法避免...

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

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

这在某些情况下是有意义的,尤其是在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。如果有,请告诉我。

于 2009-03-24T12:03:17.537 回答
2

您不能安全地调用Dispose()AContainer实例的SomeDisposableObject原因是缺乏封装。公共财产提供对部分内部状态的无限制访问。由于这部分内部状态必须遵守 IDisposable 协议的规则,因此确保良好封装非常重要。

该问题类似于允许访问用于锁定的实例。如果你这样做,就很难确定在哪里获取锁。

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

于 2009-03-23T20:21:45.787 回答
1

我遇到的一件有趣的事情是 SqlCommand 通常拥有一个 SqlConnection(两者都实现 IDisposable)实例。但是,在 SqlCommand 上调用 dispose 也不会释放连接。

我在这里也借助 Stackoverflow 发现了这一点。

因此,换句话说,“子”(嵌套?)实例是否可以/将在以后重用很重要。

于 2009-03-24T12:22:19.927 回答
0

总的来说,我认为创建对象的人应该对 Disposal 负责。在这种情况下,AContainer 会创建 SomeDisposableObject,所以它应该在 AContainer 存在的时候被 Disposed。

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

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

不过,总而言之——我不太确定这个设计是否有意义。毕竟,您似乎期望客户端代码如下:

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

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

于 2009-03-23T20:24:25.547 回答
0

您在这里提到的设计不是可以处理这种情况的。您说该类有一个容器,然后它应该将其与自身一起处理。如果其他对象可能正在使用它,那么它不是容器,您的类的范围会扩大,它需要在该范围的边界处进行处理。

于 2009-03-23T22:32:56.487 回答
-1

您可以在 Dispose() 中标记 Disposal。毕竟 Disposal 不是析构函数 - 该对象仍然存在。

所以:

class AContainer : IDisposable
{
    bool _isDisposed=false;

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

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

于 2009-03-23T19:30:31.900 回答