c# - method - ¿Quién se deshace de una propiedad pública identificable?
metodo dispose() c# (10)
El diseño que ha mencionado aquí no es algo que pueda manejar este escenario. Usted dijo que hay un contenedor para esa clase, entonces debería disponerlo junto a sí mismo. Si otros objetos pueden estar usándolo, entonces no es el contenedor y el alcance de su clase se amplía y debe disponerse en el límite de ese alcance.
Si tengo una clase SomeDisposableObject
que implementa IDisposable
:
class SomeDisposableObject : IDisposable
{
public void Dispose()
{
// Do some important disposal work.
}
}
Y tengo otra clase llamada AContainer
, que tiene una instancia de SomeDisposableObject
como propiedad pública:
class AContainer
{
SomeDisposableObject m_someObject = new SomeDisposableObject();
public SomeDisposableObject SomeObject
{
get { return m_someObject; }
set { m_someObject = value; }
}
}
Entonces, FxCop insistirá en que AContainer
también sea IDisposable
.
Lo cual está bien, pero no puedo ver cómo puedo llamar con seguridad m_someObject.Dispose()
desde AContainer.Dispose()
, ya que otra clase aún puede tener una referencia a la instancia m_someObject
.
¿Cuál es la mejor manera de evitar este escenario?
(Suponga que el otro código se basa en AContainer.SomeObject
siempre tiene un valor no nulo, por lo que simplemente mover la creación de la instancia fuera del AContainer
no es una opción)
Edición : expandiré con algún ejemplo, ya que creo que a algunos comentaristas les falta el problema. Si acabo de implementar un método Dispose()
en AContainer
que llama a m_someObject.Dispose (), entonces me quedo con estas situaciones:
// Example One
AContainer container1 = new AContainer();
SomeDisposableObject obj1 = container1.SomeObject;
container1.Dispose();
obj1.DoSomething(); // BAD because obj1 has been disposed by container1.
// Example Two
AContainer container2 = new AContainer();
SomeObject obj2 = new SomeObject();
container2.SomeObject = obj2; // BAD because the previous value of SomeObject not disposed.
container2.Dispose();
obj2.DoSomething(); // BAD because obj2 has been disposed by container2, which doesn''t really "own" it anyway.
¿Eso ayuda?
El problema real podría ser su diseño orientado a objetos. Si AContainer está Desechado, todos sus objetos miembros también deben eliminarse. Si no, suena como que puedes disponer de un cuerpo pero quieres mantener viva la instancia de la pierna. No suena bien
En general, creo que quien crea el objeto debe ser responsable de la eliminación. En este caso, AContainer crea SomeDisposableObject, por lo que debería estar Desechado cuando AContainer lo está.
Si, por alguna razón, piensa que SomeDisposableObject debería vivir más que AContainer, solo puedo pensar en los siguientes métodos:
- deje SomeDisposableObject unDisposed, en cuyo caso el GC se encargará de usted.
- dé a SomeDisposableObject una referencia a AContainer (vea Controles de WinForms y propiedades principales). Siempre que se pueda acceder a SomeDisposableObject, también lo es AContainer. Eso evitará que el GC se deshaga de un contenedor, pero si alguien llama Desechar manualmente, bueno, desecharía un objeto desechable. Yo diría que se espera.
- Implemente SomeDisposableObject como un método, digamos CreateSomeDisposableObject (). Eso deja claro (er) que el cliente es responsable de la eliminación.
Sin embargo, en general, no estoy seguro de que el diseño tenga sentido. Después de todo, parece que estás esperando un código de cliente como:
SomeDisposableObject d;
using (var c = new AContainer()) {
d = c.SomeObject;
}
// do something with d
Eso me parece un código de cliente roto. Es una violación de la Ley de Deméter, y un simple sentido común para mí.
Intentaré responder a mi propia pregunta:
Evitarlo en el primer lugar
La forma más fácil de salir de esta situación es refactorizar el código para evitar el problema por completo.
Hay dos formas obvias de hacer esto.
Creación de instancia externa
Si AContainer
no crea una instancia SomeDisposableObject
, sino que se basa en un código externo para suministrarlo, AContainer
ya no será "propietario" de la instancia y no es responsable de eliminarla.
La instancia creada externamente podría suministrarse a través del constuctor o estableciendo la propiedad.
public class AContainerClass
{
SomeDisposableObject m_someObject; // No creation here.
public AContainerClass(SomeDisposableObject someObject)
{
m_someObject = someObject;
}
public SomeDisposableObject SomeObject
{
get { return m_someObject; }
set { m_someObject = value; }
}
}
Mantener la instancia privada
El principal problema con el código publicado es que la propiedad está confundida. En el momento de AContainer
clase AContainer
no puede decir quién es el propietario de la instancia. Podría ser la instancia que creó o podría ser alguna otra instancia que se creó externamente y se set
través de la propiedad.
Incluso si hace un seguimiento de esto y sabe con certeza que se trata de la instancia que creó, entonces todavía no puede deshacerse de ella con seguridad , ya que otras clases pueden ahora tener una referencia a la misma que obtuvieron de la propiedad pública.
Si el código puede ser refactorizado para evitar hacer pública la instancia (es decir, eliminando la propiedad por completo), entonces el problema desaparece.
Y si no se puede evitar ...
Si, por alguna razón, el código no puede ser refactorizado de esta manera (como lo estipulé en la pregunta), entonces, en mi opinión, le quedan algunas opciones de diseño bastante difíciles.
Siempre disponer de la instancia.
Si elige este enfoque, entonces está declarando efectivamente que AContainer
será el propietario de la instancia SomeDisposableObject
cuando se establezca la propiedad.
Esto tiene sentido en algunas situaciones, particularmente cuando SomeDisposableObject
es claramente un objeto pasajero o subordinado. Sin embargo, debe documentarse cuidadosamente ya que requiere que el código de llamada esté al tanto de esta transferencia de propiedad.
(Puede ser más apropiado usar un método, en lugar de una propiedad, ya que el nombre del método se puede usar para dar una pista adicional sobre la propiedad).
public class AContainerClass: IDisposable
{
SomeDisposableObject m_someObject = new SomeDisposableObject();
public SomeDisposableObject SomeObject
{
get { return m_someObject; }
set
{
if (m_someObject != null && m_someObject != value)
m_someObject.Dispose();
m_someObject = value;
}
}
public void Dispose()
{
if (m_someObject != null)
m_someObject.Dispose();
GC.SuppressFinalize(this);
}
}
Solo desechar si aún es la instancia original
En este enfoque, usted rastrearía si la instancia fue cambiada desde la creada originalmente por AContainer
y la eliminaría solo cuando era la original. Aquí se mezcla el modelo de propiedad. AContainer
sigue siendo el propietario de su propia instancia SomeDisposableObject
, pero si se proporciona una instancia externa, entonces es responsabilidad del código externo deshacerse de él.
Este enfoque refleja mejor la situación real aquí, pero puede ser difícil de implementar correctamente. El código del cliente todavía puede causar problemas al realizar operaciones como esta:
AContainerClass aContainer = new AContainerClass();
SomeDisposableObject originalInstance = aContainer.SomeObject;
aContainer.SomeObject = new SomeDisposableObject();
aContainer.DoSomething();
aContainer.SomeObject = originalInstance;
Aquí se intercambió una nueva instancia, se llamó a un método y luego se restauró la instancia original. Desafortunadamente, AContainer
habrá llamado a Dispose()
en la instancia original cuando fue reemplazado, por lo que ahora no es válido.
Solo ríndete y deja que el GC lo maneje
Esto obviamente es menos que ideal. Si la clase SomeDisposableObject
realmente contiene algún recurso escaso, entonces no desecharlo de inmediato causará problemas.
Sin embargo, también puede representar el enfoque más sólido en términos de cómo el código del cliente interactúa con AContainer
ya que no requiere ningún conocimiento especial de cómo AContainer
trata la propiedad de la instancia SomeDisposableObject
.
Si sabe que el recurso disponible no es realmente escaso en su sistema, este puede ser el mejor enfoque.
Algunos comentadores han sugerido que puede ser posible usar el conteo de referencias para rastrear si alguna otra clase todavía tiene una referencia a la instancia SomeDisposableObject
. Esto sería muy útil ya que nos permitiría deshacernos de él solo cuando sabemos que es seguro hacerlo y, de lo contrario, dejar que el GC lo maneje.
Sin embargo, no conozco ninguna API C # / .NET para determinar el recuento de referencia de un objeto. Si hay uno, por favor hágamelo saber.
La razón por la que no puede llamar de forma segura a Dispose()
en la instancia de SomeDisposableObject
de SomeDisposableObject
es debido a la falta de encapsulación. La propiedad pública proporciona acceso sin restricciones a parte del estado interno. Como esta parte del estado interno debe obedecer las reglas del protocolo IDisposable, es importante asegurarse de que esté bien encapsulado.
El problema es similar a permitir el acceso a una instancia utilizada para el bloqueo. Si lo hace, es mucho más difícil determinar dónde se adquieren los bloqueos.
Si puede evitar exponer su instancia desechable, el problema de quién manejará la llamada a Dispose()
desaparecerá.
No hay una respuesta única, depende de su escenario, y el punto clave es la propiedad del recurso disponible representado por la propiedad, como lo señala Jon Skeet .
A veces es útil ver ejemplos de .NET Framework. Aquí hay tres ejemplos que se comportan de manera diferente:
El contenedor siempre dispone . System.IO.StreamReader expone una propiedad desechable BaseStream. Se considera que posee el flujo subyacente, y la eliminación del StreamReader siempre desecha el flujo subyacente.
El contenedor nunca se deshace . System.DirectoryServices.DirectoryEntry expone una propiedad principal. No se considera que sea el propietario de su principal, por lo que la eliminación de DirectoryEntry nunca dispone de su principal.
En este caso, se devuelve una nueva instancia de DirectoryEntry cada vez que se elimina la referencia a la propiedad principal y se supone que se espera que la persona que llama la descarte. Podría decirse que esto rompe las pautas para las propiedades, y tal vez debería haber un método GetParent () en su lugar.
Contenedor a veces dispone . System.Data.SqlClient.SqlDataReader expone una propiedad de conexión desechable, pero el autor de la llamada decide si el lector posee (y, por lo tanto, elimina) la conexión subyacente mediante el argumento CommandBehavior de SqlCommand.ExecuteReader.
Otro ejemplo interesante es System.DirectoryServices.DirectorySearcher, que tiene una propiedad desechable de lectura / escritura SearchRoot. Si esta propiedad se establece desde el exterior, se supone que el recurso subyacente no es propiedad, por lo que el contenedor no lo desecha. Si no se establece desde el exterior, se genera una referencia interna y se establece un indicador para garantizar que se eliminará. Puedes ver esto con el Reflector Lutz.
Debe decidir si su contenedor es dueño del recurso y asegurarse de documentar su comportamiento con precisión.
Si decide que es el propietario del recurso y la propiedad es de lectura / escritura, debe asegurarse de que su configurador elimine cualquier referencia que esté reemplazando, por ejemplo:
public SomeDisposableObject SomeObject
{
get { return m_someObject; }
set
{
if ((m_someObject != null) &&
(!object.ReferenceEquals(m_someObject, value))
{
m_someObject.Dispose();
}
m_someObject = value;
}
}
private SomeDisposableObject m_someObject;
ACTUALIZACIÓN : GrahamS señala correctamente en los comentarios que es mejor probar m_someObject! = Value en el setter antes de desechar: He actualizado el ejemplo anterior para tener esto en cuenta (usando ReferenceEquals en lugar de! = Para ser explícito). Aunque en muchos escenarios del mundo real, la existencia de un colocador puede implicar que el objeto no es propiedad del contenedor y, por lo tanto, no se eliminará.
Realmente depende de quién teóricamente "posee" el objeto desechable. En algunos casos, es posible que desee pasar el objeto, por ejemplo, en el constructor, sin que su clase asuma la responsabilidad de limpiarlo. Otras veces es posible que desee limpiarlo usted mismo. Si está creando el objeto (como en su código de muestra), es casi seguro que sea su responsabilidad limpiarlo.
En cuanto a la propiedad, no creo que tener una propiedad realmente deba transferir la propiedad o algo así. Si su tipo es responsable de deshacerse del objeto, debe mantener esa responsabilidad.
Si tiene un objeto desechable en su clase, implementa IDisposable
con un método de IDisposable
que elimina los desechables envueltos. Ahora el código de llamada debe garantizar que se using()
o que se utiliza un código equivalente try
/ finally
que elimina el objeto.
Una cosa interesante que he encontrado es que SqlCommand posee una instancia de SqlConnection (ambos implementan IDisposable) por lo general. Sin embargo, las llamadas a disposición en SqlCommand NO eliminarán la conexión también.
He descubierto esto también con la ayuda de aquí .
Entonces, en otras palabras, importa si la instancia del "niño" (¿anidada?) Puede ser reutilizada más adelante.
Usted podría simplemente marcar la eliminación en Disposición (). Después de todo, la eliminación no es un destructor, el objeto todavía existe.
asi que:
class AContainer : IDisposable
{
bool _isDisposed=false;
public void Dispose()
{
if (!_isDisposed)
{
// dispose
}
_isDisposed=true;
}
}
Agrega esto a tu otra clase, también.