stringtranslate.com

Finalizador

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 del objeto , antes de que el objeto sea desasignado , y es complementario de un inicializador , que se ejecuta durante la creación del objeto , después de la asignación . Algunos desaconsejan enfáticamente los finalizadores, debido a la dificultad de su uso adecuado y la complejidad que agregan, y en su lugar se sugieren alternativas, principalmente el patrón de eliminación [1] (consulte problemas con los finalizadores).

El término finalizador se utiliza principalmente en lenguajes de programación funcionales y orientados a objetos que utilizan la recolección de basura , cuyo arquetipo es Smalltalk . Esto contrasta con un destructor , que es un método llamado para finalización en lenguajes con vidas deterministas de objetos , arquetípicamente C++ . [2] [3] Generalmente son exclusivos: 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 el caso del recuento de referencias (en lugar de rastrear la recolección de basura), la terminología varía. En 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 terminología). El término final también indica una clase que no se puede heredar ; esto no tiene relación.

Terminología

La terminología de finalizador y finalización versus destructor y destrucción varía entre los autores y, a veces, no está clara.

En el uso común, un destructor es un método llamado deterministamente sobre destrucción de objetos, y el arquetipo son los destructores de C++; mientras que el recolector de basura llama a un finalizador de forma no determinista, y el arquetipo son los métodos Java finalize .

Para los lenguajes que implementan la recolección de basura mediante el recuento de referencias , la terminología varía: algunos lenguajes como Objective-C y Perl usan destructor , y otros lenguajes como Python usan finalizador (según la especificación, Python recolecta basura, pero la implementación de CPython de referencia desde su la versión 2.0 usa una combinación de recuento de referencias y recolección de basura). Esto refleja que el recuento de referencias da como resultado una vida útil semideterminista del objeto: para los objetos que no forman parte de un ciclo, los objetos se destruyen de forma determinista cuando el recuento de referencias cae a cero, pero los objetos que forman parte de un ciclo se destruyen de forma no determinista, como parte de una forma separada de recolección de basura.

En cierto uso técnico limitado, 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# se recolecta como basura, pero la especificación para Common Language Infrastructure (CLI) y la implementación de su entorno de ejecución como Common Language Runtime (CLR) ), denominados "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[es] de instancia". [4] [5] Esta terminología es confusa y, por lo tanto, las versiones más recientes de la especificación 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 se recolectan basura, sus funciones de limpieza se denominan destructores. [7]

Usar

La finalización se utiliza principalmente para la limpieza, para liberar memoria u otros recursos: para desasignar la memoria asignada mediante la gestión manual de la memoria ; para borrar las referencias si se utiliza el recuento de referencias (disminuir los recuentos de referencias); liberar recursos, particularmente en el lenguaje de adquisición de recursos es inicialización (RAII); o para dar de baja un objeto. La cantidad de finalización varía significativamente entre lenguajes, desde una finalización extensa en C++, que tiene administración manual de memoria, recuento de referencias y duración determinista de los objetos; a menudo no hay finalización en Java, que tiene vidas útiles de objetos no deterministas y a menudo se implementa con un recolector de basura de seguimiento. También es posible que haya poca o ninguna finalización explícita (especificada por el usuario), pero sí una finalización implícita significativa, realizada por el compilador, el intérprete o el tiempo de ejecución; esto es común en el caso del recuento automático de referencias, como en la implementación de referencias 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 idioma); en Java esto ocurre con Java Native Interface (JNI) y ByteBufferobjetos en New I/O (NIO). Esto último puede causar problemas debido a que el recolector de basura no puede rastrear estos recursos externos, por lo que no se recolectarán de manera suficientemente agresiva 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 recurso y utilizando el patrón de eliminación , como se analiza 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 (pueden no ser llamados de manera oportuna, o incluso no ser llamados en absoluto, y el entorno de ejecución no se puede predecir) y, por lo tanto, cualquier la limpieza que debe realizarse de forma determinista debe realizarse mediante algún otro método, con mayor frecuencia manualmente a través del patrón de eliminación . En particular, tanto Java como Python no garantizan que alguna vez se llame a los finalizadores 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, generalmente se recomienda evitar finalizadores excepto para las operaciones más triviales. En particular, las operaciones que a menudo se realizan en los destructores no suelen ser apropiadas para los finalizadores. Un antipatrón común es escribir finalizadores como si fueran destructores, lo cual 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 usan mucho en C++ idiomático, siguiendo el lenguaje de adquisición de recursos es inicialización (RAII).

Sintaxis

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 idioma.

En Java, un finalizador es un método llamado finalize, que anula el Object.finalizemétodo. [8]

En JavaScript, FinalizationRegistry le permite solicitar una devolución de llamada cuando un objeto se recolecta como basura.

En Python, un finalizador es un método llamado __del__.

En Perl, un finalizador es un método llamado DESTROY.

Clase UML en C# que contiene un constructor y un finalizador.

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 como "finalizadores" debido a la confusión que esto causaba. [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 único puntero llamando a la runtime.SetFinalizerfunción en la biblioteca estándar. [9]

Implementación

Se llama a un finalizador cuando se recolecta basura de un objeto , después de que un objeto se haya convertido en basura (inalcanzable), pero antes de que se desasigne su memoria. La finalización se produce de forma no determinista, a discreción del recolector de basura, y es posible que nunca ocurra. Esto contrasta con los destructores, que se llaman de forma determinista tan pronto como un objeto ya no está en uso, y siempre se llaman, excepto en el caso de una terminación incontrolada del programa. Los finalizadores suelen ser métodos de instancia , debido a la necesidad de realizar operaciones específicas de objetos.

El recolector de basura también debe tener en cuenta la posibilidad de resurrección del objeto. Por lo general, esto se hace ejecutando primero los finalizadores, luego verificando si algún objeto ha resucitado y, de ser así, abortando 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 esta razón, los objetos con finalizadores pueden recopilarse 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 un objeto resucita, existe la pregunta adicional de si su finalizador se llama nuevamente la próxima vez que se destruya; a diferencia de los destructores, los finalizadores potencialmente se llaman varias veces. Si se convoca a finalizadores para objetos resucitados, los objetos pueden resucitarse 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 implementaciones recientes de Apple) y Python desde Python 3.4, los objetos se finalizan como máximo una vez, lo que requiere un seguimiento si el objeto ya se ha finalizado.

En otros casos, en particular los lenguajes CLR como C#, el seguimiento de la finalización se realiza por separado de los propios objetos, y los objetos se pueden registrar o cancelar repetidamente para su finalización.

Problemas

Dependiendo de la implementación, los finalizadores pueden causar una cantidad significativa de problemas y, por lo tanto, varias autoridades los desaconsejan firmemente. [10] [11] Estos problemas incluyen: [10]

Además, es posible que los finalizadores no se ejecuten debido a que los objetos permanecen accesibles más allá de cuando se espera que sean basura, ya sea debido a errores de programación o debido a una accesibilidad inesperada. Por ejemplo, cuando Python detecta una excepción (o una excepción no se detecta en el modo interactivo), mantiene una referencia al marco de la pila donde se generó la excepción, lo que mantiene vivos los objetos a los que se hace referencia desde ese marco de la 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 potencialmente hacer referencia a campos en la subclase y, por lo tanto, el campo no puede recolectarse basura hasta el siguiente ciclo, una vez que se haya ejecutado el finalizador. [10] Esto se puede evitar utilizando la composición en lugar de la herencia .

Gestión de recursos

Un antipatrón común es utilizar finalizadores para liberar recursos, por analogía con el modismo de adquisición de recursos es inicialización (RAII) de C++: adquirir un recurso en el inicializador (constructor) y liberarlo en el finalizador (destructor). Esto no funciona por varias razones. Básicamente, es posible que nunca se llame a los finalizadores, e incluso si se llaman, es posible que no se llamen de manera oportuna; por lo tanto, el uso de finalizadores para liberar recursos generalmente causará fugas de recursos . Además, los finalizadores no se llaman en un orden prescrito, mientras que los recursos a menudo deben liberarse en un orden específico, frecuentemente el orden opuesto al que fueron adquiridos. Además, como los finalizadores se llaman a discreción del recolector de basura, a menudo solo se llamarán 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 muchos. de memoria administrada disponible, es posible que no se realice la recolección de basura y, por lo tanto, no se recuperen estos recursos.

Por lo tanto, en lugar de utilizar finalizadores para la gestión automática de recursos, en los lenguajes de recolección de basura se deben gestionar los recursos manualmente, generalmente utilizando el patrón dispose . En este caso, es posible que aún se adquieran recursos en el inicializador, que se llama explícitamente al crear una instancia del objeto, pero se liberan en el método de eliminación. El método dispose puede ser invocado explícita o implícitamente mediante construcciones de lenguaje como C# using, Java try-with-resources o Python 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 al destruirse el objeto, incluso si el recurso no está disponible. liberado por eliminación manual.

Duración de los objetos determinista y no determinista

En lenguajes con vidas deterministas de los objetos, en particular C++, la gestión de recursos se realiza frecuentemente vinculando la vida útil de la posesión de recursos con la vida útil del objeto, adquiriendo recursos durante la inicialización y liberándolos durante la finalización; esto se conoce como adquisición de recursos e 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 Es posible que no se publique durante mucho tiempo o incluso que no se publique en absoluto, lo que provocará fugas de recursos . En cambio, en estos lenguajes, los recursos generalmente se administran manualmente mediante el patrón de disposición : los recursos aún pueden adquirirse durante la inicialización, pero se liberan llamando a un disposemétodo. Sin embargo, utilizar la finalización para liberar recursos en estos lenguajes es un antipatrón común y olvidarse de llamar disposeseguirá provocando una fuga de recursos.

En algunos casos, ambas técnicas se combinan, utilizando un método de eliminación explícito, pero también liberando los recursos aún retenidos durante la finalización como copia de seguridad. 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.

Resurrección del objeto

Si se permiten finalizadores especificados por el usuario, es posible que la finalización provoque la resurrección del objeto , ya que los finalizadores pueden ejecutar código arbitrario, lo que puede crear referencias desde objetos activos a objetos que se están destruyendo. Para 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, el recolector de basura evita esto, generalmente agregando otro paso a la recolección de basura (después de ejecutar todos los finalizadores especificados por el usuario, verifique la resurrección), lo que complica y ralentiza la recolección de basura.

Además, la resurrección de un objeto significa que un objeto no puede ser destruido y, en casos patológicos, un objeto siempre puede resucitarse durante la finalización, volviéndose indestructible. Para evitar esto, algunos lenguajes, como Java y Python (desde Python 3.4) solo finalizan los objetos una vez y no finalizan los objetos resucitados. [ cita necesaria ] En concreto, esto se hace mediante el seguimiento de si un objeto se ha finalizado objeto por objeto. Objective-C también rastrea la finalización (al menos en las versiones recientes [ ¿cuándo? ] de Apple [ aclaración necesaria ] ) por razones similares, tratando la resurrección como un error.

Se utiliza un enfoque diferente en .NET Framework , en particular C# y Visual Basic .NET , donde la finalización es rastreada por una "cola", en lugar de por un 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 en el momento de la creación y se retira de la cola una vez finalizado), pero esto se puede cambiar llamando al GCmódulo. La finalización se puede evitar llamando a GC.SuppressFinalize, que retira el objeto de la cola, o reactivada llamando a GC.ReRegisterForFinalize, que retira el objeto de la cola. Estos se utilizan particularmente 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 .

Contraste con inicialización

La finalización es formalmente complementaria a la inicialización (la inicialización ocurre al comienzo de la 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 hay necesidad de borrar los valores: el sistema operativo puede simplemente desasignar y reclamar la memoria.

Más allá de asignar valores iniciales, la inicialización se usa principalmente para adquirir recursos o registrar un objeto con algún servicio (como un controlador de eventos ). Estas acciones tienen acciones de liberación o cancelación de registro simétricas, y se pueden manejar simétricamente en un finalizador, lo cual se realiza en RAII. Sin embargo, en muchos lenguajes, especialmente aquellos con recolección de basura, la vida útil de los objetos es asimétrica: la creación de objetos ocurre de manera determinista en algún punto explícito del 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 utilizar eficazmente 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 restablece parcialmente al deshacerse también del 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 "desechado pero aún vivo", lo que debilita la clase . invariantes y complica su uso.

Las variables generalmente se inicializan al comienzo de su vida útil, pero no se finalizan al final de su vida útil; aunque 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.

Conexión confinally

Como se refleja en la denominación, la "finalización" y la finallyconstrucción cumplen propósitos similares: realizar alguna acción final, generalmente limpiar, después de que algo más haya terminado. Se diferencian en el momento en que ocurren: una finallycláusula se ejecuta cuando la ejecución del programa abandona el cuerpo de la trycláusula asociada; esto ocurre durante el desenrollado de la pila y, por lo tanto, hay una pila de finallycláusulas pendientes en orden; mientras que la finalización ocurre cuando se destruye un objeto. lo cual sucede dependiendo del método de administración de la memoria y, en general, simplemente hay un conjunto de objetos en espera de finalización (a menudo en el montón) que no necesitan suceder en ningún orden específico.

Sin embargo, en algunos casos estos coinciden. En C++, la destrucción de objetos es determinista y el comportamiento de una finallycláusula se puede producir al tener una variable local con un objeto como valor, cuyo alcance es un bloque que corresponde al cuerpo de una trycláusula: el objeto se finaliza (se destruye) cuando la ejecución sale de este alcance, exactamente como si hubiera una finallycláusula. Por esta razón, C++ no tiene una finallyconstrucción; 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 finallycláusula.

Por el contrario, en el caso de una finallycláusula en una corrutina , como en un generador de Python, es posible que la corrutina nunca termine (solo ceda) y, por lo tanto, en la ejecución ordinaria la finallycláusula nunca se ejecuta. Si uno interpreta las instancias de una corrutina como objetos, entonces la finallyclá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 finallycláusula en una función generadora se convierte en un finalizador en los iteradores generadores instanciados a partir de esta función.

Historia

La noción de finalización como un paso separado en la destrucción de objetos se remonta a Montgomery (1994), [13] por analogía con la distinción anterior de inicialización en la construcción de objetos en Martin y Odell (1992). [14] La literatura anterior a este punto usaba "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 finalizemétodos que popularizaron el término y lo asociaron con la recolección de basura, y los lenguajes a partir de ese momento generalmente hacen esta distinción y usan el término "finalización", particularmente en el contexto de la recolección de basura. .

Notas

  1. ^ Publicado en 1994, con derechos de autor de 1995.

Referencias

  1. ^ Jagger, Perry y Sestoft 2007, pág. 542, "En C++, un destructor se llama de una manera determinada, mientras que, en C# , un finalizador no. Para obtener un comportamiento determinado de C#, se debe usarDispose.
  2. ^ Boehm, Hans-J. (2002). Destructores, Finalizadores y Sincronización. Simposio sobre principios de lenguajes de programación (POPL).
  3. ^ Jagger, Perry y Sestoft 2007, pág. 542, Destructores de C++ versus finalizadores de C# Los destructores de C++ están determinados en el sentido de que se ejecutan en momentos conocidos, en un orden conocido y desde un subproceso conocido. Por lo tanto, son semánticamente muy diferentes de los finalizadores de C#, que se ejecutan en momentos desconocidos, en un orden desconocido, desde un subproceso desconocido y a discreción del recolector de basura.
  4. ^ En su totalidad: "Vamos a utilizar el término" destructor "para el miembro que se ejecuta cuando se reclama una instancia. Las clases pueden tener destructores; las estructuras no. A diferencia de C++, no se puede llamar explícitamente a un destructor. La destrucción es no determinista: no se puede saber de manera confiable cuándo se ejecutará el destructor, excepto para decir que se ejecuta en algún momento después de que se hayan liberado todas las referencias al objeto. Los destructores en una cadena de herencia se llaman en orden, desde el más descendiente hasta el más descendiente. menos descendiente. No hay necesidad (y no hay forma) de que la clase derivada llame explícitamente al destructor base. El compilador de C# compila los destructores en la representación CLR adecuada. puede proporcionar finalizadores estáticos en el futuro; no vemos ninguna barrera para que C# utilice finalizadores estáticos", 12 de mayo de 1999.
  5. ^ ¿ Cuál es la diferencia entre un destructor y un finalizador?, Eric Lippert, Blog de Eric Lippert: Fabulous Adventures In Coding, 21 de enero de 2010
  6. ^ ab Jagger, Perry y Sestoft 2007, pág. 542, "En la versión anterior de este estándar, lo que ahora se conoce como "finalizador" se llamaba "destructor". La experiencia ha demostrado que el término "destructor" causaba confusión y a menudo generaba expectativas incorrectas, especialmente para los programadores que sabían En C++, un destructor se llama de una manera determinada, mientras que, en C#, un finalizador no. Para obtener un comportamiento determinado de C#, se debe usar Dispose.".
  7. ^ Destructores de clases Destructores de clases en D
  8. ^ java.lang, objeto de clase: finalizar
  9. ^ "Paquete de tiempo de ejecución - tiempo de ejecución - PKG.go.dev".
  10. ^ abc "MET12-J. No utilice finalizadores", Dhruv Mohindra, The CERT Oracle Secure Coding Standard for Java, 05. Methods (MET) Archivado el 4 de mayo de 2014 en Wayback Machine.
  11. ^ object.__del__(self), Referencia del lenguaje Python, 3. Modelo de datos: "... __del__()los métodos deben hacer el mínimo absoluto necesario para mantener invariantes externos".
  12. ^ Hans-J. Boehm, Finalización, subprocesos y el modelo de memoria basado en tecnología Java™, Conferencia JavaOne, 2005.
  13. ^ Montgomery 1994, pag. 120, "Al igual que con la creación de instancias de objetos, el diseño para la terminación de objetos puede beneficiarse de la implementación de dos operaciones para cada clase: una operación de finalización y una operación de finalización . Una operación de finalización rompe asociaciones con otros objetos, asegurando la integridad de la estructura de datos".
  14. ^ Montgomery 1994, pag. 119, "Considere implementar la creación de instancias de clases como una operación de creación e inicialización , como lo sugieren Martin y Odell. La primera asigna almacenamiento para nuevos objetos y la segunda construye el objeto para que cumpla con las especificaciones y restricciones".
  15. ^ "Cada nueva clase tiene una sobrecarga de implementación fija (inicialización, finalización, etc.)", " destructor En C++, una operación que se invoca automáticamente para finalizar un objeto que está a punto de eliminarse".

Lectura adicional

Enlaces externos