Zpět Obsah Další

Foundation Kit: konkrétní třídy 4


V posledním přehledu tříd Foundation Kitu — a vlastně i vůbec posledním dílu našeho seriálu, věnovaném Foundation Kitu — se podíváme na několik zbývajících nesmírně zajímavých tříd; zběžně se seznámíme i s několika málo neobjektovými službami, jež Foundation Kit nabízí.

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ízejí, v ukázkách programů:

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

Požadujeme-li bohatší 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]];
// datum lze číst z libovolného zadaného formátu
x=[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
y=[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];

NSException

Použití třídy NSException bude zcela jasné těm, kdo znají výjimky např. z C++ nebo z Javy. 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 výjimkami.

Základem obsluhy chyb v API Cocoa 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čí:

FK4_Exceptions

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 (to je hlavní důvod pro využití speciálních maker — standardní obsluha výjimek např. v C++ tohle zajistit v žádném případě nedokáže). Jinými slovy, je-li z chráněného úseku kódu vyvolána funkce nebo metoda nebo služba serveru, a uvnitř ní dojde k ohlášení chyby, předá se řízení vždy na stejné místo (NS_HANDLER).

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 pomocí zpráv name a reason. Prostřednictvím tohoto objektu můžeme také pohodlně chybu předat na vyšší úroveň. Podívejme se na 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 i 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 API Cocoa, 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ř chybové 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 textových řetězců "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

Ačkoli v praxi využívaná poměrně zřídkakdy, stojí třída NSInvocation za samostatnou zmínku, protože jejím prostřednictvím máme v API Cocoa k dispozici nesmírně silný nástroj — přesměrování zpráv. Možnosti, jež nám tato služba nabízí, si programátoři ve statických prostředích typu C++ obvykle vůbec nedokážou představit. Podívejte se na pátý a šestý příklad z popisu Objective C pro ilustraci možností, jež přesměrování zpráv dává!

Třída NSInvocation však dovoluje daleko více — díky ní můžeme za běhu dynamicky 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:

NSMutableDictionary *dict=...;
NSString *a=...,*b=...;

// selektor požadované zprávy:
SEL sel=@selector(setObject:forKey:);
// signatura (popis argumentů) požadované zprávy:
NSMethodSignature *sig=[NSMutableDictionary instanceMethodSignatureForSelector:sel];
// odpovídající objekt NSInvocation:
NSInvocation *inv=[NSInvocation invocationWithMethodSignature:sig];
// základní atributy — cílový objekt, zpráva:
[inv setSelector:sel]; [inv setTarget:dict];
// nastavíme parametry — objekty a a b:
[inv setArgument:&a atIndex:2];
[inv setArgument:&b atIndex:3];
// odešleme zprávu:
[inv invoke];
// pokud by zpráva vrátila nějakou hodnotu,
// získali bychom ji také snadno:
// [inv getReturnValue:...];

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ů.

NSNotification, NSNotificationCenter, NSNotificationQueue

Úkolem tří tříd NSNotification... je zajistit předávání zpráv i v případech, kdy objekt který zprávu odesílá neví, které objekty ji mají dostat. Můžeme to vyjádřit také jinak — zatímco v klasickém mechanismu předávání zpráv volí jak obsah zprávy tak i jejího příjemce odesilatel, umožňuje Cocoa aby odesilatel zvolil pouze obsah zprávy, zatímco příjemce sám určí podle obsahu které všechny zprávy chce dostávat. Pro dynamické systémy, složené z řady samostatných modulů, jež jsou za běhu podle potřeby spojovány, je tato možnost nesmírně výhodná, a programátorům ušetří obrovské množství práce.

Jednou ze zásadních výhod tohoto přístupu je to, že není zapotřebí navazovat přímé spojení mezi samostatnými programovými moduly. Vazba mezi takovými moduly může být realizována pouze prostřednictvím smluvených jmen zpráv; to zvyšuje flexibilitu takových modulů a snižuje pravděpodobnost programových chyb. Podívejte se na obrázek:

FK4_Notification1

V levém horním rohu vidíme klasický přístup, kdy spolu objekty A a B komunikují přímo. Jsou-li oba objekty pevnými částmi jediného modulu, je to samozřejmě optimální; jestliže se však jedná o objekty z různých modulů, které se mohou dynamicky spojovat a oddělovat, mohou nastat při chybě programátora nepříjemné problémy — typické situace vidíme níže v levé části obrázku: korektní navázání spojení z objektu B na objekt A, ale chybné spojení v opačném směru, nebo odstranění objektu B aniž by byla zároveň zrušena vazba uvnitř objektu A. Při vazbě prostřednictvím třídy NSNotification nic takového nehrozí — žádné explicitní vazby mezi objekty totiž nejsou. Oba objekty pouze vysílají a přijímají zprávy "Xyz"; o korektní doručení — je-li vůbec komu doručovat — se postará sám systém.

Další významnou výhodou je to, že můžeme snadno, bezpečně a bez zvláštní programátorské práce zajistit rozeslání zprávy více objektům:

FK4_Notification2

V levé části obrázku opět vidíme klasický přístup, při kterém objekt musí využít služeb třídy NS(Mutable)Array a starat se o udržování jejího obsahu (obrázek ukazuje i nebezpečí nekorektního obsahu pole — čtvrtý objekt B byl zrušen, ale pole o tom "neví"). Pravá část obrázku znovu ukazuje bezproblémový přístup prostřednictvím třídy NSNotification — o nic se nemusíme starat; objekt A prostě odešle zprávu "Xyz" a systém ji předá všem objektům, které mají o zprávu "Xyz" zájem.

Konkrétní API je nesmírně jednoduché — objekt, který má zájem o přijímání zpráv, se zaregistruje u centra jako "observer" takto:

NSNotificationCenter *nc=[NSNotificationCenter defaultCenter];
[nc addObserver:obj selector:@selector(msg:) name:@"Xyz" object:nil];

Kdokoli pak kdykoli může odeslat zprávu takto:

[[NSNotificationCenter defaultCenter] postNotificationName:@"Xyz" object:self];

a objekt obj (a kdokoli další, kdo se zaregistroval pro příjem zpráv "Xyz") ji automaticky dostane prostřednictvím vlastní metody msg: (již uvedl v registraci).

NSUserDefaults

Dříve, než se začneme seznamovat s konkrétními službami třídy NSUserDefaults, si musíme vysvětlit, co to vlastně jsou a jak fungují v API Cocoa uživatelské předvolby — pokud vím, žádný jiný systém zatím podobnou službu nenabízí, takže většina čtenářů tohoto článku zřejmě nebude mít s ničím podobným zkušenosti.

Základním prvkem předvoleb je dvojice klíč-hodnota, podobně, jako u třídy NSDictionary. Klíč vždy určuje jméno předvolby (např. "BarvaTextuVHlavnímOkně"), zatímco hodnota určuje skutečnou hodnotu předvolby ("Black"). Dvojice jsou uloženy v tzv. doménách; doménu si můžeme představit jako NSDictionary, obsahující libovolný počet dvojic. Systém uživatelských předvoleb pak spravuje libovolný počet pojmenovaných domén v předem zadaném pořadí; chceme-li vyhledat hodnotu k zadanému klíči, prohledává třída NSUserDefaults postupně domény podle jejich pořadí, až nalezne odpovídající dvojici. Celá skupina domén přitom může obsahovat hodnoty specifické pro každého uživatele.

Neurčíme-li jinak, obsahuje objekt třídy NSUserDefaults následující domény v uvedeném pořadí:

  1. argumenty: doména argumentů je vytvořena při spuštění aplikace na základě parametrů příkazové řádky a slouží především pro ladicí účely — jejím prostřednictvím můžeme specifikovat jakékoli požadované předvolby pouze pro jediné spuštění aplikace;
  2. aplikace: aplikační doména obsahuje všechny předvolby, specifické pro danou aplikaci. Je druhá v pořadí; to znamená, že předvolby v ní uložené mají přednost před předvolbami uloženými ve všech ostatních doménách vyjma domény argumentů;
  3. globální: globální doména obsahuje předvolby, které uživatel specifikuje pro všechny aplikace — je-li např. v globální doméně uvedena dvojice ("NSApplicationFontSize",12), budou všechny aplikace — jež nemají jiné nastavení v aplikační doméně ani v doméně argumentů — používat standardně dvanáctibodové písmo;
  4. jazykové: domény odpovídající specifickým jazykům, v pořadí jež může uživatel měnit na úrovni systémových předvoleb.V těchto doménách budou samozřejmě obvykle uloženy předvolby související se zvoleným jazykem — např. v doméně "Czech" bude třeba předvolba ("NSApplicationFont","HelveticaCE"), která zajistí, že aplikace která běží v češtině bude standardně využívat českou Helvetiku;
  5. poslední doménou je tzv. doména registrační; ta slouží k určení standardních hodnot, které se použijí jestliže není požadovaná předvolba uložena nikde jinde.

Kromě toho můžeme vytvářet vlastní domény podle potřeby. Každá doména je buď persistentní nebo dočasná; obsah persistentních domén je uložen na disku, zatímco obsah dočasných domén zanikne ve chvíli ukončení aplikace. Mezi standardními doménami jsou dvě dočasné — argumentová a registrační; všechny ostatní jsou persistentní. Vytváříme-li vlastní domény, závisí pouze na nás, jakého budou typu.

Ukažme si opět konkrétní příklad zdrojového textu. Jedna z prvních věcí které každá aplikace udělá bude pravděpodobně získání standardního objektu třídy NSUserDefaults obsahujícího standardní domény, a nastavení hodnot domény registrační (která je po startu aplikace samozřejmě prázdná):

NSUserDefaults *def=[NSUserDefaults standardUserDefaults];
[def registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:
  @"12",
@"Počet",
  
@"YES",@"JeToTak?",
  
@"Pepa z depa",@"Jméno",
nil]];

Kdekoli v aplikaci pak můžeme volně používat hodnoty z databáze; pro tento účel máme k dispozici řadu zpráv, mezi kterými jsou např. tyto:

NSLog(@"Počet je %d",[def integerForKey:@"Počet"]);
NSLog([def boolForKey:@"JeToTak?"]?@"Je to pravda":@"Nejni to pravda!");
NSLog(@"Jméno: %@",[def stringForKey:@"Jméno"]);
id o=[def objectForKey:@"Tralala"];
if (o) ...
else NSLog(@"Default \"Tralala\" neexistuje");

Uvědomme si, co se v takovém případě vlastně děje: jakmile pošleme objektu def např. zprávu integerForKey:@"Počet", začnou se prohledávat domény uživatele, který aplikaci spustil, v pořadí daném pořadím domén uložených v objektu def. Standardně se tedy nejprve zjistí, nebyl-li při spuštění aplikace použit parametr "Počet=n" (argumentová doména), pak se klíč "Počet" postupně hledá v aplikační doméně, v doméně globální a v doménách jazykových. Jestliže není nalezen v žádné z nich, použije se nakonec hodnota 12 z domény registrační, do které byla uložena při startu aplikace.

Aplikace samozřejmě může měnit obsah své aplikační domény (jakmile se uživatel rozhodl některou z předvoleb v jejím rámci změnit):

[def setBool:NO forKey:@"JeToTak?"];
[def setInteger:333 forKey:@"Počet"];
[def setObject:anyObject forKey:@"Tralala"];

Co ale v případě, že jeden modul změní hodnoty v doménách a jiný je využívá? Uvědomme si, že pro takové případy již známe dokonalé řešení — třídu NSNotificationCenter, se kterou jsme se seznámili před chvilkou: jakmile dojde ke změně předvoleb, je prostřednictvím této třídy rozeslána zpráva "NSUserDefaultsChanged". Pokud tedy některý objekt potřebuje být informován o aktuálním stavu předvoleb, stačí, vyžádá-li si zprávou addObserver... zasílání zpráv "NSUserDefaultsChanged".

Třída NSUserDefaults nabízí řadu dalších metod pro správu persistentních i dočasných domén, pro přidávání i odebírání domén ze standardního seznamu, pro rušení existujících domén a vytváření nových, pro přístup k předvolbám jiných uživatelů a tak dále. Nemá smysl je zde podrobně popisovat, protože jsou využívány jen zřídka.

Ostatní služby FoundationKitu

Ačkoli je Foundation Kit především objektová knihovna, existuje přece jen několik služeb, které je daleko pohodlnější reprezentovat klasickými funkcemi jazyka C. Většina těchto funkcí je pouze nadstavbou nad jádrem operačního systému:

První skupina funkcí umožňuje programátorům užší spolupráci se systémem virtuální paměti: funkce NSPageSize vrací velikost jedné stránky, zatímco funkce NSRoundDownToMultipleOfPageSize a NSRoundUpToMultipleOfPageSize umožňují pohodlné zaokrouhlování na hranice stránek. Virtuální paměť můžeme i fakticky alokovat funkcí NSAllocateMemoryPages a uvolňovat pomocí funkce NSDeallocateMemoryPages; rychlé kopírování rozsáhlých úseků dat ve virtuální paměti umožňuje funkce NSCopyMemoryPages. Minimální praktický smysl naproti tomu má služba NSRealMemoryAvailable, která zjistí rozsah reálné paměti, instalované v počítači; tu a tam se ale může hodit, např. pro automatické optimalizování výpočtu.

Foundation Kit definuje řadu typů, které reprezentují bod, obdélník, rozměry blíže neurčeného objektu a podobně; k dispozici jsou funkce pro všechny potřebné geometrické operace — jako je ověření je-li bod součástí obdélníku, vyhledání průniku dvou obdélníků, vytvoření minimálního obdélníku obsahujícího dva zadané obdélníky... Nebudeme plýtvat místem v tomto článku vyjmenováváním všech geometrických funkcí (je jich více než třicet); programátor mezi nimi nalezne vše, co může při práci s body nebo obdélníky v ploše potřebovat.

Funkce NSUserName vrátí jméno uživatele, v rámci jehož konta program pracuje; podobně pomocí funkce NSHomeDirectory můžeme zjistit, kde leží domovský adresář aktivního uživatele a pomocí funkce NSHomeDirectoryForUser nalezneme domovský adresář kteréhokoli uživatele. Pomocí funkce NSLocalizedString získáme lokalizovanou verzi libovolného textového řetězce v jazyce, zvoleném uživatelem pro komunikaci (pokud je samozřejmě překlad součástí překladových tabulek).

Nakonec se můžeme zmínit o čtyřech velmi zajímavých funkcích, které umožňují v případě potřeby jazyk Objective C vlastně interpretovat (tj. vyhodnocovat jeho výrazy za běhu): funkce NSClassFromString vyhledá třídu zadaného jména a vrátí ji; podobně funkce NSSelectorFromString za běhu přeloží zprávu na selektor. Inverzní funkce NSStringFromClass a NSStringFromSelector pak umožňují zjistit jméno třídy nebo zprávu za běhu.

Shrnutí

Dnešním dílem jsme dokončili popis Foundation Kitu. V příštím dílu se začneme zabývat grafickým uživatelským rozhraním aplikací, a podíváme se blíž na unikátní prostředek API Cocoa, na InterfaceBuilder.


Zpět Obsah Další

Copyright © Chip, O. Čada 2000-2003