¿Quién debe llamar a Dispose en objetos IDisposable cuando se pasa a otro objeto?

¿Hay alguna guía o mejores prácticas en torno a quién debería llamar a Dispose() sobre los objetos desechables cuando se hayan pasado a los métodos o al constituyente de otro objeto?

Aquí hay un par de ejemplos sobre lo que quiero decir.

El objeto IDisposable se pasa a un método (¿Debería deshacerse de él una vez hecho?):

 public void DoStuff(IDisposable disposableObj) { // Do something with disposableObj CalculateSomething(disposableObj) disposableObj.Dispose(); } 

El objeto IDisposable se pasa a un método y se guarda una referencia (¿Debería deshacerse de él cuando se elimine MyClass ?):

 public class MyClass : IDisposable { private IDisposable _disposableObj = null; public void DoStuff(IDisposable disposableObj) { _disposableObj = disposableObj; } public void Dispose() { _disposableObj.Dispose(); } } 

Actualmente estoy pensando que en el primer ejemplo, la persona que llama de DoStuff() debería deshacerse del objeto, ya que probablemente creó el objeto. Pero en el segundo ejemplo, parece que MyClass debería deshacerse del objeto, ya que mantiene una referencia al mismo. El problema con esto es que la clase de llamada podría no saber que MyClass ha conservado una referencia y, por lo tanto, podría decidir deshacerse del objeto antes de que MyClass haya terminado de usarlo. ¿Hay reglas estándar para este tipo de escenario? Si los hay, ¿difieren cuando el objeto desechable se pasa a un constructor?

Una regla general es que si usted creó (o adquirió la propiedad de) el objeto, entonces es su responsabilidad deshacerse de él. Esto significa que si recibe un objeto desechable como parámetro en un método o constructor, por lo general no debería desecharlo.

Tenga en cuenta que algunas clases en .NET Framework eliminan los objetos que recibieron como parámetros. Por ejemplo, disponer un StreamReader también elimina el Stream subyacente.

PD: He publicado una nueva respuesta (que contiene un conjunto simple de reglas que deben llamar a Dispose y cómo diseñar una API que se IDisposable objetos IDisposable ). Si bien la presente respuesta contiene ideas valiosas, he llegado a creer que su sugerencia principal a menudo no funcionará en la práctica: IDisposable objetos IDisposable en objetos “de grano grueso” a menudo significa que esos deben llegar a ser IDisposable sí mismos; entonces uno termina donde comenzó, y el problema persiste.


¿Hay alguna guía o mejores prácticas en torno a quién debería llamar a Dispose() sobre los objetos desechables cuando se hayan pasado a los métodos o al constituyente de otro objeto?

Respuesta corta:

Sí, hay muchos consejos sobre este tema, y ​​lo mejor que conozco es el concepto de Agregados en el Diseño Dirigido por Dominio de Eric Evans . (En pocas palabras, la idea central aplicada a IDisposable es la siguiente: Encapsular el IDisposable en un componente de grano más grueso de forma tal que no sea visto por el exterior y nunca se pase al consumidor del componente).

Además, la idea de que el creador de un objeto IDisposable también deba encargarse de su eliminación es demasiado restrictiva y, a menudo, no funcionará en la práctica.

El rest de mi respuesta entra en más detalles sobre ambos puntos, en el mismo orden. Terminaré mi respuesta con algunos consejos para material adicional relacionado con el mismo tema.

Respuesta más larga: de qué se trata esta pregunta en términos más amplios:

El asesoramiento sobre este tema generalmente no es específico de IDisposable . Cada vez que las personas hablan sobre la duración de los objetos y la propiedad, se están refiriendo al mismo problema (pero en términos más generales).

¿Por qué este tema casi nunca surge en el ecosistema .NET? Debido a que el entorno de tiempo de ejecución de .NET (el CLR) realiza la recolección de basura automática, que hace todo el trabajo por usted: si ya no necesita un objeto, simplemente puede olvidarse de él y el recolector de basura finalmente recuperará su memoria.

¿Por qué, entonces, surge la pregunta con objetos IDisposable ? Porque IDisposable es todo sobre el control explícito y determinista de la vida útil de un recurso (a menudo escaso o costoso): se supone que los objetos IDisposable se liberarán tan pronto como ya no se necesiten y la garantía indeterminada del recolector de basura (” eventualmente reclamaré”). ¡la memoria que usaste! “) simplemente no es lo suficientemente buena.

Su pregunta, reformulada en los términos más amplios de la duración y propiedad del objeto:

¿Qué objeto O debería ser responsable de terminar la vida útil de un objeto (desechable) D , que también pasa a los objetos X,Y,Z ?

Establezcamos algunas suposiciones:

  • Llamar a D.Dispose() para un objeto IDisposable D básicamente finaliza su vida útil.

  • Lógicamente, la duración de un objeto solo puede finalizar una vez. (No importa por el momento que esto se oponga al protocolo IDisposable , que explícitamente permite múltiples llamadas a Dispose ).

  • Por lo tanto, en aras de la simplicidad, exactamente un objeto O debería ser responsable de eliminar a D Llamemos a O el dueño.

Ahora llegamos al meollo del problema: ni el lenguaje C # ni VB.NET proporcionan un mecanismo para imponer las relaciones de propiedad entre los objetos. Entonces esto se convierte en un problema de diseño: todos los objetos O,X,Y,Z que reciben una referencia a otro objeto D deben seguir y adherirse a una convención que regula exactamente quién tiene la propiedad sobre D

Simplifica el problema con Agregados!

El mejor consejo que he encontrado sobre este tema proviene del libro de 2004 de Eric Evans , Domain-Driven Design . Permítanme citar del libro:

Supongamos que está eliminando un objeto Persona de una base de datos. Junto con la persona vaya un nombre, fecha de nacimiento y una descripción del trabajo. Pero, ¿qué pasa con la dirección? Podría haber otras personas en la misma dirección. Si elimina la dirección, esos objetos Person tendrán referencias a un objeto eliminado. Si lo dejas, acumulas direcciones basura en la base de datos. La recolección automática de basura podría eliminar las direcciones basura, pero esa solución técnica, incluso si está disponible en su sistema de base de datos, ignora un problema de modelado básico. (p 125)

Vea cómo esto se relaciona con su problema? Las direcciones de este ejemplo son equivalentes a sus objetos desechables, y las preguntas son las mismas: ¿quién debería eliminarlas? ¿Quién “los posee”?

Evans continúa sugiriendo Agregados como una solución a este problema de diseño. Del libro de nuevo:

Un agregado es un conjunto de objetos asociados que tratamos como una unidad con el propósito de cambios en los datos. Cada Agregado tiene una raíz y un límite. El límite define lo que está dentro del Agregado. La raíz es una Entidad única y específica contenida en el Agregado. La raíz es el único miembro del Agregado al que los objetos externos pueden contener referencias, aunque los objetos dentro del límite pueden contener referencias entre sí. (pp. 126-127)

El mensaje principal aquí es que debe restringir el traspaso de su objeto IDisposable a un conjunto estrictamente limitado (“agregado”) de otros objetos. Los objetos fuera de ese límite agregado nunca deberían obtener una referencia directa a su IDisposable . Esto simplifica enormemente las cosas, ya que ya no tiene que preocuparse de si la mayor parte de todos los objetos, es decir, los que están fuera del agregado, pueden Dispose su objeto. Todo lo que necesita hacer es asegurarse de que todos los objetos dentro del límite saben quién es el responsable de eliminarlo. Esto debería ser un problema bastante fácil de resolver, ya que generalmente los implementaría juntos y se cuidaría de mantener los límites agregados razonablemente “ajustados”.

¿Qué pasa con la sugerencia de que el creador de un objeto IDisposable también debería eliminarlo?

Esta guía parece razonable y hay una simetría atractiva para ella, pero por sí sola, a menudo no funcionará en la práctica. Podría decirse que significa lo mismo que decir: “Nunca pase una referencia a un objeto IDisposable a otro objeto”, porque tan pronto como lo haga, corre el riesgo de que el objeto receptor asum su propiedad y lo deshaga sin su conocimiento.

Veamos dos tipos de interfaz prominentes de .NET Base Class Library (BCL) que violan claramente esta regla de oro: IEnumerable e IObservable . Ambas son esencialmente fábricas que devuelven objetos IDisposable :

  • IEnumerator IEnumerable.GetEnumerator()
    (Recuerde que IEnumerator hereda de IDisposable ).

  • IDisposable IObservable.Subscribe(IObserver observer)

En ambos casos, se espera que la persona que llama disponga el objeto devuelto. Podría decirse que nuestra guía simplemente no tiene sentido en el caso de fábricas de objetos … a menos que, tal vez, IDisposable que el solicitante (no su creador inmediato) del IDisposable libere.

Dicho sea de paso, este ejemplo también demuestra los límites de la solución agregada descrita anteriormente: tanto IEnumerable como IObservable son de naturaleza demasiado general como para formar parte de un agregado. Los agregados suelen ser muy específicos de un dominio.

Recursos e ideas adicionales:

  • En UML, las relaciones “tiene una” entre objetos se pueden modelar de dos maneras: como agregación (diamante vacío) o como composición (diamante relleno). La composición difiere de la agregación en que la duración del objeto contenido / referido finaliza con la del contenedor / referencia. Su pregunta original ha implicado agregación (“propiedad transferible”), mientras que en su mayoría he dirigido hacia soluciones que usan composición (“propiedad fija”). Vea el artículo de Wikipedia sobre “Composición de objetos” .

  • Autofac (un contenedor de IOC de .NET) resuelve este problema de dos maneras: mediante la comunicación, utilizando un tipo de relación llamado, Owned , que adquiere la propiedad de un IDisposable ; o a través del concepto de unidades de trabajo, llamadas ámbitos de vida en Autofac.

  • Con respecto a este último, Nicholas Blumhardt, el creador de Autofac, ha escrito “Un manual de vida de Autofac” , que incluye una sección “IDisposable y de propiedad”. Todo el artículo es un excelente tratado sobre propiedad y problemas de duración en .NET. Recomiendo leerlo, incluso para aquellos que no estén interesados ​​en Autofac.

  • En C ++, la expresión idiomática de Adquisición de recursos es inicialización (RAII) (en general) y los tipos de punteros inteligentes (en particular) ayudan al progtwigdor a resolver correctamente los problemas de duración y propiedad de los objetos. Desafortunadamente, estos no son transferibles a .NET, porque .NET carece del elegante soporte de C ++ para la destrucción determinística de objetos.

  • Consulte también esta respuesta a la pregunta sobre Desbordamiento de stack, “¿Cómo dar cuenta de las necesidades de implementación dispares?” , que (si lo entiendo correctamente) sigue una idea similar a la respuesta basada en Agregado: IDisposable componente de grano grueso alrededor del IDisposable manera que esté completamente contenido (y oculto para el consumidor del componente).

En general, una vez que se trata de un objeto desechable, ya no se encuentra en el mundo ideal del código administrado donde la propiedad vitalicia es un punto discutible. En consecuencia, debe considerar qué objeto lógicamente “posee”, o es responsable de la vida útil de su objeto desechable.

En general, en el caso de un objeto desechable que acaba de pasar a un método, diría que no, el método no debería eliminar el objeto porque es muy raro que un objeto asum la propiedad de otro y luego termine con él en el mismo método. La persona que llama debe ser responsable de la eliminación en esos casos.

No hay una respuesta automática que diga “Sí, siempre deseche” o “No, nunca se deshaga” al hablar de los datos de los miembros. Por el contrario, debe pensar en los objetos en cada caso específico y preguntarse: “¿Es este objeto responsable de la vida útil del objeto desechable?”

La regla general es que el objeto responsable de crear un desechable lo posee, y por lo tanto es responsable de eliminarlo más tarde. Esto no se aplica si hay una transferencia de propiedad. Por ejemplo:

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

Foo es claramente responsable de crear dispObj , pero está pasando la propiedad a la instancia de MyClass .

Esto es una continuación de mi respuesta anterior ; vea su comentario inicial para saber por qué estoy publicando otro.

Mi respuesta anterior tiene una cosa bien: cada IDisposable debe tener un “propietario” exclusivo que será responsable de su eliminación una sola vez. La gestión de objetos IDisposable es muy similar a la gestión de la memoria en escenarios de código no administrados.

La tecnología predecesora de .NET, el Modelo de Objetos Componentes (COM), utilizó el siguiente protocolo para las responsabilidades de administración de memoria entre objetos:

  • “In-parameters debe ser asignado y liberado por la persona que llama.
  • “Los parámetros de salida deben ser asignados por el llamado, son liberados por la persona que llama […].
  • “In-out-parameters son inicialmente asignados por la persona que llama, y ​​luego liberados y reasignados por el llamado, si es necesario. Como ocurre con los parámetros de salida, la persona que llama es responsable de liberar el valor final devuelto”.

(Hay reglas adicionales para casos de error, vea la página vinculada a arriba para más detalles).

Si tuviéramos que adaptar estas pautas para IDisposable , podríamos establecer lo siguiente …

Reglas con respecto a la propiedad de IDisposable :

  1. Cuando un IDisposable se pasa a un método a través de un parámetro regular, no hay transferencia de propiedad. El método llamado puede usar el IDisposable , pero no debe IDisposable (ni transferir la propiedad, ver la regla 4 a continuación).
  2. Cuando se devuelve un IDisposable de un método a través de un parámetro de out o el valor de retorno, la propiedad se transfiere desde el método a su llamador. El que llama tendrá que Dispose (o pasar la propiedad sobre el IDisposable de la misma manera).
  3. Cuando se proporciona un IDisposable a un método a través de un parámetro ref , la propiedad sobre él se transfiere a ese método. El método debe copiar el IDisposable en una variable local o campo de objeto y luego establecer el parámetro ref en null .

Una regla posiblemente importante se desprende de lo anterior:

  1. Si no tiene propiedad, no debe pasarla. Eso significa que, si recibió un objeto IDisposable través de un parámetro regular, no coloque el mismo objeto en un parámetro ref IDisposable , ni lo exponga a través de un valor de retorno o parámetro de out .

Ejemplo:

 sealed class LineReader : IDisposable { public static LineReader Create(Stream stream) { return new LineReader(stream, ownsStream: false); } public static LineReader Create(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` } } 

Esta clase tiene dos métodos estáticos de fábrica y, por lo tanto, le permite a su cliente elegir si quiere conservar o transmitir la propiedad:

  • Uno acepta un objeto Stream través de un parámetro regular. Esto le indica a la persona que llama que no se tomará la propiedad. Por lo tanto, la persona que llama debe Dispose :

     using (var stream = File.OpenRead("Foo.txt")) using (var reader = LineReader.Create(stream)) { string line; while (reader.TryReadLine(out line)) { Console.WriteLine(line); } } 
  • Uno que acepta un objeto Stream través de un parámetro ref . Esto le indica a la persona que llama que se transferirá la propiedad, por lo que la persona que llama no necesita Dispose :

     var stream = File.OpenRead("Foo.txt"); using (var reader = LineReader.Create(ref stream)) { string line; while (reader.TryReadLine(out line)) { Console.WriteLine(line); } } 

    Curiosamente, si se declara stream como una variable de using : using (var stream = …) , la comstackción fallaría porque el using variables no se puede pasar como parámetros ref , por lo que el comstackdor C # ayuda a hacer cumplir nuestras reglas en este caso específico.

Finalmente, tenga en cuenta que File.OpenRead es un ejemplo de un método que devuelve un objeto IDisposable (es decir, un Stream ) a través del valor de retorno, de modo que la propiedad sobre el flujo devuelto se transfiere al llamador.

Desventaja:

La principal desventaja de este patrón es que AFAIK, nadie lo usa (todavía). Por lo tanto, si interactúa con cualquier API que no siga las reglas anteriores (por ejemplo, la Biblioteca de clases de Base de .NET Framework), debe leer la documentación para averiguar quién debe llamar a Dispose en objetos IDisposable .

Una cosa que decidí hacer antes de saber mucho acerca de la progtwigción de .NET, pero aún parece una buena idea, es tener un constructor que acepte un IDisposable aceptar un booleano que IDisposable también se transferirá la propiedad del objeto. Para objetos que pueden existir completamente dentro del scope de using sentencias, esto generalmente no será demasiado importante (dado que el objeto externo se eliminará dentro del scope del bloque Using del objeto interno, no hay necesidad de que el objeto externo disponga el interior uno, de hecho, puede ser necesario que no lo haga). Sin embargo, tal semántica puede volverse esencial cuando el objeto externo pasará como una interfaz o clase base a un código que no conoce la existencia del objeto interno. En ese caso, se supone que el objeto interno debe vivir hasta que se destruya el objeto externo, y lo que sabe que el objeto interno se supone que muere cuando el objeto externo lo hace es el objeto externo en sí, por lo que el objeto externo debe poder destruir el interno.

Desde entonces, he tenido un par de ideas adicionales, pero no las he probado. Me gustaría saber qué piensan los demás:

  1. Un contenedor de conteo de referencia para un objeto IDisposable . Realmente no he descubierto el patrón más natural para hacer esto, pero si un objeto utiliza el recuento de referencias con incrementos / decrementos entrelazados, y si (1) todo el código que manipula el objeto lo usa correctamente, y (2) no hay referencias cíclicas se crean utilizando el objeto, esperaría que fuera posible tener un objeto IDisposable compartido que se destruya cuando se IDisposable el último uso. Probablemente lo que debería pasar sería que la clase pública debería ser un contenedor para una clase contada de referencia privada, y debería admitir un constructor o método de fábrica que creará un nuevo contenedor para la misma instancia base (reemplazando el recuento de referencias de la instancia por uno ) O bien, si la clase debe limpiarse incluso cuando se abandonan los contenedores, y si la clase tiene alguna rutina de sondeo periódica, la clase podría mantener una lista de WeakReference a sus contenedores y verificar que al menos algunos de ellos aún existan. .
  2. Haga que el constructor de un objeto IDisposable acepte un delegado al que llamará la primera vez que se IDisposable objeto (un objeto IDisposable debe usar Interlocked.Exchange en el indicador isDisposed para asegurarse de que se elimine exactamente una vez). Ese delegado podría encargarse de deshacerse de cualquier objeto nested (posiblemente con un cheque para ver si alguien más todavía los tenía).

¿Alguno de estos parece un buen patrón?