题
如果我有一个 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()
方法上 AContainer
它调用 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.
这有帮助吗?
解决方案
没有单一的答案,这取决于您的场景,关键点是属性所代表的可支配资源的所有权,如 乔恩·斯基特指出.
有时查看 .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 的存在可能意味着该对象不属于容器,因此不会被释放。
其他提示
这真的取决于谁名义上“拥有”一次性对象。在某些情况下,你可能希望能够在对象传递,例如在构造函数中,没有你的班级为此承担责任清除它。其他时候,你可能要清理自己。如果您正在创建的对象(如在示例代码中),那么它应该几乎可以肯定是你的责任把它清理干净。
至于财产 - 我不认为有一个属性应真正转移所有权或者类似的东西。如果你的类型是负责该对象的处置,应该保持这一责任。
真正的问题可能是你的面向对象的设计。如果的aContainer布置,其所有成员对象也应该设置。如果没有它听起来像是你可以配置一个身体,但要保持腿实例生活。不健全的权利。
如果你有你的类中的一次性对象要实现与处置一次性包装一个IDisposable
方法Dispose
。现在调用代码具有以确保using()
使用或处置该物体的等效try
/ finally
代码。
我会尽力回答我的问题:
避免它在第一位置
出这种情况的最简单方法是重构代码完全避免这个问题。结果, 有两个明显的方式来做到这一点。
<强>外部实例创建强>结果
如果AContainer
不创建SomeDisposableObject
实例,而是依赖于外部代码来提供,然后AContainer
将不再是“自己”的实例,是不负责的处置它。
在外部创建的实例可以经由constuctor或通过设置属性来提供。
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; }
}
}
保留实例的专用点击
与发布代码的主要问题是所有权混淆。在处置时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的。如果有一个,那么请让我知道。
您不能安全地调用就Dispose()
的AContainer
的实例SomeDisposableObject
的原因是由于缺乏封装。公共属性提供对内部状态的一部分不受限制的访问。由于内部状态必须服从了IDisposable协议的规则的一部分,它确保良好封装是很重要的。
问题是类似于允许访问用于锁定一个实例。如果你这样做,它变得更加困难,以确定获取锁定。
如果你能避免暴露自己可支配的实例谁将会处理呼叫到Dispose()
问题消失为好。
这是我遇到了一个有趣的事情是的SqlCommand拥有一个的SqlConnection(均实现IDisposable)实例通常。但是调用dispose中SqlCommand将不会配置的连接了。
我发现这也与的帮助下在这里。
因此,换句话说,它很重要,如果“子”(拼图?)例如可以/将在以后重复使用。
一般来说,我认为无论谁创建了对象都应该负责Disposal。在这种情况下,AContainer创建了SomeDisposableObject,因此它应该在AContainer的时候被Dispose。
如果出于某种原因,您认为 SomeDisposableObject 应该比 AContainer 寿命更长 - 我只能想到以下方法:
- 将 SomeDisposableObject 保留为未释放状态,在这种情况下,GC 会为您处理它
- 为 SomeDisposableObject 提供对 AContainer 的引用(请参阅 WinForms 控件和 Parent 属性)。只要 SomeDisposableObject 是可访问的,AContainer 也是可访问的。这将阻止 GC 处置 AContainer,但如果有人手动调用 Dispose,那么您将处置 SomeDisposableObject。我想说这是预料之中的。
- 将 SomeDisposableObject 实现为方法,例如 CreateSomeDisposableObject()。这清楚地表明客户负责处置。
总而言之,我不太确定这个设计是否有意义。毕竟,您似乎期待这样的客户端代码:
SomeDisposableObject d;
using (var c = new AContainer()) {
d = c.SomeObject;
}
// do something with d
对我来说,这似乎是损坏的客户端代码。这违反了德墨忒尔法则,对我来说也是显而易见的常识。
您这里所说的设计是不是能处理这种情况。你说有该类的容器,那么它应该有自行处理它一起。如果其他对象可以使用它那么它是不是容器和范围的您阶级扩大,它需要在该范围的边界部署。
您可以只标志处置的处置()。毕竟处置不是析构函数 - 的对象仍然存在
这样:
class AContainer : IDisposable
{
bool _isDisposed=false;
public void Dispose()
{
if (!_isDisposed)
{
// dispose
}
_isDisposed=true;
}
}
添加到您的其他类也。