Вызов MemoryStream.GetBuffer() завершается успешно даже после MemoryStream.Close(); Почему?

Я нашел следующую конструкцию в некотором открытом коде:

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

Я думал, что этот код будет иметь «неожиданное» поведение и, возможно, вызовет исключение, поскольку вызов Close должен фактически быть вызовом Dispose в соответствии с документация MSDN.

Однако, насколько я мог судить из экспериментов, вызов GetBuffer() всегда завершается успешно и возвращает действительный результат, даже если я выполняю Thread.Sleep в течение 20 секунд или принудительно выполняю сборку мусора через GC.Collect().

Должен ли вызов GetBuffer() успешно выполняться даже после Close/Dispose? В таком случае, почему базовый буфер не освобождается при удалении MemoryStream?


person Anders Gustafsson    schedule 26.05.2014    source источник
comment
Просто примечание - GetBuffer даст вам весь буфер, включая все отступы, которые есть для роста. Если поток не был инициализирован с фиксированной емкостью, это, вероятно, означает, что у вас есть нули в конце, которые вам не нужны. Тем не менее, GetBuffer работает только тогда, когда размер буфера фиксирован, если я правильно читаю код.   -  person Luaan    schedule 26.05.2014


Ответы (4)


  1. Это не нужно. Буфер является управляемой памятью, поэтому с ним будет работать обычная сборка мусора, без необходимости его включения в удаление.
  2. Полезно иметь возможность получать байты потока памяти даже после того, как поток был закрыт (что могло произойти автоматически после того, как пар был передан методу, который что-то записывает в поток, а затем закрывает указанный поток). И чтобы это работало, объект должен хранить буфер вместе с записью о том, сколько в него было записано.

Что касается второго пункта, то на самом деле во многих случаях имеет смысл вызывать ToArray() (что, как было сказано, требует, чтобы хранилище в памяти, которое возвращает GetBuffer(), оставалось живым) после закрытия потока, потому что закрытие потока гарантирует, что любая дальнейшая попытка записи в поток завершится ошибкой. Следовательно, если у вас есть ошибка, из-за которой вы получаете массив слишком рано, он вызовет исключение, а не просто даст вам неверные данные. (Очевидно, если вы явно хотите получить текущий массив частично через потоковые операции, это другое дело). Это также гарантирует, что все потоки полностью очищены, а не часть их данных находится во временном буфере (MemoryStream не буферизуется, потому что MemoryStream по существу является буфером, но вы могли использовать его со связанными потоками). или писатели, у которых был свой отдельный буфер).

person Jon Hanna    schedule 26.05.2014

Технически в MemoryStream нечего утилизировать. Буквально ничего, у него нет дескрипторов операционной системы, неуправляемых ресурсов, ничего. это просто оболочка вокруг byte[]. Все, что вы можете сделать, это установить buffer (внутренний массив) в 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 нет выброшенных чеков.

person Sriram Sakthivel    schedule 26.05.2014
comment
Фактически, в справочном источнике есть комментарии о это: не устанавливайте для буфера значение null — разрешите работу GetBuffer и ToArray. - person Mike Zboray; 26.05.2014
comment
Причина, по которой для buffer не задано значение null, вероятно, заключается в том, что он не оказывает никакого влияния на сборку мусора при нормальной работе (вы бы использовали поток памяти при использовании, поэтому buffer будет вне области действия, как только сам поток). Кроме того, когда вы используете MemoryStream в цепочке потоков, вы хотите получить доступ к данным после того, как вы все закроете. - person Luaan; 26.05.2014
comment
@mikez Спасибо, я искал отражатель. Обновлен мой ответ со ссылкой на источник. - person Sriram Sakthivel; 26.05.2014

Поскольку GC не является детерминированным, вы не можете заставить его немедленно удалить MemoryStream, поэтому экземпляр не будет помечен как удаленный немедленно, вместо этого он будет просто помечен для удаления. Это означает, что в течение некоторого времени, пока он действительно не будет утилизирован, вы можете использовать некоторые его функции. Поскольку он хранит сильную ссылку на свой буфер, вы можете получить его, вот как выглядит метод GetBuffer:

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

В отличие от большинства интерфейсных методов, IDisposable.Dispose ничего не обещает. Вместо этого он предоставляет стандартные средства, с помощью которых владелец объекта может сообщить этому объекту, что его службы больше не требуются, на случай, если объекту может понадобиться использовать эту информацию. Если объект попросил внешние объекты сделать что-то от его имени и пообещал этим внешним объектам, что сообщит им, когда их услуги больше не понадобятся, его Dispose метод может передать уведомление этим объектам.

Если у объекта есть метод, который может быть выполнен только тогда, когда у объекта есть внешние сущности, действующие от его имени, попытка вызвать этот метод после того, как эти сущности были отклонены, должна вызывать ObjectDisposedException а не сбой каким-либо другим образом. Кроме того, если есть метод, который не может быть полезен после закрытия объекта, он часто должен выдавать ObjectDisposedException, даже если конкретному объекту на самом деле не нужно использовать объект. С другой стороны, если конкретный вызов будет иметь разумное значение после того, как объект отклонил все сущности, которые действовали от его имени, нет особых причин, по которым такой вызов не должен быть допущен к успеху.

Я бы просматривал ObjectDisposedException так же, как я просматриваю InvalidOperationException с измененной коллекцией из IEnumerator<T>.MoveNext(): если какое-то условие (либо Dispose, либо модификация коллекции, соответственно) не позволяет методу вести себя «нормально», методу разрешено выбрасывать указанное исключение, и ему не разрешается вести себя каким-либо другим ошибочным образом. С другой стороны, если метод способен без труда достичь своих целей и если это имеет смысл, такое поведение следует считать столь же приемлемым, как и генерирование исключения. Как правило, объекты не обязаны работать в таких неблагоприятных условиях, но иногда это может быть полезно для них [например, перечисление ConcurrentDictionary не будет аннулировано изменениями в коллекции, поскольку такое аннулирование сделало бы параллельное перечисление бесполезным].

person supercat    schedule 30.05.2014