Životnost objektu - Object lifetime

V objektově orientovaném programování (OOP) je životnost objektu (nebo životní cyklus ) objektu čas mezi vytvořením objektu a jeho zničením. Pravidla pro životnost objektu se mezi jazyky výrazně liší, v některých případech mezi implementacemi daného jazyka a životnost konkrétního objektu se může lišit od jednoho spuštění programu k druhému.

V některých případech se životnost objektu shoduje s životností proměnné proměnné s tímto objektem jako hodnotou (jak pro statické proměnné, tak pro automatické proměnné ), ale obecně životnost objektu není svázána s životností jakékoli jedné proměnné. V mnoha případech-a standardně v mnoha objektově orientovaných jazycích , zejména v těch, které používají uvolňování paměti (GC)-jsou objekty přiděleny na haldě a životnost objektu není určena životností dané proměnné: hodnota proměnné držení objektu ve skutečnosti odpovídá odkazu na objekt, nikoli na samotný objekt, a zničení proměnné pouze zničí odkaz, nikoli podkladový objekt.

Přehled

Základní myšlenka životnosti objektu je jednoduchá - objekt je vytvořen, použit a poté zničen - detaily se mezi jazyky a v rámci implementací daného jazyka podstatně liší a jsou úzce spjaty s tím, jak je implementována správa paměti . Dále je mezi kroky a mezi koncepty na úrovni jazyka a koncepcí na úrovni implementace provedeno mnoho jemných rozdílů. Terminologie je poměrně standardní, ale které kroky odpovídají danému výrazu se mezi jazyky výrazně liší.

Pojmy obecně přicházejí v párech antonym, jeden pro koncept vytváření, jeden pro odpovídající koncept destrukce, jako je inicializace/dokončení nebo konstruktor/destruktor. Dvojice vytvoření/zničení je mimo jiné také známá jako zahájení/ukončení . Pojmy alokace a deallokace nebo uvolňování se také používají, analogicky se správou paměti, ačkoli vytváření a ničení objektů může zahrnovat podstatně více než jen alokaci paměti a deallokaci, a alokace/deallokace jsou vhodněji považovány za kroky při vytváření a destrukci.

Determinismus

Zásadním rozdílem je, zda je životnost objektu deterministická nebo nedeterministická. To se liší podle jazyka a v rámci jazyka se liší podle přidělení paměti objektu; životnost objektu se může lišit od proměnné životnosti.

Objekty se přidělením statické paměti , zejména objekty uložené ve statických proměnných a moduly tříd (pokud jsou třídy nebo moduly samy objekty a jsou staticky přiděleny), mají v mnoha jazycích jemný nedeterminismus: zatímco se zdá, že se jejich životnost shoduje s dobou běhu programu, pořadí vytvoření a zničení - který statický objekt je vytvořen jako první, který druhý atd. - je obecně nedeterministické.

U objektů s automatickou přidělování paměti nebo dynamického přidělování paměti , vytvoření objektu zpravidla děje deterministicky, buď výslovně když je objekt výslovně vytvořeny (jako například pomocí newv C ++ nebo Java), nebo nepřímo na začátku proměnné životnosti, a to zejména, když je rozsah dusičnanu amonného zadává se automatická proměnná , například při deklaraci. Destrukce objektů se však liší - v některých jazycích, zejména v jazyce C ++, jsou automatické a dynamické objekty zničeny v deterministických časech, jako je ukončení rozsahu, explicitní destrukce (pomocí manuální správy paměti ) nebo počet referencí dosahující nuly; zatímco v jiných jazycích, jako je C#, Java a Python, jsou tyto objekty zničeny v nedeterministických časech, v závislosti na popeláři, a během ničení může dojít ke vzkříšení objektu , což prodlužuje životnost.

V jazycích shromažďovaných odpadky jsou objekty obecně dynamicky přidělovány (na hromadě), i když jsou zpočátku vázány na automatickou proměnnou, na rozdíl od automatických proměnných s primitivními hodnotami, které jsou obvykle automaticky přidělovány (na zásobníku nebo v registru). To umožňuje vrátit objekt z funkce („uniknout“), aniž by byl zničen. V některých případech je však možná optimalizace kompilátoru , konkrétně provádění únikové analýzy a prokázání, že únik není možný, a proto může být objekt přidělen do zásobníku; to je v Javě významné. V tomto případě dojde ke zničení objektu okamžitě - případně i během životnosti proměnné (před koncem jejího rozsahu), pokud je nedosažitelná.

Složitým případem je použití objektu , kde objekty mohou být vytvořeny předem nebo opakovaně použity, a tedy zjevné vytváření a ničení nemusí odpovídat skutečnému vytvoření a zničení objektu, pouze (opětovné) inicializace pro vytvoření a dokončení pro zničení. V tomto případě může být stvoření i zničení nedeterministické.

Kroky

Vytváření objektů lze rozdělit na dvě operace: alokaci paměti a inicializaci , kde inicializace zahrnuje přiřazení hodnot k objektovým polím a případně spuštění libovolného jiného kódu. Jedná se o koncepty na úrovni implementace, zhruba analogické rozdílu mezi deklarací a definicí proměnné, i když později se jedná o rozdíly na jazykové úrovni. U objektu, který je svázán s proměnnou, může být deklarace kompilována do alokace paměti (rezervování prostoru pro objekt) a definice do inicializace (přiřazování hodnot), ale deklarace mohou být také pouze pro použití kompilátoru (například rozlišení názvu ), neodpovídá přímo kompilovanému kódu.

Analogicky lze destrukci objektu rozdělit na dvě operace, v opačném pořadí: finalizace a uvolnění paměti . Ty nemají analogické koncepty jazykových úrovní pro proměnné: životnost proměnné implicitně končí (u automatických proměnných, při odvíjení zásobníku; u statických proměnných při ukončení programu) a v tuto chvíli (nebo později, v závislosti na implementaci) je paměť uvolněna, ale obecně se neprovádí žádná finalizace. Když je však životnost objektu svázána s životností proměnné, konec životnosti proměnné způsobí finalizaci objektu; toto je standardní paradigma v C ++.

Dohromady tyto výnosy čtyři kroky na úrovni implementace:

alokace, inicializace, finalizace, deallokace

Tyto kroky mohou být prováděny automaticky jazykovým modulem runtime, tlumočníkem nebo virtuálním strojem, nebo je může programátor specifikovat ručně v podprogramu , konkrétně prostřednictvím metod - frekvence těchto kroků se mezi kroky a jazyky výrazně liší. Inicializace je velmi často programátorem specifikovaná v jazycích založených na třídách , zatímco v přísných jazycích založených na prototypech se inicializace automaticky provádí kopírováním. Finalizace je také velmi běžná v jazycích s deterministickou destrukcí, zejména v C ++, ale mnohem méně běžná v jazycích sbíraných odpadky. Přidělení se zadává zřídka a deallokaci obecně nelze zadat.

Stav během vytváření a ničení

Důležitou jemností je stav objektu během vytváření nebo ničení a řešení případů, kdy dojde k chybě nebo dojde k výjimce, například pokud se vytvoření nebo zničení nezdaří. Přesně řečeno, životnost objektu začíná, když je alokace dokončena, a končí, když začne deallokace. Během inicializace a finalizace je tedy objekt živý, ale nemusí být v konzistentním stavu - zajištění invariantů třídy je klíčovou součástí inicializace - a období od dokončení inicializace do zahájení finalizace je, když je objekt naživu a očekává se, že bude být v konzistentním stavu.

Pokud vytvoření nebo zničení selže, může být hlášení chyb (často vyvoláním výjimky) komplikované: objekt nebo související objekty mohou být v nekonzistentním stavu a v případě zničení - což se obecně děje implicitně, a tedy v blíže neurčeném prostředí - zpracování chyb může být obtížné. Opačným problémem - příchozích výjimek, nikoli odchozích výjimek - je, zda by se vytvoření nebo zničení měly chovat odlišně, pokud k nim dojde během zpracování výjimek, kdy může být žádoucí jiné chování.

Další jemností je, když dochází k vytváření a ničení statických proměnných , jejichž životnost se shoduje s dobou běhu programu - dochází k vytváření a ničení během pravidelného provádění programu nebo ve speciálních fázích před a po pravidelném provádění - a jak jsou objekty v programu zničeny ukončení, kdy program nemusí být v obvyklém nebo konzistentním stavu. To je problém zejména pro jazyky shromažďované odpadky, protože při ukončení programu mohou mít spoustu odpadků.

Programování na základě třídy

Při programování třídní, vytvoření objektu je také známý jako instance (vytvoření instance o třídy ), a vytvoření a ničení je možné ovládat pomocí známých metod, jako konstruktoru a destruktoru , nebo inicializátoru a finalizer . Tvorba a destrukce jsou tedy také známé jako konstrukce a destrukce, a když se těmto metodám říká, objekt se říká, že je konstruován nebo zničen (nikoli „zničen“) - respektive inicializován nebo dokončen, když jsou tyto metody volány.

Vztah mezi těmito metodami může být komplikovaný a jazyk může mít jak konstruktory, tak inicializátory (jako Python), nebo oba destruktory a finalizátory (jako C ++/CLI ), nebo výrazy „destruktor“ a „finalizátor“ mohou odkazovat na úroveň konstrukce versus implementace (jako v C# versus CLI).

Klíčovým rozdílem je, že konstruktory jsou třídní metody, protože dokud není objekt vytvořen, není k dispozici žádný objekt (instance třídy), ale ostatní metody (destruktory, inicializátory a finalizátory) jsou instančními metodami, jak byl objekt vytvořen. Dále mohou konstruktory a inicializátory přijímat argumenty, zatímco destruktory a finalizátory obecně nikoli, protože se jim obvykle říká implicitně.

V běžném používání je konstruktor metodou přímo volanou explicitně uživatelským kódem k vytvoření objektu, zatímco „destruktor“ je podprogram nazývaný (obvykle implicitně, ale někdy výslovně) pro ničení objektů v jazycích s deterministickými životnostmi objektů - archetyp je C ++ -a „finalizer“ je podprogram, který implicitně nazývá sběrač odpadků při ničení objektů v jazycích s nedeterministickou životností objektu-archetyp je Java.

Kroky během finalizace se výrazně liší v závislosti na správě paměti: při manuální správě paměti (jako v C ++ nebo při ručním počítání referencí) musí být odkazy výslovně zničeny programátorem (odkazy vymazány, počty odkazů sníženy); v automatickém počítání referencí se to také děje během finalizace, ale je automatizováno (jako v Pythonu, když k tomu dojde po vyvolání finalizátorů specifikovaných programátorem); a při sledování odvozu odpadků to není nutné. V automatickém počítání referencí jsou tedy finalizátory určené programátorem často krátké nebo chybí, ale stále je možné udělat značnou práci, zatímco při trasování popelářů je finalizace často zbytečná.

Správa zdrojů

V jazycích, kde mají objekty deterministickou životnost, lze pro správu zdrojů použít životnost objektu : toto se nazývá idiom Resource Acquisition Is Initialization (RAII): prostředky jsou získávány během inicializace a uvolňovány během finalizace. V jazycích, kde mají objekty nedeterministický život, zejména kvůli uvolňování paměti, je správa paměti obecně udržována odděleně od správy jiných zdrojů.

Vytvoření objektu

V typickém případě je postup následující:

  • vypočítat velikost objektu - velikost je většinou stejná jako u třídy, ale může se lišit. Pokud předmětný předmět není odvozen ze třídy, ale z prototypu , velikost objektu je obvykle velikost vnitřní datové struktury (například hash), která obsahuje své sloty.
  • alokace - přidělení paměťového prostoru o velikosti objektu plus růst později, pokud je to možné předem vědět
  • metody vazby - to je obvykle buď ponecháno na třídě objektu, nebo je vyřešeno v době odeslání , ale přesto je možné, že některé modely objektů váží metody v době vytvoření.
  • volání inicializačního kódu (jmenovitě konstruktoru ) nadtřídy
  • volání inicializačního kódu třídy, která se vytváří

Tyto úkoly lze dokončit najednou, ale někdy zůstávají nedokončené a pořadí úkolů se může lišit a může způsobit několik zvláštních chování. Například u multidědičnosti , na kterou by měl být inicializační kód nazýván jako první, je obtížné odpovědět. Konstruktory nadtřídy by však měly být volány před konstruktory podtříd.

Je složitý problém vytvořit každý objekt jako prvek pole. Některé jazyky (např. C ++) to nechávají na programátorech.

Zpracování výjimek uprostřed vytváření objektu je obzvláště problematické, protože obvykle implementace výjimek vyvolávání závisí na platných stavech objektu. Například neexistuje způsob, jak přidělit nový prostor pro objekt výjimky, když alokace objektu předtím selhala kvůli nedostatku volného místa v paměti. Z tohoto důvodu by implementace jazyků OO měla poskytovat mechanismy umožňující vyvolávání výjimek, i když je nedostatek zdrojů, a programátoři nebo typový systém by měli zajistit, aby jejich kód byl bezpečný pro výjimky . Propagace výjimky pravděpodobně uvolní prostředky, než je přidělí. Ale v objektově orientovaném programování může konstrukce objektu selhat, protože konstrukce objektu by měla vytvořit invarianty třídy , které často nejsou platné pro každou kombinaci argumentů konstruktoru. Konstruktéři tedy mohou vyvolávat výjimky.

Abstraktní továrna je způsob, jak oddělit konkrétní realizaci objektu z kódu pro vytvoření tohoto objektu.

Metody tvorby

Způsob vytváření objektů se v různých jazycích liší. V některých jazycích založených na třídách je za ověření stavu objektu zodpovědná speciální metoda známá jako konstruktor . Stejně jako běžné metody lze konstruktory přetížit , aby bylo možné vytvořit objekt s různými zadanými atributy. Konstruktor je také jediným místem, kde lze nastavit stav neměnných objektů . Kopie konstruktor je konstruktor, který trvá (jediný) parametr stávajícího objektu stejného typu jako třídou konstruktéra, a vrací kopii objektu odeslané jako parametr.

Jiné programovací jazyky, jako například Objective-C , mají třídní metody, které mohou zahrnovat metody typu konstruktoru, ale nejsou omezeny pouze na vytváření instancí objektů.

C ++ a Java byly kritizovány za to, že neposkytují pojmenované konstruktory - konstruktor musí mít vždy stejný název jako třída. To může být problematické, pokud chce programátor poskytnout dva konstruktory se stejnými typy argumentů, např. Pro vytvoření bodového objektu buď z kartézských souřadnic, nebo z polárních souřadnic , přičemž oba by byly reprezentovány dvěma čísly s plovoucí desetinnou čárkou. Objective-C může tento problém obejít, že programátor může vytvořit třídu Point, s metodami inicializace, například + newPointWithX: Andy: a + newPointWithR: andTheta: . V C ++ lze něco podobného provést pomocí statických členských funkcí.

Konstruktor může také odkazovat na funkci, která se používá k vytvoření hodnoty tagovaného spojení , zejména ve funkčních jazycích.

Ničení objektů

Obecně platí, že po použití objektu je odstraněn z paměti, aby uvolnil místo jiným programům nebo objektům. Pokud je však dostatek paměti nebo program má krátkou dobu běhu, nemusí dojít ke zničení objektu, paměť se při ukončení procesu jednoduše uvolní. V některých případech ničení objektů jednoduše spočívá v uvolňování paměti, zejména v jazycích shromažďovaných odpadky, nebo pokud je „objekt“ ve skutečnosti prostá stará datová struktura . V jiných případech je některá práce provedena před opětovným přidělením, zejména zničením členských objektů (v manuální správě paměti) nebo odstraněním odkazů z objektu na jiné objekty za účelem snížení počtu referencí (v počítání referencí). Může to být automatické nebo může být na objekt vyvolána speciální metoda ničení.

V jazyky založenými na třídě s deterministické životnosti objektu, zejména C ++, je destruktor je metoda volána, když je odstraněn instance třídy, než je paměť uvolnit. V C ++ se destruktory liší od konstruktorů různými způsoby: nemohou být přetíženy, nesmí mít žádné argumenty, nemusí udržovat invarianty tříd a mohou způsobit ukončení programu, pokud vyvolávají výjimky.

V jazycích shromažďování odpadků mohou být objekty zničeny, když je již nelze dosáhnout spuštěným kódem. V jazycích GCed založených na třídě jsou analogem destruktorů finalizátory , které se nazývají před shromážděním objektu. Ty se liší spuštěním v nepředvídatelném čase a v nepředvídatelném pořadí, protože sběr odpadků je nepředvídatelný, a jsou výrazně méně používané a méně složité než destruktory C ++. Mezi příklady takových jazyků patří Java , Python a Ruby .

Zničení objektu způsobí, že všechny odkazy na objekt budou neplatné a v manuální správě paměti se všechny existující odkazy stanou visícími odkazy . Při shromažďování odpadků (jak shromažďování odpadků, tak počítání odkazů) jsou objekty zničeny pouze tehdy, když na ně neexistují žádné odkazy, ale finalizace může vytvořit nové odkazy na objekt a aby se zabránilo visícím odkazům, dochází ke vzkříšení objektu, takže odkazy zůstávají platné.

Příklady

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

Jáva

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

Cíl-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;
}

Objekt Pascal

Související jazyky: "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 *)

Krajta

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()

Zásuvka bude uzavřena v příštím kole shromažďování odpadků po spuštění a návratu funkce „f“, protože byly ztraceny všechny odkazy na ni.

Viz také

Poznámky

Reference