Druhy objektů

Velmi důležitým atributem kteréhokoli objektu je doba jeho existence: kdy objekt vznikne? Kdy zanikne? Je jeho vznik -- nebo zánik -- vedlejším efektem některé jiné akce programu, nebo si jej musí programátor vyžádat? Z tohoto hlediska můžeme objekty rozdělit v zásadě do čtyř skupin. První tři skupiny dobře známe: odpovídají trvání proměnných ve standardních programovacích jazycích. Objektovou novinkou je čtvrtá skupina -- objekty, které dokáží 'přežít' i ukončení procesu, který s nimi pracuje. V tomto textu budeme skupiny nazývat následujícími jmény:

automatické objekty jsou objekty s obecně nejkratší dobou života (i když v konkrétních případech mohou samozřejmě dynamické objekty existovat kratší dobu) a v neobjektových prostředích jim zhruba odpovídají lokální proměnné. Automatický objekt vznikne na základě požadavku programu; často tento požadavek musí být určen staticky v okamžiku překladu. Automatický objekt -- jak jeho jméno naznačuje -- zaniká automaticky ve chvíli, kdy program opustí blok, ve kterém byl automatický objekt vytvořen. Objektový systém nemusí podporovat automatické objekty; namísto nich mohou stejně dobře posloužit dynamické. Není-li však součástí systému tzv. garbage collector (viz níže), může být někdy programování v systému bez automatických objektů trochu nepohodlné.

dynamické objekty jsou základním typem objektů a z hlediska doby trvání jim v neobjektových prostředích nejblíže odpovídají bloky paměti, alokované příkazy malloc, calloc a podobně. Vznik i zánik dynamického objektu je vždy výsledkem explicitního požadavku programátora (není-li součástí systému samostatný modul -- tzv. garbage collector -- který může rušit dynamické objekty 'automaticky' usoudí-li, že je již nikdo nebude potřebovat). Nevyžádá-li si nikdo zrušení dynamického objektu, zanikne objekt nejpozději při ukončení procesu, jehož byl součástí. Bez podpory dynamických objektů se neobejde žádný objektový systém.

statické objekty trvají po celou dobu existence procesu a jejich ekvivalentem v neobjektových prostředích jsou globální proměnné. Statický objekt vznikne ve chvíli vytvoření procesu -- de facto tedy musí být vytvořen již při překladu -- a zaniká vždy ve chvíli zániku procesu. Objektový systém nemusí podporovat práci se statickými objekty; v takovém případě však musí nabízet i neobjektové služby pro prvotní vytváření dynamických objektů. V některých případech může podpora statických objektů také usnadnit programování.

trvalé objekty jsou vytvořeny i zrušeny na základě požadavku programátora. Speciálně trvalé objekty 'přežijí' i ukončení procesu který je vytvořil; trvalý objekt který nikdo nezrušil bude existovat navěky (přesněji řečeno, po celou dobu existence výpočetního systému ve kterém trvalý objekt leží). Nejbližším ekvivalentem trvalých objektů v neobjektových prostředích jsou datové soubory. Objektový systém nemusí vůbec podporovat trvalé objekty; ochuzuje tím však programátory o velmi široké možnosti jejich využití.

Pro rozhodnutí o typech objektů které vývojové prostředí bude podporovat existují -- stejně jako téměř kdekoli jinde- dvě protichůdné tendence: na jednu stranu je výhodné umožnit práci s co nejširší paletou možných typů, aby programátor měl k dispozici flexibilní aparát služeb; na druhou stranu existence řady různých typů objektů komplikuje programátorské rozhraní a zvyšuje pravděpodobnost chyb.

OpenStep proto vůbec nepodporuje automatické objekty (pro programátorské pohodlí však obsahuje jednoduchý ale efektivní poloautomatický garbage collector, který je z programátorského hlediska dokáže plně nahradit). Podpora statických objektů je v OpenStepu omezena pouze na třídy (připomeňme, že třídy v Objective C slouží především pro tvorbu nových objektů -- musí tedy být statické, protože jinak bychom po spuštění programu neměli k dispozici nic, co by objekty dokázalo vytvořit) a na vyjímečné speciální případy, usnadňující programování.

Vyjímečným speciálním případem jsou statické objekty třídy NSString. Důvodem pro podporu statických objektů této třídy je to, že běžný program obsahuje řadu řetězcových konstant; kdybychom neměli k dispozici statické objekty třídy NSString, museli bychom používat konstanty typu char* a 'řetězcové konstanty' vytvářet dynamicky na jejich základě:

NSString *adresar=[NSString stringWithCString:"/NextDeveloper/Demos"];

Speciální podpora pro tvorbu statických instancí NSString naproti tomu podobné konstrukce výrazně zjednoduší:

NSString *adresar=@"/NextDeveloper/Demos";

Pro ostatní třídy podobnou podporu nepotřebujeme, protože jim odpovídající konstanty se používají jen zcela vyjímečně (pokud vůbec). Snad jen třída NSNumber by si zasloužila také podporu statických instancí.

V následujících odstavcích se blíže seznámíme s implementací jednotlivých typů objektů v OpenStepu:



Automatické objekty

OpenStep automatické objekty nepodporuje. Díky existenci garbage collectoru však můžeme s dynamickými objekty pracovat stejně jako s automatickými:

{
id anObject=[NSArray arrayWithObjects:.........];
....
// objekt zanikne automaticky jakmile přestane být zapotřebí
}

Na rozdíl od automatického objektu však je zde naprosto korektní objekt např. použít jako návratovou hodnotu:

{
id anObject=[NSArray arrayWithObjects:.........];
....
return anObject; // to by s automatickým nešlo!!!
}



Dynamické objekty a garbage collector

Dynamické objekty již vlastně známe: objekt je vytvořen na základě explicitního požadavku nějakým jiným objektem (obvykle, ale ne nutně, třídou) -- každý řádek v následujícím příkladu vytvoří nový objekt:

id anObject=[NSImage imageNamed:@"....."]; // objekt vytvořen třídou
id aString=[anObject description]; // objekt vytvořen jiným objektem
id anotherString=[aString lowercaseString]; // objekt vytvořen jiným objektem

Určitou zvláštností OpenStepu je poloautomatický garbage collector. Díky jeho existenci se na dynamický objekt standardně musíme dívat spíše jako na automatický: objekt bude jistě existovat po celou dobu zpracování aktuální metody, ale potom jej garbage collector může odstranit.

Konkrétně to tedy znamená, že nebudeme-li žádný z objektů vytvořených v posledním příkladu potřebovat později, nemusíme se o jejich uvolnění vůbec starat -- garbage collector je uvolní automaticky po ukončení metody, která objekty vytvořila.

Nechceme-li aby objekt byl odstraněn, musíme garbage collectoru sdělit že si nad objektem hodláme udržovat kontrolu (proto hovoříme o poloautomatickém garbage collectoru). To uděláme tak, že objektu odešleme zprávu retain -- takový objekt pak bude existovat (nejméně) tak dlouho, dokud jej opět neuvolníme. Předpokládejme, že v minulém příkladu si chceme zachovat poslední textový řetězec  (popis obrázku, uvedený malými písmeny), zatímco zbývající dva objekty byly zapotřebí pouze pro jeho získání a již nás nezajímají:

[anotherString retain];

Po ukončení metody garbage collector uvolní objekty 'anObject' a 'aString'; objekt 'anotherString' však existuje nadále a můžeme s ním i nadále volně pracovat. Jakmile zjistíme, že již nebudeme objekt potřebovat, uvolníme jej pomocí zprávy autorelease:

[anotherString autorelease];

a garbage collector jej zruší po ukončení metody, ve které jsme jej uvolnili.

Je vhodné si uvědomit, že pokud jsme napsali "zruší jej po ukončení metody", neznamená to "zruší jej okamžitě po ukončení metody" -- objekt může 'přežít' ještě velmi dlouho. Důvod je jednoduchý: s jedním a tím samým objektem může chtít komunikovat více jiných objektů; každý z nich si může vyžádat udržení objektu zprávou retain. Garbage collector sleduje kolikrát objekt dostal zprávu retain a uvolní jej teprve tehdy, když pro každý retain dostal odpovídající zprávu autorelease.

Poloautomatický garbage collector tohoto typu má řadu výhod. Hlavní z nich je, že se nemusíme explicitně starat o uvolnění sdílených objektů -- zcela běžnou situací v objektovém prostředí je, že řada objektů spolupracuje s jedním dalším:

Pokud není k dispozici garbage collector, není jasné který z objektů 1 až 5 má nakonec uvolnit objekt A. Samozřejmě že ten, který jej přestane potřebovat jako poslední; jak to ale v programu zjistit? Tato situace bývá zdrojem častých chyb (kdy si např. objekt 3 myslí že již nikdo nebude objekt A potřebovat a tak jej uvolní, pak se ale na -- již neexistující -- objekt A obrátí ještě objekt 4 a program se zhroutí); možnost takových chyb garbage collector definitivně odstraňuje.

Garbage collector tohoto typu má také jednu nevýhodu -- ilustruje ji

Zde objekt A poslal zprávu retain objektu B, objekt B objektu C, objekt C objektu D a ten zase objektu B. Jinými slovy, objekt A hodlá ještě komunikovat s objektem B, ten s objektem C, ten s D a ten s B. Je zřejmé, že jakmile pošle objekt A objektu B zprávu autorelease, měly by se uvolnit všechny tři objekty B, C a D (protože jsou závislé jen samy na sobě a nikdo již je nebude potřebovat). Garbage collector to ale neví -- ten pouze zjistí, že každý z objektů B, C a D dostal vícekrát zprávu retain než autorelease a neuvolní ani jeden z nich.

Musíme si tedy dávat pozor, abychom mezi objekty při odesílání zpráv retain nevytvořili 'cyklus', protože garbage collector takový cyklus neumí rozpoznat a objekty nikdy neuvolní.

Nakonec se seznámíme s metodou release. Zatímco metoda autorelease řekne garbage collectoru "tento objekt po ukončení této metody nebudu potřebovat", říká metoda release "tento objekt od této chvíle nebudu potřebovat". Je tedy její použití o něco málo efektivnější, protože objekt se uvolní ihned a neleží v paměti zbytečně po dobu zpracování metody; při jejím používání si však musíme důkladně rozmyslet víme-li opravdu jistě, že již objekt nebudeme potřebovat.

Podívejme se např. na následující úsek kódu:

...
aFont=[text font];
[currentFont release];
currentFont=[aFont retain];
...

Na první pohled je vše v pořádku -- starý font uvolníme, a místo něj si zapamatujeme aktuální. Přesto toto použití metody release může snadno vést k chybě: pokud minulý font je stejný jako momentální, uvolní se tento objekt ve chvíli provedení metody release a metoda retain se již pošle neexistujícímu -- právě uvolněnému -- objektu! Použijeme-li však metodu autorelease, je vše v pořádku -- garbage collector by objekt uvolnil až po ukončení metody (ale neuvolní jej, protože objekt mezitím dostal zprávu retain).

Úplně poslední zmínkou v tomto odstavci bude upozornění, že z technických důvodů které na této úrovni nemá smysl podrobně rozebírat existují v OpenStepu dvě výjimky: vytvoříme-li nový objekt odesláním zprávy alloc libovolné třídě nebo odesláním zprávy copy nebo mutableCopy libovolnému jinému objektu, dostaneme objekt který již je 'retainován'. Ukažme si příklad:

id a1=[NSArray alloc],a2=[NSArray array],a3=[a2 copy],a4=[nejakyObjekt vraciJinyObjekt];
id b1=[NSArray alloc],b2=[NSArray array],b3=[b2 copy],b4=[nejakyObjekt vraciJinyObjekt];

// chceme-li, aby garbage collector všechny čtyři objekty a1-4 uvolnil, musíme provést
[a1 autorelease];
[a3 autorelease];
// naopak, chceme-li aby garbage collector neuvolnil ani jeden z objektů b1-4, stačí provést
[b2 retain];
[b4 retain];



Statické objekty

Kromě tříd -- které jsou všechny standardně statickými objekty -- podporuje OpenStep pouze statické objekty třídy NSString. Takový objekt vytvoříme zápisem podobné konstanty, jakou určujeme v C řetězec; před otevírací uvozovkou však umístíme navíc znak '@':

id aString=@"Text";

Objective C automaticky převede ASCII znaky řetězcové konstanty do UNICODE formátu NSStringu.



Trvalé objekty

OpenStep standardně umožňuje zapsat libovolný objekt na disk a opět jej z disku obnovit (přesněji řečeno, OpenStep podporuje zápis objektu a jeho opětovné obnovení prostřednictvím libovolného zařízení -- disk zajišťuje trvalé objekty, síť předávání objektů mezi počítači a podobně).

Vytváříme-li vlastní třídu objektů, stačí velmi jednoduchým způsobem určit jakým způsobem bude nový objekt kódován a dekódován (s podrobnostmi se seznámíme později, až budeme popisovat třídy NSArchiver a NSUnarchiver). Všechny standardní objekty OpenStepu samozřejmě zápis a obnovení podporují.

Díky tomu můžeme libovolný objekt nebo skupinu objektů kdykoli zapsat na disk -- objekty se tak stanou trvalými -- nebo naopak z disku obnovit.



Měnitelné a neměnné objekty

Základní myšlenkou koncepce měnitelných a neměnných objektů je dosažení vyšší efektivity. Typickým příkladem je kopírování objektů: v praxi poměrně často potřebujeme vytvořit privátní kopii objektu -- jakýsi jeho 'snímek', který uchová momentální stav objektu i v případě, že se původní objekt změní. Představme si například objekt, který reprezentuje hashovací tabulku -- takový objekt v OpenStepu skutečně existuje a jmenuje se NSDictionary. Základní dvě zprávy, které je schopen zpracovat, jsou

- (void)setObject:(id)anObject forKey:(id)aKey;
- (id)objectForKey:(id)aKey;

První z nich uloží do tabulky dvojici <klíč, hodnota>, druhá vyhledá hodnotu k zadanému klíči (v čase nezávisejícím na počtu hodnot v tabulce). Je zřejmé, že má-li hashovací tabulka být konsistentní, musí interně udržovat ne odkazy na klíče, ale jejich neměnné kopie -- kdyby v tabulce byly jen odkazy na klíče, mohl by se objekt reprezentující klíč kdykoli změnit, aniž by se o tom tabulka vůbec dozvěděla; hashovací tabulka by v takovém případě byla samozřejmě nekorektní. Implementace metody setObject:forKey: tedy musí vypadat přibližně takto:

- (void)setObject:(id)anObject forKey:(id)aKey
{
id myKey=[aKey copy]; // potřebuji vlastní neměnnou kopii
id myVal=[anObject retain]; // hodnota se může klidně měnit (ale nesmí zaniknout)

zaradit_do_tabulky(myKey,myVal);
}

Za těchto podmínek bude hashovací tabulka pracovat korektně, ovšem 'zaplatíme' za to zpomalením programu a větší spotřebou paměti: každý klíč vkládaný do tabulky se musí nejprve zkopírovat -- to znamená, že potřebujeme dvakrát tolik paměti a navíc program musí kopírovat data objektu. Přitom to v řadě případů není doopravdy zapotřebí -- za normálních okolností se obsah klíčů stejně nebude měnit; hashovací tabulka by si tedy mohla často (i když ne vždy) udržovat pouze odkazy na klíče -- musela by ale 'vědět', které klíče se ještě mohou měnit a které ne.

Objektové prostředí ale nabízí velmi elegantní řešení: hashovací tabulka samozřejmě nemůže vědět které objekty se budou měnit; mohou to ale vědět tyto objekty samy. Stačí zavést pro každý typ objektů pro který to dává rozumný smysl dvě třídy: třídu neměnných objektů a třídu objektů. které se mohou měnit -- např. NSString (neměnné) a NSMutableString (měnitelné). Neměnné objekty pak nemusí nikdy vytvářet kopie -- jejich metoda copy může být implementována takto:

-copy; // NSString
{
return [self retain]; // stačí vrátit odkaz na sebe sama a upozornit garbage collector že
nesmí objekt uvolnit

}

Nyní funguje vše automaticky s nejvyšší možnou efektivitou: vkládáme-li do hashovací tabulky klíč, který se nikdy nebude měnit, hashovací tabulka bude udržovat pouze odkaz -- žádná paměť navíc, nic se nekopíruje. Pouze v případě že jako klíč využijeme měnitelný objekt -- např. NSMutableString -- kopie se vytvoří; v takovém případě se tomu ale stejně nemůžeme vyhnout. Navíc tentýž 'trik' automaticky funguje nejen v hashovací tabulce, ale kdekoli, kde potřebujeme okamžité kopie objektů -- připravujeme např. program, který si pro funkci 'undo' musí zapamatovat momentální stav svých datových objektů? Nic jednoduššího -- prostě vytvoříme kopie všech objektů reprezentujících data tak, že jim pošleme zprávu copy. Díky koncepci měnitelných a neměnných objektů nemusíme zkoumat která data se mohou měnit a která ne -- fakticky se zkopírují jen ta, kterých se změny mohou týkat.

OpenStep proto v řadě případů nabízí dvojice tříd NSXXX a NSMutableXXX, kde objekty třídy NSXXX se nemohou měnit, zatímco objekty třídy NSMutableXXX ano (je tomu tak mimochodem i u třídy NSDictionary -- metoda setObject:forKey: je tedy samozřejmě k dispozici pouze u objektů třídy NSMutableDictionary). Třída NSMutableXXX je vždy dědicem třídy NSXXX; měnitelné objekty tedy 'umí' všechno co neměnné, a navíc jsou schopny změn. Pošleme-li kterémukoli objektu třídy NSXXX zprávu copy, nevytvoří se žádná kopie; namísto toho získáme další odkaz na tentýž (neměnný) objekt. Pošleme-li zprávu copy objektu třídy NSMutableXXX, dostaneme nový objekt třídy NSXXX, který bude obsahovat neměnnou kopii momentálního stavu původního objektu.

Uvědomme si, že koncepce měnitelných a neměnných objektů zaručuje co nejefektivnější zkopírování i u složených objektů. Jako příklad vezměme objekt třídy NSMutableArray, který reprezentuje pole libovolných dalších objektů, do kterého můžeme přidávat nebo z něj odebírat (odpovídající neměnná třída NSArray reprezentuje pole, jehož obsah nemůžeme měnit).

Obrázek ukazuje příklad objektu třídy NSMutableArray, obsahujícího jak měnitelné, tak neměnné objekty. Vyžádáme-li si nyní zprávou copy neměnnou kopii momentálního stavu objektu mutableArray, musí se vytvořit nový objekt třídy NSArray (protože mutableArray je měnitelný) se stejným (a také neměnným) obsahem. Nový objekt tedy může se starým sdílet odkazy na neměnné vnořené objekty, ale musí obsahovat vlastní (neměrné) kopie objektů, které byly měnitelné:

Čas od času bychom mohli potřebovat 'přece jenom' změnit neměnný objekt. Doslova to samozřejmě není možné -- tím bychom celou koncepci měnitelných a neměnných objektů postavili na hlavu; můžeme si však pomocí zprávy mutableCopy vyžádat vytvoření měnitelné kopie objektu. Obsahuje-li původní objekt vnořené objekty, bude jeho měnitelná kopie obsahovat (odkazy na) tytéž objekty, a to i v případě, že tyto objekty samy jsou neměnné (chceme-li např. vytvořit měnitelnou kopii pole, je to proto, abychom do něj mohli přidávat nebo z něj odebírat další objekty; ne proto, abychom mohli měnit objekty v něm obsažené):

Koncepce měnitelných a neměnných objektů je velmi silným a šikovným mechanismem, který kromě výrazného zvýšení efektivity programů dokáže i omezit programátorské chyby: používáme-li neměnný objekt, nemůže se nám omylem stát že jej některý úsek programu změní (z podobného důvodu byl např. v ANSI C zaveden modifikátor const). Rozdělení měnitelných a neměnných objektů na samostatné třídy NSXXX a NSMutableXXX navíc umožňuje některé takové chyby odchytit již při překladu -- pokusíme-li se např. staticky typovanému objektu třídy NSArray poslat zprávu addObject: překladač vydá varování.



Skryté podtřídy

Zatímco koncepce měnitelných a neměnných objektů trochu zkomplikovala programátorské rozhraní OpenStepu (namísto jediné třídy např. NSString máme dvě -- NSString a NSMutableString) pro zajištění větší efektivity a větší robustnosti, je hlavním účelem koncepce skrytých podtříd programátorské rozhraní bez ztráty efektivity co nejvíce zjednodušit (nebo naopak -- při zachování jednoduchého programátorského rozhraní dosáhnout maximální efektivity).

Koncepci skrytých podtříd si opět ukážeme na příkladu. Dejme tomu, že chceme vytvořit třídu, jejíž instance by reprezentovaly čísla (taková třída je součástí OpenStepu a jmenuje se NSNumber). Pokud bychom nevyužili koncepce skrytých podtříd, máme v podstatě dvě možnosti:

(1) Vytvoříme třídu NSNumber, která bude sama o sobě schopna pracovat s jakýmkoli typem čísla (char, int, unsigned, long, float...). To je samozřejmě možné, ale tento přístup má dvě nevýhody: naprogramování takové komplikované třídy je složité, snadno se při něm udělá chyba a složitý zdrojový kód se špatně udržuje. Druhou -- a možná závažnější -- nevýhodou je, že implementace takové třídy není efektivní, protože musí zahrnovat potřeby všech číselných typů a nemůže být optimalizována pro potřeby jednoho konkrétního typu.

(2) Třída NSNumber sama bude pouze abstraktní nadtřídou, shrnující obecné vlastnosti všech čísel, a skutečnými reprezentanty jednotlivých typů budou její podtřídy:

To je lepší, skutečně objektové řešení -- každá z podtříd je jednoduchá, snadno udržovatelná a snadno může být také maximálně optimalizována. Jedinou nevýhodou je zde komplikované programátorské rozhraní -- programátor by si musel pamatovat třídy NSCharNumber, NSUnsignedCharNumber... a musel by se sám starat vždy o to, aby použil potřebnou třídu.

Koncepce skrytých podtříd využívá implementaci podle bodu (2), ale programátorům nabízí rozhraní podle bodu (1): programátor využívá vždy služeb třídy NSNumber a její podtřídy vůbec nezná. Třída sama při vytváření objektu rozhodne která z jejích (skrytých) podtříd je pro dané číslo optimální a vytvoří odpovídající objekt; i s ním programátor komunikuje jako s objektem třídy NSNumber (což je v naprostém pořádku, protože objekt je dědicem třídy NSNumber):

Vytvoříme-li tedy několik 'instancí třídy NSNumber':

NSNumber *aChar = [NSNumber numberWithChar:'a'];
NSNumber *anInt = [NSNumber numberWithInt:1];
NSNumber *aFloat = [NSNumber numberWithFloat:1.0];
NSNumber *aDouble = [NSNumber numberWithDouble:1.0];

může být ve skutečnosti každý z nově vytvořených objektů instancí jiné třídy. Všechny však jsou dědici třídy NSNumber a jako s takovými s nimi můžeme pracovat.

Řada tříd OpenStepu využívá koncepce skrytých podtříd. Typickým příkladem jsou prakticky všechny třídy FoundationKitu, které reprezentují složené objekty (jako NSArray nebo NSDictionary) -- ty využívají skrytých podtříd pro volbu optimální implementace z hlediska poměru efektivity a paměťové náročnosti, aniž by se tím musel programátor explicitně zabývat. Programátor samozřejmě může sám doplnit další skryté podtřídy pro rozšíření služeb celé skupiny tříd.





(další článek)


Copyright (c) Ondra Čada