Kirjautuminen

Haku

Tehtävät

Keskustelu: Ohjelmointikysymykset: Pascal: Delphin itsetuhoiset oliot

Sivun loppuun

Janezki [08.09.2008 19:01:20]

#

Noh, ei rakko laiskan kädessä komeile. Tällä kertaa eteen tuli olioiden itsetuho.

Yritän tehdä TKE:hen luokkia jotka voivat vapauttaa omat instanssinsa. Homma on hieman monimutkainen kun free-komentoa pitää kutsua olion oman metodin sisältä. Komento näyttää ensin onnistuvan mutta sitten alkaa tulemaan outoja poikkeuksia satunnaisissa kohteissa. Luokat osaavat kyllä vapauttaa instanssien osoitteisiin viittaavat pointterit. Selvennetään hieman...

Teen animoitua spriteä joka vapauttaa itsensä kun animaatio on loppu

procedure TAniSplatSprite.Processor();
begin
 ...
 Self.Free;
 ...
end;

TAniSplatSprite perii suoraan yläluokkiensa tuhoajat...

Destructor TVisualMapEntity.Free; //Ensimmäinen tuhoaja
begin
 DeleteSceneProgressCall(Processor); //Vapauta Processor-metodi synkkauslistasta
 FreeAndNil(SpritePlane); //Tuhoaa objektin SpritePlane ja asettaa pointterin nilliksi
 Inherited; //Perii TMapEntityn tuhoajan...
end;

Destructor TMapEntity.Free;
var
 i : Integer;
begin
 FreeAndNil(FDummy); //Tuhoaa objektin FDummy ja asettaa pointterin nilliksi
 //Tämä etsii entiteetin pointterin ja asettaa sen nilliksi
 for i := 0 to High(EntInstances) do
  if Assigned(EntInstances[i]) and (EntInstances[i] = Self) then
   begin
    EntInstances[i] := Nil;
    Break;
   end;
end;

Eli koodista näkee että pointterit tuhoutuvat, ja sen olenkin jo testannut perinpohjin. Mutta onko tuo Free-kutsu luokan oman metodin sisältä sallittua?

User137 [08.09.2008 20:33:50]

#

Tavallisesti destructori tehdään Destroy metodille, ei Free. Tuo Free kutsuu kyllä Destroy mutta se tekee jotain muutakin. Toiseksi tuon Destroy määrittelyyn kuuluu sitten laittaa override;

No tuolla selitetty hienommin:
http://www.delphibasics.co.uk/RTL.asp?Name­=Destructor

Janezki [08.09.2008 20:40:07]

#

User137 kirjoitti:

Tavallisesti destructori tehdään Destroy metodille, ei Free. Tuo Free kutsuu kyllä Destroy mutta se tekee jotain muutakin. Toiseksi tuon Destroy määrittelyyn kuuluu sitten laittaa override;

No tuolla selitetty hienommin:
http://www.delphibasics.co.uk/RTL.asp?Name­=Destructor

Eikö Free vain tarkista onko objekti edelleen elossa ja sitten vasta kutsuu destroytä? Tyyliin

if Assigned(Self) then
 Destroy;

Niin no voisihan sitä kokeilla tehdä noiden destoy-metodien varaan, en vain usko että tekisi eroa. Voihan niitä edelleen sitten tuhota freellä.

Miksi muuten tuossa linkin esimerkissä constructorilla ei ole yliajoa? Kai TObject-luokalla on oma constructor.

Metabolix [08.09.2008 23:36:51]

#

Janezki kirjoitti:

Miksi muuten tuossa linkin esimerkissä constructorilla ei ole yliajoa? Kai TObject-luokalla on oma constructor.

Override kertoo, että annettu metodi ylikirjoittaa aiemmassa toteutuksessa olleen virtuaalisen metodin. Create ei ole virtuaalinen.

Järjen mukaan sanoisin, että oikein toteutettuna tuon pitäisi toimia, mutta omatkin testit sanovat nyt muuta. En voi ilman Delphiä mennä takuuseen Delphin toiminnasta, mutta ainakin FreePascal yhteensopivuustilassaan toimii hyvin omituisesti. Tulostan testiohjelmassani osoitteen @Self eri tilanteissa (constructor, destructor, procedure):

X := TX.Create: @Self = $...14
X.Proc:         @Self = $...18
  Self.Destroy: @Self = $...04
X.Destroy:      @Self = $...14

Destroy saa siis aivan eri @Self-osoitteen sen mukaan, kutsutaanko sitä ulkoa päin vai sisäisesti suoraan. (Self.)Destroy ei tuhoa objektia vaan ajaa vain koodin normaalina proseduurina. Kuitenkin jos sijoittaa Apu := Self ja kutsuu Apu.Destroy, tuleekin Runtime error. Selvästikään tämä merkillinen suojamekanismi ei siis silloin päde.

Vaikka tuon jollain konstilla saisikin toimimaan, objektin tuhoaminen itsensä sisältä on jo enemmän kuin vähän arveluttavaa. Joissakin kielissä se on mielekästä ja selvästi mahdollista, toisissa se taas on jo käytännön syistä mahdotonta. Delphi saattaa kuulua niihin välineisiin, jotka ovat estäneet tällaisen ihan ilkeyttään. :)

Kaiken kaikkiaan olisin taipuvainen suosittelemaan jonkinlaista roskakoria tuhoamista kaipaaville objekteille. Roskat voisi sitten hoidella keskitetysti jossakin turvallisessa vaiheessa. Jopa tällainen outo ratkaisu voisi tulla kyseeseen:

program LazyKill;

type
    TMapEntity = class
        constructor Create;
        destructor Destroy; override;
        procedure LazyKill; virtual; { Virtuaalisuus ehdoton! }
        Num: Integer;
    end;
    TSomeObject = class (TMapEntity)
        constructor Create;
        destructor Destroy; override;
        procedure LazyKill; override; { Override virtualin parina. }
    end;

{ Seuraa funktio, joka säilyttää viimeisimmän tapettavaksi määrätyn olion ja tappaa sen vasta, kun seuraava käsky tulee. Näin siis olio voi ilmoittautua itse tapettavaksi, mutta ongelmia ei tule, kunhan olion koodista poistutaan ennen seuraavaa tappokäskyä. Pitää siis varoa tällaisia toistotilanteita:
A.Funktio
  B.Funktio
    A.Toinen
      A.Tapa  // A jonoon
  B.Tapa  // A tuhoutuu
Runtime error, A tuhottu jo!
Toisin sanoen objektihierarkian on hyvä olla mahdollisimman puumainen. }

var
    PrevVictim: TMapEntity; { Säilytetään viimeisin tappokäsky }

procedure LazyKillMapEntity(NextVictim: TMapEntity);
begin
    Write('LazyKill: finishing ');
    if PrevVictim <> nil then Write(PrevVictim.Num) else Write('-');
    Write(', preparing ');
    if NextVictim <> nil then WriteLn(NextVictim.Num) else WriteLn('-');

    { Tuhotaan vanha ja otetaan uusi ylös }
    if PrevVictim <> nil then PrevVictim.Destroy;
    PrevVictim := NextVictim;
end;

{ Pienet esimerkkiluokat. Huomaa Inherited: Createssa alkuun, Destroyssa ja muissa vastaavissa loppuun. Yleensä näin on turvallisinta, ja poikkeustapauksissa kannattaa tietää, miksi tekee eri tavalla ja onko se todella tarpeen. Muissa kuin luonti- ja tuhoamisfunktioissa sijoittelu on vapaampaa, ja toki näissäkin, jos on selvillä tekemisistään. :) }

var
    MapEntityCount: Integer = 0;

constructor TMapEntity.Create;
begin
    Inherited;
    { Keksitään objektille tunniste, jotta debug-tulostus on mukavampaa. }
    Inc(MapEntityCount);
    Num := MapEntityCount;
    WriteLn('TMapEntity.Create     ', Num);
end;

destructor TMapEntity.Destroy;
begin
    WriteLn('TMapEntity.Destroy    ', Num);
    Inherited;
end;

procedure TMapEntity.LazyKill;
begin
    WriteLn('TMapEntity.LazyKill   ', Num);
    { Tähän väliin oma vapautuskoodi }
    { Lopuksi mennään tappojonoon }
    LazyKillMapEntity(Self);
    Inherited;
end;

constructor TSomeObject.Create;
begin
    Inherited;
    WriteLn('TSomeObject.Create    ', Num);
end;

destructor TSomeObject.Destroy;
begin
    WriteLn('TSomeObject.Destroy   ', Num);
    Inherited;
end;

procedure TSomeObject.LazyKill;
begin
    WriteLn('TSomeObject.LazyKill  ', Num);
    { Tähän väliin oman vapautuskoodin ne osat, joita TMapEntity ei hoida }
    Inherited;
end;

{ Esimerkkiohjelma. Samalla havaitaan, kuinka virtual mahdollistaa TSomeObject-olion sijoittamisen TMapEntity-tyyppiin toimivasti. }

var
    MapEntity: TMapEntity;
    SomeObject: TSomeObject;

begin
    WriteLn('MapEntity := TMapEntity.Create;');
    MapEntity := TMapEntity.Create;
    MapEntity.LazyKill;

    WriteLn('MapEntity := TSomeObject.Create;');
    MapEntity := TSomeObject.Create;
    MapEntity.LazyKill;

    WriteLn('SomeObject := TSomeObject.Create;');
    SomeObject := TSomeObject.Create;
    SomeObject.LazyKill;

    { Tyhjennetään jono }
    LazyKillMapEntity(nil);
end.

Kaikki rivit, joilla on teksti Write(Ln), ovat ylimääräistä debug-tulostetta, jotta selviää, miten ohjelma toimii. Tässä vielä esimerkkituloste, josta siis nähdään, missä vaiheessa mikäkin objekti (huom. numerot) luodaan ja tuhotaan.

MapEntity := TMapEntity.Create;
TMapEntity.Create     1
TMapEntity.LazyKill   1
LazyKill: finishing -, preparing 1
MapEntity := TSomeObject.Create;
TMapEntity.Create     2
TSomeObject.Create    2
TSomeObject.LazyKill  2
TMapEntity.LazyKill   2
LazyKill: finishing 1, preparing 2
TMapEntity.Destroy    1
SomeObject := TSomeObject.Create;
TMapEntity.Create     3
TSomeObject.Create    3
TSomeObject.LazyKill  3
TMapEntity.LazyKill   3
LazyKill: finishing 2, preparing 3
TSomeObject.Destroy   2
TMapEntity.Destroy    2
LazyKill: finishing 3, preparing -
TSomeObject.Destroy   3
TMapEntity.Destroy    3

Edit. Tuli niin pitkä koodi, että kirjoitin sitten saman tien sekaan vielä vähän ylimääräisiä kommentteja siltä varalta, että jokin selvemmistäkin asioista on epäselvä jollekulle, joka tätä eksyy lukemaan. :) Kiva, että tulee välillä Pascal-taitojakin muisteltua, kun en ole sitä vuosiin käytännössä lainkaan käyttänyt. :P

Janezki [09.09.2008 08:34:27]

#

Hyvä vain että yrität auttaa minua. Koko projekti on muutenkin yhdelle miehelle aivan liikaa, mutta pitää vain yrittää painaa eteenpäin. Esimerkiksi tuo unit jossa on kaikki entiteetit ja niiden käsittelyt on jo yli 1700 riviä, puhumattakaan muista uniteista :(

Sinällään tuo LazyKill-periaate on hyvä, mutta muuttaisin sitä hieman. Kaikilla TMapEntity (ja alaluokilla) on processor() -metodi jonka voi lähettää parametrina peli renderöintimoduulille, joka kutsuu metodeja yksitellen kun oikea hetki tulee (nyt päivitysväli on 0.01 sekuntia). Sen sijaan että tuhottavia entiteettejä lisättäisiin listaan, niille voisi vain asettaa muuttuja-arvon, vaikkapa ReadyToBeKilled : Boolean. Tämän lisäksi määritetään kiinteä TMapEntity-objekti, joka omalla suoritusvuorollaan sahaa kaikki entiteetit läpi ja katsoo onko tämä flag kenelläkään aktiivinen. Tietysti sitten tuhoaa ne ja vapauttaa pointterit.

Instanssitaulukkoon (dynaaminen taulukko missä entiteettien instanssit ovat) olen laatinut sellaisen suojeluksen että aina kun luokka viittaa siihen niin pitää kysyä onko entiteetti elossa. Tämän lisäksi jokaisella entiteetillä on Neutral:boolean -muuttuja, joka kertoo reagoiko entiteetti muiden kanssa ja näkyykö se kartalla (tämä ominaisuus tuli tarpeelliseksi esim. uudelleenspawnattavia varusteita suunniteltaessa). Periaatteessa kun entiteetti listataan tapettavaksi, se voidaan merkitä tällä flagilla, jolloin se on turvassa myös kaikelta ulkoiselta käsittelyltä ne muutamat millisekunnit ennenkuin Tuhoajaentiteetti ehtii paikalle.

Metabolix [09.09.2008 10:19:57]

#

Niin, aivan, jos taulu tosiaan on jo olemassa, niin tietenkin sitä voi sitten käyttää ilman ylimääräistä taulukonkäsittelykoodia. Tuo on ehdottomasti helpompi tapa, jos tuo järjestelmään helposti istuu. (On ehkä hyvä silti käyttää jotain wrapperifunktiota, jonka toimintaa voi helposti muuttaa tarvittaessa.)

Oikeastaan esittämäni menetelmän käyttöön on vain pari mahdollista syytä: Objektin tuhoaminen tapahtuu yhdellä kutsulla, siis näennäisesti samalla tavalla kuin Destroyn kanssa, eikä muuta käsittely- ja tarkistuskoodia tarvita. (Voi toki olla, että systeemisi on niin rakennettu, ettei tämä tuota kummempia lisätarkistuksia.) Ylimääräisiä objekteja on olemassa aina vain yksi, joten muisti vapautuu tehokkaammin. Tällä on merkitystä vain silloin, jos objekteja on todella paljon ja niitä saman kierroksen aikana sekä tuhotaan että luodaan suuria määriä (satojatuhansia). Haittapuolena on tuo jo mainitsemani samankaltaisuus Destroy-funktion kanssa myös siinä, että objekti todella saattaa tuhoutua jo matkalla, jos funktioita kutsutaan "väärällä" tavalla ristiin.

Janezki [09.09.2008 18:23:41]

#

Elikkä toteutin nyt aikasemmin mainitun Destructor Destroy -tapahtuman, eli nyt Destroy on objektin tuhoaja eikä free. Tein myös eräänlaisen GarbageCollectorin, joka vapauttelee muita entiteettejä. Vapauttaessaan itseään entiteetti asettaa itsensä tuhottavaksi ja neutraaliin tilaan, minkä yhteydessä sen processor-metodi poistuu synkkauslistalta. Tosin nyt tulee jotain kummallisia poikkeuksia, mutta uskon että systeemi on toimiva kunhan korjailen sitä hieman.

Tuossa tuleekin aika hyvin selville se että Delphissä objektit eivät voi tuhota itseään

Delphi Help kirjoitti:

Never explicitly free a component within one of its own event handlers or the event handler of a component it owns or contains. For example, don’t free a button, or the form that owns the button, in its OnClick event handler.

User137 [09.09.2008 19:53:32]

#

Tarkemmin sanottuna tuo ei kyllä tarkoita ettei se voisi itseään poistaa. Event handler on jotakuinkin tällanen:

...
  if assigned(FOnClick) then FOnClick(self);
  // Tekee tässä välissä vielä jotain joka käyttää self:ä
...

Eli se joka virheen aiheuttaisi on tuo muu koodi joka eventin jälkeen suoritetaan, en usko että ongelmia tulee jos olion itsensä poistaa kunhan huolehtii että se on viimeinen käsky joka suoritetaan.

Janezki [09.09.2008 20:41:44]

#

User137 kirjoitti:

Tarkemmin sanottuna tuo ei kyllä tarkoita ettei se voisi itseään poistaa. Event handler on jotakuinkin tällanen:

...
  if assigned(FOnClick) then FOnClick(self);
  // Tekee tässä välissä vielä jotain joka käyttää self:ä
...

Eli se joka virheen aiheuttaisi on tuo muu koodi joka eventin jälkeen suoritetaan, en usko että ongelmia tulee jos olion itsensä poistaa kunhan huolehtii että se on viimeinen käsky joka suoritetaan.

Tulihan tuota itsekin kokeiltua, kuten aloitusviestissäni demonstroisin, että ei se kovin tervettäkään ole vaikka free olisi viimeinen käsky.

User137 [09.09.2008 22:11:20]

#

No aloitusviestin koodissa oli virheitä, mm. tuossa:

procedure TAniSplatSprite.Processor();
begin
 ...
 Self.Free;
 ... // Free:n jälkeenhän ei mitään kuulu enää tapahtua
end;

...ja kokeilin sitä itsekin tyyliin:

procedure TTestObject.Vapauta;
begin
  self.Free;
end;

...
var o: TTestObject;
begin
  o:=TTestObject.Create;
  o.Vapauta;
  o.Free; // Tässä kohtaa tulee virhe koska oliota ei enää ole,
  // kuten ei kuulukaan olla olemassa. Vapauttaminen siis toimii...
  // Mikäli kommentoin o.Free; niin virheitä ei tule
end;

Jos joku keksii simppelin esimerkin jolla virhettä tai muistivuotoa tulee niin katsotaan tarkemmin.

Janezki [10.09.2008 19:01:44]

#

User137 kirjoitti:

No aloitusviestin koodissa oli virheitä, mm. tuossa:

procedure TAniSplatSprite.Processor();
begin
 ...
 Self.Free;
 ... // Free:n jälkeenhän ei mitään kuulu enää tapahtua
end;

Siinä oli yksi end; if-lausetta varten. Koko ohjelma bugittaa nyt voimakkaasti riippumatta siitä vapauttaako sisältä vai ulkoa. Testailen vielä kumpaakin vaihtoehtoa kunhan saan tämän edes jotenkin toimimaan.


Sivun alkuun

Vastaus

Aihe on jo aika vanha, joten et voi enää vastata siihen.

Tietoa sivustosta