Foundation Kit
Foundation Kit je nejzákladnějším z "kitů" OpenStepu; nabízí třídy (a několik obyčejných céčkových funkcí) sloužící pro zcela obecné účely:
Na obrázku vidíme přehled tříd Foundation Kitu a jejich vzájemných vazeb v objektové hierarchii. Účel jednotlivých tříd je následující:
NSObject je základní třídou celé objektové hierarchie OpenStepu -- jsou od něj odvozeny nejen ostatní třídy Foundation Kitu, ale i všechny ostatní třídy ostatních kitů OpenStepu. Jeho metody -- sdílené tedy i všemi ostatními objekty OpenStepu -- zajišťují především služby garbage collectoru (metody retain, release a autorelease, které již známe z minulého dílu). Na tom s nimi spolupracují také objekty třídy NSAutoreleasePool, které navíc umožňují programátorovi garbage collector v potřebné míře řídit.
NSArray a NSMutableArray jsou třídy, zajišťující práci s obecnými poli jiných objektů. K objektům v nich uložených můžeme přistupovat na základě indexů nebo přímo na základě identifikace objektu; pole můžeme také setřídit nebo procházet sekvenčně s využitím objektu NSEnumerator. Pro svou univerzálnost patří objekty tříd NS(Mutable)Array spolu s NSStringy a s tabulkami NSDictionary k vůbec nejčastěji používaným objektům OpenStepu.
NSAssertionHandler nabízí luxusní objektové služby pro zachycení a ohlášení neočekávaných nebo chybných stavů programu a není vlastně ničím jiným než rozšířením standardního makra ASSERT.
Objekty tříd NSBtreeBlock a NSBtreeCursor umožňují rychlý a efektivní indexsekvenční přístup k databázím objektů, uložených pomocí tříd NSByteStore a NSByteStoreFile. Samy třídy NSByteStore a NSByteStoreFile umožňují ukládání trvalých objektů a jejich opětovné vybavování velmi efektivním způsobem, využívajícím stránkování operační paměti.
NSBundle nabízí aplikaci velmi pohodlný přístup ke zdrojům v nejobecnějším smyslu slova -- od doplňkových obrázků, zvuků nebo jakýchkoli podobných dokumentů "zabalených" do OpenStepové aplikace až po dynamicky zaváděné třídy z doplňkových modulů. Třída přitom automaticky zajišťuje lokalizaci (tj. výběr zdroje na základě jazyka pro který je zdroj určen a momentálně platných uživatelských jazykových předvoleb).
Objekty tříd NSCharacterSet a NSMutableCharacterSet reprezentují libovolnou množinu znaků -- slouží tedy k podobným účelům, pro které se v PASCALu využívaly proměnné typu 'set of char'. Na rozdíl od nich však podporují kompletní znakovou sadu UNICODE a nabízejí daleko luxusnější sadu operací.
NSCoder reprezentuje univerzální služby, potřebné pro zakódování jakéhokoli objektu do podoby potřebné např. pro jeho uložení do souboru nebo odeslání po síti na jiný počítač a jeho opětovné dekódování. Jeho podtřídy NSArchiver a NSUnarchiver pak zajišťují konkrétní archivaci a dearchivaci objektů programátorsky velmi pohodlným způsobem. Obdobné služby pro některé speciální případy nabízejí třídy NSSerializer a NSDeserializer.
Třídy NSConditionLock, NSLock a NSRecursiveLock nabízejí velmi luxusní primitiva pro synchronizaci paralelně běžících procesů nebo threadů. Vytváření a řízení procesů a threadů OpenStep podporuje prostřednictvím tříd NSProcessInfo a NSThread.
Třída NSConnection spolu s pomocnými třídami NSProxy a NSDistantObject je základem technologie distribuovaných objektů -- zajišťuje navázání a udržování spojení a předávání zpráv mezi objekty uloženými v různých adresových prostorech nebo dokonce na různých počítačích.
NSData a NSMutableData jsou abstrakcí beztypových dat. Jejich přímým ekvivalentem v neobjektovém programování je blok paměti, objektový přístup prostřednictvím tříd NS(Mutable)Data však nabízí daleko širší paletu služeb a daleko větší efektivitu díky spolupráci se stránkovacím mechanismem.
Třídy NSDate a NSCalendarDate reprezentují datum a čas a nabízejí všechny potřebné služby, včetně převodů mezi jednotlivými formáty a vytváření nebo načtení textové podoby data a času. Pro práci s datem nezávislým na zeměpisné poloze navíc přistupují třídy NSTimeZone a NSTimeZoneDetail, které umožňují práci s časovými zónami včetně jejich symbolických jmen (jako je např. "US/Pacific"). Třída NSTimeZoneDetail se navíc stará o korektní práci s letním časem.
Se třídami NSDictionary a NSMutableDictionary jsme se již velmi stručně seznámili v minulém dílu našeho seriálu. Jejich objekty reprezentují hashovací tabulky, které umožňují ukládání dvojic libovolných jiných objektů <klíč, data> a zajišťují vyhledání datového objektu na základě klíče v čase nezávislém na počtu dvojic v tabulce. Navíc můžeme samozřejmě procházet všemi objekty uloženými v tabulce pomocí třídy NSEnumerator. Tabulka ukládající dvojice objektů a nabízející velmi rychlé vyhledání je nesmírně praktickým prostředkem; proto jsou objekty tříd NS(Mutable)Dictionary spolu s poli NSArray a NSStringy asi vůbec nejčastěji využívanými objekty OpenStepu.
Třída NSException zajišťuje centralizovanou obsluhu výjimek -- její služby můžeme zhruba přirovnat např. k mechanismu výjimek v nejnovějších implementacích C++. Všechny třídy OpenStepu samozřejmě samy využívají služeb třídy NSException pro hlášení vlastních chybových stavů.
Prostřednictvím třídy NSInvocation můžeme velmi pohodlně přesměrovávat zprávy jiným objektům. Služby třídy NSInvocation jsou však daleko širší -- její pomocí můžeme programově zkonstruovat předání libovolné zprávy s libovolnými parametry libovolnému objektu a zpracovat libovolnou návratovou hodnotu. Jedná se tedy vlastně o "metaprostředek", který slouží v případech kdy v době překladu programu není známa metoda kterou chceme odeslat, její parametry nebo typ její návratové hodnoty.
Neznáme-li vůbec počet a typy parametrů nebo typ návratové hodnoty metody, můžeme si vyžádat objekt třídy NSMethodSignature, který metodu podrobně popisuje. Chceme-li tedy např. odeslat prostřednictvím počítačové sítě objektu "x" zprávu "add:" s parametrem "1", a nemáme-li o objektu "x" dosud žádné informace, vyžádáme si od něj nejprve objekt NSMethodSignature pro metodu "add:". Ze signatury zjistíme požaduje-li metoda "add:" objektu "x" parametr celočíselného nebo neceločíselného typu a podle toho můžeme parametr "1" zakódovat. Poznamenejme, že všechny třídy OpenStepu které standardně předávají zprávy (jako např. NSProxy nebo NSInvocation) samy automaticky tento mechanismus využívají; programátor jej tedy sám použije pouze v případě, rozhodne-li se z nějakého důvodu standardní služby obejít.
Třídy NSNotification, NSNotificationCenter a NSNotificationQueue spravují předávání informací o událostech mezi objekty. Kdykoli objekt změní svůj stav nějakým způsobem, který by mohl být významný pro ostatní objekty (např. okno je zavřeno), odešle zprávu o této události (ve formě objektu třídy NSNotification); třída NSNotificationCenter se pak postará o to, aby tuto zprávu dostaly všechny objekty, které si dříve "předplatily" přijímání podobných zpráv.
Úkolem objektů třídy NSRunLoop je zajišťovat efektivním a pohodlným způsobem vstup. Objekt třídy NSRunLoop sleduje libovolný počet vstupních zařízení (jako je myš nebo klávesnice, ale i časový čítač nebo libovolný jiný zdroj událostí) a odešle zprávu dojde-li k aktivitě na kterémkoli vstupu. OpenStep automaticky spravuje jeden objekt třídy NSRunLoop pro každý thread, takže programátor se jím vůbec nepotřebuje zabývat, nechce-li z nějakého důvodu obcházet standardní služby.
Třída NSScanner vlastně není ničím jiným, než patřičně rozbujelou objektovou nadstavbou staré dobré funkce scanf: její metody procházejí znakově orientovaný vstup a podle zadaných požadavků z něj vybírají jednotlivé části a případně je převádějí do jiných formátů.
Třídy NSSet, NSMutableSet a NSCountedSet reprezentují množiny jiných objektů, na rozdíl od tříd NS(Mutable)Array v nich nejsou objekty seřazeny. Zato však nabízejí mnohem efektivnější testování je-li objekt součástí množiny nebo ne -- doba potřebná pro zjištění je-li objekt uložen v NS(Mutable)Array je závislá na počtu objektů v poli uložených, zatímco množina je to schopna zjistit v konstantním čase. NSCountedSet reprezentuje speciální množinu, ve které může jeden a tentýž objekt být uložen vícekrát. Všechny objekty množiny můžeme projít pomocí třídy NSEnumerator.
Třídy NSString a NSMutableString jsou pravděpodobně vůbec nejpoužívanějšími třídami OpenStepu. Jejich objekty odpovídají textovým řetězcům a nabízejí velmi luxusní sadu služeb od převodů čísel do a z textového tvaru přes ekvivalent klasické funkce printf až po metody interpretující řetězec jako jméno souboru nebo adresáře a provádějící potřebné operace nad systémem souborů. Objekty tříd NS(Mutable)String jsou schopny pracovat s formátem UNICODE, takže podporují znaky prakticky všech ve světě běžně používaných abeced; čeština nebo slovenština pro ně samozřejmě není vůbec žádným problémem.
Objekty třídy NSTimer jsou časovými čítači, které čekají určenou dobu (nebo do určené chvíle) a pak odešlou předem zadanou zprávu předem danému objektu.
OpenStep spravuje databázi standardních hodnot, nabízející programátorům konzistentní a velmi luxusní služby pro určování a volbu standardních nastavení aplikací. Tento mechanismus je velmi zajímavý a ještě se mu budeme podrobněji věnovat; zatím se pouze zmíníme o tom, že přístup k této databázi zajišťuje třída NSUserDefaults.
Poslední dvě třídy FoundationKitu jsou NSValue a NSNumber. První z nich slouží jako objektová abstrakce hodnot všech typů známých z jazyka C a Objective C; druhá (která je jejím dědicem) se omezuje na číselné typy.
Nejzajímavější třídy
V následujících odstavcích se blíže seznámíme s nejzajímavějšími třídami FoundationKitu. Zdaleka nepopíšeme všechny -- to by rozsah textu ani neumožnil, navíc jeho účelem je nabídnout čtenáři podrobnou představu o tom, jak vypadá moderní vývojové prostředí, ale rozhodně ne suplovat kompletní programátorskou dokumentaci. Z téhož důvodu ani vybrané třídy nepopíšeme kompletně; namísto toho se u každé na několika příkladech seznámíme s tím, jak se v OpenStepu programuje.
Příklady budou -- jak jinak -- v Objective C, které je základním programovacím jazykem pro OpenStep. Základní informace o tomto jazyce jsme uvedli již v prvním dílu; připomeňme nyní pouze to, že typ 'id' je odkaz na jakýkoli objekt, typ 'třída *' je odkazem na objekt dané třídy a že konstrukce '[x y]' v Objective C znamená zaslat objektu x zprávu y.
NSArray a NSEnumerator
Nemůžeme samozřejmě alespoň stručně nepopsat tak užitečnou třídu, jakou je bezesporu NSArray. První příklad který si ukážeme bude naprosto triviální -- vytvoříme služby, které budou schopny převádět navzájem čísla a jim odpovídající texty podle předem daného seznamu:
// globální proměnná obsahující seznam
static NSArray *seznam;
// vytvoření seznamu
(void)initSeznam()
{
seznam=[NSArray arrayWithObjects:
@"Jedna",@"Dva",@"Tři",@"Čtyři",
nil];
// vzpomeňme si na garbage
// collector popsaný v minulém
// dílu: protože chceme odkaz
// na seznam zachovat,
// pošleme mu zprávu retain:
[seznam retain];
}
// převod čísla na text
(NSString*)textSIndexem(int i)
{
return [seznam objectAtIndex:i];
}
// převod textu na číslo
(int)indexTextu(NSString *t)
{
return [seznam indexOfObject:t];
}
Vidíme, že při takto triviálním použití není velký rozdíl mezi NSArray a obyčejným céčkovým polem (char *pole[]); přesto NSArray přináší několik drobných výhod: funkce textSIndexem automaticky chrání proti použití špatného indexu (pokud by k němu došlo, ohlásí se chyba prostřednictvím třídy NSException, se kterou se seznámíme později); implementace funkce indexTextu by byla mnohem komplikovanější -- zde bychom museli sami procházet pole a porovnávat řetězce.
Druhý příklad bude trochu složitější, a ačkoli se v něm seznámíme s několika dalšími vlastnostmi třídy NSArray, je jeho hlavním účelem ukázat jak se v Objective C vytvářejí nové třídy. Ukážeme si (nikoli ideální) implementaci triviálního N-árního stromu, tj. datové struktury tohoto typu:
ve které má každý objekt libovolně mnoho 'následníků'. Vytvoříme třídu TreeNode, jejíž objekty budou reprezentovat jednotlivé uzly stromu; každý objekt bude obsahovat odkaz na libovolný jiný objekt (vlastní obsah uzlu) a NSArray svých podřízených uzlů. Předpokládáme, že strom se bude moci dynamicky měnit; použijeme proto dynamickou variantu NSMutableArray:
@interface TreeNode:NSObject
// příkaz @interface uvádí popis nové
// třídy; objekty této třídy budou
// reprezentovat jednotlivé uzly
// stromu. V každém uzlu budou
// uloženy následující proměnné:
{
| id obj; | // objekt v uzlu | |
| NSMutableArray *pole; | // pole následníků |
}
// sama třída TreeNode bude schopna
// zpracovat tuto zprávu:
| +nodeWith:o; | // vytvoří nový uzel |
// každý objekt třídy TreeNode
// bude schopen zpracovat
// tyto zprávy:
| -object; | // vrátí obsah | |
| -childs; | // vrátí následníky | |
| -(void)addNode:n; | // připojí následníka | |
| -(int)count; | // počet uzlů | |
| @end |
Známe-li interface třídy -- tj. její jméno a seznam zpráv, které umí zpracovat -- můžeme již psát programy, které ji využívají. Následující krátký prográmek například vytvoří strom z minulého obrázku (předpokládáme, že A-J jsou nějaké již známé objekty):
id b,d,strom=[TreeNode nodeWith:A];
// vyžádali jsme si vytvoření nejvyššího uzlu
[strom addNode:b=[TreeNode nodeWith:B]];
// vytvořili jsme 2. uzel a připojili jsme jej
// do stromu jako prvního následníka; přitom
// jsme si zapamatovali i jeho id. Podobně
// vytvoříme a zapojíme i ostatní uzly:
[strom addNode:[TreeNode nodeWith:C]];
[strom addNode:d=[TreeNode nodeWith:D]];
[b addNode:[TreeNode nodeWith:E]];
[b addNode:[TreeNode nodeWith:F]];
[d addNode:[TreeNode nodeWith:G]];
[d addNode:[TreeNode nodeWith:H]];
[d addNode:[TreeNode nodeWith:I]];
[d addNode:[TreeNode nodeWith:J]];
// zjistíme počet uzlů ve stromě
NSLog(@"%d",[strom count]);// vypíše 10
// a v jeho podstromě
NSLog(@"%d",[d count]);// vypíše 5
Dosud neznámá funkce NSLog je funkce OpenStepu, která odpovídá funkci printf v klasickém ANSI C; jejím parametrem však je objekt třídy NSString namísto céčkového pole znaků. Podívejme se nyní na implementaci -- tj. skutečné naprogramování -- metod, které budou výše uvedeným zprávám odpovídat. Uvidíme, že bude triviální:
@implementation TreeNode
// příkaz @implementation uvádí
// implementaci třídy. Začneme
// pomocnou metodou initWith:,
// která již existující objekt
// inicializuje
-initWith:o
{
// jméno super v Objective C
// reprezentuje třídu, od které
// dědíme (zde tedy NSObject).
// Nejprve si vyžádáme inicializaci
// všeho, co jsme po této třídě
// zdědili:
[super init];
// a pak si prostě zapamatujeme
// odkaz na objekt o (a kvůli
// garbage collectoru mu
// pošleme zprávu retain):
obj=[o retain];
// připravíme prázdné pole
pole=[[NSMutableArray array] retain];
// jméno self v Objective C
// reprezentuje "objekt který
// právě provádí tento úsek
// programu"; za chvíli
// uvidíme, proč jej vracet
return self;
}
// nyní již snadno připravíme
// metodu nodeWith: prostě si
// standardní metodou alloc
// vyžádáme vytvoření nového
// objektu, a ten pak inicializujeme
// právě vytvořenou metodou
// initWith:
+nodeWith:o
{
// zde využijeme toho, že metoda
// initWith vrací self -- tj. právě
// inicializovaný objekt:
id no=[[TreeNode alloc] initWith:o];
// opět si vzpomeňme na garbage
// collector: všechny metody které
// vytváří objekty je vrací
// 'autoreleasované' (takže
// pokud takový objekt nikdo nepoužije,
// garbage collector jej zruší).
// Protože metoda nodeWith vytváří
// nový objekt, musíme se o
// to postarat odesláním zprávy
// autorelease:
return [no autorelease];
}
// vrácení obsahu/následníků je triviální
-obsah
{
return obj;
}
-childs
{
return pole;
}
// metoda addNode již nedělá nic
// jiného, než přidání nového uzlu do pole:
-(void)addNode:n
{
// zde objektu n nemusíme posílat
// retain, protože to za nás udělá
// samo pole: pokud v něm je nějaký
// objekt, pole si ho samo přidrží.
[pole addObject:n];
}
// zatím žádné '@end' -- budeme pokračovat
Přerušme na chvilku příklad. Stojí za to si uvědomit, že by mohly vzniknout problémy při rušení uzlů stromu -- jistěže bychom mohli kterémukoli jeho uzlu poslat zprávu release nebo autorelease; co by se ale stalo s objektem a s polem, které jsou v něm uloženy? Těm jsme při vytváření uzlů poslali zprávu retain; proto by nebyly zrušeny a zabíraly by paměť dokud by celý program nebyl ukončen. Objective C samozřejmě takovou situaci řeší: objekty nejsou rušeny 'někým jiným', ale ruší se samy v rámci zprávy dealloc; tuto zprávu objektu pošle garbage collector jakmile zjistí, že objekt již může být uvolněn (tj. jakmile mu každý z jeho vlastníků poslal zprávu release nebo autorelease). Nám tedy stačí, implementujeme-li metodu dealloc a v ní uvolníme objekty, které si držíme:
-(void)dealloc
{
[obj release];
[pole release];
// faktické zrušení objektu
// zajistí implementace
// dealloc ve třídě NSObject:
[super dealloc];
}
// pro metodu count použijeme třídu
// NSEnumerator, která zajistí průchod
// všemi prvky pole:
-(int)count
{
id n,en=[pole objectEnumerator];
| int sum=1; | // počítáme sebe sama |
while (n=[en nextObject])
sum+=[n count];
return sum;
}
@end
Pro lepší pochopení služeb třídy NSEnumerator si ukážeme o něco méně pohodlnou a méně efektivní implementaci metody count, využívající přímého přístupu k objektům pole podle jejich indexů:
-(int)count
{
int i,n,sum=1;
// počet podřízených uzlů
n=[pole count];
for (i=0;i<n;i++)
sum+=[[pole objectAtIndex:i] count];
return sum;
}
Několik dalších možností Objective C i třídy NSArray si ukážeme na implementaci zprávy isEqual:. Libovolný objekt OpenStepu je schopen porovnat sám sebe s jiným objektem na základě této zprávy; je proto vhodné to 'naučit' i náš nový objekt. Zároveň si ukážeme prostředky, které Objective C nabízí pro zjištění je-li neznámý objekt objektem určité třídy:
-(BOOL)isEqual:o
{
// pokud objekt o není objektem
// téže třídy jako "my", jistě
// není stejný. Zeptáme se jej tedy!
// Vlastní třídu zjistíme také
// snadno, stačí pošleme-li "sami
// sobě" zprávu class:
if (![o isKindOfClass:[self class]])
return NO;
// víme-li již, že objekt je téže
// třídy, porovnáme jeho obsah s
// "naším" obsahem:
if (![obj isEqual:[o object]])
return NO;
// zbytek zařídí NSArray:
return [pole isEqual:[o childs]];
}
Stojí za to si podrobně ukázat, co se vlastně stane v rámci posledního příkazu. Je zřejmé, že porovnáváme "náš" objekt NSArray obsahující "naše" následníky s obdobným objektem uvnitř objektu o. Implementace isEqual: ve třídě NSArray nejprve ověří, jsou-li oba objekty skutečně pole a mají-li stejný počet prvků; je-li tomu tak, porovná postupně odpovídající prvky obou objektů -- jednoduše tak, že opět využije zprávy isEqual.
Nakonec si můžeme uvědomit, že práce s obsahem stromu není příliš pohodlná -- chceme-li cokoli dělat s obsahem některého uzlu, musíme si od něj nejprve obsah vyžádat zprávou object a pak jej teprve zpracovat. -- ukažme si několik příkladů:
// výpis obsahu uzlu (obsahuje-li strom stringy)
NSlog(@"%s",[[uzelStromu object] cString]);
// přičtení číselné hodnoty uzlu
sum+=[[uzelStromu object] intValue];
Nabízí se myšlenka, zda bychom nemohli nějak "našvindlovat", aby se uzly stromu dokázaly samy chovat jako objekty v nich obsažené, takže výše uvedené příklady by bylo možné zapsat přímo a přehledně:
NSlog(@"%s",[uzelStromu cString]);
sum+=[uzelStromu intValue];
V Objective C díky jeho dynamickému objektovému mechanismu to skutečně možné je, a implementaci -- ostatně velmi jednoduchou -- si ukážeme při popisu třídy NSInvocation.