为什么在MemoryStream.Close()之后调用MemoryStream.GetBuffer()会成功?

6

我在一些开源代码中发现了以下结构:

var mstream = new MemoryStream();
// ... write some data to mstream
mstream.Close();
byte[] b = mstream.GetBuffer();

我认为这段代码可能会出现“意外”行为,甚至会抛出异常,因为根据MSDN文档的说明,对Close方法的调用应该等同于对Dispose方法的调用。
然而,通过我的实验,我发现即使我使用Thread.Sleep让程序休眠20秒,或者通过GC.Collect()强制进行垃圾回收,对GetBuffer()方法的调用始终成功并返回有效结果。
Close/Dispose后,GetBuffer()方法是否应该成功?如果是这样,为什么MemoryStream中的底层缓冲区没有被释放呢?

2
仅供参考 - GetBuffer将获取整个缓冲区,包括为增长而存在的任何填充。如果流没有使用固定容量初始化,这可能意味着您有不想要的末尾零。话虽如此,如果我正确阅读了代码,GetBuffer仅在缓冲区大小固定时起作用。 - Luaan
4个回答

5
从技术上讲,在MemoryStream中没有任何需要处理的内容。真的没有什么,它没有操作系统句柄,非托管资源,什么都没有。它只是一个围绕byte[]的包装器。你所能做的就是将缓冲区(内部数组)设置为null,但BCL团队出于某种原因没有这样做。
正如@mike在评论中指出,BCL团队希望GetBuffer和ToArray在释放后仍然可用,虽然我们不确定为什么?参考来源
下面是Dispose的实现方式。
protected override void Dispose(bool disposing)
{
    try {
        if (disposing) {
            _isOpen = false;
            _writable = false;
            _expandable = false;
            // Don't set buffer to null - allow GetBuffer & ToArray to work.
    #if FEATURE_ASYNC_IO
                        _lastReadTask = null;
    #endif
        }
    }
    finally {
        // Call base.Close() to cleanup async IO resources
        base.Dispose(disposing);
    }
}

GetBuffer 在下面

public virtual byte[] GetBuffer()
{
    if (!this._exposable)
    {
        throw new UnauthorizedAccessException(Environment.GetResourceString("UnauthorizedAccess_MemStreamBuffer"));
    }
    return this._buffer;
}

正如您在Dispose中所看到的,_buffer没有被修改,在GetBuffer中也没有被处理的检查。

4
实际上,这个参考来源对此有评论:"不要将缓冲区设置为null - 允许GetBuffer和ToArray正常工作。" - Mike Zboray
buffer 没有被设置为 null 的原因可能是在正常操作中对垃圾回收没有任何影响(你会在使用内存流时使用 using,这样 buffer 就会在流本身之外的范围内)。此外,当你在流链中使用 MemoryStream 时,你希望在关闭所有内容后仍然能够访问数据。 - Luaan
@mikez 谢谢,我正在研究反射器。已经更新了我的答案并添加了参考来源。 - Sriram Sakthivel

5
  1. 不必这样做。缓冲区是管理内存的,因此正常的垃圾回收将处理它,无需在清理时包含它。
  2. 即使流已关闭(可能已自动发生在将流传递给写入数据并关闭该流的方法后),获取内存流的字节仍然很有用。为了使其起作用,对象需要保存缓冲区以及记录已写入多少的记录。

考虑第二点,在许多情况下,先关闭流后再调用ToArray()更加合理(正如前面所述,GetBuffer()返回需要内存存储器仍然存活的内容)。因为关闭流确保任何进一步尝试向流中写入的操作都将失败。因此,如果您提前获得数组的bug,它将抛出异常而不是给您错误的数据。(显然,如果您明确想在流操作的一部分中获取当前数组,则另当别论)。它还保证所有流都被完全刷新,而不是在临时缓冲区中具有部分数据(MemoryStream没有缓冲区,因为MemoryStream本质上就是缓冲区,但您可能一直在使用它与具有它们自己独立缓冲区的链接流或写入器一起使用)。


1

由于垃圾回收器是不确定性的,您无法强制立即处理MemoryStream,因此该实例将不会立即标记为已处理,而只是被标记为待处理。这意味着在一段时间内,在它真正被处理之前,您可以使用其某些功能。由于它保持对其缓冲区的强引用,因此您可以获取它,这是GetBuffer方法的样子:

public virtual byte[] GetBuffer()
{
    if (!this._exposable)
    {
        throw new UnauthorizedAccessException(Environment.GetResourceString("UnauthorizedAccess_MemStreamBuffer"));
    }
    return this._buffer;
}

1
与大多数接口方法不同,IDisposable.Dispose并不承诺要做任何事情。相反,它提供了一种标准方式,让对象的所有者知道该对象的服务不再需要,以防该对象可能需要使用该信息。如果一个对象已经请求外部实体代表其执行某些操作,并承诺将在不再需要它们的服务时通知它们,那么它的Dispose方法可以将通知传递给这些实体。
如果一个对象有一个只能在该对象有外部实体代表其执行操作时才能执行的方法,在这些实体被解雇后尝试调用该方法应抛出ObjectDisposedException而不是以其他方式失败。此外,如果存在一个方法,在实体被解雇后根本不可能有用,即使实际上没有必要使用该实体,也应该经常抛出ObjectDisposedException。另一方面,如果特定的调用在对象解雇了代表其执行操作的所有实体后仍然具有合理的意义,那么没有特殊的原因不允许这样的调用成功。
我认为 ObjectDisposedExceptionIEnumerator<T>.MoveNext() 中的集合修改 InvalidOperationException 很相似:如果某些条件(分别是 Dispose 或集合修改)会阻止方法正常运行,那么该方法可以抛出指定的异常,并且不允许以其他错误方式运行。另一方面,如果该方法能够轻松实现其目标,并且这样做是有意义的,那么此行为应被视为与抛出异常同样可接受。通常情况下,对象不需要在这种逆境下操作,但有时这样做可能会很有帮助 [例如,对 ConcurrentDictionary 进行枚举不会因更改集合而无效,因为这种无效会使并发枚举无用]。

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