Establecer un objeto en nulo frente a Dispose ()


108

Estoy fascinado por la forma en que funcionan CLR y GC (estoy trabajando para expandir mi conocimiento sobre esto leyendo CLR a través de C #, los libros / publicaciones de Jon Skeet y más).

De todos modos, ¿cuál es la diferencia entre decir:

MyClass myclass = new MyClass();
myclass = null;

¿O haciendo que MyClass implemente IDisposable y un destructor y llame a Dispose ()?

Además, si tengo un bloque de código con una declaración de uso (por ejemplo, a continuación), si paso por el código y salgo del bloque de uso, ¿se elimina el objeto en ese momento o cuando ocurre una recolección de basura? ¿Qué pasaría si llamo a Dispose () en el bloque using de todos modos?

using (MyDisposableObj mydispobj = new MyDisposableObj())
{

}

¿Las clases de flujo (por ejemplo, BinaryWriter) tienen un método Finalize? ¿Por qué querría usar eso?

Respuestas:


210

Es importante separar la eliminación de la recolección de basura. Son cosas completamente separadas, con un punto en común al que llegaré en un minuto.

Dispose, recolección y finalización de basura

Cuando escribe una usingdeclaración, es simplemente azúcar sintáctico para un bloque try / finalmente, por lo que Disposese llama incluso si el código en el cuerpo de la usingdeclaración arroja una excepción. Es no significa que el objeto es basura recogida al final del bloque.

La eliminación se refiere a recursos no administrados ( recursos que no son de memoria). Estos pueden ser identificadores de interfaz de usuario, conexiones de red, identificadores de archivos, etc. Son recursos limitados, por lo que generalmente querrá liberarlos tan pronto como sea posible. Debe implementar IDisposablesiempre que su tipo "posea" un recurso no administrado, ya sea directamente (generalmente a través de un IntPtr) o indirectamente (por ejemplo, a través de a Stream, a, SqlConnectionetc.).

La recolección de basura en sí solo se trata de memoria, con un pequeño giro. El recolector de basura puede encontrar objetos a los que ya no se puede hacer referencia y liberarlos. Sin embargo, no busca basura todo el tiempo, solo cuando detecta que lo necesita (por ejemplo, si una "generación" del montón se queda sin memoria).

El giro es la finalización . El recolector de basura mantiene una lista de objetos que ya no son accesibles, pero que tienen un finalizador (escrito como ~Foo()en C #, algo confuso - no son nada como destructores de C ++). Ejecuta los finalizadores en estos objetos, por si acaso necesitan hacer una limpieza adicional antes de que se libere su memoria.

Los finalizadores casi siempre se utilizan para limpiar recursos en el caso de que el usuario del tipo haya olvidado deshacerse de ellos de manera ordenada. Entonces, si abre un FileStreampero se olvida de llamar Disposeo Close, el finalizador eventualmente liberará el identificador del archivo subyacente por usted. En un programa bien escrito, los finalizadores casi nunca deberían dispararse, en mi opinión.

Establecer una variable en null

Un pequeño punto al establecer una variable en null: esto casi nunca es necesario por el bien de la recolección de basura. Es posible que a veces desee hacerlo si es una variable miembro, aunque en mi experiencia es raro que "parte" de un objeto ya no sea necesaria. Cuando se trata de una variable local, el JIT suele ser lo suficientemente inteligente (en modo de lanzamiento) como para saber cuándo no volverá a usar una referencia. Por ejemplo:

StringBuilder sb = new StringBuilder();
sb.Append("Foo");
string x = sb.ToString();

// The string and StringBuilder are already eligible
// for garbage collection here!
int y = 10;
DoSomething(y);

// These aren't helping at all!
x = null;
sb = null;

// Assume that x and sb aren't used here

El único momento en el que puede valer la pena establecer una variable local nulles cuando estás en un bucle, y algunas ramas del bucle necesitan usar la variable, pero sabes que has llegado a un punto en el que no. Por ejemplo:

SomeObject foo = new SomeObject();

for (int i=0; i < 100000; i++)
{
    if (i == 5)
    {
        foo.DoSomething();
        // We're not going to need it again, but the JIT
        // wouldn't spot that
        foo = null;
    }
    else
    {
        // Some other code 
    }
}

Implementando IDisposable / finalizadores

Entonces, ¿sus propios tipos deberían implementar finalizadores? Es casi seguro que no. Si solo tiene recursos no administrados indirectamente (por ejemplo, tiene una FileStreamvariable miembro), agregar su propio finalizador no ayudará: es casi seguro que la transmisión sea elegible para la recolección de basura cuando su objeto lo sea, por lo que puede confiar en FileStreamtener un finalizador (si es necesario, puede referirse a otra cosa, etc.). Si desea mantener un recurso no administrado "casi" directamente, ¿ SafeHandlees su amigo? Se necesita un poco de tiempo para empezar, pero significa que casi nunca necesitará escribir un finalizador de nuevo . Por lo general, solo debería necesitar un finalizador si tiene un control realmente directo sobre un recurso (an IntPtr) y debería buscar pasar aSafeHandletan pronto como puedas. (Hay dos enlaces allí, lea ambos, idealmente).

Joe Duffy tiene un conjunto muy extenso de pautas sobre finalizadores e IDisposable (coescrito con mucha gente inteligente) que vale la pena leer. Vale la pena tener en cuenta que si sellas tus clases, la vida será mucho más fácil: el patrón de anulación Disposepara llamar a un nuevo Dispose(bool)método virtual , etc., solo es relevante cuando tu clase está diseñada para herencia.

Esto ha sido un poco complicado, pero pide una aclaración sobre dónde te gustaría :)


Re "El único momento en el que puede valer la pena establecer una variable local en nula", quizás también algunos de los escenarios de "captura" más espinosos (capturas múltiples de la misma variable), ¡pero puede que no valga la pena complicar la publicación! +1 ...
Marc Gravell

@Marc: Eso es cierto, ni siquiera había pensado en las variables capturadas. Hmm. Sí, creo que dejaré eso en paz;)
Jon Skeet

¿Podría decirnos qué pasará cuando establezca "foo = null" en el fragmento de código anterior? Hasta donde yo sé, ¿esa línea solo borra el valor de una variable que apunta al objeto foo en el montón administrado? entonces la pregunta es ¿qué pasará con el objeto foo allí? ¿No deberíamos llamar a deshacernos de eso?
odiseh

@odiseh: Si el objeto fuera desechable, entonces sí, debes desecharlo. Sin embargo, esa sección de la respuesta solo trataba sobre la recolección de basura, que está completamente separada.
Jon Skeet

1
Estaba buscando una aclaración sobre algunas preocupaciones de IDisposable, así que busqué en Google "IDisposable Skeet" y encontré esto. ¡Excelente! : D
Maciej Wozniak

22

Cuando desecha un objeto, los recursos se liberan. Cuando asigna nulo a una variable, simplemente está cambiando una referencia.

myclass = null;

Después de ejecutar esto, el objeto al que se refería myclass aún existe y continuará hasta que el GC lo limpie. Si se llama explícitamente a Dispose, o si está en un bloque de uso, los recursos se liberarán lo antes posible.


7
Es posible que aún no exista después de ejecutar esa línea; es posible que se haya recolectado la basura antes de esa línea. El JIT es inteligente, por lo que líneas como esta casi siempre son irrelevantes.
Jon Skeet

6
Establecer en nulo podría significar que los recursos que tiene el objeto nunca se liberan. El GC no desecha, solo finaliza, por lo que si el objeto contiene directamente recursos no administrados y su finalizador no desecha (o no tiene un finalizador), esos recursos se filtrarán. Algo a tener en cuenta.
LukeH

6

Las dos operaciones no tienen mucho que ver entre sí. Cuando establece una referencia en nulo, simplemente lo hace. En sí mismo, no afecta en absoluto a la clase a la que se hizo referencia. Su variable simplemente ya no apunta al objeto al que solía, pero el objeto en sí no se modifica.

Cuando llamas a Dispose (), es una llamada a un método en el objeto mismo. Cualquier cosa que haga el método Dispose, ahora se hace en el objeto. Pero esto no afecta su referencia al objeto.

La única área de superposición es que cuando no hay más referencias a un objeto, eventualmente se recolectará la basura. Y si la clase implementa la interfaz IDisposable, entonces se llamará a Dispose () en el objeto antes de que se recolecte la basura.

Pero eso no sucederá inmediatamente después de establecer su referencia en nula, por dos razones. Primero, pueden existir otras referencias, por lo que todavía no se recolectará la basura, y segundo, incluso si esa fue la última referencia, por lo que ahora está lista para ser recolectada, no sucederá nada hasta que el recolector de basura decida eliminar el objeto.

Llamar a Dispose () en un objeto no "mata" el objeto de ninguna manera. Se usa comúnmente para limpiar para que el objeto se pueda eliminar de forma segura después, pero en última instancia, no hay nada mágico en Dispose, es solo un método de clase.


Creo que esta respuesta complementa o es un detalle de la respuesta de "recursivo".
dance2die
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.