c# - ¿De dónde vino la variable=nula como "destrucción de objetos"?
.net design-patterns (14)
Al trabajar en varios sistemas heredados escritos en varias versiones de .NET, en muchas compañías diferentes, sigo encontrando ejemplos del siguiente patrón:
public void FooBar()
{
object foo = null;
object bar = null;
try
{
foo = new object();
bar = new object();
// Code which throws exception.
}
finally
{
// Destroying objects
foo = null;
bar = null;
}
}
Para cualquiera que sepa cómo funciona la administración de memoria en .NET, este tipo de código es extremadamente innecesario; el recolector de basura no necesita que usted asigne manualmente un null
para indicar que se puede recopilar el objeto anterior, ni asignar un null
indica al GC que recoja el objeto de inmediato.
Este patrón es solo ruido, lo que dificulta la comprensión de lo que el código intenta lograr.
¿Por qué, entonces, sigo encontrando este patrón? ¿Hay alguna escuela que enseñe esta práctica? ¿Hay algún idioma en el que se requiera la asignación de valores null
a variables de ámbito local para administrar correctamente la memoria? ¿Hay algún valor adicional en la asignación explícita de null
que no he percibido?
Considere una ligera modificación:
public void FooBar()
{
object foo = null;
object bar = null;
try
{
foo = new object();
bar = new object();
// Code which throws exception.
}
finally
{
// Destroying objects
foo = null;
bar = null;
}
vaboom(foo,bar);
}
Es posible que el (los) autor (es) hayan querido asegurarse de que el gran Vaboom (*) no haya recibido los punteros a los objetos con formato incorrecto si una excepción fue lanzada y capturada anteriormente. La paranoia, que resulta en una codificación defensiva, no es necesariamente algo malo en este negocio.
(*) Si sabes quién es, lo sabes.
Creo que solía ser un malentendido común entre los antiguos desarrolladores de C / C ++. Sabían que el GC liberará su memoria, pero no entendieron realmente cuándo y cómo. Solo límpialo y sigue :)
Es la programación de culto de la carga de FUD (gracias a Daniel Earwicker ) por desarrolladores que están acostumbrados a recursos "gratuitos", implementaciones incorrectas de GC y API incorrecta.
Algunos GC no se adaptaron bien a las referencias circulares. Para deshacerte de ellos, tenías que romper el ciclo "en algún lugar". ¿Dónde? Bueno, en caso de duda, entonces en todas partes. Haz eso por un año y se moverá en la punta de tus dedos.
También establecer el campo en null
te da la idea de "hacer algo" porque como desarrolladores, siempre tememos "olvidar algo".
Por último, tenemos API que deben cerrarse explícitamente porque no hay un soporte de lenguaje real que diga "cierre esto cuando termine" y deje que la computadora lo descubra igual que con GC. Así que tienes una API donde tienes que llamar código de limpieza y API donde no. Esto apesta y alienta patrones como los anteriores.
Es más común en idiomas con recolección de basura determinista y sin RAII, como el antiguo Visual Basic, pero incluso allí no es necesario y por lo general es necesario romper las referencias cíclicas. Así que, posiblemente, realmente se debe a malos programadores de C ++ que usan punteros tontos en todo el lugar. En C ++, tiene sentido establecer punteros tontos en 0 después de eliminarlos para evitar la eliminación doble.
Es posible que provenga de VB que utilizó una estrategia de conteo de referencias para la administración de la memoria y el tiempo de vida del objeto. Establecer una referencia a Nothing
(equivalente a nulo) disminuiría el recuento de referencias. Una vez que el recuento se convirtió en cero, el objeto fue destruido sincrónicamente. El recuento se reduciría automáticamente al dejar el alcance de un método, por lo que incluso en VB esta técnica fue en su mayoría inútil, sin embargo, hubo situaciones especiales en las que querría destruir un objeto con avidez, como se ilustra en el siguiente código.
Public Sub Main()
Dim big As Variant
Set big = GetReallyBigObject()
Call big.DoSomething
Set big = Nothing
Call TimeConsumingOperation
Call ConsumeMoreMemory
End Sub
En el código anterior, el objeto al que hace referencia big
se habría demorado hasta el final sin la llamada a Set big = Nothing
. Eso puede ser indeseable si las otras cosas en el método fueron una operación que consumió mucho tiempo o generó más presión de memoria.
He visto esto en algún código de Java antes. Se usó en una variable estática para indicar que el objeto debería ser destruido.
Sin embargo, probablemente no se originó en Java, ya que usarlo para otra cosa que no sea una variable estática tampoco tendría sentido en Java.
He visto esto mucho en el código de VBScript (ASP clásico) y creo que viene de ahí.
Los desarrolladores de VB tuvieron que desechar todos sus objetos, para intentar mitigar la posibilidad de una pérdida de memoria. Puedo imaginar que es de donde viene cuando los desarrolladores de VB migraron a .NEt / c #
Puede ser la convención de asignar nulos originados por el hecho de que foo
ha sido una variable de instancia en lugar de una variable local, debe eliminar la referencia antes de que GC pueda recopilarla. Alguien durmió durante la primera oración y comenzó a anular todas sus variables; La multitud lo siguió.
Puedo verlo como resultado de una mala interpretación de cómo funciona la recolección de basura, o de un esfuerzo para forzar al GC a activarse de inmediato, tal vez porque los objetos foo
y bar
son bastante grandes.
Sospecho que este patrón proviene de traducir el código C ++ a C # sin detenerse para comprender las diferencias entre la finalización de C # y la finalización de C ++. En C ++ a menudo nulo las cosas en el destructor, ya sea con fines de depuración (para que pueda ver en el depurador que la referencia ya no es válida) o, rara vez, porque quiero que se libere un objeto inteligente. (Si ese es el significado, prefiero llamar a Liberar en él y hacer que el significado del código sea muy claro para los mantenedores). Como se puede observar, esto no tiene sentido en C #.
Usted ve este patrón en VB / VBScript todo el tiempo también, por diferentes razones. Reflexioné un poco sobre lo que podría causar eso aquí:
http://blogs.msdn.com/b/ericlippert/archive/2004/04/28/122259.aspx
Viene de C / C ++ donde hacer un free () / delete en un puntero ya liberado podría ocasionar un bloqueo al soltar un puntero NULL simplemente no hizo nada.
Esto significa que esta construcción (C ++) causará problemas
void foo()
{
myclass *mc = new myclass(); // lets assume you really need new here
if (foo == bar)
{
delete mc;
}
delete mc;
}
mientras esto funcione
void foo()
{
myclass *mc = new myclass(); // lets assume you really need new here
if (foo == bar)
{
delete mc;
mc = NULL;
}
delete mc;
}
Conclusión: es totalmente innecesario en C #, Java y en casi cualquier otro lenguaje de recolección de basura.
Viene de C / C ++, donde la norma (para eliminar los punteros colgantes ) hizo que establecer sus punteros en nulo de forma explícita
Después de llamar gratis ():
#include <stdlib.h>
{
char *dp = malloc ( A_CONST );
// Now that we''re freeing dp, it is a dangling pointer because it''s pointing
// to freed memory
free ( dp );
// Set dp to NULL so it is no longer dangling
dp = NULL;
}
Los desarrolladores de Visual Basic también hicieron lo mismo al escribir sus componentes COM para evitar pérdidas de memoria.
Viene de código C ++ especialmente punteros inteligentes . En ese caso, es bastante equivalente a un .Dispose()
en C #.
No es una buena práctica, a lo sumo el instinto de un desarrollador. No hay un valor real al asignar null
en C #, excepto que puede ayudar al GC a romper una referencia circular.