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?
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
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.
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
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.
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.
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.
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.
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.
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.
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.
Aihe on jo aika vanha, joten et voi enää vastata siihen.