¿Necesita deshacerse de los objetos y configurarlos como nulos?

310

¿Necesita deshacerse de los objetos y ponerlos en nulo, o el recolector de basura los limpiará cuando salgan del alcance?

CJ7
fuente
44
Parece haber un consenso de que no necesita establecer el objeto en nulo, pero ¿necesita hacer Dispose ()?
CJ7
3
como Jeff dijo: codinghorror.com/blog/2009/01/…
tanathos
99
Mi consejo es siempre eliminar si un objeto implementa IDisposable. Use un bloque de uso cada vez. No haga suposiciones, no lo deje al azar. Sin embargo, no tiene que configurar las cosas como nulas. Un objeto solo tiene que salir del alcance.
Peter
11
@peter: No use bloques "en uso" con servidores proxy de WCF: msdn.microsoft.com/en-us/library/aa355056.aspx
nlawalker
99
¡SIN EMBARGO, PUEDE querer establecer algunas referencias nulas dentro de su Dispose()método! Esta es una variación sutil en esta pregunta, pero importante porque el objeto que se está eliminando no puede saber si está "fuera de alcance" (llamar Dispose()no es una garantía). Más aquí: stackoverflow.com/questions/6757048/…
Kevin P. Rice el

Respuestas:

239

Los objetos se limpiarán cuando ya no se usen y cuando el recolector de basura lo considere adecuado. A veces, es posible que necesite establecer un objeto para nullque quede fuera del alcance (como un campo estático cuyo valor ya no necesita), pero en general generalmente no es necesario configurarlo null.

En cuanto a la eliminación de objetos, estoy de acuerdo con @Andre. Si el objeto es IDisposable, es una buena idea deshacerse de él cuando ya no lo necesite, especialmente si el objeto utiliza recursos no administrados. No disponer de recursos no administrados dará lugar a pérdidas de memoria .

Puede usar la usingdeclaración para disponer automáticamente un objeto una vez que su programa abandone el alcance de la usingdeclaración.

using (MyIDisposableObject obj = new MyIDisposableObject())
{
    // use the object here
} // the object is disposed here

Que es funcionalmente equivalente a:

MyIDisposableObject obj;
try
{
    obj = new MyIDisposableObject();
}
finally
{
    if (obj != null)
    {
        ((IDisposable)obj).Dispose();
    }
}
Zach Johnson
fuente
44
Si obj es un tipo de referencia, entonces el bloque finalmente es equivalente a:if (obj != null) ((IDisposable)obj).Dispose();
Randy apoya a Monica
1
@Tuzo: ¡Gracias! Editado para reflejar eso.
Zach Johnson
2
Un comentario al respecto IDisposable. No eliminar un objeto generalmente no provocará una pérdida de memoria en ninguna clase bien diseñada. Al trabajar con recursos no administrados en C #, debe tener un finalizador que aún libere los recursos no administrados. Esto significa que, en lugar de desasignar los recursos cuando debería hacerse, se diferirá cuando el recolector de basura finalice el objeto administrado. Sin embargo, aún puede causar muchos otros problemas (como bloqueos inéditos). ¡Deberías descartar un IDisposablepensamiento!
Aidiakapi
@RandyLevy ¿Tienes una referencia para eso? Gracias
Básico
Pero mi pregunta es Dispose () necesita implementar alguna lógica? ¿Tiene que hacer algo? ¿O internamente cuando Dispose () se llama señales GC que es bueno? Verifiqué el código fuente de TextWriter, por ejemplo, y Dispose no tiene implementación.
Mihail Georgescu
137

Los objetos nunca salen del alcance en C # como lo hacen en C ++. El recolector de basura los trata automáticamente cuando ya no se usan. Este es un enfoque más complicado que C ++ donde el alcance de una variable es completamente determinista. El recolector de basura CLR revisa activamente todos los objetos que se han creado y funciona si se están utilizando.

Un objeto puede quedar "fuera de alcance" en una función, pero si se devuelve su valor, entonces GC vería si la función de llamada mantiene o no el valor de retorno.

No nulles necesario establecer referencias de objeto ya que la recolección de basura funciona al determinar a qué objetos hacen referencia otros objetos.

En la práctica, no tienes que preocuparte por la destrucción, simplemente funciona y es genial :)

Disposedebe invocarse en todos los objetos que se implementan IDisposablecuando termina de trabajar con ellos. Normalmente usarías un usingbloque con esos objetos así:

using (var ms = new MemoryStream()) {
  //...
}

EDITAR En alcance variable. Craig ha preguntado si el alcance variable tiene algún efecto en la vida útil del objeto. Para explicar adecuadamente ese aspecto de CLR, necesitaré explicar algunos conceptos de C ++ y C #.

Alcance variable real

En ambos idiomas, la variable solo se puede usar en el mismo alcance que se definió: clase, función o un bloque de enunciado encerrado entre llaves. La sutil diferencia, sin embargo, es que en C #, las variables no se pueden redefinir en un bloque anidado.

En C ++, esto es perfectamente legal:

int iVal = 8;
//iVal == 8
if (iVal == 8){
    int iVal = 5;
    //iVal == 5
}
//iVal == 8

En C #, sin embargo, obtienes un error de compilación:

int iVal = 8;
if(iVal == 8) {
    int iVal = 5; //error CS0136: A local variable named 'iVal' cannot be declared in this scope because it would give a different meaning to 'iVal', which is already used in a 'parent or current' scope to denote something else
}

Esto tiene sentido si observa el MSIL generado: todas las variables utilizadas por la función se definen al comienzo de la función. Echa un vistazo a esta función:

public static void Scope() {
    int iVal = 8;
    if(iVal == 8) {
        int iVal2 = 5;
    }
}

A continuación se muestra la IL generada. Tenga en cuenta que iVal2, que se define dentro del bloque if, se define realmente a nivel de función. Efectivamente, esto significa que C # solo tiene un alcance de nivel de clase y función en lo que respecta a la vida útil de la variable.

.method public hidebysig static void  Scope() cil managed
{
  // Code size       19 (0x13)
  .maxstack  2
  .locals init ([0] int32 iVal,
           [1] int32 iVal2,
           [2] bool CS$4$0000)

//Function IL - omitted
} // end of method Test2::Scope

Alcance de C ++ y vida útil del objeto

Cada vez que una variable de C ++, asignada en la pila, queda fuera de alcance, se destruye. Recuerde que en C ++ puede crear objetos en la pila o en el montón. Cuando los crea en la pila, una vez que la ejecución abandona el alcance, se sacan de la pila y se destruyen.

if (true) {
  MyClass stackObj; //created on the stack
  MyClass heapObj = new MyClass(); //created on the heap
  obj.doSomething();
} //<-- stackObj is destroyed
//heapObj still lives

Cuando se crean objetos C ++ en el montón, deben destruirse explícitamente; de ​​lo contrario, se trata de una pérdida de memoria. Sin embargo, no existe tal problema con las variables de pila.

C # Objeto de por vida

En CLR, los objetos (es decir, los tipos de referencia) siempre se crean en el montón administrado. Esto se ve reforzado por la sintaxis de creación de objetos. Considere este fragmento de código.

MyClass stackObj;

En C ++ esto crearía una instancia MyClassen la pila y llamaría a su constructor predeterminado. En C # crearía una referencia a la clase MyClassque no apunta a nada. La única forma de crear una instancia de una clase es utilizando el newoperador:

MyClass stackObj = new MyClass();

En cierto modo, los objetos de C # son muy parecidos a los objetos que se crean utilizando la newsintaxis en C ++: se crean en el montón pero, a diferencia de los objetos de C ++, son administrados por el tiempo de ejecución, por lo que no tiene que preocuparse por destruirlos.

Dado que los objetos siempre están en el montón, el hecho de que las referencias a objetos (es decir, punteros) se salgan del alcance se vuelve discutible. Hay más factores involucrados en determinar si un objeto se va a recolectar que simplemente la presencia de referencias al objeto.

C # referencias de objeto

Jon Skeet comparó las referencias de objetos en Java con trozos de cuerda que están unidos al globo, que es el objeto. La misma analogía se aplica a las referencias de objetos de C #. Simplemente apuntan a una ubicación del montón que contiene el objeto. Por lo tanto, establecerlo como nulo no tiene un efecto inmediato en la vida útil del objeto, el globo continúa existiendo, hasta que el GC lo "explota".

Continuando con la analogía del globo, parecería lógico que una vez que el globo no tiene ataduras, puede ser destruido. De hecho, así es exactamente cómo funcionan los objetos contados de referencia en lenguajes no administrados. Excepto que este enfoque no funciona para referencias circulares muy bien. Imagine dos globos que están unidos por una cuerda pero ninguno de los globos tiene una cuerda para nada más. Bajo simples reglas de conteo de referencias, ambos continúan existiendo, a pesar de que todo el grupo de globos está "huérfano".

Los objetos .NET son muy parecidos a los globos de helio bajo un techo. Cuando se abre el techo (el GC funciona): los globos no utilizados se alejan flotando, a pesar de que puede haber grupos de globos unidos entre sí.

.NET GC utiliza una combinación de GC generacional y marca y barrido. El enfoque generacional implica el tiempo de ejecución que favorece la inspección de los objetos que se han asignado más recientemente, ya que es más probable que no se usen y marcar y barrer implica el tiempo de ejecución que recorre todo el gráfico de objetos y funciona si hay grupos de objetos que no se utilizan. Esto trata adecuadamente con el problema de dependencia circular.

Además, .NET GC se ejecuta en otro subproceso (llamado subproceso finalizador) ya que tiene bastante que hacer y hacerlo en el subproceso principal interrumpiría su programa.

Igor Zevaka
fuente
1
@Igor: Al ir 'fuera de alcance' quiero decir que la referencia del objeto está fuera de contexto y no se puede hacer referencia a ella en el alcance actual. Seguramente esto todavía sucede en C #.
CJ7
@ Craig Johnston, no confunda el alcance variable utilizado por el compilador con la vida útil variable que está determinada por el tiempo de ejecución: son diferentes. Es posible que una variable local no esté "en vivo" aunque todavía esté dentro del alcance.
Randy apoya a Mónica
1
@Craig Johnston: Ver blogs.msdn.com/b/ericgu/archive/2004/07/23/192842.aspx : "no hay garantía de que una variable local permanezca activa hasta el final de un alcance si no lo es El tiempo de ejecución es libre de analizar el código que tiene y determinar qué no hay más usos de una variable más allá de cierto punto, y por lo tanto no mantener esa variable viva más allá de ese punto (es decir, no tratarla como una raíz para los fines de GC) ".
Randy apoya a Monica el
1
@Tuzo: Cierto. Para eso es GC.KeepAlive.
Steven Sudit
1
@ Craig Johnston: No y sí. No, porque el tiempo de ejecución de .NET lo gestiona por usted y hace un buen trabajo. Sí, porque el trabajo del programador no es escribir código que (solo) compila, sino escribir código que se ejecuta . A veces es útil saber qué está haciendo el tiempo de ejecución debajo de las cubiertas (por ejemplo, solución de problemas). Se podría argumentar que es el tipo de conocimiento que ayuda a separar a los buenos programadores de los grandes programadores.
Randy apoya a Monica el
18

Como otros han dicho, definitivamente quieres llamar Disposesi la clase se implementa IDisposable. Tomo una posición bastante rígida sobre esto. Algunos afirman que poder llamar Disposeen DataSet, por ejemplo, no tiene sentido, ya que la desmontaron y vi que no hizo nada significativo. Pero, creo que abundan las falacias en ese argumento.

Lea esto para un debate interesante de personas respetadas sobre el tema. Entonces lea mi razonamiento aquí por qué creo que Jeffery Richter está en el campamento equivocado.

Ahora, sobre si debe establecer una referencia o no null. La respuesta es no. Permítanme ilustrar mi punto con el siguiente código.

public static void Main()
{
  Object a = new Object();
  Console.WriteLine("object created");
  DoSomething(a);
  Console.WriteLine("object used");
  a = null;
  Console.WriteLine("reference set to null");
}

Entonces, ¿cuándo crees que el objeto al que hace referencia aes elegible para la colección? Si dijo después de la llamada, a = nullentonces está equivocado. Si dijo después de que el Mainmétodo se completa, también está equivocado. La respuesta correcta es que es elegible para la recolección en algún momento durante la llamada a DoSomething. Eso es correcto. Es elegible antes de que se establezca la referencia nully quizás incluso antes de que DoSomethingfinalice la llamada . Esto se debe a que el compilador JIT puede reconocer cuándo las referencias a objetos ya no se desreferencian, incluso si aún están enraizadas.

Brian Gideon
fuente
3
¿Qué pasa si aes un campo de miembro privado en una clase? Si ano se establece como nulo, el GC no tiene forma de saber si ase usará nuevamente en algún método, ¿verdad? Por alo tanto , no se recopilará hasta que se recopile toda la clase que contiene. ¿No?
Kevin P. Rice
44
@ Kevin: Correcto. Si afuera un miembro de la clase y la clase que contiene atodavía estuviera rooteada y en uso, entonces también se quedaría. Ese es un escenario en el que configurarlo nullpodría ser beneficioso.
Brian Gideon
1
Su punto se vincula con una razón por la cual Disposees importante: no es posible invocar Dispose(o cualquier otro método no en línea) en un objeto sin una referencia arraigada a él; llamar Disposedespués de que uno haya terminado de usar un objeto asegurará que una referencia arraigada continuará existiendo durante la última acción realizada en él. Abandonar todas las referencias a un objeto sin invocar Dispose, irónicamente, puede tener el efecto de ocasionar que los recursos del objeto se liberen ocasionalmente demasiado pronto .
supercat
Este ejemplo y explicación no parece definitivo en la sugerencia difícil de nunca establecer referencias nulas. Quiero decir, excepto por el comentario de Kevin, una referencia establecida como nula después de que se elimina parece bastante benigna , entonces, ¿cuál es el daño? ¿Me estoy perdiendo de algo?
dathompson
13

Nunca necesita establecer objetos en nulo en C #. El compilador y el tiempo de ejecución se encargarán de averiguar cuándo ya no están dentro del alcance.

Sí, debe deshacerse de los objetos que implementan IDisposable.

EMP
fuente
2
Si tiene una referencia de larga duración (o incluso estática) a un objeto grande, wantdebe anularlo tan pronto como haya terminado con él para que sea libre de ser reclamado.
Steven Sudit
12
Si alguna vez "terminaste", no debería ser estático. Si no es estático, pero es "de larga duración", entonces debería quedar fuera de alcance poco después de que haya terminado con él. La necesidad de establecer referencias a nulo indica un problema con la estructura del código.
EMP
Puede tener un elemento estático con el que haya terminado. Considere: Un recurso estático que se lee del disco en un formato fácil de usar y luego se analiza en un formato adecuado para el uso del programa. Puede terminar con una copia privada de los datos sin procesar que no tiene ningún otro propósito. (Ejemplo del mundo real: el análisis es una rutina de dos pasos y, por lo tanto, no puede simplemente procesar los datos a medida que se leen.)
Loren Pechtel el
1
Entonces no debería almacenar ningún dato sin procesar en un campo estático si solo se usa temporalmente. Claro, puede hacer eso, simplemente no es una buena práctica exactamente por esta razón: luego debe administrar su vida útil manualmente.
EMP
2
Lo evita almacenando los datos sin procesar en una variable local en el método que lo procesa. El método devuelve los datos procesados, que usted conserva, pero el local para los datos sin procesar queda fuera de alcance cuando el método sale y se GCed automáticamente.
EMP
11

Estoy de acuerdo con la respuesta común aquí que sí, debe deshacerse y no, generalmente no debe establecer la variable en nula ... pero quería señalar que deshacerse NO se trata principalmente de la administración de memoria. Sí, puede ayudar (y a veces lo hace) con la administración de la memoria, pero su propósito principal es brindarle una liberación determinista de los escasos recursos.

Por ejemplo, si abre un puerto de hardware (serie, por ejemplo), un socket TCP / IP, un archivo (en modo de acceso exclusivo) o incluso una conexión de base de datos, ahora ha evitado que cualquier otro código use esos elementos hasta que se liberen. Dispose generalmente libera estos elementos (junto con GDI y otros identificadores "os", etc., que hay 1000 disponibles, pero en general son limitados). Si no llama a dipose en el objeto propietario y libera explícitamente estos recursos, intente abrir el mismo recurso nuevamente en el futuro (u otro programa lo hace), ese intento de apertura fallará porque su objeto no expuesto y no recolectado todavía tiene el elemento abierto . Por supuesto, cuando el GC recolecta el elemento (si el patrón Dispose se ha implementado correctamente), el recurso se liberará ... pero no sabe cuándo será, así que no No sé cuándo es seguro volver a abrir ese recurso. Este es el problema principal con el que Dispose funciona. Por supuesto, la liberación de estos controladores a menudo también libera memoria, y nunca liberarlos nunca puede liberar esa memoria ... de ahí toda la charla sobre fugas de memoria o retrasos en la limpieza de la memoria.

He visto ejemplos del mundo real de esto causando problemas. Por ejemplo, he visto aplicaciones web ASP.Net que eventualmente no se conectan a la base de datos (aunque sea por cortos períodos de tiempo, o hasta que se reinicie el proceso del servidor web) porque el 'grupo de conexiones del servidor SQL está lleno' ... es decir , se han creado tantas conexiones y no se han lanzado explícitamente en un período de tiempo tan corto que no se pueden crear nuevas conexiones y muchas de las conexiones en el grupo, aunque no están activas, todavía están referenciadas por objetos no recogidos y no recogidos, y también pueden ' No se reutilizará. La disposición correcta de las conexiones de la base de datos cuando sea necesario asegura que este problema no ocurra (al menos no a menos que tenga un acceso simultáneo muy alto).

Yort
fuente
11

Si el objeto se implementa IDisposable, entonces sí, debe deshacerse de él. El objeto podría estar colgado en recursos nativos (identificadores de archivo, objetos del sistema operativo) que de lo contrario no podrían liberarse de inmediato. Esto puede conducir a la falta de recursos, problemas de bloqueo de archivos y otros errores sutiles que de otro modo podrían evitarse.

Consulte también Implementar un método de eliminación en MSDN.

Chris Schmich
fuente
¿Pero el recolector de garabage no llamará a Dispose ()? Si es así, ¿por qué necesitaría llamarlo?
CJ7
A menos que lo llame explícitamente, no hay garantía de que Disposese llame. Además, si su objeto está reteniendo un recurso escaso o está bloqueando algún recurso (por ejemplo, un archivo), entonces querrá liberarlo lo antes posible. Esperar a que el GC haga eso es subóptimo.
Chris Schmich
12
GC nunca llamará a Dispose (). GC podría llamar a un finalizador que, por convención, debería limpiar los recursos.
adrianm
@adrianm: No mightllamar, sino willllamar.
leppie
2
@leppie: los finalizadores no son deterministas y es posible que no se invoquen (p. ej., cuando el dominio de aplicación está descargado). Si necesita una finalización determinista, debe implementar lo que creo que se llama un controlador crítico. El CLR tiene un manejo especial de estos objetos para garantizar que se finalicen (por ejemplo, los que pre-jits el código de finalización para manejar la memoria baja)
adrianm
9

Si implementan la interfaz IDisposable, entonces debe eliminarlos. El recolector de basura se encargará del resto.

EDITAR: lo mejor es usar el usingcomando cuando se trabaja con elementos desechables:

using(var con = new SqlConnection("..")){ ...
Andre
fuente
5

Cuando se implementa un objeto IDisposable, debe llamar Dispose(o Close, en algunos casos, eso llamará Dispose para usted).

Normalmente no tiene que establecer objetos en null, porque el GC sabrá que un objeto ya no se utilizará.

Hay una excepción cuando configuro objetos en null. Cuando recupero muchos objetos (de la base de datos) en los que necesito trabajar, y los almaceno en una colección (o matriz). Cuando se hace el "trabajo", configuro el objeto en null, porque el GC no sabe que he terminado de trabajar con él.

Ejemplo:

using (var db = GetDatabase()) {
    // Retrieves array of keys
    var keys = db.GetRecords(mySelection); 

    for(int i = 0; i < keys.Length; i++) {
       var record = db.GetRecord(keys[i]);
       record.DoWork();
       keys[i] = null; // GC can dispose of key now
       // The record had gone out of scope automatically, 
       // and does not need any special treatment
    }
} // end using => db.Dispose is called
GvS
fuente
4

Normalmente, no hay necesidad de establecer campos en nulo. Sin embargo, siempre recomendaría disponer de recursos no administrados.

Por experiencia, también te aconsejaría que hagas lo siguiente:

  • Anule la suscripción a eventos si ya no los necesita.
  • Establezca cualquier campo que contenga un delegado o una expresión en nulo si ya no es necesario.

Me he encontrado con algunos problemas muy difíciles de encontrar que fueron el resultado directo de no seguir los consejos anteriores.

Un buen lugar para hacerlo es Dispose (), pero antes suele ser mejor.

En general, si existe una referencia a un objeto, el recolector de basura (GC) puede tardar un par de generaciones más en darse cuenta de que un objeto ya no está en uso. Todo el tiempo el objeto permanece en la memoria.

Eso puede no ser un problema hasta que descubra que su aplicación está usando mucha más memoria de la que esperaba. Cuando eso sucede, conecte un generador de perfiles de memoria para ver qué objetos no se están limpiando. Establecer campos que hagan referencia a otros objetos como nulos y borrar colecciones al eliminarlas realmente puede ayudar al GC a descubrir qué objetos puede eliminar de la memoria. El GC recuperará la memoria usada más rápido, haciendo que su aplicación tenga menos memoria y sea más rápida.

Marnix van Valen
fuente
1
¿Qué quiere decir con "eventos y delegados"? ¿Qué debe "limpiarse" con estos?
CJ7
@ Craig: edité mi respuesta. Esperemos que esto lo aclare un poco.
Marnix van Valen
3

Siempre llame a disponer. No vale la pena el riesgo. Las grandes aplicaciones empresariales gestionadas deben tratarse con respeto. No se pueden hacer suposiciones o de lo contrario volverá a morderte.

No escuches leppie.

Muchos objetos en realidad no implementan IDisposable, por lo que no tiene que preocuparse por ellos. Si realmente se salen del alcance, serán liberados automáticamente. Además, nunca me he encontrado con la situación en la que he tenido que establecer algo en nulo.

Una cosa que puede suceder es que muchos objetos se pueden mantener abiertos. Esto puede aumentar considerablemente el uso de memoria de su aplicación. A veces es difícil determinar si esto es realmente una pérdida de memoria o si su aplicación solo está haciendo muchas cosas.

Las herramientas de perfil de memoria pueden ayudar con cosas como esa, pero puede ser complicado.

Además, siempre cancele la suscripción a eventos que no son necesarios. También tenga cuidado con los enlaces y controles de WPF. No es una situación habitual, pero me encontré con una situación en la que tenía un control WPF que estaba vinculado a un objeto subyacente. El objeto subyacente era grande y ocupaba una gran cantidad de memoria. El control de WPF estaba siendo reemplazado por una nueva instancia, y el viejo todavía estaba dando vueltas por alguna razón. Esto causó una gran pérdida de memoria.

En retrospectiva, el código estaba mal escrito, pero el punto es que desea asegurarse de que las cosas que no se usan están fuera de alcance. Se tardó mucho tiempo en encontrarlo con un generador de perfiles de memoria, ya que es difícil saber qué cosas en la memoria son válidas y qué no debería estar allí.

Peter
fuente
2

Tengo que responder también. El JIT genera tablas junto con el código a partir de su análisis estático del uso de variables. Esas entradas de la tabla son las "Raíces GC" en el marco de la pila actual. A medida que avanza el puntero de instrucciones, esas entradas de la tabla se vuelven inválidas y están listas para la recolección de basura. Por lo tanto: si se trata de una variable de ámbito, no necesita establecerla como nula; el GC recopilará el objeto. Si es un miembro o una variable estática, debe configurarlo como nulo

Hui
fuente