¿Se crea una pérdida de memoria si no se cierra un MemoryStream en .NET?

112

Tengo el siguiente código:

MemoryStream foo(){
    MemoryStream ms = new MemoryStream();
    // write stuff to ms
    return ms;
}

void bar(){
    MemoryStream ms2 = foo();
    // do stuff with ms2
    return;
}

¿Existe alguna posibilidad de que el MemoryStream que he asignado no se elimine de alguna manera más tarde?

Tengo una revisión por pares que insiste en que cierre esto manualmente y no puedo encontrar la información para saber si él tiene un punto válido o no.

Coderer
fuente
41
Pregúntele a su revisor exactamente por qué cree que debería cerrarlo. Si habla de buenas prácticas generales, probablemente esté siendo inteligente. Si habla de liberar la memoria antes, está equivocado.
Jon Skeet

Respuestas:

60

Si algo es desechable, siempre debe desecharlo. Debería utilizar una usingdeclaración en su bar()método para asegurarse de que ms2se elimine.

Eventualmente, el recolector de basura lo limpiará, pero siempre es una buena práctica llamar a Dispose. Si ejecuta FxCop en su código, lo marcará como una advertencia.

Rob Prouse
fuente
16
Las llamadas al bloque de uso se deshacen por ti.
Nick
20
@Grauenwolf: tu afirmación rompe la encapsulación. Como consumidor, no debería importarle si es no operativa: si es IDisposable, es su trabajo Dispose ().
Marc Gravell
4
Esto no es cierto para la clase StreamWriter: eliminará el flujo conectado solo si desecha el StreamWriter; nunca eliminará el flujo si se recolecta la basura y se llama a su finalizador; esto es por diseño.
springy76
4
Sé que esta pregunta fue de 2008, pero hoy tenemos la biblioteca de tareas .NET 4.0. Dispose () es innecesario en la mayoría de los casos cuando se utilizan Tasks. Si bien estoy de acuerdo en que IDisposable debería significar "Será mejor que se deshaga de esto cuando haya terminado", en realidad ya no significa eso.
Phil
7
Otro ejemplo de que no debe desechar el objeto IDisposable es HttpClient aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong Solo otro ejemplo de BCL donde hay un objeto IDisposable y no necesita (o ni siquiera debería) desechar eso. Esto es solo para recordar que generalmente hay algunas excepciones a la regla general, incluso en BCL;)
Mariusz Pawelski
166

No filtrará nada, al menos en la implementación actual.

Llamar a Dispose no limpiará la memoria utilizada por MemoryStream más rápido. Se va a detener el flujo de ser viable para llamadas de lectura / escritura después de la llamada, que pueden o no ser útil para usted.

Si está absolutamente seguro de que nunca desea pasar de un MemoryStream a otro tipo de flujo, no le hará ningún daño no llamar a Dispose. Sin embargo, por lo general es una buena práctica en parte porque si alguna vez haces el cambio se utilizará un flujo diferente, que no quiere ser mordido por un error difícil de encontrar porque usted eligió el camino más fácil desde el principio. (Por otro lado, está el argumento YAGNI ...)

La otra razón para hacerlo de todos modos es que una nueva implementación puede introducir recursos que se liberarían en Dispose.

Jon Skeet
fuente
En este caso, la función está devolviendo un MemoryStream porque proporciona "datos que se pueden interpretar de manera diferente según los parámetros de llamada", por lo que podría haber sido una matriz de bytes, pero por otras razones fue más fácil hacerlo como MemoryStream. Así que definitivamente no será otra clase de Stream.
Coderer
En ese caso, todavía trataría de deshacerme de él solo por principio general (desarrollar buenos hábitos, etc.) pero no me preocuparía demasiado si se volviera complicado.
Jon Skeet
1
Si uno está realmente preocupado por liberar recursos lo antes posible, anule la referencia inmediatamente después de su bloque "using", de modo que los recursos no administrados (si los hay) se limpian y el objeto se vuelve elegible para la recolección de basura. Si el método regresa de inmediato, probablemente no hará mucha diferencia, pero si continúa haciendo otras cosas en el método como solicitar más memoria, entonces ciertamente puede marcar la diferencia.
Triynko
@Triynko No es realmente cierto: consulte: stackoverflow.com/questions/574019/… para obtener más detalles.
George Stocker
10
El argumento de YAGNI podría tomarse en ambos sentidos: dado que decidir no desechar algo que implemente IDisposablees un caso especial que va en contra de las mejores prácticas normales, podría argumentar que es ese caso que no debe hacer hasta que realmente lo necesite, según el YAGNI principio.
Jon Hanna
26

Sí, hay una fuga , dependiendo de cómo defina LEAK y cuánto DESPUÉS quiera decir ...

Si por fuga te refieres a "la memoria permanece asignada, no disponible para su uso, aunque hayas terminado de usarla" y por último te refieres en cualquier momento después de llamar a dispose, entonces sí, puede haber una fuga, aunque no es permanente (es decir, para la vida del tiempo de ejecución de sus aplicaciones).

Para liberar la memoria administrada utilizada por MemoryStream, debe eliminar la referencia anulando su referencia a ella, de modo que sea elegible para la recolección de basura de inmediato. Si no lo hace, crea una fuga temporal desde el momento en que termina de usarlo, hasta que su referencia se sale del alcance, porque mientras tanto la memoria no estará disponible para su asignación.

El beneficio de la instrucción using (sobre simplemente llamar a dispose) es que puede DECLARAR su referencia en la instrucción using. Cuando finaliza la instrucción using, no solo se llama a dispose, sino que su referencia sale del alcance, anulando efectivamente la referencia y haciendo que su objeto sea elegible para la recolección de basura inmediatamente sin que tenga que recordar escribir el código "reference = null".

Si bien no eliminar la referencia a algo de inmediato no es una fuga de memoria "permanente" clásica, definitivamente tiene el mismo efecto. Por ejemplo, si mantiene su referencia al MemoryStream (incluso después de llamar a dispose), y un poco más abajo en su método intenta asignar más memoria ... la memoria en uso por su flujo de memoria aún referenciado no estará disponible hasta que anule la referencia o salga del alcance, aunque haya llamado a dispose y haya terminado de usarlo.

Triynko
fuente
6
Amo esta respuesta. A veces, la gente olvida el doble deber de usar: ansiosa recuperación de recursos y ansiosa eliminación de referencias.
Kit
1
De hecho, aunque escuché que, a diferencia de Java, el compilador de C # detecta el "último uso posible", por lo que si la variable está destinada a quedar fuera del alcance después de su última referencia, puede ser elegible para la recolección de basura justo después de su último uso posible. .antes de que realmente salga de su alcance. Ver stackoverflow.com/questions/680550/explicit-nulling
Triynko
2
El recolector de basura y el jitter no funcionan de esa manera. El alcance es una construcción del lenguaje y no algo a lo que obedecerá el motor de ejecución. De hecho, probablemente esté alargando el tiempo que la referencia está en la memoria, agregando una llamada a .Dispose () cuando finaliza el bloque. Ver ericlippert.com/2015/05/18/…
Pablo Montilla
8

No es necesario llamar .Dispose()(o envolver con Using).

La razón por la que llama .Dispose()es para liberar el recurso lo antes posible .

Piense en términos de, digamos, el servidor de Stack Overflow, donde tenemos un conjunto limitado de memoria y miles de solicitudes ingresan. No queremos esperar la recolección de basura programada, queremos liberar esa memoria lo antes posible para que esté disponible para nuevas solicitudes entrantes.

Jeff Atwood
fuente
24
Sin embargo, llamar a Dispose en un MemoryStream no liberará memoria. De hecho, aún puede obtener los datos en un MemoryStream después de haber llamado a Dispose; pruébelo :)
Jon Skeet
12
-1 Si bien es cierto para MemoryStream, como consejo general, esto es simplemente incorrecto. Dispose es liberar recursos no administrados , como identificadores de archivos o conexiones de bases de datos. La memoria no entra en esa categoría. Casi siempre debe esperar a que la recolección de basura programada libere memoria.
Joe
1
¿Cuál es la ventaja de adoptar un estilo de codificación para asignar y eliminar FileStreamobjetos y otro diferente para los MemoryStreamobjetos?
Robert Rossney
3
Un FileStream implica recursos no administrados que en realidad podrían liberarse inmediatamente al llamar a Dispose. Un MemoryStream, por otro lado, almacena una matriz de bytes administrada en su variable _buffer, que no se libera en el momento de su eliminación. De hecho, el _buffer ni siquiera se anula en el método Dispose de MemoryStream, que es un ERROR VERGONZOSO IMO porque anular la referencia podría hacer que la memoria sea elegible para GC en el momento de la eliminación. En cambio, una referencia de MemoryStream persistente (pero eliminada) todavía se mantiene en la memoria. Por lo tanto, una vez que lo deseche, también debe anularlo si todavía está dentro del alcance.
Triynko
@Triynko - "Por lo tanto, una vez que lo deseche, también debe anularlo si todavía está dentro del alcance" - No estoy de acuerdo. Si se usa nuevamente después de la llamada a Dispose, esto causará una NullReferenceException. Si no se vuelve a utilizar después de Dispose, no es necesario anularlo; el GC es lo suficientemente inteligente.
Joe
8

Esto ya está respondido, pero solo agregaré que el buen principio antiguo de ocultación de información significa que es posible que en algún momento desee refactorizar:

MemoryStream foo()
{    
    MemoryStream ms = new MemoryStream();    
    // write stuff to ms    
    return ms;
}

a:

Stream foo()
{    
   ...
}

Esto enfatiza que a las personas que llaman no debería importarles qué tipo de Stream se está devolviendo y hace posible cambiar la implementación interna (por ejemplo, cuando se burlan de las pruebas unitarias).

Entonces tendrá problemas si no ha utilizado Dispose en la implementación de su barra:

void bar()
{    
    using (Stream s = foo())
    {
        // do stuff with s
        return;
    }
}
Joe
fuente
5

Todos los flujos implementan IDisposable. Envuelva su flujo de memoria en una declaración de uso y estará bien y elegante. El bloque de uso asegurará que su flujo esté cerrado y eliminado pase lo que pase.

dondequiera que llame a Foo, puede hacerlo usando (MemoryStream ms = foo ()) y creo que aún debería estar bien.

Mella
fuente
1
Un problema con el que me he encontrado con este hábito es que debes asegurarte de que la transmisión no se esté utilizando en ningún otro lugar. Por ejemplo, creé un JpegBitmapDecoder apuntando a un MemoryStream y devolví Frames [0] (pensando que copiaría los datos en su propia tienda interna) pero descubrí que el mapa de bits solo mostraría el 20% del tiempo - resultó que era porque Estaba deshaciéndome del flujo de memoria.
devios1
Si su flujo de memoria debe persistir (es decir, un bloque de uso no tiene sentido), entonces debe llamar a Dispose y establecer inmediatamente la variable en nula. Si lo desecha, entonces ya no está destinado a ser utilizado, por lo que también debe configurarlo como nulo de inmediato. Lo que chaiguy está describiendo suena como un problema de administración de recursos, porque no debe entregar una referencia a algo a menos que la cosa a la que se la entrega asuma la responsabilidad de deshacerse de ella, y la cosa que entrega la referencia sabe que ya no es responsable de haciéndolo.
Triynko
2

No perderá memoria, pero su revisor de código está en lo correcto al indicar que debe cerrar su transmisión. Es de buena educación hacerlo.

La única situación en la que puede perder memoria es cuando deja accidentalmente una referencia a la secuencia y nunca la cierra. Todavía no está perdiendo memoria, pero está extendiendo innecesariamente la cantidad de tiempo que dice estar usándola.

OwenP
fuente
1
> Todavía no está perdiendo memoria, pero está extendiendo innecesariamente la cantidad de tiempo que dice estar usándola. ¿Estás seguro? Dispose no libera memoria y llamarlo tarde en la función puede extender el tiempo que no se puede recopilar.
Jonathan Allen
2
Sí, Jonathan tiene razón. Hacer una llamada a Dispose tarde en la función podría hacer que el compilador piense que necesita acceder a la instancia de flujo (para cerrarla) muy tarde en la función. Esto podría ser peor que no llamar a dispose (por lo tanto, evitar una referencia tardía en la función a la variable de flujo), ya que un compilador podría calcular un punto de liberación óptimo (también conocido como "punto de último uso posible") antes en la función .
Triynko
2

Recomendaría envolver el MemoryStream bar()en una usingdeclaración principalmente por coherencia:

  • En este momento MemoryStream no libera memoria .Dispose(), pero es posible que en algún momento en el futuro lo haga, o usted (o alguien más en su empresa) podría reemplazarlo con su propio MemoryStream personalizado que lo haga, etc.
  • Ayuda a establecer un patrón en su proyecto para garantizar que se eliminen todas las transmisiones: la línea se dibuja con más firmeza al decir "todas las transmisiones deben eliminarse" en lugar de "algunas transmisiones deben eliminarse, pero algunas no tienen que hacerlo". ...
  • Si alguna vez cambia el código para permitir la devolución de otros tipos de transmisiones, deberá cambiarlo para eliminarlo de todos modos.

Otra cosa que suelo hacer en casos como foo()cuando creo y devuelvo un IDisposable es asegurarme de que cualquier falla entre la construcción del objeto y el returnsea ​​detectada por una excepción, elimine el objeto y vuelva a generar la excepción:

MemoryStream x = new MemoryStream();
try
{
    // ... other code goes here ...
    return x;
}
catch
{
    // "other code" failed, dispose the stream before throwing out the Exception
    x.Dispose();
    throw;
}
Chris R. Donnelly
fuente
1

Si un objeto implementa IDisposable, debe llamar al método .Dispose cuando haya terminado.

En algunos objetos, Dispose significa lo mismo que Close y viceversa, en ese caso, cualquiera de los dos es bueno.

Ahora, para su pregunta particular, no, no perderá memoria.

Lasse V. Karlsen
fuente
3
"Debe" es una palabra muy fuerte. Siempre que hay reglas, vale la pena conocer las consecuencias de romperlas. Para MemoryStream, hay muy pocas consecuencias.
Jon Skeet
-1

No soy un experto en .net, pero quizás el problema aquí son los recursos, es decir, el identificador del archivo y no la memoria. Supongo que el recolector de basura eventualmente liberará el flujo y cerrará el identificador, pero creo que siempre sería una buena práctica cerrarlo explícitamente, para asegurarse de eliminar el contenido del disco.

Steve
fuente
Un MemoryStream está todo en memoria; aquí no hay un identificador de archivo.
Jon Skeet
-2

La eliminación de recursos no administrados no es determinista en los lenguajes de recolección de basura. Incluso si llama a Dispose explícitamente, no tiene absolutamente ningún control sobre cuándo se libera realmente la memoria de respaldo. Dispose se llama implícitamente cuando un objeto sale del alcance, ya sea al salir de una instrucción using o al abrir la pila de llamadas de un método subordinado. Dicho todo esto, a veces el objeto puede ser un contenedor para un recurso administrado (por ejemplo, un archivo). Es por eso que es una buena práctica cerrar explícitamente en las declaraciones finalmente o usar la declaración using. Salud


fuente
1
No es exactamente cierto. Se llama a Dispose al salir de una instrucción using. Dispose no se llama cuando un objeto simplemente sale del alcance.
Alexander Abramov
-3

MemorySteram no es más que una matriz de bytes, que es un objeto administrado. Olvídese de desechar o cerrar, esto no tiene ningún efecto secundario que no sea el de finalización.
Simplemente verifique el constructor o el método de vaciado de MemoryStream en el reflector y quedará claro por qué no necesita preocuparse por cerrarlo o desecharlo, aparte de seguir las buenas prácticas.

user1957438
fuente
6
-1: Si vas a publicar una pregunta para más de 4 años con una respuesta aceptada, intenta que sea algo útil.
Tieson T.