c# - recursos - ¿Quién debe llamar a Dispose en objetos IDisposable cuando se pasa a otro objeto?
para que sirve dispose(); (5)
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 seIDisposable
objetosIDisposable
). 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
objetosIDisposable
en objetos "de grano grueso" a menudo significa que esos deben llegar a serIDisposable
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 Aggregates 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 resto 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 objetosX,Y,Z
?
Establezcamos algunas suposiciones:
Llamar a
D.Dispose()
para un objetoIDisposable
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 aDispose
).Por lo tanto, en aras de la simplicidad, exactamente un objeto
O
debería ser responsable de eliminar aD
Llamemos aO
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 Aggregates 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 asuma 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<T>
e IObservable<T>
. Ambas son esencialmente fábricas que devuelven objetos IDisposable
:
IEnumerator<T> IEnumerable<T>.GetEnumerator()
(Recuerde queIEnumerator<T>
hereda deIDisposable
).IDisposable IObservable<T>.Subscribe(IObserver<T> 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<T>
como IObservable<T>
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 Autofac .NET) resuelve este problema de dos maneras: mediante la comunicación, utilizando un tipo de relación llamado,
Owned<T>
, que adquiere la propiedad de unIDisposable
; 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 programador 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 pila, "¿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 delIDisposable
manera que esté completamente contenido (y oculto para el consumidor del componente).
¿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?
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 asuma 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
:
- 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 elIDisposable
, pero no debeIDisposable
(ni transferir la propiedad, ver la regla 4 a continuación). - Cuando se devuelve un
IDisposable
de un método a través de un parámetro deout
o el valor de retorno, la propiedad se transfiere desde el método a su llamador. El que llama tendrá queDispose
(o pasar la propiedad sobre elIDisposable
de la misma manera). - Cuando se proporciona un
IDisposable
a un método a través de un parámetroref
, la propiedad sobre él se transfiere a ese método. El método debe copiar elIDisposable
en una variable local o campo de objeto y luego establecer el parámetroref
ennull
.
Una regla posiblemente importante se desprende de lo anterior:
- 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ámetroref IDisposable
, ni lo exponga a través de un valor de retorno o parámetro deout
.
Ejemplo:
sealed class LineReader : IDisposable
{
public static LineReader Create(Stream stream)
{
return new LineReader(stream, ownsStream: false);
}
public static LineReader Create<TStream>(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 debeDispose
: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ámetroref
. Esto le indica a la persona que llama que se transferirá la propiedad, por lo que la persona que llama no necesitaDispose
: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 deusing
:using (var stream = …)
, la compilación fallaría porque elusing
variables no se puede pasar como parámetrosref
, por lo que el compilador 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 programació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 alcance de using
sentencias, esto generalmente no será demasiado importante (dado que el objeto externo se eliminará dentro del alcance 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:
- 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 objetoIDisposable
compartido que se destruya cuando seIDisposable
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 deWeakReference
a sus contenedores y verificar que al menos algunos de ellos aún existan. . - Haga que el constructor de un objeto
IDisposable
acepte un delegado al que llamará la primera vez que seIDisposable
objeto (un objetoIDisposable
debe usarInterlocked.Exchange
en el indicador isDisposed para asegurarse de que se elimine exactamente una vez). Ese delegado podría encargarse de deshacerse de cualquier objeto anidado (posiblemente con un cheque para ver si alguien más todavía los tenía).
¿Alguno de estos parece un buen patrón?
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.