En informática , un finalizador o método de finalización es un método especial que realiza la finalización , generalmente alguna forma de limpieza. Un finalizador se ejecuta durante la destrucción de un objeto , antes de que el objeto sea desasignado , y es complementario a un inicializador , que se ejecuta durante la creación del objeto , después de la asignación . Algunos desaconsejan enfáticamente el uso de finalizadores debido a la dificultad de su uso adecuado y la complejidad que agregan, y se sugieren alternativas en su lugar, principalmente el patrón de disposición [1] (consulte los problemas con los finalizadores).
El término finalizador se utiliza principalmente en lenguajes de programación orientados a objetos y funcionales que utilizan recolección de basura , cuyo arquetipo es Smalltalk . Esto contrasta con un destructor , que es un método llamado para la finalización en lenguajes con tiempos de vida de objetos deterministas , arquetípicamente C++ . [2] [3] Estos son generalmente excluyentes: un lenguaje tendrá finalizadores (si se recolecta basura automáticamente) o destructores (si se administra la memoria manualmente), pero en casos raros un lenguaje puede tener ambos, como en C++/CLI y D , y en caso de conteo de referencias (en lugar de rastrear la recolección de basura), la terminología varía. En el uso técnico, finalizador también puede usarse para referirse a destructores, ya que estos también realizan la finalización, y se establecen algunas distinciones más sutiles; consulte la terminología. El término final también indica una clase que no se puede heredar ; esto no está relacionado.
La terminología de finalizador y finalización versus destructor y destrucción varía entre autores y a veces no es clara.
En el uso común, un destructor es un método llamado de manera determinista al destruir un objeto, y el arquetipo son los destructores de C++; mientras que un finalizador es llamado de manera no determinista por el recolector de basura, y el arquetipo son los métodos Java finalize
.
Para los lenguajes que implementan la recolección de basura a través del conteo de referencias , la terminología varía, con algunos lenguajes como Objective-C y Perl que usan destructor , y otros lenguajes como Python que usan finalizador (según la especificación, Python es recolectado por basura, pero la implementación de referencia de CPython desde su versión 2.0 usa una combinación de conteo de referencias y recolección de basura). Esto refleja que el conteo de referencias da como resultado una vida útil de objeto semideterminista: para los objetos que no son parte de un ciclo, los objetos se destruyen de manera determinista cuando el conteo de referencias cae a cero, pero los objetos que son parte de un ciclo se destruyen de manera no determinista, como parte de una forma separada de recolección de basura.
En ciertos usos técnicos restringidos, constructor y destructor son términos a nivel de lenguaje, es decir, métodos definidos en una clase , mientras que inicializador y finalizador son términos a nivel de implementación, es decir, métodos llamados durante la creación o destrucción de objetos . Así, por ejemplo, la especificación original para el lenguaje C# se refería a "destructores", aunque C# es un lenguaje de recolección de basura, pero la especificación para la Infraestructura de lenguaje común (CLI) y la implementación de su entorno de ejecución como Common Language Runtime (CLR) se refería a "finalizadores". Esto se refleja en las notas del comité del lenguaje C#, que dicen en parte: "El compilador de C# compila destructores para... [probablemente] finalizador[s] de instancia". [4] [5] Esta terminología es confusa, y por eso las versiones más recientes de la especificación de C# se refieren al método a nivel de lenguaje como "finalizadores". [6]
Otro lenguaje que no hace esta distinción terminológica es D. Aunque las clases D son recolectadas como basura, sus funciones de limpieza se denominan destructores. [7]
La finalización se utiliza principalmente para limpieza, para liberar memoria u otros recursos: para desasignar memoria asignada a través de la gestión manual de memoria ; para borrar referencias si se utiliza el recuento de referencias (disminuir el recuento de referencias); para liberar recursos, particularmente en el idioma de adquisición de recursos es inicialización (RAII); o para anular el registro de un objeto. La cantidad de finalización varía significativamente entre lenguajes, desde la finalización extensiva en C++, que tiene gestión manual de memoria, recuento de referencias y tiempos de vida de objetos deterministas; hasta la a menudo nula finalización en Java, que tiene tiempos de vida de objetos no deterministas y a menudo se implementa con un recolector de basura de rastreo. También es posible que haya poca o ninguna finalización explícita (especificada por el usuario), pero una finalización implícita significativa, realizada por el compilador, intérprete o entorno de ejecución; esto es común en el caso del recuento automático de referencias, como en la implementación de referencia CPython de Python, o en el recuento automático de referencias en la implementación de Objective-C de Apple , que rompen automáticamente las referencias durante la finalización. Un finalizador puede incluir código arbitrario; Un uso particularmente complejo es devolver automáticamente el objeto a un grupo de objetos .
La desasignación de memoria durante la finalización es común en lenguajes como C++, donde la administración manual de memoria es estándar, pero también ocurre en lenguajes administrados cuando la memoria se ha asignado fuera del montón administrado (externamente al lenguaje); en Java, esto ocurre con Java Native Interface (JNI) y ByteBuffer
objetos en New I/O (NIO). Esto último puede causar problemas debido a que el recolector de elementos no utilizados no puede rastrear estos recursos externos, por lo que no se recolectarán con la suficiente agresividad y puede causar errores de falta de memoria debido al agotamiento de la memoria no administrada; esto se puede evitar tratando la memoria nativa como un recurso y utilizando el patrón dispose , como se explica a continuación.
Los finalizadores son generalmente mucho menos necesarios y mucho menos utilizados que los destructores. Son mucho menos necesarios porque la recolección de basura automatiza la gestión de la memoria , y mucho menos utilizados porque generalmente no se ejecutan de manera determinista (es posible que no se los llame de manera oportuna, o incluso que no se los llame en absoluto, y no se puede predecir el entorno de ejecución) y, por lo tanto, cualquier limpieza que deba realizarse de manera determinista debe realizarse mediante algún otro método, con mayor frecuencia de manera manual a través del patrón dispose . Cabe destacar que tanto Java como Python no garantizan que los finalizadores se llamen alguna vez y, por lo tanto, no se puede confiar en ellos para la limpieza.
Debido a la falta de control del programador sobre su ejecución, se suele recomendar evitar los finalizadores para cualquier operación, excepto las más triviales. En particular, las operaciones que suelen realizarse en destructores no suelen ser apropiadas para los finalizadores. Un antipatrón común es escribir los finalizadores como si fueran destructores, lo que es innecesario e ineficaz, debido a las diferencias entre finalizadores y destructores. Esto es particularmente común entre los programadores de C++ , ya que los destructores se utilizan mucho en C++ idiomático, siguiendo el modismo de adquisición de recursos es inicialización (RAII).
Los lenguajes de programación que utilizan finalizadores incluyen C++/CLI , C# , Clean , Go , Java , JavaScript y Python . La sintaxis varía significativamente según el lenguaje.
En Java, un finalizador es un método llamado finalize
, que anula el Object.finalize
método. [8]
En JavaScript, FinalizationRegistry le permite solicitar una devolución de llamada cuando se recolecta basura de un objeto.
En Python, un finalizador es un método llamado __del__
.
En Perl, un finalizador es un método llamado DESTROY
.
En C#, un finalizador (llamado "destructor" en versiones anteriores del estándar) es un método cuyo nombre es el nombre de la clase con ~
prefijo, como en ~Foo
– esta es la misma sintaxis que un destructor de C++ , y estos métodos originalmente se llamaban "destructores", por analogía con C++, a pesar de tener un comportamiento diferente, pero fueron renombrados a "finalizadores" debido a la confusión que esto causó. [6]
En C++/CLI, que tiene destructores y finalizadores, un destructor es un método cuyo nombre es el nombre de la clase con ~
prefijo, como en ~Foo
(como en C#), y un finalizador es un método cuyo nombre es el nombre de la clase con !
prefijo, como en !Foo
.
En Go, los finalizadores se aplican a un solo puntero llamando a la runtime.SetFinalizer
función en la biblioteca estándar. [9]
Un finalizador se llama cuando un objeto es recolectado como basura, después de que un objeto se ha convertido en basura (inalcanzable), pero antes de que su memoria sea desasignada. La finalización ocurre de manera no determinista, a discreción del recolector de basura, y puede que nunca ocurra. Esto contrasta con los destructores, que se llaman de manera determinista tan pronto como un objeto ya no está en uso, y siempre se llaman, excepto en caso de finalización no controlada del programa. Los finalizadores son con mayor frecuencia métodos de instancia , debido a la necesidad de realizar operaciones específicas del objeto.
El recolector de basura también debe tener en cuenta la posibilidad de resucitar objetos. Lo más común es que esto se haga primero ejecutando los finalizadores, luego verificando si se han resucitado objetos y, de ser así, cancelando su destrucción. Esta verificación adicional es potencialmente costosa (una implementación simple vuelve a verificar toda la basura si incluso un solo objeto tiene un finalizador) y, por lo tanto, ralentiza y complica la recolección de basura. Por este motivo, los objetos con finalizadores pueden recolectarse con menos frecuencia que los objetos sin finalizadores (solo en ciertos ciclos), lo que agrava los problemas causados por depender de la finalización rápida, como las fugas de recursos.
Si se resucita un objeto, existe la cuestión adicional de si se vuelve a llamar a su finalizador la próxima vez que se destruye; a diferencia de los destructores, los finalizadores se llaman potencialmente varias veces. Si se llaman a los finalizadores para objetos resucitados, los objetos pueden resucitarse a sí mismos repetidamente y ser indestructibles; esto ocurre en la implementación CPython de Python anterior a Python 3.4 y en lenguajes CLR como C#. Para evitar esto, en muchos lenguajes, incluidos Java, Objective-C (al menos en las implementaciones recientes de Apple) y Python a partir de Python 3.4, los objetos se finalizan como máximo una vez, lo que requiere un seguimiento para saber si el objeto ya se ha finalizado.
En otros casos, especialmente en lenguajes CLR como C#, la finalización se rastrea por separado de los objetos mismos, y los objetos pueden registrarse o anularse repetidamente para su finalización.
Dependiendo de la implementación, los finalizadores pueden causar una cantidad significativa de problemas y, por lo tanto, varias autoridades los desaconsejan enérgicamente. [10] [11] Estos problemas incluyen: [10]
Además, los finalizadores pueden no ejecutarse debido a que los objetos permanecen accesibles más allá del momento en que se espera que sean basura, ya sea por errores de programación o por una accesibilidad inesperada. Por ejemplo, cuando Python captura una excepción (o no se captura una excepción en el modo interactivo), mantiene una referencia al marco de pila donde se generó la excepción, lo que mantiene activos los objetos a los que se hace referencia desde ese marco de pila.
En Java, los finalizadores en una superclase también pueden ralentizar la recolección de basura en una subclase, ya que el finalizador puede hacer referencia potencialmente a campos en la subclase y, por lo tanto, el campo no puede ser recolectado como basura hasta el siguiente ciclo, una vez que se haya ejecutado el finalizador. [10] Esto se puede evitar utilizando composición en lugar de herencia .
Un antipatrón común es usar finalizadores para liberar recursos, por analogía con el modismo de C++ "la adquisición de recursos es la inicialización " (RAII): adquirir un recurso en el inicializador (constructor) y liberarlo en el finalizador (destructor). Esto no funciona, por varias razones. Básicamente, los finalizadores pueden no ser llamados nunca, e incluso si son llamados, pueden no ser llamados de manera oportuna; por lo tanto, el uso de finalizadores para liberar recursos generalmente causará fugas de recursos . Además, los finalizadores no son llamados en un orden prescrito, mientras que los recursos a menudo necesitan ser liberados en un orden específico, frecuentemente el orden opuesto en el que fueron adquiridos. Además, como los finalizadores son llamados a discreción del recolector de basura, a menudo solo serán llamados bajo presión de memoria administrada (cuando hay poca memoria administrada disponible), independientemente de la presión de recursos: si los recursos escasos están retenidos por la basura pero hay mucha memoria administrada disponible, la recolección de basura puede no ocurrir, por lo que no se recuperan estos recursos.
Por lo tanto, en lugar de utilizar finalizadores para la gestión automática de recursos, en los lenguajes con recolección de basura se deben gestionar los recursos manualmente, generalmente mediante el uso del patrón dispose . En este caso, los recursos aún se pueden adquirir en el inicializador, que se llama explícitamente en la instanciación del objeto, pero se liberan en el método dispose. El método dispose se puede llamar explícitamente o implícitamente mediante construcciones del lenguaje como , -with-resources using
de C# o .try
with
Sin embargo, en ciertos casos, tanto el patrón de eliminación como los finalizadores se utilizan para liberar recursos. Esto se encuentra principalmente en lenguajes CLR como C#, donde la finalización se utiliza como respaldo para la eliminación: cuando se adquiere un recurso, el objeto que lo adquiere se pone en cola para su finalización, de modo que el recurso se libera cuando se destruye el objeto, incluso si el recurso no se libera mediante la eliminación manual.
En lenguajes con tiempos de vida deterministas de los objetos, en particular C++, la gestión de recursos se realiza con frecuencia vinculando el tiempo de vida de la posesión de recursos con el tiempo de vida del objeto, adquiriendo recursos durante la inicialización y liberándolos durante la finalización; esto se conoce como adquisición de recursos en la inicialización (RAII). Esto garantiza que la posesión de recursos sea una clase invariante y que los recursos se liberen rápidamente cuando se destruye el objeto.
Sin embargo, en lenguajes con tiempos de vida de objetos no deterministas (que incluyen todos los lenguajes principales con recolección de basura, como C#, Java y Python), esto no funciona, porque la finalización puede no ser oportuna o puede no ocurrir en absoluto, y por lo tanto los recursos pueden no liberarse durante mucho tiempo o incluso en absoluto, lo que causa fugas de recursos . En estos lenguajes, en cambio, los recursos generalmente se administran manualmente a través del patrón dispose : los recursos aún pueden adquirirse durante la inicialización, pero se liberan llamando a un dispose
método. Sin embargo, usar la finalización para liberar recursos en estos lenguajes es un antipatrón común , y olvidarse de llamar dispose
seguirá causando una fuga de recursos.
En algunos casos, se combinan ambas técnicas, utilizando un método de eliminación explícito, pero también liberando los recursos que aún se conservan durante la finalización como respaldo. Esto se encuentra comúnmente en C# y se implementa registrando un objeto para su finalización cada vez que se adquiere un recurso y suprimiendo la finalización cada vez que se libera un recurso.
Si se permiten los finalizadores especificados por el usuario, es posible que la finalización provoque la resurrección de objetos , ya que los finalizadores pueden ejecutar código arbitrario, que puede crear referencias de objetos activos a objetos que se están destruyendo. Para los lenguajes sin recolección de basura, este es un error grave y provoca referencias colgantes y violaciones de seguridad de la memoria ; para los lenguajes con recolección de basura, esto se evita mediante el recolector de basura, más comúnmente agregando otro paso a la recolección de basura (después de ejecutar todos los finalizadores especificados por el usuario, verificar la resurrección), lo que complica y ralentiza la recolección de basura.
Además, la resurrección de objetos significa que un objeto no puede ser destruido, y en casos patológicos un objeto siempre puede resucitarse a sí mismo durante la finalización, volviéndose indestructible. Para evitar esto, algunos lenguajes, como Java y Python (a partir de Python 3.4) solo finalizan los objetos una vez, y no finalizan los objetos resucitados. [ cita requerida ] Concretamente, esto se hace rastreando si un objeto ha sido finalizado objeto por objeto. Objective-C también rastrea la finalización (al menos en versiones recientes [ ¿ cuándo? ] de Apple [ aclaración necesaria ] ) por razones similares, tratando la resurrección como un error.
En .NET Framework , en particular en C# y Visual Basic .NET , se utiliza un enfoque diferente, en el que la finalización se realiza mediante una "cola" en lugar de por objeto. En este caso, si se proporciona un finalizador especificado por el usuario, de forma predeterminada el objeto solo se finaliza una vez (se pone en cola para su finalización al crearse y se saca de la cola una vez que se finaliza), pero esto se puede cambiar llamando al GC
módulo. La finalización se puede evitar llamando a GC.SuppressFinalize
, que saca de la cola el objeto, o se puede reactivar llamando a GC.ReRegisterForFinalize
, que pone en cola el objeto. Estos se utilizan especialmente cuando se utiliza la finalización para la gestión de recursos como complemento del patrón de eliminación, o cuando se implementa un grupo de objetos .
La finalización es formalmente complementaria a la inicialización (la inicialización ocurre al comienzo del ciclo de vida, la finalización al final), pero difiere significativamente en la práctica. Tanto las variables como los objetos se inicializan, principalmente para asignar valores, pero en general solo se finalizan los objetos y, en general, no es necesario borrar valores: el sistema operativo puede simplemente desasignar y recuperar la memoria.
Más allá de asignar valores iniciales, la inicialización se utiliza principalmente para adquirir recursos o para registrar un objeto con algún servicio (como un controlador de eventos ). Estas acciones tienen acciones de liberación o anulación de registro simétricas, y estas pueden manejarse simétricamente en un finalizador, lo que se hace en RAII. Sin embargo, en muchos lenguajes, en particular aquellos con recolección de basura, la duración de vida de los objetos es asimétrica: la creación de objetos ocurre de manera determinista en algún punto explícito en el código, pero la destrucción de objetos ocurre de manera no determinista, en algún entorno no especificado, a discreción del recolector de basura. Esta asimetría significa que la finalización no se puede usar de manera efectiva como complemento de la inicialización, porque no ocurre de manera oportuna, en un orden específico o en un entorno específico. La simetría se restaura parcialmente al desechar también el objeto en un punto explícito, pero en este caso la eliminación y la destrucción no ocurren en el mismo punto, y un objeto puede estar en un estado "eliminado pero aún vivo", lo que debilita las invariantes de clase y complica el uso.
Las variables generalmente se inicializan al comienzo de su vida útil, pero no se finalizan al final de su vida útil; sin embargo, si una variable tiene un objeto como valor, el objeto puede finalizarse. En algunos casos, las variables también se finalizan: las extensiones GCC permiten la finalización de variables.
Como se refleja en el nombre, finally
tanto la "finalización" como el constructo cumplen propósitos similares: realizar alguna acción final, generalmente limpieza, después de que algo más haya terminado. Se diferencian en cuándo ocurren: una finally
cláusula se ejecuta cuando la ejecución del programa abandona el cuerpo de la try
cláusula asociada; esto ocurre durante el desenrollado de la pila y, por lo tanto, hay una pila de finally
cláusulas pendientes, en orden; mientras que la finalización ocurre cuando se destruye un objeto, lo que sucede según el método de gestión de memoria y, en general, simplemente hay un conjunto de objetos que esperan la finalización, a menudo en el montón, lo que no necesita suceder en ningún orden específico.
Sin embargo, en algunos casos estos términos coinciden. En C++, la destrucción de objetos es determinista, y el comportamiento de una finally
cláusula puede producirse al tener una variable local con un objeto como valor, cuyo ámbito es un bloque que corresponde al cuerpo de una try
cláusula – el objeto se finaliza (destruye) cuando la ejecución sale de este ámbito, exactamente como si hubiera una finally
cláusula. Por esta razón, C++ no tiene un finally
constructo – la diferencia es que la finalización se define en la definición de clase como el método destructor, en lugar de en el sitio de llamada en una finally
cláusula.
Por el contrario, en el caso de una finally
cláusula en una corrutina , como en un generador de Python, la corrutina nunca puede terminar (solo ceder) y, por lo tanto, en la ejecución ordinaria, la finally
cláusula nunca se ejecuta. Si uno interpreta las instancias de una corrutina como objetos, entonces la finally
cláusula puede considerarse un finalizador del objeto y, por lo tanto, puede ejecutarse cuando la instancia se recolecta como basura. En la terminología de Python, la definición de una corrutina es una función generadora, mientras que una instancia de ella es un iterador generador y, por lo tanto, una finally
cláusula en una función generadora se convierte en un finalizador en iteradores generadores instanciados a partir de esta función.
La noción de finalización como un paso separado en la destrucción de objetos data de Montgomery (1994), [13] por analogía con la distinción anterior de inicialización en la construcción de objetos en Martin & Odell (1992). [14] La literatura anterior a este punto utilizó "destrucción" para este proceso, sin distinguir entre finalización y desasignación, y los lenguajes de programación que datan de este período, como C++ y Perl, usan el término "destrucción". Los términos "finalizar" y "finalización" también se utilizan en el influyente libro Design Patterns (1994). [a] [15] La introducción de Java en 1995 contenía finalize
métodos, que popularizaron el término y lo asociaron con la recolección de basura, y los lenguajes a partir de este punto generalmente hacen esta distinción y usan el término "finalización", particularmente en el contexto de la recolección de basura.
Dispose.
Dispose.
"__del__()
los métodos deben hacer el mínimo absoluto necesario para mantener invariantes externos".