NSBundle

Úkolem třídy NSBundle je vyhledávat a načítat z disku soubory, obsahující pomocná data víceméně libovolného typu -- od obrázků nebo zvuků až po zaveditelné části programu. Přitom služby třídy NSBundle automaticky zajistí i lokalizaci podle uživatelských preferencí. Ukažme si několik typických příkladů:

// tzv. main bundle reprezentuje
// adresář aplikace:
NSBundle *b=[NSBundle mainBundle];
NSLog(@"Aplikace je v adresáři %@",[b path]);
// v adresáři aplikace můžeme vyhledávat
// soubory
NSString *fname=[b pathForResource:@"ÚvodníText" ofType:@"ascii"];

První příklad je zřejmý -- třída NSBundle vytvoří objekt, který odpovídá adresáři aplikace a chceme-li, můžeme zjistit a vytisknout jméno tohoto adresáře (konverze "%@" v printf-formátech znamená "vytisknout objekt"; je-li objektem string, vytiskne se jeho obsah). Abychom smysl této služby zcela pochopili, musíme si uvědomit, že v OpenStepu je aplikace reprezentována adresářem, který obsahuje jak vlastní program, tak i všechny potřebné pomocné soubory (správce souborů automaticky takovéto adresáře rozpozná a prezentuje je jako celistvé soubory). Máme-li tedy například v adresáři "/Users/oc/Aplikace" uloženou aplikaci Test, je tato aplikace reprezentována adresářem "/Users/oc/Aplikace/Test.app" a jméno tohoto adresáře by také vypsala funkce NSLog z našeho příkladu.

Druhý příklad ukazuje vyhledání pomocného souboru -- bude-li tedy ve výše uvedeném případě uvnitř adresáře aplikace Test ležet soubor "Resources/ÚvodníText.ascii" (podadresář Resources je určen pro pomocné soubory aplikace), vrátí služba pathForResource:ofType: string "/Users/oc/Aplikace/Test.app/Resources/ÚvodníText.ascii". Tak tomu bude v případě, že úvodní text nemusí být lokalizován. V -- daleko pravděpodobnějším -- případě, že úvodní text lokalizován je, bude situace složitější. Uvnitř aplikačního adresáře může být libovolně mnoho podadresářů nazvaných <jméno jazyka>.lproj; každý z nich bude obsahovat lokalizovanou verzi souboru "ÚvodníText.ascii" pro jazyk, odpovídající názvu adresáře lproj -- celá situace tedy může v aplikaci lokalizované do angličtiny, němčiny a češtiny vypadat např. takto:

/Users/oc/Aplikace/Test.app/Resources
English.lproj
ÚvodníText.ascii (obsahuje "Welcome ...")
German.lproj
ÚvodníText.ascii (obsahuje "Willkommen ...")
Czech.lproj
ÚvodníText.ascii (obsahuje "Vítáme vás ...")

Každý uživatel si může v preferencích určit vlastní pořadí jazyků, podle toho, jakému jazyku dává přednost:

Třída NSBundle pak při vyhledávání zadaného souboru nejprve prochází všechny zvolené jazyky v uživatelem zadaném pořadí, a nalezne-li soubor v odpovídajícím lproj podadresáři, vrátí jej. V tomto případě by tedy služba z posledního příkladu do proměnné fname uložila jméno "/Users/oc/Aplikace/Test.app/Resources/Czech.lproj/ÚvodníText.ascii"; pokud by však tutéž aplikaci spustil uživatel, který preferuje němčinu před češtinou, vrátila by služba řetězec "/Users/oc/Aplikace/Test.app/Resources/German.lproj/ÚvodníText.ascii".

Třída NSBundle umožňuje řadu dalších kouzel -- můžeme např. vytvořit "bundle" pro libovolný zadaný adresář (což je výhodné v případě, že má podobná struktura s lokalizovanými zdroji sloužit více aplikacím zároveň), NSBundle umí na základě překladových tabulek uložených v 'lproj' podadresářích automaticky překládat texty do jazyka, ve kterém aplikace běží a podobně. Prostřednictvím třídy NSBundle můžeme také k programu připojovat další, samostatně přeložené úseky programu (ať již byly vytvořeny spolu s aplikací a slouží jako overlaye, nebo jsou vytvářeny dodatečně pro doplnění služeb, jež aplikace poskytuje).



NSDate a NSCalendarDate

O třídách NSDate a NSCalendarDate, které slouží pro práci s datem a časem, nebudeme zbytečně vykládat -- namísto toho si prostě ukážeme několik z mnoha služeb, které nabízí, v ukázkách programů:

dnes=[NSDate date];
// interval se zadává ve vteřinách:
vcera=[NSDate dateWithTimeIntervalSinceNow:-24*60*60];
// občas potřebujeme datum "určitě starší než
// cokoli jiného":
davno=[NSDate distantPast];
// jiné systémy udávají datum od 1.1.1970 -- převod
// je snadný:
long unixTime=time(NULL);
dnes=[NSDate dateWithTimeIntervalSince1970:unixTime];

Požadujeme-li luxusnější služby, použijeme NSCalendarDate, který navíc k datu připojuje informaci o zóně a požadovaném formátu zobrazení:

// můžeme pracovat s časovými zónami
dnesUS=[dnes
dateWithCalendarFormat:@"%m/%d/%y"   // 12/31/95
timeZone:[NSTimeZone timeZoneWithName:@"US/Pacific"]];
dnesTady=[dnes
dateWithCalendarFormat:@"%d. %m. %Y" // 31. 12. 1995
timeZone:[NSTimeZone localTimeZone]];
// NSCalendarDate umí dekódovat datum z
// libovolného zadaného formátu
zadano=[NSCalendarDate
dateWithString:@"31.12. léta Páně 95"
calendarFormat:@"%d.%m. léta Páně %y"];
// všechny údaje můžeme určit i přímo
zadano2=[NSCalendarDate
dateWithYear:1995 month:12 day:31
hour:12 minute:55 second:5
timeZone:[NSTimeZone localTimeZone]];
// každý údaj je i samostatně k dispozici, včetně
// údajů odvozených
NSLog(@"Je %d. %d, %d. den v roce, %s",
[dnesTady dayOfMonth],
[dnesTady monthOfYear],
[dnesTady dayOfYear],
poleJmenDnu[[dnesTady dayOfWeek]]);
// interval pro relativní datum lze zadat obecně
lonskyZitrekZaDvaMesicePredHodinouTady=[dnesTady
addYear:-1 month:2 day:1
hour:-1 minute:0 second:0];



NSDictionary

Základní službou třídy NSDictionary je vytvářet tabulky dvojic <klíč,hodnota> a pak v nich podle klíče vyhledávat -- a to v lineárním čase (NSDictionary udržuje údaje v hashovací tabulce). Nejjednodušším příkladem může být např. převodník anglických jmen dní na česká:

static NSDictionary *mesice;

void inicializace()
{
mesice=[NSDictionaryWithObjects:
[NSArray arrayWithObjects:
@"Pondělí",@"Úterý",@"Středa",@"Čtvrtek",
@"Pátek",@"Sobota",@"Neděle",nil]
forKeys:
[NSArray arrayWithObjects:
@"Monday",@"Tuesday",@"Wednesday",@"Thursday",
@"Friday",@"Saturday",@"Sunday",nil]
];
[mesice retain];
}

NSString* czJmenoPro(NSString *ukJmeno)
{
return [mesice objectForKey:ukJmeno];
}

Ve druhém příkladu si ukážeme, jak se pracuje s třídou NSMutableDictionary, umožňující práci s proměnnou tabulkou. Dejme tomu, že chceme používat strom z příkladu z minulého dílu, a navíc potřebujeme mít k dispozici velmi rychlou metodu, která pro zadaný objekt nalezne uzel stromu, ve kterém je objekt umístěn. Je jasné, že klasické rekurzivní prohledávání stromu nemůžeme použít -- fungovalo by samozřejmě dobře, ale pro velké stromy příliš pomalu. Pomůžeme si proto jinak -- doplníme strom pomocnou globální tabulkou NSMutableDictionary, do které budeme ukládat dvojice <objekt, uzel>. Předpokládejme, že tabulka již existuje a je uložena v globální proměnné prvkyStromu; pak stačí upravíme-li metody addNode: a dealloc takto:

-(void)addNode:n
{
[pole addObject:n];
[prvkyStromu setObject:n forKey:[n object]];
}
-(void)dealloc
{
[prvkyStromu removeObjectForKey:obj];
[obj release];
[pole release];
[super dealloc];
}

Metoda addNode: nyní automaticky zařadí nově vkládaný objekt do tabulky; metoda dealloc naopak dvojici (identifikovanou klíčem -- tj. objektem, který je v uzlu uložen) z tabulky odstraní. Chceme-li nyní zjistit, který uzel stromu odpovídá objektu x, stačí provést

node=[prvkyStromu objectForKey:x];

Nakonec si ukážeme, jak lze procházet všechny objekty, uložené v tabulce -- slouží k tomu opět třída NSEnumerator, kterou již známe z popisu třídy NSArray. Pokud bychom např. chtěli vypsat všechny objekty, které jsou ve stromě uloženy -- bez ohledu na umístění -- mohli bychom použít následující úsek kódu:

NSEnumerator *en=[prvkyStromu keyEnumerator];
id o;

while (o=[en nextObject])
NSLog(@"%@\n",o);



NSException

Použití třídy NSException bude křišťálově jasné těm, kdo znají nelokální obsluhu chyb např. z C++. Pokusíme se však mechanismus popsat tak, aby mu rozuměli i ti čtenáři, kteří zatím neměli možnost pracovat v žádném systému s nelokální obsluhou chyb.

Základem obsluhy chyb v OpenStepu jsou makra NS_DURING, NS_HANDLER, NS_ENDHANDLER a sama třída NSException. Celý mechanismus funguje tak, že kód mezi makry NS_DURING a NS_HANDLER je chráněný; dojde-li v něm kdekoli k chybě (ohlášené prostřednictvím třídy NSException), předá se řízení ihned na kód obsluhy chyby, který leží mezi makry NS_HANDLER a NS_ENDHANDLER. Jestliže k chybě nedojde, kód mezi NS_HANDLER a NS_ENDHANDLER se prostě přeskočí:

Na obrázku tenká černá šipka ukazuje předání řízení po chybě, zatímco široká tmavě šedá šipka ukazuje průběh programu pokud k chybě nedojde.

Je-li obsluha chyby umístěna ve stejné funkci nebo metodě jako její ohlášení, stačilo by pro dosažení stejného efektu prosté goto; výhoda třídy NSException však spočívá v tom, že korektně pracuje i v případě, že zdroj chyby je v jiné funkci, nebo -- při využití systému distribuovaných objektů -- třeba i v jiném procesu na úplně jiném počítači. Jinými slovy, je-li z chráněného úseku kódu vyvolána funkce nebo metoda a uvnitř ní dojde k ohlášení chyby, předá se řízení na stejné místo.

Uvnitř kódu pro obsluhu chyby -- tj. mezi makry NS_HANDLER a NS_ENDHANDLER -- máme k dispozici lokální proměnnou exception, která obsahuje objekt třídy NSException který chybu vyvolal -- a od kterého si můžeme vyžádat její podrobnější popis. Prostřednictvím tohoto objektu můžeme také pohodlně chybu předat na vyšší úroveň. Obě situace ilustruje následující příklad:

void fncA(void)
{
...
// dojde-li k chybě...
if (/*error*/)
// ohlásíme ji!
[NSException raise:@"A" format:@"nic extra"];
// jestliže došlo k chybě, následující
// kód se již neprovede
...
}
void fncB(int i)
{
fncA();
...
if (/*error*/)
// při hlášení chyby můžeme uvést parametry
[NSException raise:@"B" format:@"parametr=%d",i];
...
}
void main()
{
int i;

NS_DURING
...
for (i=0;i<X;i++)
fncB(i);
...
if (/*error*/)
[NSException raise:@"main" format:@"taky nic"];
...
NS_HANDLER
if ([[exception name] isEqual:@"A"])
NSLog(@"Chyba ve fncA, ");
else if ([[exception name] isEqual:@"B"])
NSLog(@"Chyba ve fncB, ");
else if ([[exception name] isEqual:@"main"])
NSLog(@"Chyba v main, ");
NSLog(@"%@",[exception reason]);
NS_ENDHANDLER
}

Dojde-li ke kterékoli z chyb, předá se řízení okamžitě na obsluhu ve funkci main a vypíše se jeden z textů "Chyba ve fncA, nic extra", "Chyba ve fncB, parametr=X" (kde X je hodnota parametru, při které k chybě došlo)  nebo "Chyba v main, taky nic". Protože za NS_ENDHANDLER již není žádný kód, skončí po výpisu chyby ihned celý program. Přesně stejně by celý systém pracoval kdyby na místě funkcí fncA a fncB byly metody (vyvolané odesláním zprávy nějakému objektu), a to i v případě, že zprávy posíláme prostřednictvím distribuovaných objektů do jiného procesu. Hlášení chyb prostřednictvím třídy NSException samozřejmě využívají i všechny třídy OpenStepu, takže např. program

void main()
{
NSArray *a=[NSArray arrayWithObjects:@"A",@"B",nil];

NS_DURING
[a objectAtIndex:3];
NS_HANDLER
NSLog(@"Chyba %@ %@\n",
[exception name],[exception reason]);
NS_ENDHANDLER
}

vypíše "Chyba NSRangeException *** objectAtIndex:: index (3) beyond bounds (2)".

Ukažme si ještě možnost víceúrovňového zpracování chyb. Předpokládejme, že vnořená funkce se o některé chyby, ke kterým může dojít, dokáže postarat sama; jiné však obsloužit neumí a proto je předá "vyšší instanci". Příklad odpovídajícího programu by mohl vypadat např. takto:

void fncA(void)
{
NS_DURING
...
if (/*error1*/)
[NSException raise:@"A1" format:nil];
...
if (/*error2*/)
[NSException raise:@"A2" format:nil];
...
NS_HANDLER
if ([[exception name] isEqual:@"A1"]) {
// lokální obsluha chyby
...
} else
// předáme chybu výše
[exception raise];
if (/*error3*/)
[NSException raise:@"A3" format:nil];
NS_ENDHANDLER
}
void main()
{
NS_DURING
...
fncA();
...
NS_HANDLER
NSLog(@"chyba %@ -- %@",
[exception name],[exception reason]);
NS_ENDHANDLER
}

V tomto případě může dojít ke třem různým chybám. U chyb A1 a A2 se řízení vždy předá na obsluhu uvnitř fncA; ta sama nějak zpracuje případ že došlo k chybě A1, ale chybu A2 beze změny předá výše. Navíc může dojít k chybě A3 přímo uvnitř obsluhy; taková chyba se samozřejmě také hlásí výše. To znamená, že obslužné rutině ve funkci main se předá řízení po chybě A2 a po chybě A3.

Nakonec poznamenejme, že objekt NSException může kromě dvou stringů "name" a "reason" (druhý z nich se vytvoří při hlášení chyby na základě formátu a parametrů) obsahovat navíc zcela libovolné údaje, určující blíže proč a jak k chybě došlo; tyto údaje může do objektu uložit kód který chybu hlásí a obslužný handler je pak může zpracovat.



NSInvocation

V minulém dílu našeho seriálu jsme si slíbili, že se seznámíme se způsobem jak "našvindlovat" aby se uzel stromu (tj. objekt třídy TreeNode) choval stejně jako objekt v něm obsažený (jinými slovy, aby uměl zpracovat stejné zprávy). V Objective C je díky jeho plně dynamickým objektům máloco jednoduššího -- stačí přimět objekt třídy TreeNode, aby jakoukoli zprávu kterou nezná předal ke zpracování svému vloženému objektu. Zařídíme to jednoduše -- stačí do implementace třídy TreeNode přidat následující kratičkou metodu:

-(void)forwardInvocation:(NSInvocation*)inv
{
[inv invokeWithTarget:obj];
}

To je vše. Jestliže totiž pošleme jakémukoli objektu zprávu, kterou objekt není schopen zpracovat, zkonstruuje runtime systém Objective C objekt třídy NSInvocation, reprezentující zaslanou zprávu a její parametry, a objektu pošle zprávu forwardInvocation:. Jestliže v ní pošleme objektu třídy NSInvocation zprávu invokeWithTarget:, třída NSInvocation zajistí předání téže zprávy objektu, specifikovanému jako 'target'.

Třída NSInvocation však dovoluje daleko více -- díky ní můžeme za běhu zkonstruovat objekt reprezentující předání libovolné zprávy s libovolnými parametry libovolnému jinému objektu, a tuto akci pak kdykoli realizovat. Ukažme si triviální příklad:

NSDictionary *dict=...
...
// OpenStep nabízí makro NS_MESSAGE, které usnadní
// vytvoření "popisu" zprávy (tj. získání informace
// o její návratové hodnotě, počtu a typech parametrů
// apod. -- informace se automaticky získají od
// objektu, kterému později zprávu zašleme
NSInvocation *inv=NS_MESSAGE(dict,setObject:forKey:);

// nastavíme parametry -- objekty a a b:
[inv setArgument:a atIndex:2];
[inv setArgument:b atIndex:3];
// stejně snadno bychom mohli měnit cílový objekt
// (zatím dict) nebo zprávu, která bude
// odeslána (zatím setObject:forKey:)
...
// odešleme zprávu ...
[inv invoke];
// pokud by zpráva vrátila nějakou hodnotu,
// získali bychom ji také snadno:
// [inv getRetunValue:xxx];

V praxi se samozřejmě s takto složitým (nebo ještě složitějším) využitím třídy NSInvocation setkáme jen málokdy; málokdy ale také potřebujeme konstruovat odeslání libovolné zprávy s libovolnými parametry libovolnému objektu za běhu, aniž bychom v době překladu znali třeba jen počet a typy parametrů.





(další článek)


Copyright (c) Ondra Čada