为什么当一个流的写入器被释放时,该流也会被释放?

9

Consider the following code:

using (var ms = new MemoryStream())
{
    using(var writer = BinaryWriter(ms))
    {
        writer.Write(/*something*/);
        writer.Flush();
    }

    Assert.That(ms.Length > 0); // Throws ObjectDisposedException
}

一方面,一个可处置的对象应该处理它的资源;我理解这一点,但另一方面,这个对象没有创建和拥有这个资源,它是提供的 - 调用代码应该承担责任...对吧?

我想不到其他类似的情况,但在框架中,任何接收可处置对象的类都要在其自己的dispose中处理它们吗?

4个回答

13

隐含的假设是每个流只有一个写入器,因此为方便起见,写入器会承担流的所有权 - 然后您只需要清理一件事。

但我同意; 这并不总是真实的,并且经常是不方便的。某些实现(例如DeflateStream,GZipStream)允许您进行选择。否则,唯一的真正选项是在写入器和底层流之间插入一个虚拟流; 如果我没记错,Jon Skeet的“MiscUtil”库中有一个非关闭流包装器NonClosingStreamWrapper可以实现这一点:http://www.yoda.arachsys.com/csharp/miscutil/

用法类似于:

using (var ms = new MemoryStream())
{
    using(var noClose = new NonClosingStreamWrapper(ms))
    using(var writer = BinaryWriter(noClose))
    {
        writer.Write(/*something*/);
        writer.Flush();
    }

    Assert.That(ms.Length > 0);
}

4

我完全同意你的观点。这并不是一致的行为,但这就是它的实现方式。在文档的末尾有关于这种不太直观的行为的评论。所有流写入器都会获取底层流的所有权并释放它。个人而言,我总是像这样嵌套我的using语句:

using (var ms = new MemoryStream())
using(var writer = BinaryWriter(ms))
{
    writer.Write(/*something*/);
}

这样就不需要编写像您在Assert中输入的代码那样的代码。


0
正确的做法应该是在构造函数参数中添加一个流写入器,指示当构造函数结束时是否应该处理流。鉴于Microsoft没有这样做,定义一个NonDisposingStream(Of T as Stream)类来包装流但不将Dispose调用传递给包装的流可能是一个好主意。然后可以将一个新的NonDisposingStream传递给StreamWriter的构造函数,底层流将安全地免受处理(当然,需要自己处理流的处理)。
拥有一个可以处理传入对象的对象非常有用。虽然这种行为与对象的创建者处理其处理不符合通常的模式,但通常存在一些情况,其中对象的创建者将不知道对象实际上需要多长时间。例如,一个方法可能需要创建一个使用新流的新StreamWriter。 StreamWriter的所有者将知道何时应该处理它,但可能不知道内部流的存在。内部流的创建者将不知道外部StreamWriter将被使用多长时间。将流的所有权“移交”给StreamWriter解决了特定(常见)情况下的处理问题。

0

我提议使用这个包装类:

public class BetterStreamWriter : StreamWriter
{
    private readonly bool _itShouldDisposeStream;

    public BetterStreamWriter(string filepath)
        :base(filepath)
    {
        _itShouldDisposeStream = true;
    }

    public BetterStreamWriter(Stream stream)
        : base(stream)
    {
        _itShouldDisposeStream = false;
    }

    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing && _itShouldDisposeStream);
    }
}

对象不应该处理它们没有实例化的东西。如果它是一个文件流写入器,它应该进行处理。如果它是外部流,则不应该。

一开始就不应该实现打开文件路径。这违反了单一责任原则,因为该对象既管理文件的写入,也管理文件的生命周期。


1
对象不应该处理它们没有"拥有"的东西。除了通过直接实例化之外,还可以通过其他方式获得对象的所有权(最常见的是调用工厂方法)。由于工厂方法通常仅返回单个对象,因此必须使方法获取的任何资源由该对象拥有。让“StreamWriter”掌管传入的流,这样就可以合法地使用工厂方法构造流并返回封装它的“StreamWriter”。 - supercat
1
@supercat 嗯,我同意你的观点,“对象不应该处置他们不拥有的东西”,这是正确的说法。然而,在这种情况下并不适用。设计 StreamWriter 类本身时,您无法了解调用者如何处理流,也不应该了解。考虑多个读取器使用流的情况。然后,您必须根据是否要处置它来有条件地使用 using 语句包装 StreamReader?IDisposable 应该被包装在 using 语句中。毫无疑问。 - Sleeper Smith
正确的方法是让 StreamReader/StreamWriter 的构造函数允许调用者指定是否转移所有权(在 .NET 的后续版本中已经这样做了)。否则,考虑如何编写一个方法,该方法应异步播放从 StreamReader 获取的音频数据。播放音频的代码可能对底层流一无所知,而构造 StreamReader 的代码可能不知道播放代码何时完成它。在流纯粹为音频播放而打开的常见情况下... - supercat
有关播放代码的处理,确保“StreamReader”也处理底层流是一个好主意。问题在于,“StreamReader”的设计是:虽然在大多数情况下,它并不重要是否释放,但是需要释放的情况大致分为无其他方式容易释放流的情况和不应该释放流的情况。后者可能更常见,但如果“StreamReader”不清理底层流,则前者会更难处理。 - supercat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接