Nastal čas na kakao - Vkládání objektů a přesměrování zpráv - MujMAC.cz - Apple, Mac OS X, Apple iPod

Odběr fotomagazínu

Fotografický magazín "iZIN IDIF" každý týden ve Vašem e-mailu.
Co nového ve světě fotografie!

 

Zadejte Vaši e-mailovou adresu:

Kamarád fotí rád?

Přihlas ho k odběru fotomagazínu!

 

Zadejte e-mailovou adresu kamaráda:

Soutěž

Sponzorem soutěže je:

IDIF

 

Jak se jmenuje autor knihy Mistrovství práce s DSLR?

V dnešní soutěži hrajeme o:

Dárkový certifikát v hodnotě 1000,- Kč

Seriály

Více seriálů



Informace

Nastal čas na kakao - Vkládání objektů a přesměrování zpráv

29. července 2004, 00.00 | V předminulém dílu jsme si začali povídat o tom, jaké alternativní techniky se v Cocoa často využívají namísto dědičnosti: ukázali jsme si, jak funguje delegace a zběžně jsme se seznámili s mechanismem akce/cíl; minule jsme se seznámili s využitím kategorií pro přidání jednoduchého API k již existující třídě. Dnes toto téma dokončíme ukázkou "pokročilejšího triku" vkládání objektů.

V předminulém dílu jsme si začali povídat o tom, jaké alternativní techniky se v Cocoa často využívají namísto dědičnosti: ukázali jsme si, jak funguje delegace a zběžně jsme se seznámili s mechanismem akce/cíl; minule jsme se seznámili s využitím kategorií pro přidání jednoduchého API k již existující třídě. Dnes toto téma dokončíme ukázkou "pokročilejšího triku" vkládání objektů.

Minule jsme mezi nevýhody využití kategorie zařadili to, že není možné přímo přidávat instanční proměnné, a také to, že nelze změnit standardní chování základní třídy. V takových případech je tedy na místě využít buď dědičnosti, nebo vkládání objektů. Dědění je "stará vesta"; dnes si vysvětlíme, jak funguje vkládání objektů a jaké jsou jeho výhody i nevýhody. Pro lepší ilustraci znovu připomeňme obrázek, uvedený již minule, který schematicky ukazuje využití dědičnosti:

Pro lepší pochopení toho, jak vkládání objektů funguje, se vyplatí si nejprve uvědomit, co se vlastně děje v případě, že objektu Zásobník pošleme nějakou zprávu:

  • pokud je odpovídající metoda přímo součástí třídy Zásobník (je-li tedy na naší ilustraci ve skupině "nové služby"), prostě se provede;
  • pokud tomu tak není, hledá se odpovídající metoda ve třída Pole.

Vkládání objektů dosahuje přesně téhož efektu s využitím trochu odlišného mechanismu: třída Zásobník není dědicem třídy Pole; zato však (mimo jiné) obsahuje vložený objekt třídy Pole. Sama třída pak je implementována tak, aby se zprávy, jimž sama "nerozumí", přesměrovaly právě na tento vnořený objekt. Graficky bychom si tento případ mohli ilustrovat nějak takto:

Implicitní dědičnost tedy nahrazuje explicitní předávání zpráv; celkový výsledek je ovšem týž.

Samozřejmě, jak si za chvilku ukážeme, vkládání objektů je složitější, než prosté využití dědičnosti: kromě nových služeb – jež jsou ve všech případech implementovány stejným způsobem –, totiž musíme navíc explicitně implementovat předávání zpráv. Výměnou za to ovšem získáme značnou flexibilitu:

  • vložený objekt nemusí být vytvořen hned při tvorbě hlavního objektu; lze jej vytvořit "on-demand" až ve chvíli, kdy je poprvé skutečně zapotřebí. V některých případech to může znamenat skutečně zásadní rozdíl: pokud se instancí základních objektů vytváří mnoho, pokud jsou vložené objekty velké a používají se spíše výjimečně, může tato technika mnohonásobně snížit spotřebu paměti a zrychlit aplikaci;
  • vložený objekt nemusí být po celou dobu existence základního objektu týž: podle potřeby může základní objekt kdykoli vložený objekt nahradit jinou instancí; v komplexnějších případech dokonce i instancí jiné třídy. Podobně může být vložených objektů více, a základní objekt se může podle podmínek a potřeby dynamicky rozhodnout, kterému z nich zprávu předá. Tak můžeme efektivně implementovat de-facto vícenásobnou dědičnost – aniž bychom narazili na kteroukoli z nevýhod, jež jsou s ní spjaty v jazycích typu C++, jež ji nabízejí přímo;
  • díky tomu, že předávání zpráv mezi základním a vloženým objektem je "jejich interní věc", skrytá v rámci zapouzdření uvnitř API základního objektu a nikterak neovlivňující zbytek aplikace, můžeme pro něj použít různé nestandardní metody. Typickým využitím této možnosti jsou distribuované objekty – odkaz na objekt obsahuje identifikaci počítače a procesu, a předávání zpráv je zajištěno prostřednictvím meziprocesové komunikace a/nebo počítačové sítě. Tak můžeme transparentně "přímo" pracovat s objekty, jež jsou fakticky součástí jiného procesu na odlišném počítači...

Kromě toho je další výhodou vkládání objektů, specifickou pro API Cocoa, to, že bez problémů funguje i se sdruženými třídami (class clusters) – "klasická" dědičnost je u těchto speciálních tříd problematická, resp. slouží u nich pro zcela jiný účel.

Rozhraní

Ze stejných důvodů jako minule si ukážeme implementaci jednoduchého zásobníku pomocí vkládání objektů, jakkoli pro tento účel by se v praxi samozřejmě kategorie hodila lépe. Rozhraní je poměrně jednoduché – kromě deklarace API potřebujeme jen jednu instanční proměnnou, jež bude obsahovat odkaz na vložený objekt:

@interface Stack:NSObject {
  NSMutableArray *array;
}
+stack; // vytvoří a vrátí nový zásobník
-(void)push:object;
-pop; // pro prázdný zásobník vyvolá výjimku
@end

Samozřejmě, v praxi bychom nejspíš vkládání objektů využili ve složitějších případech, kdy by zřejmě instančních proměnných bylo více.

Implementace

Implementace je tentokrát samozřejmě trochu složitější, a proto si ji ukážeme v několika krocích. Nejprve to nejjednodušší: zásobníková primitiva převedeme na primitiva dynamického pole prakticky stejně, jako tomu bylo u kategorie; jen namísto odkazu self sám na sebe v tomto případě ovšem použijeme odkaz na vložený objekt:

@implementation Stack
-(void)push:object {
  [array addObject:object];
}
-pop {
  id o=[[[array lastObject] retain] autorelease];
  [array removeLastObject];
  return o;
}
...

Celkem jednoduchá je i standardní implementace metody stack a metody init, v níž vytvoříme vložený objekt (správně bychom měli init naprogramovat trošku složitěji, a měli bychom implementovat také metodu dealloc; protentokrát to však obojí přeskočíme, protože "to jsme zatím nebrali" – podrobnosti správné implementace metod init a metodu dealloc si podrobně ukážeme hned v příštím dílu).

...
-init {
  [super init];
  array=[[NSMutableArray alloc] init];
  return self;
}
+stack {
  return [[[self alloc] init] autorelease];
}
...

Pokud – jako v případě našeho zásobníku – požadujeme právě jen nové API (zde reprezentované zprávami stack, push: a pop), jsme hotovi: sice jsme museli napsat trošku víc kódu, než by tomu bylo při použití kategorie nebo podtřídy; zato však se nám neplete dohromady "staré" a "nové" API, a použitá technika funguje stejně dobře se sdruženými i s normálními třídami. Navíc máme výhodu možnosti inicializovat vložený objekt dynamicky až v případě potřeby – prostě bychom zrušili implementaci metody init, a na začátek metod push: a pop bychom přidali řádek

if (!array) array=[[NSMutableArray alloc] init];

V obecném případě však zmíněná výhoda – že se nám neplete "staré" a "nové" API – je ve skutečnosti spíše nevýhodou, neboť to znamená, že nad objekty třídy Stack nemůžeme užívat služeb třídy NSMutableArray, byť bychom třeba chtěli. To je proto, že jsme dosud neimplementovali vlastní přesměrování zpráv – až dosud jsme využívali pouze vkládání objektů; hned to napravíme.

Přesměrování zpráv

Pro přesměrování zpráv musíme využít novou vlastnost Objective C a Cocoa, s níž jsme se dosud neseznámili. Dosavadní popis toho, jak objekty zpracovávají přijaté zprávy, totiž nebyl zcela přesný: před časem jsme si řekli, že

... pokud není součástí implementace metoda pro přijatou zprávu, hledá se metoda pro tuto zprávu v nadtřídě. Není-li ani tam, hledá se v její nadtřídě, a tak pořád dál... dokud nenarazíme na "nejvyšší" třídu, jež již nadtřídu nemá. Teprve nenajde-li se metoda ani tam, je zpráva odmítnuta.

Tak nějak tomu je kupříkladu v Javě; Objective C/Cocoa však nabízí komplexnější a flexibilnější služby. Pokud se totiž nenajde odpovídající metoda v žádné nadtřídě, runtime systém se pokusí odeslat zprávu dynamicky, nezávisle na metodách, jež jsou součástí tříd. Takové dynamické odeslání zprávy probíhá ve dvou krocích:

  • nejprve se runtime systém "zeptá" objektu na to, jaké jsou typy argumentů a jaká je návratová hodnota, odpovídající dané zprávě;
  • pak runtime systém vytvoří "balíček", reprezentující zprávu i její argumenty (právě proto, aby to bylo korektně možné s libovolnými typy, musel nejprve proběhnout první krok), a ten objektu předá. Objekt s ním může udělat cokoli uzná za vhodné – v našem případě jej prostě předá vloženému objektu, aby jej zpracoval za něj.

Oba kroky jsou realizovány zcela standardním způsobem, pomocí standardizovaných zpráv. To přináší dvě výhody: první, není zapotřebí jazyk komplikovat přidáváním dalších rysů; druhá, dokonce i tyto speciální zprávy lze v případě potřeby dědit či přesměrovávat.

Pro první krok slouží zpráva methodSignatureForSelector:, jejímž argumentem je selektor dané zprávy. Vrácená hodnota je speciální objekt třídy NSMethodSignature; ten určuje podrobně všechny atributy metody, tj. počet a typy jejích argumentů i typ její návratové hodnoty. Pro druhý krok je určena zpráva forwardInvocation:, jejímž argumentem je speciální objekt třídy NSInvocation. Ten právě slouží jako výše zmíněný "balíček" – jeho součástí je jak selektor zprávy, tak i konkrétní hodnoty jejích argumentů, a jeho prostřednictvím se předává i návratová hodnota.

Napoprvé to snad zní trochu složitě, ale konkrétní implementace přesměrování všech neznámých zpráv na vložený objekt je velmi jednoduchá:

...
-(NSMethodSignature*)methodSignatureForSelector:(SEL)sel {
  NSMethodSignature *sig=[super methodSignatureForSelector:sel];
  if (!sig) sig=[array methodSignatureForSelector:sel];
  return sig;
}
-(void)forwardInvocation:(NSInvocation*)inv {
  [inv invokeWithTarget:array];
}
@end

Malinko složitější je prvá metoda, a to jen proto, že existují i jiné příležitosti než přesměrování neznámé zprávy, při nichž se může runtime systém "zeptat" objektu na signaturu pro některý selektor. Proto je zapotřebí si nejprve vyžádat standardní signaturu pomocí odeslání zprávy speciálnímu příjemci super – a teprve v případě, že pro daný selektor žádná signatura známá není ("if (!sig)"), vyžádáme si signaturu od vloženého objektu.

Implementace druhé metody forwardInvocation: je už jednodušší, neboť tato metoda se zavolá jen a jenom v případě, že je skutečně zapotřebí přesměrovat některou zprávu. Prostě si tedy vyžádáme od objektu inv, aby zpráva, již reprezentuje, byla zaslána objektu array – to zajistí standardní zpráva invokeWithTarget:, jíž instance třídy NSInvocation rozumějí.

Výhody a nevýhody

Řadu výhod vkládání objektů a přesměrování zpráv jsme si už řekli:

+ vkládání objektů a přesměrování zpráv korektně podporuje sdružené i normální třídy;

+ vložený objekt nemusí být vytvořen hned při tvorbě hlavního objektu; lze jej vytvořit "on-demand" až ve chvíli, kdy je poprvé skutečně zapotřebí;

+ vložený objekt nemusí být po celou dobu existence základního objektu týž: podle potřeby může základní objekt kdykoli vložený objekt nahradit jinou instancí; v komplexnějších případech dokonce i instancí jiné třídy. Podobně může být vložených objektů více, a základní objekt se může podle podmínek a potřeby dynamicky rozhodnout, kterému z nich zprávu předá;

+ díky tomu, že předávání zpráv mezi základním a vloženým objektem je "jejich interní věc", skrytá v rámci zapouzdření uvnitř API základního objektu a nikterak neovlivňující zbytek aplikace, můžeme pro něj použít různé nestandardní metody;

+ při vkládání objektů a přesměrování zpráv lze snadno skrýt API, jež nechceme publikovat (tak, jak by zůstalo skryto kompletní API třídy NSMutableArray, kdybychom přesměrování zpráv neimplementovali; stejně dobře můžeme při přesměrovávání dynamicky volit, které zprávy předáme a které ne);

- oproti využití kategorie ztrácíme výhodu oboustranné kompatibility (dostaneme-li instanci třídy NSMutableArray, nemůžeme ji přímo využívat jako zásobník).

Oproti dědičnosti přesměrování nemá takřka žádnou nevýhodu, nepočítáme-li těch cca osm řádků zdrojového kódu, jež musíme napsat navíc pro implementaci vlastního přesměrování.

Obsah seriálu (více o seriálu):

Tématické zařazení:

 » Rubriky  » Informace  

 » Rubriky  » Agregator  

 » Rubriky  » Začínáme s  

 

 

 

 

Přihlášení k mému účtu

Uživatelské jméno:

Heslo: