当传递到另一个对象时,谁应该在IDisposable对象上调用Dispose?

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,使其完全包含(并从组件使用者隐藏).

  • 很好的答案.一些注意事项:(1)我认为"所有权"的概念对几乎所有可变对象都很重要,无论它们是否拥有资源.我认为99.44%的可变对象应该有一个所有者对象,它将可变对象的状态视为自己的状态.持有对可变对象的引用的其他对象应该将这些引用视为封装其身份而不是其状态.(2)聚合概念在这里是强大的,因为它使聚合中的对象的身份成为局部概念而不是全局概念. (2认同)

Mar*_*ers 35

一般规则是,如果您创建(或获得了对象的所有权),则您有责任处置该对象.这意味着如果您在方法或构造函数中接收一次性对象作为参数,则通常不应将其丢弃.

需要注意的是在.NET框架的一些类处理,他们作为参数收到的对象.例如,StreamReader配置a 也配置底层Stream.

  • @Jon:Mark对所有权是正确的.然而,您可以在这里进入API设计的艺术.在.NET框架中的一些地方设计者没有遵循这个规则,因为他们认为它使这些类型的使用在常见场景中更容易.例如,现在你可以编写:`new StreamReader(File.Open("log.txt"))`而不必将`File.Open`包装在它自己的`using`中.这种设计将用户引向"成功的障碍".然而,这种设计是有问题的IMO,因为用户可能不会期望这种行为,这使得在其他场景中使用起来更加困难,并且会损害整体可用性. (8认同)

Gre*_*g D 8

一般来说,一旦你处理了一个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.


sta*_*ica 8

这是我之前回答的后续行动; 看到它最初的评论,以了解我为什么要发布另一个.

我之前的回答有一件事是正确的:每个人都IDisposable应该有一个独家的"所有者",他将负责Dispose一次.IDisposable然后,管理对象与在非托管代码方案中管理内存非常相似.

.NET的前身技术组件对象模型(COM)使用以下协议来处理对象之间的内存管理职责:

  • "必须由调用者分配和释放参数.
  • "out-parameters必须由被调用者分配;它们由调用者释放[...].
  • "输入参数最初由调用者分配,然后在必要时由被调用者释放和重新分配.对于输出参数,调用者负责释放最终返回值."

(错误情况还有其他规则;有关详细信息,请参阅上面链接的页面.)

如果我们要适应这些指导方针IDisposable,我们可以制定以下内容......

IDisposable所有权规则:

  1. IDisposablea通过常规参数传递给方法时,不会转移所有权.被调用的方法可以使用IDisposable,但不能使用Dispose它(也不能传递所有权;参见下面的规则4).
  2. IDisposable通过out参数或返回值从方法返回a时,所有权将从方法传输到其调用方.呼叫者将不得不Dispose(或IDisposable以同样的方式传递所有权).
  3. IDisposable通过ref参数给出方法时,对它的所有权将转移到该方法.该方法应将其复制IDisposable到局部变量或对象字段中,然后将ref参数设置为null.

一个可能重要的规则来自上述:

  1. 如果您没有所有权,则不得传递.这意味着,如果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基础类库),你仍然需要阅读的文件,以找出谁有权调用DisposeIDisposable对象.