En la programación orientada a objetos (POO), el tiempo de vida del objeto (o ciclo de vida) de un objeto es el tiempo entre la creación de un objeto y su destrucción. Las reglas para la vida útil de los objetos varían significativamente entre idiomas, en algunos casos entre implementaciones de un idioma dado, y la vida útil de un objeto en particular puede variar de una ejecución del programa a otra.
En algunos casos objeto coincide por vida con vida variable de de una variable con ese objeto como valor (tanto para las variables estáticas y variables automáticas ), pero en toda la vida objeto general no está ligada a la vida útil de cualquier variable uno. En muchos casos, y de forma predeterminada en muchos lenguajes orientados a objetos, particularmente aquellos que usan recolección de basura (GC), los objetos se asignan en el montón, y la vida útil del objeto no está determinada por la vida útil de una variable dada: el valor de una variable sostener un objeto en realidad corresponde a una referencia al objeto, no al objeto en sí, y la destrucción de la variable simplemente destruye la referencia, no el objeto subyacente.
Si bien la idea básica de la vida útil de un objeto es simple: un objeto se crea, se usa y luego se destruye, los detalles varían sustancialmente entre los idiomas y dentro de las implementaciones de un idioma dado, y están íntimamente ligados a cómo se implementa la administración de la memoria. Además, se establecen muchas distinciones precisas entre los pasos y entre los conceptos a nivel de lenguaje y los conceptos a nivel de implementación. La terminología es relativamente estándar, pero los pasos que corresponden a un término dado varían significativamente entre idiomas.
Los términos generalmente vienen en pares de antónimos, uno para un concepto de creación, otro para el concepto de destrucción correspondiente, como inicializar / finalizar o constructor / destructor. El par de creación / destrucción también se conoce como iniciación / terminación, entre otros términos. Los términos asignación y desasignación o liberación también se utilizan, por analogía con la gestión de memoria, aunque la creación y destrucción de objetos puede implicar mucho más que simplemente asignación y desasignación de memoria, y la asignación / desasignación se consideran pasos de creación y destrucción, respectivamente.
Una distinción importante es si la vida útil de un objeto es determinista o no determinista. Esto varía según el idioma, y dentro del idioma varía con la asignación de memoria de un objeto; la vida útil del objeto puede ser distinta de la vida útil variable.
Los objetos con asignación de memoria estática, en particular los objetos almacenados en variables estáticas y los módulos de clases (si las clases o los módulos son en sí mismos objetos y están asignados estáticamente), tienen un sutil no determinismo en muchos lenguajes: mientras que su vida útil parece coincidir con el tiempo de ejecución. del programa, el orden de creación y destrucción (qué objeto estático se crea primero, cuál segundo, etc.) es generalmente no determinista.
Para los objetos con asignación automática de memoria o asignación dinámica de memoria, la creación de objetos generalmente ocurre de manera determinista, ya sea explícitamente cuando un objeto se crea explícitamente (como a través new
de C ++ o Java), o implícitamente al comienzo de la vida útil de la variable, particularmente cuando el alcance de una se ingresa una variable automática, como en la declaración. Sin embargo, la destrucción de objetos varía: en algunos lenguajes, especialmente C ++, los objetos automáticos y dinámicos se destruyen en momentos deterministas, como la salida del alcance, la destrucción explícita (a través de la gestión manual de la memoria ) o el recuento de referencias que llega a cero; mientras que en otros lenguajes, como C #, Java y Python, estos objetos se destruyen en momentos no deterministas, según el recolector de basura, y la resurrección del objeto puede ocurrir durante la destrucción, extendiendo la vida útil.
En los lenguajes de recolección de basura, los objetos generalmente se asignan dinámicamente (en el montón) incluso si inicialmente están vinculados a una variable automática, a diferencia de las variables automáticas con valores primitivos, que generalmente se asignan automáticamente (en la pila o en un registro). Esto permite que el objeto sea devuelto desde una función ("escape") sin ser destruido. Sin embargo, en algunos casos es posible una optimización del compilador, es decir, realizar un análisis de escape y demostrar que el escape no es posible y, por lo tanto, el objeto se puede asignar a la pila; esto es significativo en Java. En este caso, la destrucción del objeto se producirá de inmediato, posiblemente incluso durante la vida útil de la variable (antes del final de su alcance), si es inalcanzable.
Un caso complejo es el uso de un grupo de objetos, donde los objetos pueden crearse con anticipación o reutilizarse y, por lo tanto, la creación y destrucción aparentes pueden no corresponder a la creación y destrucción reales de un objeto, solo a la (re) inicialización para la creación y finalización de destrucción. En este caso, tanto la creación como la destrucción pueden ser no deterministas.
La creación de objetos se puede dividir en dos operaciones: asignación de memoria e inicialización, donde la inicialización incluye la asignación de valores a los campos del objeto y posiblemente ejecutar otro código arbitrario. Estos son conceptos a nivel de implementación, aproximadamente análogos a la distinción entre declaración y definición de una variable, aunque estos últimos son distinciones a nivel de lenguaje. Para un objeto que está vinculado a una variable, la declaración puede compilarse para la asignación de memoria (reservando espacio para el objeto) y la definición para la inicialización (asignando valores), pero las declaraciones también pueden ser solo para uso del compilador (como la resolución de nombres ), no corresponde directamente al código compilado.
De manera análoga, la destrucción de objetos se puede dividir en dos operaciones, en el orden opuesto: finalización y desasignación de memoria. Estos no tienen conceptos análogos a nivel de lenguaje para las variables: la vida útil de la variable termina implícitamente (para las variables automáticas, en la pila de desenrollado; para las variables estáticas, en la terminación del programa), y en este momento (o más tarde, dependiendo de la implementación) la memoria se desasigna, pero no se realiza ninguna finalización en general. Sin embargo, cuando la vida útil de un objeto está vinculada a la vida útil de una variable, el final de la vida útil de la variable provoca la finalización del objeto; este es un paradigma estándar en C ++.
Juntos, estos producen cuatro pasos a nivel de implementación:
Estos pasos pueden ser realizados automáticamente por el tiempo de ejecución del idioma, el intérprete o la máquina virtual, o pueden ser especificados manualmente por el programador en una subrutina, concretamente a través de métodos; la frecuencia de esto varía significativamente entre los pasos y los idiomas. La inicialización es muy comúnmente especificada por el programador en lenguajes basados en clases, mientras que en lenguajes estrictamente basados en prototipos, la inicialización se realiza automáticamente mediante copia. La finalización también es muy común en lenguajes con destrucción determinista, notablemente C ++, pero mucho menos común en lenguajes de recolección de basura. La asignación se especifica con menos frecuencia y, por lo general, no se puede especificar la desasignación.
Una sutileza importante es el estado de un objeto durante la creación o destrucción, y el manejo de casos en los que ocurren errores o surgen excepciones, como si la creación o la destrucción fallan. Estrictamente hablando, la vida útil de un objeto comienza cuando se completa la asignación y finaliza cuando comienza la desasignación. Por lo tanto, durante la inicialización y finalización, un objeto está vivo, pero es posible que no esté en un estado coherente, lo que garantiza que las invariantes de clase sean una parte clave de la inicialización, y el período desde que se completa la inicialización hasta que comienza la finalización es cuando el objeto está vivo y se espera que lo haga. estar en un estado consistente.
Si la creación o la destrucción fallan, la notificación de errores (a menudo mediante la creación de una excepción) puede ser complicada: el objeto o los objetos relacionados pueden estar en un estado inconsistente y, en el caso de destrucción, que generalmente ocurre de manera implícita y, por lo tanto, en un entorno no especificado. puede resultar difícil manejar los errores. El problema opuesto, excepciones entrantes, no excepciones salientes, es si la creación o la destrucción deben comportarse de manera diferente si ocurren durante el manejo de excepciones, cuando se puede desear un comportamiento diferente.
Otra sutileza es cuando ocurren la creación y destrucción de variables estáticas, cuya vida útil coincide con el tiempo de ejecución del programa - ¿la creación y destrucción ocurren durante la ejecución regular del programa, o en fases especiales antes y después de la ejecución regular? Y cómo se destruyen los objetos en el programa. terminación, cuando el programa puede no estar en un estado habitual o consistente. Esto es particularmente un problema para los lenguajes recolectados de basura, ya que pueden tener mucha basura al finalizar el programa.
En la programación basada en clases, la creación de objetos también se conoce como instanciación (creación de una instancia de una clase), y la creación y destrucción se pueden controlar mediante métodos conocidos como constructor y destructor, o inicializador y finalizador. Por lo tanto, la creación y la destrucción también se conocen como construcción y destrucción, y cuando estos métodos se denominan, se dice que un objeto está construido o destruido (no "destruido"), respectivamente, inicializado o finalizado cuando se llaman esos métodos.
La relación entre estos métodos puede ser complicada, y un lenguaje puede tener tanto constructores como inicializadores (como Python), o destructores y finalizadores (como C ++ / CLI ), o los términos "destructor" y "finalizador" pueden referirse al lenguaje. construcción de nivel versus implementación (como en C # versus CLI).
Una distinción clave es que los constructores son métodos de clase, ya que no hay ningún objeto (instancia de clase) disponible hasta que se crea el objeto, pero los otros métodos (destructores, inicializadores y finalizadores) son métodos de instancia, ya que se ha creado un objeto. Además, los constructores e inicializadores pueden aceptar argumentos, mientras que los destructores y finalizadores generalmente no lo hacen, ya que generalmente se los llama implícitamente.
En el uso común, un constructor es un método directamente llamado explícitamente por el código de usuario para crear un objeto, mientras que "destructor" es la subrutina llamada (generalmente implícitamente, pero a veces explícitamente) en la destrucción de objetos en lenguajes con vidas de objetos deterministas - el arquetipo es C ++ - y "finalizador" es la subrutina llamada implícitamente por el recolector de basura sobre la destrucción de objetos en lenguajes con una vida útil de objeto no determinista - el arquetipo es Java.
Los pasos durante la finalización varían significativamente dependiendo de la gestión de la memoria: en la gestión manual de la memoria (como en C ++, o el recuento manual de referencias), el programador debe destruir explícitamente las referencias (referencias borradas, recuentos de referencias reducidos); en el recuento automático de referencias, esto también ocurre durante la finalización, pero está automatizado (como en Python, cuando ocurre después de que se hayan llamado a los finalizadores especificados por el programador); y al rastrear la recolección de basura esto no es necesario. Por lo tanto, en el recuento automático de referencias, los finalizadores especificados por el programador suelen ser breves o ausentes, pero aún se puede realizar un trabajo significativo, mientras que en el rastreo de recolectores de basura, la finalización a menudo es innecesaria.
En los lenguajes donde los objetos tienen una vida útil determinista, la vida útil del objeto se puede usar para llevar a cabo la gestión de recursos : esto se llama el lenguaje de adquisición de recursos es inicialización (RAII): los recursos se adquieren durante la inicialización y se liberan durante la finalización. En lenguajes donde los objetos tienen vidas útiles no deterministas, en particular debido a la recolección de basura, la administración de la memoria generalmente se mantiene separada de la administración de otros recursos.
En un caso típico, el proceso es el siguiente:
Esas tareas se pueden completar de una vez, pero a veces se dejan sin terminar y el orden de las tareas puede variar y puede causar varios comportamientos extraños. Por ejemplo, en herencia múltiple, qué código de inicialización debe llamarse primero es una pregunta difícil de responder. Sin embargo, los constructores de superclase deben llamarse antes que los constructores de subclase.
Es un problema complejo crear cada objeto como un elemento de una matriz. Algunos lenguajes (por ejemplo, C ++) dejan esto en manos de los programadores.
Manejar excepciones en medio de la creación de un objeto es particularmente problemático porque generalmente la implementación de lanzar excepciones se basa en estados de objeto válidos. Por ejemplo, no hay forma de asignar un nuevo espacio para un objeto de excepción cuando la asignación de un objeto falló antes debido a la falta de espacio libre en la memoria. Debido a esto, las implementaciones de lenguajes OO deben proporcionar mecanismos que permitan generar excepciones incluso cuando hay escasez de recursos, y los programadores o el sistema de tipos deben asegurarse de que su código sea seguro para excepciones. Propagar una excepción tiene más probabilidades de liberar recursos que de asignarlos. Pero en la programación orientada a objetos, la construcción de objetos puede fallar, porque la construcción de un objeto debe establecer las invariantes de clase, que a menudo no son válidas para todas las combinaciones de argumentos del constructor. Por tanto, los constructores pueden generar excepciones.
El patrón de fábrica abstracto es una forma de desacoplar una implementación particular de un objeto del código para la creación de dicho objeto.
La forma de crear objetos varía según los idiomas. En algunos lenguajes basados en clases, un método especial conocido como constructor es responsable de validar el estado de un objeto. Al igual que los métodos ordinarios, los constructores se pueden sobrecargar para que se pueda crear un objeto con diferentes atributos especificados. Además, el constructor es el único lugar para establecer el estado de los objetos inmutables. Un constructor de copia es un constructor que toma un parámetro (único) de un objeto existente del mismo tipo que la clase del constructor y devuelve una copia del objeto enviado como parámetro.
Otros lenguajes de programación, como Objective-C, tienen métodos de clase, que pueden incluir métodos de tipo constructor, pero no se limitan a crear instancias de objetos.
Se ha criticado a C ++ y Java por no proporcionar constructores con nombre; un constructor siempre debe tener el mismo nombre que la clase. Esto puede ser problemático si el programador quiere proporcionar dos constructores con los mismos tipos de argumentos, por ejemplo, para crear un objeto puntual a partir de las coordenadas cartesianas o de las coordenadas polares, las cuales estarían representadas por dos números de coma flotante. Objetivo-C puede eludir este problema, en que el programador puede crear una clase Point, con los métodos de inicialización, por ejemplo, + newPointWithX: andY: y + newPointWithR: andTheta:. En C ++, se puede hacer algo similar usando funciones miembro estáticas.
Un constructor también puede referirse a una función que se usa para crear un valor de una unión etiquetada, particularmente en lenguajes funcionales.
Por lo general, después de usar un objeto, se elimina de la memoria para dejar espacio para que otros programas u objetos ocupen su lugar. Sin embargo, si hay suficiente memoria o un programa tiene un tiempo de ejecución corto, es posible que no se produzca la destrucción del objeto y que la memoria simplemente se desasigne al finalizar el proceso. En algunos casos, la destrucción de objetos consiste simplemente en desasignar la memoria, particularmente en lenguajes de recolección de basura, o si el "objeto" es en realidad una estructura de datos vieja y simple. En otros casos, se realiza algún trabajo antes de la desasignación, en particular destruyendo objetos miembros (en la gestión manual de la memoria) o eliminando referencias del objeto a otros objetos para disminuir los recuentos de referencias (en el recuento de referencias). Esto puede ser automático o se puede invocar un método de destrucción especial en el objeto.
En los lenguajes basados en clases con una vida útil de objetos determinista, en particular C ++, un destructor es un método llamado cuando se elimina una instancia de una clase, antes de que se desasigne la memoria. En C ++, los destructores se diferencian de los constructores en varias formas: no pueden sobrecargarse, no deben tener argumentos, no necesitan mantener invariantes de clase y pueden causar la terminación del programa si arrojan excepciones.
En los lenguajes de recolección de basura, los objetos pueden destruirse cuando el código en ejecución ya no puede alcanzarlos. En los lenguajes GCed basados en clases, el análogo de los destructores son los finalizadores, que se llaman antes de que un objeto sea recolectado como basura. Estos difieren en ejecutarse en un momento impredecible y en un orden impredecible, ya que la recolección de basura es impredecible y son significativamente menos utilizados y menos complejos que los destructores de C ++. Ejemplos de tales lenguajes incluyen Java, Python y Ruby.
La destrucción de un objeto hará que cualquier referencia al objeto deje de ser válida y, en la gestión manual de la memoria, cualquier referencia existente se convertirá en referencias colgantes. En la recolección de elementos no utilizados (tanto el seguimiento de la recolección de elementos no utilizados como el recuento de referencias), los objetos solo se destruyen cuando no hay referencias a ellos, pero la finalización puede crear nuevas referencias al objeto y, para evitar referencias colgantes, se produce la resurrección del objeto para que las referencias sigan siendo válidas.
class Foo { public: // These are the prototype declarations of the constructors. Foo(int x); Foo(int x, int y); // Overloaded Constructor. Foo(const Foo amp;old); // Copy Constructor. ~Foo(); // Destructor. }; Foo::Foo(int x) { // This is the implementation of // the one-argument constructor. } Foo::Foo(int x, int y) { // This is the implementation of // the two-argument constructor. } Foo::Foo(const Foo amp;old) { // This is the implementation of // the copy constructor. } Foo::~Foo() { // This is the implementation of the destructor. } int main() { Foo foo(14); // Call first constructor. Foo foo2(12, 16); // Call overloaded constructor. Foo foo3(foo); // Call the copy constructor. // Destructors called in backwards-order // here, automatically. }
class Foo { public Foo(int x) { // This is the implementation of // the one-argument constructor } public Foo(int x, int y) { // This is the implementation of // the two-argument constructor } public Foo(Foo old) { // This is the implementation of // the copy constructor } public static void main(String[] args) { Foo foo = new Foo(14); // call first constructor Foo foo2 = new Foo(12, 16); // call overloaded constructor Foo foo3 = new Foo(foo); // call the copy constructor // garbage collection happens under the covers, and objects are destroyed } }
namespace ObjectLifeTime { class Foo { public Foo() { // This is the implementation of // default constructor. } public Foo(int x) { // This is the implementation of // the one-argument constructor. } ~Foo() { // This is the implementation of // the destructor. } public Foo(int x, int y) { // This is the implementation of // the two-argument constructor. } public Foo(Foo old) { // This is the implementation of // the copy constructor. } public static void Main(string[] args) { Foo defaultfoo = new Foo(); // Call default constructor Foo foo = new Foo(14); // Call first constructor Foo foo2 = new Foo(12, 16); // Call overloaded constructor Foo foo3 = new Foo(foo); // Call the copy constructor } } }
#import lt;objc/Object.hgt; @interface Point : Object { double x; double y; } //These are the class methods; we have declared two constructors + (Point *) newWithX: (double) andY: (double); + (Point *) newWithR: (double) andTheta: (double); //Instance methods - (Point *) setFirstCoord: (double); - (Point *) setSecondCoord: (double); /* Since Point is a subclass of the generic Object * class, we already gain generic allocation and initialization * methods, +alloc and -init. For our specific constructors * we can make these from these methods we have * inherited. */ @end @implementation Point - (Point *) setFirstCoord: (double) new_val { x = new_val; } - (Point *) setSecondCoord: (double) new_val { y = new_val; } + (Point *) newWithX: (double) x_val andY: (double) y_val { //Concisely written class method to automatically allocate and //perform specific initialization. return [[[Point alloc] setFirstCoord:x_val] setSecondCoord:y_val]; } + (Point *) newWithR: (double) r_val andTheta: (double) theta_val { //Instead of performing the same as the above, we can underhandedly //use the same result of the previous method return [Point newWithX:r_val andY:theta_val]; } @end int main(void) { //Constructs two points, p and q. Point *p = [Point newWithX:4.0 andY:5.0]; Point *q = [Point newWithR:1.0 andTheta:2.28]; //...program text.... //We're finished with p, say, so, free it. //If p allocates more memory for itself, may need to //override Object's free method in order to recursively //free p's memory. But this is not the case, so we can just [p free]; //...more text... [q free]; return 0; }
Idiomas relacionados: "Delphi", "Free Pascal", "Mac Pascal".
program Example; type DimensionEnum = ( deUnassigned, de2D, de3D, de4D); PointClass = class private Dimension: DimensionEnum; public X: Integer; Y: Integer; Z: Integer; T: Integer; public (* prototype of constructors *) constructor Create(); constructor Create(AX, AY: Integer); constructor Create(AX, AY, AZ: Integer); constructor Create(AX, AY, AZ, ATime: Integer); constructor CreateCopy(APoint: PointClass); (* prototype of destructors *) destructor Destroy; end; constructor PointClass.Create(); begin // implementation of a generic, non argument constructor Self.Dimension:= deUnassigned; end; constructor PointClass.Create(AX, AY: Integer); begin // implementation of a, 2 argument constructor Self.X:= AX; Y:= AY; Self.Dimension:= de2D; end; constructor PointClass.Create(AX, AY, AZ: Integer); begin // implementation of a, 3 argument constructor Self.X:= AX; Y:= AY; Self.X:= AZ; Self.Dimension:= de3D; end; constructor PointClass.Create(AX, AY, AZ, ATime: Integer); begin // implementation of a, 4 argument constructor Self.X:= AX; Y:= AY; Self.X:= AZ; T:= ATime; Self.Dimension:= de4D; end; constructor PointClass.CreateCopy(APoint: PointClass); begin // implementation of a, "copy" constructor APoint.X:= AX; APoint.Y:= AY; APoint.X:= AZ; APoint.T:= ATime; Self.Dimension:= de4D; end; destructor PointClass.PointClass.Destroy; begin // implementation of a generic, non argument destructor Self.Dimension:= deUnAssigned; end; var (* variable for static allocation *) S: PointClass; (* variable for dynamic allocation *) D: ^PointClass; begin (* of program *) (* object lifeline with static allocation *) S.Create(5, 7); (* do something with "S" *) S.Destroy; (* object lifeline with dynamic allocation *) D = new PointClass, Create(5, 7); (* do something with "D" *) dispose D, Destroy; end. (* of program *)
class Socket: def __init__(self, remote_host: str) -gt; None: # connect to remote host def send(self): # Send data def recv(self): # Receive data def close(self): # close the socket def __del__(self): # __del__ magic function called when the object's reference count equals zero self.close() def f(): socket = Socket("example.com") socket.send("test") return socket.recv()
El socket se cerrará en la siguiente ronda de recolección de basura después de que la función "f" se ejecute y regrese, ya que se han perdido todas las referencias.