Objektets livslängd
I objektorienterad programmering (OOP) är objektets livslängd (eller livscykel ) för ett objekt tiden mellan ett objekts skapelse och dess förstörelse. Regler för objektlivslängd varierar avsevärt mellan språk , i vissa fall mellan implementeringar av ett givet språk, och livslängden för ett visst objekt kan variera från en körning av programmet till en annan.
I vissa fall sammanfaller objektets livslängd med variabel livslängd för en variabel med det objektet som värde (både för statiska variabler och automatiska variabler ), men i allmänhet är objektets livslängd inte bunden till livslängden för någon variabel. I många fall – och som standard i många objektorienterade språk, särskilt de som använder garbage collection (GC) – allokeras objekt på heapen och objektets livslängd bestäms inte av livslängden för en given variabel: värdet på en variabel att hålla ett objekt motsvarar faktiskt en referens till objektet, inte själva objektet, och förstörelse av variabeln förstör bara referensen, inte det underliggande objektet.
Översikt
Även om grundidén om objektlivslängd är enkel – ett objekt skapas, används och förstörs sedan – varierar detaljerna avsevärt mellan språk och inom implementeringar av ett givet språk, och är intimt knutet till hur minneshantering implementeras . Vidare görs många fina skillnader mellan stegen och mellan begrepp på språknivå och begrepp på implementeringsnivå. Terminologi är relativt standard, men vilka steg som motsvarar en given term varierar kraftigt mellan språken.
Termer kommer vanligtvis i antonympar, en för ett skapande koncept, en för motsvarande förstörelsekoncept, som initialisera/slutföra eller konstruktor/destruktor. Skapande/förstörelseparet är också känt som initiering/avslutning, bland andra termer. Termerna allokering och avallokering eller frigöring används också, i analogi med minneshantering, även om objektskapande och destruktion kan innebära betydligt mer än bara minnesallokering och -deallokering, och allokering/avallokering är mer korrekt övervägda steg i skapandet respektive förstörelsen.
Determinism
En stor skillnad är om ett objekts livstid är deterministisk eller icke-deterministisk. Detta varierar beroende på språk, och inom språket varierar med minnesallokeringen av ett objekt; objektets livslängd kan skilja sig från variabel livslängd.
Objekt med statisk minnesallokering , särskilt objekt lagrade i statiska variabler , och klassmoduler ( om klasser eller moduler själva är objekt, och statiskt allokerade), har en subtil icke-determinism på många språk: medan deras livslängd verkar sammanfalla med körtiden i programmet är ordningen för skapande och förstörelse – vilket statiskt objekt som skapas först, vilket andra, etc. – i allmänhet icke-deterministisk.
För objekt med automatisk minnesallokering eller dynamisk minnesallokering sker objektskapande i allmänhet deterministiskt, antingen explicit när ett objekt explicit skapas (som via new
i C++ eller Java), eller implicit i början av variabel livslängd, särskilt när omfattningen av en automatisk variabel matas in, t.ex. vid deklaration. Objektförstöring varierar dock – i vissa språk, särskilt C++, förstörs automatiska och dynamiska objekt vid deterministiska tidpunkter, såsom räckviddsavslut, explicit förstörelse (via manuell minneshantering) eller referensräkning som når noll; medan på andra språk, såsom C#, Java och Python, förstörs dessa objekt vid icke-deterministiska tidpunkter, beroende på sopsamlaren, och objekt kan återuppstå under förstörelsen, vilket förlänger livslängden.
I sopsamlade språk tilldelas objekt i allmänhet dynamiskt (på högen) även om de initialt är bundna till en automatisk variabel, till skillnad från automatiska variabler med primitiva värden, som vanligtvis allokeras automatiskt (på stacken eller i ett register). Detta gör att objektet kan returneras från en funktion ("escape") utan att förstöras. Men i vissa fall är en kompilatoroptimering möjlig, nämligen att utföra escape-analys och bevisa att escape inte är möjligt, och därmed kan objektet allokeras på stacken; detta är viktigt i Java. I det här fallet kommer objektdestruktion att ske omedelbart – möjligen till och med under variabelns livstid (innan dess räckvidd slutar), om den inte går att nå.
Ett komplext fall är användningen av en objektpool , där objekt kan skapas i förväg eller återanvändas, och därmed skenbar skapelse och förstörelse kanske inte motsvarar faktisk skapande och förstörelse av ett objekt, endast (om)initiering för skapande och slutförande för förstörelse. I det här fallet kan både skapande och förstörelse vara icke-deterministiska.
Steg
Objektskapande kan delas upp i två operationer: minnesallokering och initiering , där initiering båda inkluderar att tilldela värden till objektfält och eventuellt köra godtycklig annan kod. Dessa är begrepp på implementeringsnivå, ungefär analogt med distinktionen mellan deklaration och definition av en variabel, även om dessa senare är distinktioner på språknivå. För ett objekt som är knutet till en variabel kan deklarationen kompileras till minnesallokering (reserverar utrymme för objektet) och definition till initiering (tilldelning av värden), men deklarationer kan också vara för kompilatoranvändning (såsom namnupplösning ) , inte direkt motsvarar den kompilerade koden.
Analogt kan objektdestruktion delas upp i två operationer, i motsatt ordning: slutförande och minnesdeallokering . Dessa har inte analoga koncept på språknivå för variabler: variabel livslängd slutar implicit (för automatiska variabler, vid avveckling av stack; för statiska variabler, vid programavslutning), och vid denna tidpunkt (eller senare, beroende på implementering) avallokeras minne, men ingen slutbehandling görs i allmänhet. Men när ett objekts livslängd är knuten till en variabels livslängd, orsakar slutet av variabelns livslängd slutförandet av objektet; detta är ett standardparadigm i C++.
Tillsammans ger dessa fyra steg på implementeringsnivå:
- allokering, initiering, slutförande, deallokering
Dessa steg kan göras automatiskt av språkkörningstiden, tolken eller den virtuella maskinen, eller kan specificeras manuellt av programmeraren i en subrutin , konkret via metoder – frekvensen av detta varierar avsevärt mellan steg och språk. Initiering är mycket vanligt programmeringsspecificerad i klassbaserade språk , medan i strikt prototypbaserade språk initiering görs automatiskt genom kopiering. Slutförande är också mycket vanligt i språk med deterministisk förstörelse, särskilt C++, men mycket mindre vanligt i sopsamlade språk. Tilldelning specificeras mer sällan, och omallokering kan i allmänhet inte specificeras.
Status under skapande och förstörelse
En viktig subtilitet är statusen för ett objekt under skapandet eller förstörelsen, och hantering av fall där fel uppstår eller undantag görs, till exempel om skapandet eller förstörelsen misslyckas. Strängt taget börjar ett objekts livstid när allokeringen slutförs och slutar när deallokeringen startar. Alltså under initiering och slutförande är ett objekt levande, men kanske inte är i ett konsekvent tillstånd – vilket säkerställer att klassinvarianter är en nyckeldel av initiering – och perioden från det att initieringen slutförs till när slutförandet startar är när objektet både är levande och förväntas vara i ett konsekvent tillstånd.
Om skapandet eller förstörelsen misslyckas kan felrapportering (ofta genom att göra ett undantag) kompliceras: objektet eller relaterade objekt kan vara i ett inkonsekvent tillstånd, och i fallet med förstörelse – vilket vanligtvis sker implicit, och därmed i en ospecificerad miljö – det kan vara svårt att hantera fel. Den motsatta frågan – inkommande undantag, inte utgående undantag – är om skapande eller förstörelse ska bete sig annorlunda om de inträffar under undantagshantering, när annat beteende kan önskas.
En annan subtilitet är när skapande och förstörelse sker för statiska variabler , vars livslängd sammanfaller med programmets körtid – sker skapande och förstörelse under vanlig programkörning, eller i speciella faser före och efter vanlig körning – och hur objekt förstörs vid program uppsägning, när programmet kanske inte är i ett vanligt eller konsekvent tillstånd. Detta är särskilt ett problem för språk som samlas in som samlas in, eftersom de kan ha mycket skräp när programmet avslutas.
Klassbaserad programmering
I klassbaserad programmering är objektskapande också känt som instansiering (att skapa en instans av en klass ), och destruktor skapande och förstörelse kan styras via metoder som kallas en konstruktor och eller en initialiserare och slutförare . Skapande och förstörelse är alltså också känt som konstruktion och förstörelse, och när dessa metoder kallas ett objekt sägs det vara konstruerat eller förstört (inte "förstört") – respektive initierat eller slutfört när dessa metoder anropas.
Förhållandet mellan dessa metoder kan vara komplicerat, och ett språk kan ha både konstruktörer och initialiserare (som Python), eller både destruktörer och slutbehandlare (som C++/CLI ), eller så kan termerna "destructor" och "finalizer" syfta på språk- nivåkonstruktion kontra implementering (som i C# kontra CLI).
En nyckelskillnad är att konstruktörer är klassmetoder, eftersom det inte finns något objekt (klassinstans) tillgängligt förrän objektet har skapats, men de andra metoderna (destruktörer, initialiserare och slutbehandlare) är instansmetoder, eftersom ett objekt har skapats. Vidare kan konstruktörer och initialiserare ta argument, medan destruktörer och slutförare i allmänhet inte gör det, som de vanligtvis kallas implicit.
I vanlig användning är en konstruktor en metod som direkt anropas explicit av användarkod för att skapa ett objekt, medan "destructor" är subrutinen som kallas (vanligtvis implicit, men ibland explicit) på objektförstöring i språk med deterministiska objektlivslängder – arketypen är C++ – och "finalizer" är den subrutin som sopsamlaren implicit anropar för att förstöra objekt på språk med icke-deterministisk objektlivslängd – arketypen är Java.
Stegen under slutförandet varierar avsevärt beroende på minneshantering: i manuell minneshantering (som i C++, eller manuell referensräkning) måste referenser explicit förstöras av programmeraren (referenser rensas, referensantal minskade); i automatisk referensräkning sker detta även under slutförandet, men det är automatiserat (som i Python, när det inträffar efter att programmerarspecificerade finalizers har anropats); och för att spåra sophämtning är detta inte nödvändigt. Sålunda vid automatisk referensräkning är programmerarspecificerade slutbearbetare ofta korta eller frånvarande, men betydande arbete kan fortfarande göras, medan slutförandet ofta är onödigt när det gäller att spåra sophämtare.
Resurshantering
På språk där objekt har en deterministisk livslängd, kan objektlivslängden användas för att hantera resurshantering : detta kallas Resource Acquisition Is Initialization (RAII) formspråk: resurser förvärvas under initialisering och frigörs under slutförande. På språk där objekt har en icke-deterministisk livslängd, särskilt på grund av sophämtning, hålls hanteringen av minne i allmänhet åtskild från hanteringen av andra resurser.
Objektskapande
I typiska fall är processen som följer:
- beräkna storleken på ett objekt – storleken är för det mesta densamma som klassens men kan variera. När objektet i fråga inte härrör från en klass, utan från en prototyp istället, är storleken på ett objekt vanligtvis storleken på den interna datastrukturen (t.ex. en hash) som håller dess platser.
- allokering – allokera minnesutrymme med storleken på ett objekt plus tillväxten senare, om möjligt att veta i förväg
- bindningsmetoder – detta lämnas vanligtvis antingen till objektets klass eller löses vid avsändning, men det är ändå möjligt att vissa objektmodeller binder metoder vid skapandet.
- anropar en initialiseringskod (nämligen constructor ) av superklass
- anropa en initialiseringskod för klass som skapas
Dessa uppgifter kan slutföras på en gång men lämnas ibland oavslutade och ordningen på uppgifterna kan variera och kan orsaka flera konstiga beteenden. Till exempel, i multi-heritance , vilken initieringskod som ska anropas först är en svår fråga att besvara. Superklasskonstruktörer bör dock anropas före underklasskonstruktörer.
Det är ett komplext problem att skapa varje objekt som ett element i en array. [ ytterligare förklaring behövs ] Vissa språk (t.ex. C++) överlåter detta till programmerare.
Att hantera undantag mitt under skapandet av ett objekt är särskilt problematiskt eftersom implementeringen av att kasta undantag vanligtvis bygger på giltiga objekttillstånd. Det finns till exempel inget sätt att tilldela ett nytt utrymme för ett undantagsobjekt när allokeringen av ett objekt misslyckades innan dess på grund av brist på ledigt utrymme i minnet. På grund av detta bör implementeringar av OO-språk tillhandahålla mekanismer för att göra det möjligt att ta fram undantag även när det är brist på resurser, och programmerare eller typsystemet bör se till att deras kod är undantagssäker . Att sprida ett undantag är mer sannolikt att frigöra resurser än att tilldela dem. Men i objektorienterad programmering kan objektkonstruktion misslyckas, eftersom konstruktionen av ett objekt bör etablera klassen invarianter , som ofta inte är giltiga för varje kombination av konstruktorargument. Således kan konstruktörer ta upp undantag.
Det abstrakta fabriksmönstret är ett sätt att frikoppla en viss implementering av ett objekt från kod för att skapa ett sådant objekt.
Skapande metoder
Sättet att skapa objekt varierar mellan olika språk. I vissa klassbaserade språk är en speciell metod känd som en konstruktor ansvarig för att validera ett objekts tillstånd. Precis som vanliga metoder kan konstruktörer överbelastas för att göra det så att ett objekt kan skapas med olika specificerade attribut. Dessutom är konstruktorn det enda stället att ställa in tillståndet för [ Fel förtydligande behövs] oföränderliga objekt . En kopiakonstruktor är en konstruktor som tar en (enskild) parameter av ett existerande objekt av samma typ som konstruktörens klass, och returnerar en kopia av objektet som skickas som en parameter.
Andra programmeringsspråk, som Objective-C , har klassmetoder, som kan inkludera metoder av konstruktortyp, men är inte begränsade till att bara instansiera objekt.
C++ och Java har kritiserats [ av vem? ] för att inte tillhandahålla namngivna konstruktörer – en konstruktor måste alltid ha samma namn som klassen. Detta kan vara problematiskt om programmeraren vill förse två konstruktörer med samma argumenttyper, t.ex. för att skapa ett punktobjekt antingen från de kartesiska koordinaterna eller från de polära koordinaterna , som båda skulle representeras av två flyttal. Objective-C kan kringgå detta problem genom att programmeraren kan skapa en Point-klass, med initialiseringsmetoder, till exempel +newPointWithX:andY:
och +newPointWithR:andTheta:
. I C++ kan något liknande göras med statiska medlemsfunktioner.
En konstruktor kan också hänvisa till en funktion som används för att skapa ett värde för en taggad union , särskilt i funktionella språk.
Objekt förstörelse
Det är i allmänhet så att efter att ett objekt har använts tas det bort från minnet för att göra plats för andra program eller objekt att ta objektets plats. Men om det finns tillräckligt med minne eller om ett program har en kort körtid kan det hända att objekt inte förstörs, utan att minnet helt enkelt avallokeras när processen avslutas. I vissa fall består objektdestruktion helt enkelt av att deallokera minnet, särskilt i skräpinsamlade språk, eller om "objektet" faktiskt är en vanlig gammal datastruktur . I andra fall utförs en del arbete före avallokering, särskilt att förstöra medlemsobjekt (i manuell minneshantering), eller ta bort referenser från objektet till andra objekt för att minska referensräkningen (vid referensräkning). Detta kan vara automatiskt, eller så kan en speciell destruktionsmetod anropas på objektet.
I klassbaserade språk med deterministisk objektlivslängd, särskilt C++, är en destruktor en metod som kallas när en instans av en klass raderas, innan minnet avallokeras. I C++ skiljer sig destruktörer från konstruktörer på olika sätt: de kan inte överbelastas, får inte ha några argument, behöver inte underhålla klassinvarianter och kan orsaka programavslutning om de ger undantag.
På sopsamlingsspråk kan föremål förstöras när de inte längre kan nås av den löpande koden. I klassbaserade GCed-språk är analogen av destruktörer finalizers , som anropas innan ett objekt skräpsamlas. Dessa skiljer sig i att köra vid en oförutsägbar tidpunkt och i en oförutsägbar ordning, eftersom sophämtning är oförutsägbar och är betydligt mindre använda och mindre komplexa än C++-destruktörer. Exempel på sådana språk inkluderar Java , Python och Ruby .
Att förstöra ett objekt gör att alla referenser till objektet blir ogiltiga, och i manuell minneshantering blir alla befintliga referenser hängande referenser . I sophämtning (både spårning av sophämtning och referensräkning) förstörs objekt endast när det inte finns några referenser till dem, men slutförandet kan skapa nya referenser till objektet, och för att förhindra dinglande referenser inträffar objektets återuppståndelse så att referenserna förblir giltiga .
Exempel
C++
class Foo { public : // Dessa är konstruktörernas prototypdeklarationer. Foo ( int x ); Foo ( int x , int y ); // Överbelastad konstruktör. Foo ( konst Foo & gammal ); // Copy Constructor. ~ Foo (); // Destruktör. }; Foo :: Foo ( int x ) { // Detta är implementeringen av // enargumentkonstruktorn. } Foo :: Foo ( int x , int y ) { // Detta är implementeringen av // två-argumentkonstruktorn. } Foo :: Foo ( const Foo & old ) { // Detta är implementeringen av // kopieringskonstruktorn. } Foo ::~ Foo () { // Detta är implementeringen av förstöraren. } int main () { Foo foo ( 14 ); // Ring första konstruktören. Foo foo2 ( 12 , 16 ); // Anrop överbelastad konstruktör. Foo foo3 ( foo ); // Anropa kopiekonstruktorn. // Destructors anropade i baklänges-ordning // här, automatiskt. }
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 ) { // Detta är implementeringen av // copy constructor } public static void main ( String [ ] args ) { Foo foo = new Foo ( 14 ); // ring första konstruktören Foo foo2 = new Foo ( 12 , 16 ); // anrop överbelastad konstruktör Foo foo3 = ny Foo ( foo ); // ring kopia konstruktorn // sophämtning sker under täcket och objekt förstörs } }
C#
namnområde ObjectLifeTime ; class Foo { public Foo () { // Detta är implementeringen av // standardkonstruktor. } public Foo ( int x ) { // Detta är implementeringen av // enargumentkonstruktorn. } ~ Foo () { // Detta är implementeringen av // förstöraren. } public Foo ( int x , int y ) { // Detta är implementeringen av // två-argumentkonstruktorn. } public Foo ( Foo old ) { // Detta är implementeringen av // kopieringskonstruktorn. } public static void Main ( sträng [] args ) { var defaultfoo = new Foo (); // Anrop standardkonstruktorn var foo = new Foo ( 14 ); // Call first constructor var foo2 = new Foo ( 12 , 16 ); // Anrop överbelastad konstruktor var foo3 = new Foo ( foo ); // Anropa kopia konstruktorn } }
Mål-C
0
#import <objc/Object.h> @interface Punkt : Objekt { double x ; dubbelt y ; } //Dessa är klassmetoderna; vi har deklarerat två konstruktorer + ( Point * ) newWithX: ( double ) andY : ( double ); + ( Punkt * ) newWithR: ( double ) andTheta : ( double ); //Instansmetoder - ( Point * ) setFirstCoord: ( double ); - ( Point * ) setSecondCoord: ( double ); /* Eftersom Point är en underklass till den generiska Object *-klassen, får vi redan generiska allokerings- och initialiseringsmetoder *, +alloc och -init. För våra specifika konstruktörer * kan vi göra dessa från dessa metoder vi har * ärvt. */ @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 { //Koncis skriven klassmetod för att automatiskt allokera och //utföra specifik initiering. returnera [[[ Point alloc ] setFirstCoord : x_val ] setSecondCoord : y_val ]; } + ( Point * ) newWithR: ( double ) r_val andTheta: ( double ) theta_val { //Istället för att utföra samma sak som ovan, kan vi underhands //använda samma resultat av den tidigare metoden returnera [ Point newWithX : r_val andY : theta_val ]; } @end int main ( void ) { //Konstruerar två punkter, p och q. Point * p = [ Point newWithX : 4.0 andY : 5.0 ]; Point * q = [ Point newWithR : 1.0 and Theta : 2.28 ]; //...programtext... //Vi är klara med p, säg, så, frigör det. //Om p allokerar mer minne åt sig själv, kan behöva //åsidosätta objektets fria metod för att rekursivt //frigöra p:s minne. Men så är inte fallet, så vi kan bara [ p free ]; //...mer text... [ q gratis ]; återvända ; }
Objekt Pascal
Relaterade språk: "Delphi", "Free Pascal", "Mac Pascal".
program Exempel ; typ DimensionEnum = ( deUnassigned , de2D , de3D , de4D ) ; PointClass = klass privat Dimension : DimensionEnum ; public X : Heltal ; Y : Heltal ; Z : heltal ; T : Heltal ; public (* prototyp av konstruktörer *) konstruktor Skapa () ; konstruktor Skapa ( AX , AY : Heltal ) ; konstruktor Skapa ( AX , AY , AZ : heltal ) ; konstruktor Skapa ( AX , AY , AZ , ATime : Integer ) ; konstruktör CreateCopy ( APoint : PointClass ) ; (* prototyp av förstörare *) destructor Destroy ; slut ; konstruktör PointClass . Skapa () ; börja // implementering av en generisk, icke-argumentkonstruktor Self . Dimension := deUnassigned ; slut ; konstruktör PointClass . Skapa ( AX , AY : heltal ) ; börja // implementering av en, 2 argumentkonstruktor Self . X := AX ; Y := AY ; Själv . Dimension := de2D ; slut ; konstruktör PointClass . Skapa ( AX , AY , AZ : heltal ) ; börja // implementering av en, 3 argument constructor Self . X := AX ; Y := AY ; Själv . X := AZ ; Själv . Dimension := de3D ; slut ; konstruktör PointClass . Skapa ( AX , AY , AZ , ATime : Integer ) ; börja // implementering av en, 4 argument constructor Self . X := AX ; Y := AY ; Själv . X := AZ ; T := ATime ; Själv . Dimension := de4D ; slut ; konstruktör PointClass . CreateCopy ( APoint : PointClass ) ; börja // implementering av en "kopiera" konstruktor APoint . X := AX ; APoint . Y := AY ; APoint . X := AZ ; APoint . T := ATime ; Själv . Dimension := de4D ; slut ; förstörare PointClass . PointClass . Förstöra ; börja // implementering av ett generiskt, icke-argumentförstörare Self . Dimension := deUnAssigned ; slut ; var (* variabel för statisk allokering *) S : PointClass ; (* variabel för dynamisk allokering *) D : ^ PointClass ; börja (* av programmet *) (* objekt livlina med statisk allokering *) S . Skapa ( 5 , 7 ) ; (* gör något med "S" *) S . Förstöra ; (* objektlivlina med dynamisk allokering *) D = ny PointClass , Skapa ( 5 , 7 ) ; (* gör något med "D" *) dispose D , Destroy ; slut . (* av programmet *)
Pytonorm
klass Socket : def __init__ ( self , remote_host : str ) -> Ingen : # anslut till fjärrvärd def send ( self ) : # Skicka data def recv ( self ) : # Receive data def close ( self ) : # stäng uttaget def __del__ ( self ): # __del__ magisk funktion anropas när objektets referensantal är lika med noll själv . close () def f (): socket = Socket ( "example.com" ) socket . skicka ( "testa" ) returnera uttag . recv ()
Socket kommer att stängas vid nästa sophämtningsomgång efter att "f"-funktionen körs och återkommer, eftersom alla referenser till den har gått förlorade.
Se även
- Resource Acquisition Is Initialization (RAII), ett tillvägagångssätt för att hantera resurser genom att knyta dem till objektets livslängd