Jon*_*ell 57 .net c# idisposable
是否有任何指导或最佳实践,Dispose()
当他们被传递到另一个对象的方法或构造器时,谁应该调用一次性对象?
以下是我的意思的几个例子.
IDisposable对象被传递给一个方法(它应该在它完成后处理它吗?):
public void DoStuff(IDisposable disposableObj)
{
// Do something with disposableObj
CalculateSomething(disposableObj)
disposableObj.Dispose();
}
Run Code Online (Sandbox Code Playgroud)
IDisposable对象被传递给一个方法并保留一个引用(它应该在处理时处置它MyClass
吗?):
public class MyClass : IDisposable
{
private IDisposable _disposableObj = null;
public void DoStuff(IDisposable disposableObj)
{
_disposableObj = disposableObj;
}
public void Dispose()
{
_disposableObj.Dispose();
}
}
Run Code Online (Sandbox Code Playgroud)
我目前认为在第一个例子中,呼叫者的DoStuff()
,因为它可能创建的对象应该处理的对象.但是在第二个例子中,感觉MyClass
应该处理对象,因为它保留了对象的引用.这个问题是调用类可能不知道MyClass
已保留引用,因此可能决定在MyClass
完成使用之前处置该对象.这种情况是否有任何标准规则?如果存在,当一次性对象传递给构造函数时它们是否不同?
sta*_*ica 36
PS:我发布了一个新答案(包含一组应该调用的简单规则
Dispose
,以及如何设计处理IDisposable
对象的API ).虽然目前的答案包含有价值的想法,但我开始相信它的主要建议往往在实践中不起作用:将IDisposable
物品隐藏在"粗粒度"物体中通常意味着那些需要成为IDisposable
自己的物体; 所以最终一个人开始了,问题依然存在.
是否有任何指导或最佳实践,
Dispose()
当他们被传递到另一个对象的方法或构造器时,谁应该调用一次性对象?
简短回答:
是的,有关于这个话题多的建议,最好的,我知道的是埃里克·埃文斯 "的理念聚集在领域驱动设计.(简单地说,应用的核心思想IDisposable
是:将其封装IDisposable
在一个粗粒度的组件中,以便外部看不到它,并且永远不会传递给组件使用者.)
此外,一个IDisposable
对象的创建者也应该负责处理它的想法太具有限制性,并且通常在实践中不起作用.
我的其余部分以相同的顺序详细介绍了这两点.我将完成我的回答,并指出与同一主题相关的更多材料.
更长的答案 - 从更广泛的角度来看,这个问题是什么?
有关此主题的建议通常不是特定的IDisposable
.每当人们谈论对象的生命周期和所有权时,他们指的是同一个问题(但更一般地说).
为什么在.NET生态系统中几乎不会出现这个主题?因为.NET的运行时环境(CLR)执行自动垃圾收集,它为您完成所有工作:如果您不再需要对象,您可以简单地忘记它,垃圾收集器最终将回收其内存.
那么,为什么问题出现在IDisposable
对象上呢?因为IDisposable
所有关于(通常是稀疏的或昂贵的)资源的生命周期的确定性控制:IDisposable
对象应该在不再需要时立即释放 - 并且垃圾收集器的不确定性保证("我最终将回收内存你用过的!")根本不够好.
你的问题,在更广泛的对象寿命和所有权方面重新措辞:
哪个对象
O
应该负责结束(一次性)对象的生命周期D
,这也会传递给对象X,Y,Z
?
让我们建立一些假设:
调用D.Dispose()
用于IDisposable
对象D
基本上结束其寿命.
从逻辑上讲,对象的生命周期只能结束一次.(暂时不要介意这与IDisposable
协议相对立,协议明确允许多次调用Dispose
.)
因此,为简单起见,O
应该由一个对象负责处理D
.我们打电话O
给主人.
现在我们来看问题的核心:C#语言和VB.NET都没有提供强制对象之间所有权关系的机制.因此,这变成了一个设计问题:O,X,Y,Z
接收对另一个对象的引用的所有对象D
必须遵循并遵守一个约定谁拥有所有权的约定D
.
使用Aggregates简化问题!
我在这个主题上找到的最好的建议来自Eric Evans 2004年出版的" 领域驱动设计 "一书.让我引用这本书:
假设您正在从数据库中删除Person对象.与人一起去名字,出生日期和工作描述.但地址怎么样?可能有其他人在同一地址.如果删除该地址,那些Person对象将引用已删除的对象.如果保留它,则会在数据库中累积垃圾地址.自动垃圾收集可以消除垃圾地址,但即使在数据库系统中可用,技术修复也会忽略基本的建模问题.(第125页)
看看这与您的问题有什么关系?此示例中的地址等同于您的一次性对象,问题是相同的:谁应该删除它们?谁"拥有"他们?
埃文斯继续建议聚合作为解决这个设计问题的方法.从书中再次出发:
聚合是一组关联对象,我们将其视为数据更改的单位.每个聚合都有一个根和一个边界.边界定义了Aggregate中的内容.根是聚合中包含的单个特定实体.虽然允许外部对象保持对引用的引用,但是根是聚合的唯一成员,尽管边界内的对象可以保持对彼此的引用.(第126-127页)
这里的核心信息是,您应该将IDisposable
对象的传递限制为其他对象的严格限制集("聚合").聚合边界之外的对象永远不应该直接引用你的IDisposable
.这大大简化了事情,因为您不再需要担心所有对象的最大部分,即聚合之外的对象是否可能是Dispose
您的对象.您需要做的就是确保边界内的对象都知道谁负责处理它.这应该是一个容易解决的问题,因为您通常会将它们一起实施并注意保持聚合边界合理"紧密".
那个IDisposable
对象的创建者也应该处理它的建议怎么样?
这个指南听起来很合理,并且它具有吸引人的对称性,但就其本身而言,它通常在实践中不起作用.可以说它绝对意味着"永远不会将IDisposable
对象的引用传递给其他对象",因为一旦你这样做,你就冒着接收对象承担其所有权并在不知情的情况下处置它的风险.
让我们看看.NET基类库(BCL)中两个明显违反这一经验法则的着名接口类型:IEnumerable<T>
和IObservable<T>
.两者基本上都是返回IDisposable
对象的工厂:
IEnumerator<T> IEnumerable<T>.GetEnumerator()
(记住IEnumerator<T>
继承自IDisposable
.)
IDisposable IObservable<T>.Subscribe(IObserver<T> observer)
在这两种情况下,调用者都应该处理返回的对象.可以说,我们的指南在对象工厂的情况下根本没有意义......除非,或许,我们要求发布它的请求者(而不是它的直接创建者)IDisposable
.
顺便说一下,此实施例还说明上面概述的聚合溶液的限制:两个IEnumerable<T>
和IObservable<T>
是方式在本质上太笼统永远是集合的一部分.聚合通常是特定于域的.
更多资源和想法:
在UML中,"有一个"对象之间的关系可以用两种方式建模:作为聚合(空钻石),或作为合成(填充钻石).组合与聚合的不同之处在于包含/引用对象的生命周期以容器/引用者的生命周期结束.您的原始问题隐含了汇总("可转让所有权"),而我主要是针对使用构成的解决方案("固定所有权").请参阅维基百科有关"对象组合"的文章.
Autofac(一个.NET 的IoC容器)两种方式来解决这个问题:要么通过交流,使用所谓的关系类型,Owned<T>
,谁获得所有权的上方IDisposable
; 或者通过工作单元的概念,在Autofac中称为生命范围.
关于后者,Autofac的创建者Nicholas Blumhardt撰写了"An Autofac Lifetime Primer",其中包括"IDisposable and ownership"部分.整篇文章是关于.NET中所有权和生命周期问题的优秀论文.我建议阅读它,即使对那些对Autofac不感兴趣的人也是如此.
在C++中,资源获取是初始化(RAII)习惯用法(通常)和智能指针类型(特别是)帮助程序员正确获得对象的生命周期和所有权问题.不幸的是,这些不能转移到.NET,因为.NET缺乏C++对确定性对象破坏的优雅支持.
另请参阅Stack Overflow问题的答案,"如何解释不同的实施需求?" ,(如果我理解正确的话)遵循与我的基于聚合的答案类似的思想:在其周围构建一个粗粒度组件IDisposable
,使其完全包含(并从组件使用者隐藏).
Mar*_*ers 35
一般规则是,如果您创建(或获得了对象的所有权),则您有责任处置该对象.这意味着如果您在方法或构造函数中接收一次性对象作为参数,则通常不应将其丢弃.
需要注意的是在.NET框架的一些类不处理,他们作为参数收到的对象.例如,StreamReader
配置a 也配置底层Stream
.
一般来说,一旦你处理了一个Disposable对象,你就不再处于托管代码的理想世界,其中终身所有权是一个有争议的问题.结果,您需要考虑哪些对象在逻辑上"拥有",或者负责您的一次性对象的生命周期.
一般来说,对于刚刚传递给方法的一次性对象,我会说不,该方法不应该处置该对象,因为一个对象很少占用另一个对象的所有权,然后在同样的方法.在这些情况下,来电者应负责处理.
在谈论会员数据时,没有自动回答说"是,总是处置"或"不,永不丢弃".相反,你需要考虑每个特定情况下的对象,并问自己,"这个对象是否对一次性对象的生命周期负责?"
经验法则是负责创建一次性物品的对象拥有它,因此负责稍后处置它.如果有所有权转移,这不成立.例如:
public class Foo
{
public MyClass BuildClass()
{
var dispObj = new DisposableObj();
var retVal = new MyClass(dispObj);
return retVal;
}
}
Run Code Online (Sandbox Code Playgroud)
Foo
显然负责创建dispObj
,但它将所有权传递给实例MyClass
.
这是我之前回答的后续行动; 看到它最初的评论,以了解我为什么要发布另一个.
我之前的回答有一件事是正确的:每个人都IDisposable
应该有一个独家的"所有者",他将负责Dispose
一次.IDisposable
然后,管理对象与在非托管代码方案中管理内存非常相似.
.NET的前身技术组件对象模型(COM)使用以下协议来处理对象之间的内存管理职责:
- "必须由调用者分配和释放参数.
- "out-parameters必须由被调用者分配;它们由调用者释放[...].
- "输入参数最初由调用者分配,然后在必要时由被调用者释放和重新分配.对于输出参数,调用者负责释放最终返回值."
(错误情况还有其他规则;有关详细信息,请参阅上面链接的页面.)
如果我们要适应这些指导方针IDisposable
,我们可以制定以下内容......
IDisposable
所有权规则:IDisposable
a通过常规参数传递给方法时,不会转移所有权.被调用的方法可以使用IDisposable
,但不能使用Dispose
它(也不能传递所有权;参见下面的规则4).IDisposable
通过out
参数或返回值从方法返回a时,所有权将从方法传输到其调用方.呼叫者将不得不Dispose
(或IDisposable
以同样的方式传递所有权).IDisposable
通过ref
参数给出方法时,对它的所有权将转移到该方法.该方法应将其复制IDisposable
到局部变量或对象字段中,然后将ref
参数设置为null
.一个可能重要的规则来自上述:
IDisposable
通过常规参数接收到对象,请不要将同一对象放入ref IDisposable
参数中,也不要通过返回值或out
参数公开它.sealed class LineReader : IDisposable
{
public static LineReader Create(Stream stream)
{
return new LineReader(stream, ownsStream: false);
}
public static LineReader Create<TStream>(ref TStream stream) where TStream : Stream
{
try { return new LineReader(stream, ownsStream: true); }
finally { stream = null; }
}
private LineReader(Stream stream, bool ownsStream)
{
this.stream = stream;
this.ownsStream = ownsStream;
}
private Stream stream; // note: must not be exposed via property, because of rule (2)
private bool ownsStream;
public void Dispose()
{
if (ownsStream)
{
stream?.Dispose();
}
}
public bool TryReadLine(out string line)
{
throw new NotImplementedException(); // read one text line from `stream`
}
}
Run Code Online (Sandbox Code Playgroud)
这个类有两个静态工厂方法,从而让客户选择是要保留还是传递所有权:
一个人Stream
通过常规参数接受一个对象.这向呼叫者发出信号,表明所有权不会被接管.因此呼叫者需要Dispose
:
using (var stream = File.OpenRead("Foo.txt"))
using (var reader = LineReader.Create(stream))
{
string line;
while (reader.TryReadLine(out line))
{
Console.WriteLine(line);
}
}
Run Code Online (Sandbox Code Playgroud)Stream
通过ref
参数接受对象的一个.这向呼叫者发出所有权将被转移的信号,因此呼叫者不需要Dispose
:
var stream = File.OpenRead("Foo.txt");
using (var reader = LineReader.Create(ref stream))
{
string line;
while (reader.TryReadLine(out line))
{
Console.WriteLine(line);
}
}
Run Code Online (Sandbox Code Playgroud)
有趣的是,如果stream
声明为using
变量:using (var stream = …)
,编译将失败,因为using
变量不能作为ref
参数传递,因此C#编译器有助于在此特定情况下强制执行我们的规则.
最后,请注意,这File.OpenRead
是一个通过返回值返回IDisposable
对象(即a Stream
)的方法的示例,因此对返回的流的所有权将传递给调用者.
这种模式的主要缺点是AFAIK,没有人使用它(尚未).所以,如果你不遵循上述规则的任何API进行交互(例如,.NET Framework基础类库),你仍然需要阅读的文件,以找出谁有权调用Dispose
的IDisposable
对象.