【问题标题】:Who Disposes of an IDisposable public property?谁处置 IDisposable 公共财产?
【发布时间】:2010-10-15 01:31:31
【问题描述】:

如果我有一个实现IDisposableSomeDisposableObject 类:

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

这很好,但我不知道如何安全地从AContainer.Dispose() 调用m_someObject.Dispose(),因为另一个类可能仍然引用m_someObject 实例。

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

(假设其他代码依赖于AContainer.SomeObject 总是有一个非空值,所以简单地将实例的创建移到AContainer 之外是不可行的)

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

// 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.  

这有帮助吗?

【问题讨论】:

  • 将其设置为只读属性有助于使所有权更清晰,但当 AContainer 处置它时,另一个类仍然可以引用 m_SomeObject 实例。所以仍有引入错误的机会。

标签: c# .net dispose idisposable


【解决方案1】:

没有单一的答案,这取决于你的场景,关键是财产所代表的一次性资源的所有权,如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 在 cmets 中正确指出,最好在处理之前在 setter 中测试 m_someObject != 值:我已经更新了上面的示例以考虑到这一点(使用 ReferenceEquals 而不是比 != 要明确)。尽管在许多实际场景中,setter 的存在可能意味着对象不属于容器,因此不会被释放。

【讨论】:

  • 感谢 Joe,+1,这些示例确实有助于说明这种情况如何在代码中出现。看起来肯定没有明确的答案。
  • 在设置操作中,您还应该在处理它之前检查 (m_someObject != value)。
  • 那个'setter'模式很丑。一个更简洁的模式是有一个带有参数的 SetTheProperty 方法,以指示 IDisposable 被传递到的对象是否将获得所有权,以及一个 ReleaseOwnershipOfThePropert 方法,该方法将接受属性类型的参数并设置如果传入的参数与属性的当前值匹配,则“IsOwned”标志为 true。
【解决方案2】:

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

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

【讨论】:

  • 我同意房产应始终坚持其所有者。在这种情况下,处置所有者应该处置财产。如果您不希望出现这种情况,请改为从函数中返回对象。
  • 同意这是一个所有权问题 Jon。但是处理另一个类正在使用的对象感觉就像一个意想不到的副作用。那么 set 方法呢?如果我设置了一个新值,那么我应该处理旧值吗?即使我没有创建新值,我是否应该稍后处置它?
  • @GrahamS:有时能够交出某物的所有权很有用。考虑一对表示音频流和音频播放器的 IDisposable 对象。在某些情况下,能够创建流并将其传递给播放器可能很有用,播放器会在音频完成时自动处理自身和流。在其他情况下,人们可能希望能够多次播放音频流而不必每次都重新创建它,并且可能知道何时不再有兴趣重新播放它。
  • @GrahamS:我认为最好的方法是有一种明确的方法来传递对象的所有权,无论是在传入对象时还是稍后传递。对于后者,我有一个类似“ReleaseOwnershipOfStream”的方法,它可以接受流的参数。如果流仍在播放,该方法将在播放完成时设置一个标志为 auto-Dispose;如果传入的流仍未播放,则应立即 Disposed。
【解决方案3】:

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

【讨论】:

  • 请参阅 Joe 的 .NET Framework 示例。还要考虑我已经有一个 SomeDisposableObject 的实例,然后我用它设置 AContainer.SomeObject。
  • 它的所有成员对象也应该被释放 --> 这适用于组合,而不是聚合。
【解决方案4】:

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

【讨论】:

  • 通过干净的设置,没有对可能依赖于该对象的内部一次性代码的引用在包装器被处置后仍然存在。如果您依赖这种行为,请考虑使用引用计数。
  • 查看 Joe 提供的 .NET 示例。您将如何以不同的方式设计这些类以成为“干净的设置”?您将如何实现引用计数?
【解决方案5】:

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

首先避免它

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

外部实例创建
如果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 用于确定对象的引用计数。如果有的话请告诉我。

【讨论】:

  • 对象在 .NET 中没有内在引用计数。您必须自己实现它,使用管理计数的包装器对象。
  • 他们必须已经有某种引用计数,因为这就是 GC 的工作方式,但我不认为它可用于代码。实现自己的引用计数机制非常困难。
  • 你的作业是阅读所有这些:blogs.msdn.com/brada/articles/371015.aspx
  • (短版:GC != refcounting)
  • 非常有趣,感谢 Earwicker。那里有一些很好的论据,为什么“只实现引用计数”的建议不起作用。
【解决方案6】:

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

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

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

【讨论】:

    【解决方案7】:

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

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

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

    【讨论】:

    • 这有点道理。该命令通常是暂时的,但它使用的连接具有更长的生命周期。
    【解决方案8】:

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

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

    • 不释放 SomeDisposableObject,在这种情况下,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
    

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

    【讨论】:

    • 同意这样的说法,看起来很奇怪。但并非每个 IDisposable 都可以在 using 块中适应其生命周期。还要考虑我有一个现有的 SomeDisposableObject 实例,然后我用它设置 AContainer.SomeObject 的情况。
    【解决方案9】:

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

    【讨论】:

      【解决方案10】:

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

      所以:

      class AContainer : IDisposable
      {
          bool _isDisposed=false;
      
          public void Dispose()
          {
              if (!_isDisposed) 
              {
                 // dispose
              }
              _isDisposed=true;
          }
      }
      

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

      【讨论】:

      • 我认为这没有帮助。我的观点是 AContainer 不能处理 m_someObject 因为其他类可能仍在使用它。
      猜你喜欢
      • 2020-12-17
      • 1970-01-01
      • 2011-03-13
      • 2011-12-21
      • 2016-02-13
      • 2018-05-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多