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

时间:2010-11-03 10:13:16

标签: c# .net idisposable

在将一次性对象传递给另一个对象的方法或构造函数时,是否应该调用Dispose()的任何指导或最佳实践?

以下是关于我的意思的几个例子。

IDisposable对象被传递给一个方法(它应该在它完成后处理它吗?):

public void DoStuff(IDisposable disposableObj)
{
    // Do something with disposableObj
    CalculateSomething(disposableObj)

    disposableObj.Dispose();
}

将IDisposable对象传递给方法并保留引用(在MyClass处置时是否应该将其丢弃?):

public class MyClass : IDisposable
{
    private IDisposable _disposableObj = null;

    public void DoStuff(IDisposable disposableObj)
    {
        _disposableObj = disposableObj;
    }

    public void Dispose()
    {
        _disposableObj.Dispose();
    }
}

我目前正在考虑在第一个例子中DoStuff()调用者应该处理该对象,因为它可能创建了该对象。但是在第二个例子中,感觉MyClass应该处理对象,因为它保留了对象的引用。这个问题是调用类可能不知道MyClass保留了引用,因此可能决定在MyClass完成使用之前处置该对象。 这种情况是否有任何标准规则?如果有,那么当将一次性对象传递给构造函数时它们是否不同?

5 个答案:

答案 0 :(得分:36)

答案 1 :(得分:35)

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

请注意,.NET框架中的某些类执行处理它们作为参数接收的对象。例如,处置StreamReader也会处理基础Stream

答案 2 :(得分:8)

通常,一旦您处理了一个Disposable对象,您就不再处于托管代码的理想世界,其中终身所有权是一个有争议的问题。结果,您需要考虑逻辑上“拥有”哪个对象,或者负责您的一次性对象的生命周期。

一般来说,对于刚刚传入方法的一次性对象,我会说不,该方法不应该处置该对象,因为一个对象很少占用另一个对象的所有权然后完成它采用相同的方法。在这些情况下,来电者应负责处理。

在谈论会员数据时,没有自动回答说“是,总是处置”或“不,永不处理”。相反,你需要考虑每个特定情况下的对象,并问自己,“这个对象是否对一次性对象的生命周期负责?”

经验法则是负责创造一次性物品的物品拥有它,因此负责稍后处置它。如果有所有权转移,这不成立。例如:

public class Foo
{
    public MyClass BuildClass()
    {
        var dispObj = new DisposableObj();
        var retVal = new MyClass(dispObj);
        return retVal;
    }
}

Foo显然负责创建dispObj,但它将所有权传递给MyClass的实例。

答案 3 :(得分:7)

这是my previous answer的后续行动;看到它最初的评论,以了解我为什么要发布另一个。

我之前的回答有一件事是正确的:每个IDisposable都应该拥有一个独家“所有者”,他将负责Dispose一次。管理{然后,{1}}对象变得非常类似于在非托管代码方案中管理内存。

.NET的前身技术组件对象模型(COM)在对象之间使用了以下protocol for memory management职责:

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

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

如果我们要针对IDisposable修改这些指南,我们可以列出以下内容......

有关IDisposable所有权的规则:

  1. 当通过常规参数将IDisposable传递给方法时,不会转让所有权。被调用的方法可以使用IDisposable,但不能IDisposable它(也不能传递所有权;请参阅下面的规则4)。
  2. 当通过Dispose参数或返回值从方法返回IDisposable时,所有权将从方法传输到其调用方。来电者必须out(或以同样的方式传递Dispose的所有权)。
  3. 当通过IDisposable参数向方法提供IDisposable时,对其的所有权将转移到该方法。该方法应将ref复制到局部变量或对象字段中,然后将IDisposable参数设置为ref
  4. 以上是一条可能重要的规则:

    1. 如果您没有所有权,则不得传递。这意味着,如果您通过常规参数收到null对象,请不要将同一个对象放入IDisposable参数,也不要通过返回值或ref IDisposable参数公开它。
    2. 实施例

      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` } } 对象。这向呼叫者发出信号,表明所有权不会被接管。因此呼叫者需要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); } } 参数接受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); } } 被声明为stream变量:using,则编译将失败,因为using (var stream = …)变量无法作为using参数传递,因此C#编译器有助于在这种特定情况下强制执行我们的规则。

      最后,请注意ref是通过返回值返回File.OpenRead对象(即IDisposable)的方法的示例,因此对返回的流的所有权进行了转移给来电者。

      缺点:

      这种模式的主要缺点是AFAIK,没有人使用它(尚未)。因此,如果您与任何不遵循上述规则的API(例如,.NET Framework基类库)进行交互,您仍需要阅读文档以了解谁必须在{{1}上调用Stream对象。

答案 4 :(得分:2)

在我对.NET编程了解之前我决定做的一件事,但它似乎仍然是一个好主意,是否有一个接受IDisposable的构造函数也接受一个布尔值,该布尔值表示对象的所有权是否为也将被转移。对于完全可以在using语句范围内存在的对象,这通常不会太重要(因为外部对象将被置于Inner对象的Using块的范围内,所以不需要外部对象处置内部的;事实上,可能有必要不这样做)。然而,当外部对象作为接口或基类传递给不知道内部对象存在的代码时,这种语义可能变得必不可少。在这种情况下,内部对象应该存在直到外部对象被破坏,并且知道内部对象的东西应该在外部对象做的时候死亡,因此外部对象必须能够销毁内在的。

从那以后,我有了几个额外的想法,但没有尝试过。我很好奇其他人的想法:

  1. IDisposable对象的引用计数包装器。我没有真正想出这样做的最自然的模式,但是如果一个对象使用带有Interlocked递增/递减的引用计数,并且如果(1)所有操作该对象的代码正确使用它,并且(2)没有循环引用使用该对象创建,我希望有可能有一个共享的IDisposable对象,当最后一次使用时,它会被销毁。可能会发生的情况是公共类应该是私有引用计数类的包装器,它应该支持构造函数或工厂方法,它将为同一个基本实例创建一个新的包装器(将实例的引用计数加一)。或者,如果即使放弃包装器也需要清理类,并且如果类具有一些定期轮询例程,则类可以将WeakReference列表保存到其包装器中并检查以确保至少一些他们仍然存在。
  2. IDisposable对象的构造函数接受第一次处理对象时将调用的委托(IDisposable对象应在isDisposed标志上使用Interlocked.Exchange以确保它是只准备一次)。然后该代理可以处理任何嵌套对象(可能检查是否有其他人仍然持有它们。)
  3. 其中任何一个看起来都是一个好模式吗?