如果我有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.
这有帮助吗?
答案 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 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;
}
}
也将此添加到您的其他课程中。