Nastal čas na kakao - Zpracování výjimek: NSException - 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:

Seriály

Více seriálů



Informace

Nastal čas na kakao - Zpracování výjimek: NSException

6. dubna 2005, 00.00 | Nejprve si slíbíme, že se ke třídám NSDate a NSCalendarDate (respektive hlavně k té první z nich) vrátíme později. Proč to? Inu... právě okolo práce s daty je řada poměrně významných změn v nové versi operačního systému, Mac OS X 10.4 Tiger, jež už bezmála klepe na dveře: nemá proto smysl si teď podrobně popisovat už vlastně zastaralé API, abychom za měsíc nebo dva popisovali nové – raději posečkáme a k datům se vrátíme hned, jak bude Tygr puštěn z klece NDA .

Nejprve si slíbíme, že se ke třídám NSDate a NSCalendarDate (respektive hlavně k té první z nich) vrátíme později. Proč to? Inu... právě okolo práce s daty je řada poměrně významných změn v nové versi operačního systému, Mac OS X 10.4 Tiger, jež už bezmála klepe na dveře: nemá proto smysl si teď podrobně popisovat už vlastně zastaralé API, abychom za měsíc nebo dva popisovali nové – raději posečkáme, a k datům se vrátíme hned, jak bude Tygr puštěn z klece NDA . My se ostatně ještě dnes s podobnou situací setkáme jednou, na konci dnešního článku.

Vynecháme-li tedy prozatím třídy NSDate a NSCalendarDate, jsou na řadě výjimky a třída NSException.

Co jsou to výjimky?

Koncepce výjimek jako takových i vlastní použití služeb 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 (nebo alternativně direktivy @try/@catch, popsané níže – prozatím se jimi nebudeme zabývat). Celý mechanismus funguje jednoduše:

  • kód mezi makry NS_DURING a NS_HANDLER je chráněný;
  • to znamená, že 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 žádné chybě nedojde, obslužný kód chyby mezi NS_HANDLER a NS_ENDHANDLER se prostě přeskočí.

Celou situaci dobře ilustruje standardní obrázek z dokumentace Apple:

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

Samozřejmě, je-li obsluha chyby umístěna ve stejné funkci nebo metodě jako její ohlášení zprávou raise – jako tomu je na obrázku – 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 (a to je také hlavní důvod pro využití speciálních maker nebo direktiv — standardní obsluha výjimek zabudovaná do jazyka 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).

Konkrétní služby

Uvnitř kódu pro obsluhu chyby — tj. mezi makry NS_HANDLER a NS_ENDHANDLER — máme k dispozici lokální proměnnou localException, která obsahuje objekt třídy NSException jenž chybu vyvolal. Od tohoto objektu si můžeme vyžádat podrobnější popis chyby 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() {
  [NSAutoreleasePool new]; // od minula víme proč :)
  NS_DURING
    ...
    for (int 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ř. triviální testovací 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)".

Pozor na return, goto, longjmp

Výjimky samy jsou vnitřně implementovány pomocí standardního mechanismu setjmp/longjmp (zkušení programátoři v C vědí o co jde; ostatní to vědět nepotřebují). Z toho celkem samozřejmě vyplývá několik omezení, na něž je třeba si dávat pozor:

  • používání výjimek nelze "míchat" s explicitním používáním služeb setjmp/longjmp (což v praxi znamená, že v Cocoa bychom se měli službám setjmp/longjmp důsledně vyhýbat, neboť výjimky jsou používány většinou knihovních tříd);
  • z bloků NS_DURINGNS_HANDLER a NS_HANDLERNS_ENDHANDLER nelze vyskakovat ani pomocí příkazu goto, ani pomocí příkazu return: pokud to zkusíme, aplikace začne velmi podivným způsobem padat.

Cocoa nabízí dvojici maker NS_VOIDRETURN a NS_VALUERETURN(), jež umožňují simulovat příkaz return bez argumentu nebo s argumentem.

Vnořené zpracování výjimek

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

Další možnosti

Nakonec poznamenejme, že objekt třídy 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. K tomu slouží tzv. "userDictionary".

Ačkoli bychom v principu mohli použít tyto rozšiřující údaje k předávání podrobných informací o běhových chybách (např. "nelze otevřít požadovaný soubor", nebo "XML, načtené ze zadaného URL, neobsahuje potřebné údaje", to není účelem výjimek! Výjimky slouží k ošetření výjimečných situací, jež jsou zaviněny nesprávným použitím objektu – zcela typickým případem je výše uvedený pokus o získání čtvrtého objektu z pole, jež obsahuje pouze tři objekty. Čistě teoreticky by při běhu správně napsaného a bezchybného programu nemělo nikdy k výjimce dojít: jistě, šedá je teorie, a zelený je strom života – přesto je vhodné se k tomuto stavu alespoň blížit.

Pro zpracování běhových chyb, k nimž může na základě akcí uživatele docházet kdykoli, nejsou výjimky vhodné. Cocoa pro tyto případy nabízí velmi bohatý systém služeb, postavený kolem jiné standardní třídy, NSError. Podobně jako tomu bylo s daty, i popis třídy NSError a jejích možností si ale "schováme", dokud nebude Tygr volně běhat: systém 10.4 totiž obsahuje významná a podstatná rozšíření, jež je vhodné popsat zároveň s třídou NSError.

Podpora v jazyku

Pro ty, kdo nemají rádi makra, je od systému Mac OS X 10.3 k dispozici podpora výjimek zabudovaná přímo do jazyka Objective C prostřednictvím speciálních direktiv. Má-li fungovat, musíme ovšem překladači předat přepínač "-fobjc-exceptions" (v Xcode jej můžeme nastavit přímo v inspektoru targetu v režimu Build jako přepínač "Enable Objective C Exceptions"). Tyto direktivy jsou kompatibilní s makry NS_DURING/NS_HANDLER/NS_ENDHANDLER a s třídou NSException, můžeme tedy oba způsoby navzájem "míchat" (jen si musíme dát pozor na to, že direktivy umožňují hlásit výjimky s libovolnými objekty – nejen s instancemi třídy NSException).

Direktivy nabízejí o něco bohatší služby a jsou trochu bezpečnější (nemůžeme narazit na goto ani na return, neboť o jejich správné zpracování se samozřejmě postará překladač).

Ekvivalentem zprávy raise zde je direktiva @throw. Jejím argumentem může být libovolný objekt – pokud bude výjimka zachycena makrem NS_HANDLER, bude tento objekt obsahem proměnné localException. Uvnitř obslužného kódu chyby (tedy uvnitř bloku @catch, popsaného níže) můžeme použít také direktivu @throw bez argumentů – tím předáme aktuální výjimku výše (jde tedy o ekvivalent volání "[localException raise]").

Ekvivalentem bloku NS_DURINGNS_HANDLER je direktiva @try, již následuje standardní blok (tedy příkazy, uzavřené ve složených závorkách). Obsah celého bloku je chráněn, a dojde-li v něm k výjimce (ať již ohlášené direktivou @throw, nebo zprávou raise), předá se výjimka odpovídajícímu bloku @catch – viz níže.

Na místě bloku NS_HANDLERNS_ENDHANDLER stojí blok nebo bloky @catch. Ty bezprostředně následují za blokem @try. Oproti makrům zde direktivy nabízejí bohatší služby: bloků @catch může být libovolně mnoho, a každý z nich může určit vlastní třídu výjimkových objektů, pro něž se ten který blok použije. Na místě standardní proměnné localException zde slouží libovolná proměnná, deklarovaná v bloku @catch:

@catch (NSException *exc) { /* 1 */ }
@catch (NSString *s) { /* 2 */ }

Blok /*1*/ se provede pouze při vyvolání výjimky, obsahující objekt třídy NSException (ať již pomocí direktivy @throw, jejímž argumentem takový objekt byl, nebo pomocí zprávy raise), a tento objekt bude uložen v proměnné exc; blok /*2*/ se provede pouze při vyvolání výjimky, jejímž objektem je string (tedy např. "@throw @"Xyz";"), a tento objekt bude uložen v proměnné s. Pokud dojde k výjimce, jejíž třída není zachycena žádným z bloků @catch, výjimka se automaticky předá na vyšší úroveň. Třídy se mohou "překrývat" vzhledem k dědičnosti, ale jejich pořadí musí být od konkrétnějších k obecnějším (jinak by obecnější blok zachytil vše a konkrétnější by se nikdy nevyvolal; nastane-li to, překladač vydá varování):

@catch (NSException *e) { /* zachytí NSException */ }
@catch (NSObject *o) { /* zachytí cokoli jiného */ }

K dispozici je i direktiva @final, jež může uvádět blok, který se provede ve chvíli ukončení celého bloku @try/@catch/@final, bez ohledu na to, zda došlo k vyvolání výjimky nebo ne – to je šikovné pro uvolňování lokálních zdrojů:

void *ptr=malloc(1000);
@try { ... }
@catch (NSObject *o) {
  ...
  if (...) @throw; // v pořádku: paměť se uvolní také
  ...
}
@final { free(ptr); }

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

Tématické zařazení:

 » Rubriky  » Informace  

 » Rubriky  » Agregator  

 » Rubriky  » Začínáme s  

 

 

 

Nejčtenější články
Nejlépe hodnocené články
Apple kurzy

 

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

Uživatelské jméno:

Heslo: