P.S.:我已经发布了一个new answer(包含一组简单的规则应该调用Dispose,以及如何设计一个处理IDisposable 对象的API)。虽然目前的答案包含有价值的想法,但我开始相信它的主要建议在实践中通常不起作用:在“粗粒度”对象中隐藏 IDisposable 对象通常意味着那些需要成为 IDisposable 自己;所以一个在一个开始的地方结束,问题仍然存在。
当一次性对象被传递到另一个对象的方法或构造函数时,是否有任何指导或最佳实践?
简答:
是的,关于这个话题有很多建议,我所知道的最好的是Eric EvansDomain-Driven Design 中的Aggregates 概念。 (简单地说,应用于IDisposable 的核心思想是:将IDisposable 封装在一个粗粒度的组件中,使其不被外部看到,并且永远不会传递给组件消费者。)
此外,IDisposable 对象的创建者也应该负责处理它的想法过于严格,在实践中通常行不通。
我的其余答案以相同的顺序详细介绍了这两点。我将用一些指向与同一主题相关的其他材料的提示来结束我的回答。
更长的答案 - 这个问题的含义更广泛:
关于这个主题的建议通常不是针对IDisposable。每当人们谈论对象生命周期和所有权时,他们所指的都是同一个问题(但更笼统)。
为什么这个话题在 .NET 生态系统中很少出现?因为 .NET 的运行时环境(CLR)执行自动垃圾收集,它为您完成所有工作:如果您不再需要一个对象,您可以简单地忘记它,垃圾收集器最终会回收它的记忆。
那么,为什么会提出IDisposable 对象的问题?因为IDisposable 是关于对(通常是稀疏或昂贵的)资源生命周期的显式、确定性控制:IDisposable 对象应该在不再需要时立即释放——以及垃圾收集器的不确定保证(“我'将最终回收你使用的内存!”)根本不够好。
您的问题,用更广泛的对象生命周期和所有权术语重新表述:
哪个对象 O 应该负责结束(一次性)对象 D 的生命周期,该对象也会传递给对象 @987654352 @?
让我们建立一些假设:
为 IDisposable 对象调用 D.Dispose()D 基本上结束了它的生命周期。
从逻辑上讲,一个对象的生命周期只能结束一次。 (暂时不要介意这与IDisposable 协议相反,该协议明确允许多次调用Dispose。)
因此,为了简单起见,只有一个对象 O 应该负责处理 D。我们打电话给 O 所有者。
现在我们进入问题的核心:C# 语言和 VB.NET 都没有提供强制对象之间所有权关系的机制。所以这变成了一个设计问题:所有接收到另一个对象 D 引用的对象 O,X,Y,Z 必须遵循并遵守一个约定,该约定准确地规定了谁拥有所有权在D。
使用聚合简化问题!
我在这个主题上找到的最好的建议来自Eric Evans' 2004 年的书,Domain-Driven Design。让我从书中引用:
假设您正在从数据库中删除一个 Person 对象。除了这个人,还有姓名、出生日期和工作描述。但是地址呢?同一地址可能还有其他人。如果您删除该地址,这些 Person 对象将具有对已删除对象的引用。如果你离开它,你会在数据库中积累垃圾地址。自动垃圾收集可以消除垃圾地址,但该技术修复,即使在您的数据库系统中可用,也会忽略一个基本的建模问题。(第 125 页)
看看这与您的问题有何关系?这个例子中的地址相当于你的一次性物品,问题都是一样的:谁应该删除它们?谁“拥有”它们?
Evans 继续建议将 Aggregates 作为此设计问题的解决方案。再次从书中:
Aggregate 是一组关联对象,我们将其视为一个用于数据更改目的的单元。每个聚合都有一个根和一个边界。边界定义了聚合内的内容。根是聚合中包含的单个特定实体。根是 Aggregate 中唯一允许外部对象持有引用的成员,尽管边界内的对象可能持有对彼此的引用。 (pp. 126-127)
这里的核心信息是您应该将IDisposable 对象的传递限制为其他对象的严格限制集合(“聚合”)。该聚合边界之外的对象永远不应直接引用您的IDisposable。这大大简化了事情,因为您不再需要担心所有对象的最大部分,即聚合之外的那些,是否可能Dispose 您的对象。您需要做的就是确保边界内部的对象都知道谁负责处理它。这应该是一个很容易解决的问题,因为您通常会一起实现它们并注意保持聚合边界合理“紧密”。
IDisposable 对象的创建者也应该处置它的建议怎么样?
这个指导方针听起来很合理,并且有一种吸引人的对称性,但仅凭它本身,它在实践中往往行不通。可以说,这与说“永远不要将对 IDisposable 对象的引用传递给其他对象”的含义相同,因为一旦这样做,您就有可能使接收对象假设它的所有权和在你不知情的情况下处理它。
让我们看一下 .NET 基类库 (BCL) 中明显违反此经验规则的两个突出的接口类型:IEnumerable<T> 和 IObservable<T>。两者本质上都是返回 IDisposable 对象的工厂:
在这两种情况下,调用者都应该处理返回的对象。可以说,我们的指导方针在对象工厂的情况下根本没有意义……除非我们可能要求IDisposable 发布。
顺便说一下,此示例还展示了上述聚合解决方案的局限性:IEnumerable<T> 和 IObservable<T> 在本质上都过于笼统,无法成为聚合的一部分。聚合通常是非常特定于域的。
更多资源和想法: