谁处置了IDisposable公共财产?

如果我有一个实现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()安全地调用AContainer.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. 

这有帮助吗?

没有一个答案,这取决于你的情况,关键点是财产所代表的可支配资源的所有权,正如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!= value:我已经更新了上面的例子以考虑到这一点(使用ReferenceEquals而不是!=显式)。 虽然在许多现实场景中,setter的存在可能意味着该对象不归容器所有,因此不会被处置。

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

至于财产 – 我不认为拥有财产应该真正转移所有权或类似的东西。 如果您的类型负责处理对象,则应该保留该责任。

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

如果您的class级中有一个一次性对象,则使用Dispose方法实现IDisposable ,该方法可以处理包裹的一次性用品。 现在调用代码必须确保using()或者配置对象的等效try / finally代码。

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

首先避免它

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

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

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

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

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

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

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

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

一般来说,我认为创建该对象的人应该对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 

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

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

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

所以:

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

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