Vida útil do objeto - Object lifetime

Na programação orientada a objetos (OOP), o tempo de vida do objeto (ou ciclo de vida ) de um objeto é o tempo entre a criação e a destruição de um objeto. As regras para o tempo de vida do objeto variam significativamente entre as linguagens, em alguns casos entre as implementações de uma determinada linguagem, e o tempo de vida de um objeto específico pode variar de uma execução do programa para outra.

Em alguns casos objeto coincide vida com tempo de vida variável de uma variável com o objecto como valor (tanto para variáveis estáticas e variáveis automáticas ), mas em geral não vida objecto está ligado ao tempo de vida de qualquer variável de um. Em muitos casos - e por padrão em muitas linguagens orientadas a objetos , particularmente aquelas que usam coleta de lixo (GC) - os objetos são alocados no heap , e o tempo de vida do objeto não é determinado pelo tempo de vida de uma determinada variável: o valor de uma variável segurar um objeto na verdade corresponde a uma referência para o objeto, não o próprio objeto, e a destruição da variável apenas destrói a referência, não o objeto subjacente.

Visão geral

Embora a ideia básica do tempo de vida do objeto seja simples - um objeto é criado, usado e depois destruído - os detalhes variam substancialmente entre as linguagens e nas implementações de uma determinada linguagem, e estão intimamente ligados a como o gerenciamento de memória é implementado. Além disso, muitas distinções sutis são feitas entre as etapas e entre os conceitos de nível de linguagem e conceitos de nível de implementação. A terminologia é relativamente padrão, mas quais etapas correspondem a um determinado termo varia significativamente entre os idiomas.

Os termos geralmente vêm em pares antônimos, um para um conceito de criação, outro para o conceito de destruição correspondente, como inicializar / finalizar ou construtor / destruidor. O par de criação / destruição também é conhecido como iniciação / término, entre outros termos. Os termos alocação e desalocação ou liberação também são usados, por analogia com o gerenciamento de memória, embora a criação e destruição de objetos possam envolver significativamente mais do que simplesmente alocação e desalocação de memória, e alocação / desalocação são etapas consideradas mais apropriadamente na criação e destruição, respectivamente.

Determinismo

Uma distinção importante é se a vida útil de um objeto é determinística ou não determinística. Isso varia com o idioma, e dentro do idioma varia com a alocação de memória de um objeto; o tempo de vida do objeto pode ser diferente do tempo de vida variável.

Objetos com alocação de memória estática , notadamente objetos armazenados em variáveis ​​estáticas e módulos de classes (se classes ou módulos são eles próprios objetos e alocados estaticamente), têm um não-determinismo sutil em muitas linguagens: enquanto seu tempo de vida parece coincidir com o tempo de execução do programa, a ordem de criação e destruição - qual objeto estático é criado primeiro, qual segundo, etc. - é geralmente não determinística.

Para objetos com alocação automática de memória ou alocação de memória dinâmica , a criação de objetos geralmente acontece de forma determinística, seja explicitamente quando um objeto é explicitamente criado (como via newem C ++ ou Java), ou implicitamente no início da vida útil da variável, particularmente quando o escopo de um a variável automática é inserida, como na declaração. A destruição de objetos varia, entretanto - em algumas linguagens, notadamente C ++, objetos automáticos e dinâmicos são destruídos em momentos determinísticos, como saída de escopo, destruição explícita (via gerenciamento manual de memória ) ou contagem de referência chegando a zero; enquanto em outras linguagens, como C #, Java e Python, esses objetos são destruídos em momentos não determinísticos, dependendo do coletor de lixo, e a ressurreição do objeto pode ocorrer durante a destruição, estendendo a vida útil.

Em linguagens com coleta de lixo, os objetos são geralmente alocados dinamicamente (no heap) mesmo se forem inicialmente vinculados a uma variável automática, ao contrário das variáveis ​​automáticas com valores primitivos, que normalmente são alocados automaticamente (na pilha ou em um registro). Isso permite que o objeto seja retornado de uma função ("escape") sem ser destruído. No entanto, em alguns casos, uma otimização do compilador é possível, nomeadamente realizando a análise de escape e provando que o escape não é possível e, portanto, o objeto pode ser alocado na pilha; isso é significativo em Java. Nesse caso, a destruição do objeto ocorrerá imediatamente - possivelmente mesmo durante o tempo de vida da variável (antes do final de seu escopo), se ela estiver inacessível.

Um caso complexo é o uso de um pool de objetos, onde os objetos podem ser criados antes do tempo ou reutilizados e, portanto, a criação e destruição aparentes podem não corresponder à criação e destruição reais de um objeto, apenas (re) inicialização para criação e finalização para destruição. Nesse caso, tanto a criação quanto a destruição podem ser não determinísticas.

Passos

A criação de objetos pode ser dividida em duas operações: alocação de memória e inicialização , em que a inicialização inclui a atribuição de valores a campos de objeto e, possivelmente, a execução de outro código arbitrário. Esses são conceitos de nível de implementação, mais ou menos análogos à distinção entre declaração e definição de uma variável, embora essas sejam, posteriormente, distinções de nível de linguagem. Para um objeto que está vinculado a uma variável, a declaração pode ser compilada para alocação de memória (reservando espaço para o objeto) e a definição para inicialização (atribuição de valores), mas as declarações também podem ser apenas para uso do compilador (como resolução de nome ), não corresponde diretamente ao código compilado.

Analogamente, a destruição de objetos pode ser dividida em duas operações, na ordem oposta: finalização e desalocação de memória . Eles não têm conceitos de nível de linguagem análogos para variáveis: a vida útil da variável termina implicitamente (para variáveis ​​automáticas, no desenrolar da pilha; para variáveis ​​estáticas, no término do programa), e neste momento (ou mais tarde, dependendo da implementação) a memória é desalocada, mas nenhuma finalização é feita em geral. No entanto, quando o tempo de vida de um objeto está vinculado ao tempo de vida de uma variável, o fim do tempo de vida da variável causa a finalização do objeto; este é um paradigma padrão em C ++.

Juntos, eles resultam em quatro etapas de nível de implementação:

alocação, inicialização, finalização, desalocação

Essas etapas podem ser executadas automaticamente pelo runtime da linguagem, intérprete ou máquina virtual, ou podem ser especificadas manualmente pelo programador em uma sub - rotina , concretamente por meio de métodos - a frequência disso varia significativamente entre as etapas e as linguagens. A inicialização é muito comumente especificada pelo programador em linguagens baseadas em classe , enquanto em linguagens baseadas em protótipo estritas a inicialização é feita automaticamente por cópia. A finalização também é muito comum em linguagens com destruição determinística, notavelmente C ++, mas muito menos comum em linguagens com coleta de lixo. A alocação é especificada mais raramente e a desalocação geralmente não pode ser especificada.

Status durante a criação e destruição

Uma sutileza importante é o status de um objeto durante a criação ou destruição e o tratamento de casos em que ocorrem erros ou exceções são levantadas, como se a criação ou destruição falhar. Estritamente falando, o tempo de vida de um objeto começa quando a alocação é concluída e termina quando a desalocação é iniciada. Assim, durante a inicialização e finalização, um objeto está ativo, mas pode não estar em um estado consistente - garantindo que invariantes de classe são uma parte fundamental da inicialização - e o período de quando a inicialização é concluída até quando a finalização começa é quando o objeto está ativo e espera-se estar em um estado consistente.

Se a criação ou destruição falhar, o relatório de erro (muitas vezes levantando uma exceção) pode ser complicado: o objeto ou objetos relacionados podem estar em um estado inconsistente e, no caso de destruição - o que geralmente acontece implicitamente e, portanto, em um ambiente não especificado - pode ser difícil lidar com os erros. A questão oposta - exceções de entrada, não exceções de saída - é se a criação ou destruição deve se comportar de maneira diferente se ocorrerem durante o tratamento de exceções, quando um comportamento diferente pode ser desejado.

Outra sutileza é quando a criação e destruição acontecem para variáveis ​​estáticas , cuja vida útil coincide com o tempo de execução do programa - a criação e destruição acontecem durante a execução regular do programa, ou em fases especiais antes e depois da execução regular - e como os objetos são destruídos no programa rescisão, quando o programa pode não estar em um estado normal ou consistente. Isso é particularmente um problema para linguagens com coleta de lixo, pois elas podem ter muito lixo no encerramento do programa.

Programação baseada em aulas

Na programação baseada em classes, a criação do objecto também é conhecido como instanciação (criação de uma instância de uma classe ), e a criação e a destruição pode ser controlada através de métodos conhecidos como um construtor e destructor , ou um inicializador e finalizador . Criação e destruição são, portanto, também conhecidas como construção e destruição, e quando esses métodos são chamados, diz-se que um objeto foi construído ou destruído (não "destruído") - respectivamente, inicializado ou finalizado quando esses métodos são chamados.

A relação entre esses métodos pode ser complicada, e uma linguagem pode ter construtores e inicializadores (como Python), ou ambos destruidores e finalizadores (como C ++ / CLI ), ou os termos "destruidor" e "finalizador" podem se referir a linguagem- construção de nível versus implementação (como em C # versus CLI).

Uma distinção importante é que construtores são métodos de classe, já que não há objeto (instância de classe) disponível até que o objeto seja criado, mas os outros métodos (destruidores, inicializadores e finalizadores) são métodos de instância, já que um objeto foi criado. Além disso, construtores e inicializadores podem aceitar argumentos, enquanto destruidores e finalizadores geralmente não aceitam, já que são geralmente chamados implicitamente.

No uso comum, um construtor é um método chamado diretamente explicitamente pelo código do usuário para criar um objeto, enquanto "destruidor" é a sub-rotina chamada (geralmente implicitamente, mas às vezes explicitamente) na destruição de objeto em linguagens com tempos de vida de objeto determinísticos - o arquétipo é C ++ - e "finalizador" é a sub-rotina chamada implicitamente pelo coletor de lixo na destruição de objetos em linguagens com vida útil não determinística do objeto - o arquétipo é Java.

As etapas durante a finalização variam significativamente dependendo do gerenciamento de memória: no gerenciamento de memória manual (como em C ++, ou contagem de referência manual), as referências precisam ser explicitamente destruídas pelo programador (referências apagadas, contagens de referência diminuídas); na contagem automática de referências, isso também acontece durante a finalização, mas é automatizado (como em Python, quando ocorre depois que os finalizadores especificados pelo programador foram chamados); e no rastreamento da coleta de lixo isso não é necessário. Assim, na contagem automática de referência, os finalizadores especificados pelo programador são freqüentemente curtos ou ausentes, mas um trabalho significativo ainda pode ser feito, enquanto no rastreamento de coletores de lixo a finalização é freqüentemente desnecessária.

Gestão de recursos

Em linguagens em que os objetos têm tempos de vida determinísticos, o tempo de vida do objeto pode ser usado para fazer carona no gerenciamento de recursos : isso é chamado de idioma Aquisição de recursos é inicialização (RAII): os recursos são adquiridos durante a inicialização e liberados durante a finalização. Em linguagens em que os objetos têm tempos de vida não determinísticos, principalmente devido à coleta de lixo, o gerenciamento da memória é geralmente mantido separado do gerenciamento de outros recursos.

Criação de objeto

No caso típico, o processo é o seguinte:

  • calcular o tamanho de um objeto - o tamanho é basicamente o mesmo da classe, mas pode variar. Quando o objeto em questão não é derivado de uma classe, mas de um protótipo , o tamanho de um objeto é geralmente o da estrutura de dados interna (um hash, por exemplo) que contém seus slots.
  • alocação - alocação de espaço de memória com o tamanho de um objeto mais o crescimento posterior, se possível saber com antecedência
  • métodos de vinculação - isso geralmente é deixado para a classe do objeto ou é resolvido no momento do despacho , mas mesmo assim é possível que alguns modelos de objeto vinculem métodos no momento da criação.
  • chamando um código de inicialização (ou seja, construtor ) da superclasse
  • chamando um código de inicialização da classe sendo criada

Essas tarefas podem ser concluídas imediatamente, mas às vezes ficam inacabadas e a ordem das tarefas pode variar e causar vários comportamentos estranhos. Por exemplo, em herança múltipla , qual código de inicialização deve ser chamado primeiro é uma pergunta difícil de responder. No entanto, os construtores da superclasse devem ser chamados antes dos construtores da subclasse.

É um problema complexo criar cada objeto como um elemento de um array. Algumas linguagens (por exemplo, C ++) deixam isso para os programadores.

Manipular exceções no meio da criação de um objeto é particularmente problemático porque geralmente a implementação de exceções de lançamento depende de estados de objeto válidos. Por exemplo, não há como alocar um novo espaço para um objeto de exceção quando a alocação de um objeto falhou antes disso devido à falta de espaço livre na memória. Devido a isso, as implementações de linguagens OO devem fornecer mecanismos para permitir o levantamento de exceções, mesmo quando há um fornecimento curto de recursos, e os programadores ou o sistema de tipos devem garantir que seu código seja seguro para exceções . É mais provável que a propagação de uma exceção libere recursos do que alocá-los. Mas na programação orientada a objetos, a construção de objetos pode falhar, porque construir um objeto deve estabelecer as invariantes de classe , que geralmente não são válidas para todas as combinações de argumentos do construtor. Portanto, os construtores podem gerar exceções.

O padrão de fábrica abstrato é uma forma de desacoplar uma implementação particular de um objeto do código para a criação de tal objeto.

Métodos de criação

A maneira de criar objetos varia entre os idiomas. Em algumas linguagens baseadas em classes, um método especial conhecido como construtor é responsável por validar o estado de um objeto. Assim como os métodos comuns, os construtores podem ser sobrecarregados para que um objeto possa ser criado com diferentes atributos especificados. Além disso, o construtor é o único lugar para definir o estado de objetos imutáveis . Um construtor de cópia é um construtor que pega um parâmetro (único) de um objeto existente do mesmo tipo da classe do construtor e retorna uma cópia do objeto enviado como um parâmetro.

Outras linguagens de programação, como Objective-C , têm métodos de classe, que podem incluir métodos do tipo construtor, mas não estão restritos a meramente instanciar objetos.

C ++ e Java foram criticados por não fornecerem construtores nomeados - um construtor deve sempre ter o mesmo nome da classe. Isso pode ser problemático se o programador deseja fornecer dois construtores com os mesmos tipos de argumento, por exemplo, para criar um objeto de ponto a partir das coordenadas cartesianas ou das coordenadas polares , ambas representadas por dois números de ponto flutuante. Objectivo-C pode contornar este problema, em que o programador pode criar uma classe Point, com métodos de inicialização, por exemplo, + newPointWithX: Andy: e + newPointWithR: andTheta: . Em C ++, algo semelhante pode ser feito usando funções de membro estáticas.

Um construtor também pode se referir a uma função que é usada para criar um valor de uma união marcada , particularmente em linguagens funcionais.

Destruição de objetos

Em geral, depois que um objeto é usado, ele é removido da memória para abrir espaço para outros programas ou objetos tomarem o lugar desse objeto. No entanto, se houver memória suficiente ou um programa tiver um tempo de execução curto, a destruição do objeto pode não ocorrer, a memória simplesmente sendo desalocada no encerramento do processo. Em alguns casos, a destruição de objetos consiste simplesmente em desalocar a memória, particularmente em linguagens com coleta de lixo, ou se o "objeto" for na verdade uma estrutura de dados simples e antiga . Em outros casos, algum trabalho é executado antes da desalocação, principalmente destruindo objetos membros (no gerenciamento manual de memória) ou excluindo referências do objeto a outros objetos para diminuir as contagens de referência (na contagem de referência). Isso pode ser automático ou um método de destruição especial pode ser chamado no objeto.

Em linguagens baseadas em classes com vida útil de objeto determinística, notavelmente C ++, um destruidor é um método chamado quando uma instância de uma classe é excluída, antes que a memória seja desalocada. Em C ++, os destruidores diferem dos construtores de várias maneiras: eles não podem ser sobrecarregados, não devem ter argumentos, não precisam manter invariáveis ​​de classe e podem causar o encerramento do programa se lançarem exceções.

Em linguagens de coleta de lixo , os objetos podem ser destruídos quando não puderem mais ser alcançados pelo código em execução. Em linguagens GCed baseadas em classes, o análogo dos destruidores são finalizadores , que são chamados antes de um objeto ser coletado como lixo. Eles diferem na execução em um momento imprevisível e em uma ordem imprevisível, uma vez que a coleta de lixo é imprevisível e é significativamente menos usada e menos complexa do que os destruidores C ++. Exemplos de tais linguagens incluem Java , Python e Ruby .

A destruição de um objeto fará com que todas as referências ao objeto se tornem inválidas e, no gerenciamento de memória manual, quaisquer referências existentes se tornem referências pendentes . Na coleta de lixo (tanto no rastreamento da coleta de lixo quanto na contagem de referência), os objetos são destruídos apenas quando não há referências a eles, mas a finalização pode criar novas referências ao objeto e, para evitar referências pendentes, a ressurreição do objeto ocorre para que as referências permaneçam válidas.

Exemplos

C ++

class Foo {
 public:
  // These are the prototype declarations of the constructors.
  Foo(int x);
  Foo(int x, int y);    // Overloaded Constructor.
  Foo(const Foo &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 &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.
}

Java

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
    }
}

C #

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
    }
}
}

Objective-C

#import <objc/Object.h>

@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;
}

Object Pascal

Linguagens relacionadas: "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 *)

Pitão

class Socket:
    def __init__(self, remote_host: str) -> 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()

O soquete será fechado na próxima rodada de coleta de lixo após a função "f" ser executada e retornar, pois todas as referências a ela foram perdidas.

Veja também

Notas

Referências