Objective C: to si vysvětlíme podrobněji - 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

 

Kde se narodil známý fotograf František Drtikol?

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

Seriály

Více seriálů



Informace

Objective C: to si vysvětlíme podrobněji

7. května 2004, 00.00 | Ačkoli nadstavba Objective C nad standardním jazykem C je velmi jednoduchá, budeme se jí věnovat mnohem podrobněji. Důvod je zřejmý: zatímco C víceméně každý zná (pro ty, kdo v něm trochu tápou, byl určen stručný přehled jeho nejdůležitějších vlastností v minulém dílu), Objective C zdaleka tolik známé není.

Ačkoli nadstavba Objective C nad standardním jazykem C je velmi jednoduchá, budeme se jí věnovat mnohem podrobněji. Důvod je zřejmý: zatímco C víceméně každý zná (pro ty, kdo v něm trochu tápou, byl určen stručný přehled jeho nejdůležitějších vlastností v minulém dílu), Objective C zdaleka tolik známé není. Kromě toho, jde o plně objektový dynamický systém – uživatelé takového Smalltalku v něm tedy budou hned jako doma; naproti tomu programátoři v C++, jež dynamický objektový systém nenabízí, budou patrně zpočátku zmateni, neboť to, na co jsou z C++ zvyklí, objekty vlastně nejsou: C++ má na jejich místě jen trochu glorifikované "struktury".

Ještě jedna poznámka: jazyk Objective C již existuje řadu let zcela nezávisle na platformě Mac OS X a je k dispozici v několika různých variantách, lišících se především službami standardních knihoven. Tím se ale v našem seriálu nebudeme zabývat; soustředíme se na Objective C v takové podobě, v jaké jej nalezneme ve vývojovém prostředí Cocoa pro Mac OS X; kdykoli budeme mluvit o jakékoli službě standardních knihoven či runtime, budou to právě knihovny a runtime firmy Apple.

Objekty a zprávy

Začít musíme tím, že si podrobně vysvětlíme, co je to vlastně objekt (a některé související pojmy jako zpráva či třída, k nimž se dostaneme za chvilku): jde totiž o naprosto novou záležitost, která se chová o hodně jinak, než kterýkoli z běžných typů jazyka C, a ačkoli jeho deklarace má některé rysy podobné strukturám, je mnohem, mnohem více těch odlišností.

Jedním ze základních pravidel objektových systémů je zapouzdření (encapsulation): z hlediska kódu, který s objektem pracuje, musí objekt být "černá skříňka", cosi, co leží kdesi v paměti na určité adrese, a do čeho není vidět. Máme k dispozici jen určitý standardní protokol, jehož prostřednictvím můžeme po objektu (po kterémkoli objektu) požadovat služby; objekt sám se musí "rozhodnout" jakým způsobem – pokud vůbec – tyto požadavky vyplní.

Bylo by samozřejmě možné do jazyka doplnit kompletní nový aparát abstraktních objektových typů a služeb; v jazyce pascalského typu by to bylo patrně na místě. Jazyk C však vždy byl jazykem poměrně nízké úrovně, a pro "cokoli v paměti" vždy používal adresy – ukazatele; Objective C tuto tradici dodržuje, a i v něm jsou objekty representovány ukazateli. Protože do "vnitřností" objektů nám kvůli zapouzdření nic není, použijeme pro ně nejobecnější void*:

void *obj1,*obj2;
...
if (obj1==NULL) printf("Objekt 1 neexistuje"); // nebo jen "if (!obj1) ...
 if (obj1==obj2)
   printf("proměnné obj1 i obj2 representují tentýž objekt") 

Jen a jenom pro lepší přehlednost a čitelnost programů je ve standardních hlavičkových souborech Objective C definováno pro "typ objekt" nové jméno id; podobně namísto NULL máme k dispozici konstantu nil s naprosto stejným významem. Je vhodné si uvědomit, že překladači je to úplně jedno, děláme to jen pro nás lidi: pro lepší čitelnost a srozumitelnost zdrojového textu. Minulý příklad bychom tedy — s vědomím, že pro překladač se ve skutečnosti vůbec nic nemění! — mohli přepsat takto:

id obj1,obj2;
...
if (obj1==nil) printf("Objekt 1 neexistuje"); // nebo jen "if (!obj1) ...
 if (obj1==obj2)
   printf("proměnné obj1 i obj2 representují tentýž objekt") 

(Technická poznámka: ve skutečnosti je id definován nikoli jako ukazatel na void, ale jako ukazatel na jakousi interní strukturu, do níž nám vůbec nic není. To je jen proto, aby překladač mohl rozeznat případný pokus o poslání zprávy čemukoli jinému než objektu – třeba adrese nějaké proměnné typu int nebo tak něco –, a varovat programátora, že na tomto místě bude asi chyba. Žádný jiný smysl to nemá, a z praktického hlediska se takový program přeloží naprosto korektně, a je-li adresa skutečně adresou objektu, také správně poběží.)

Nad objekty je definována jedna jediná operace: objektu můžeme zaslat zprávu. Zpráva je jakýsi "balíček", obsahující jméno zprávy a její případné parametry – v tomto směru se vzdáleně, skutečně velmi, velmi vzdáleně, zasílání zprávy podobá volání funkce ve standardním C (či volání metody v C++). Základní vlastností každého objektu je schopnost přijímat a zpracovávat zprávy: objekt "balíček" rozpakuje a podle jména zprávy (a jejích případných parametrů) se rozhodne, co se zprávou provede. V nejběžnějším případě provede nějakou akci odpovídající zprávě; může však stejně dobře zprávu třeba uložit pro pozdější zpracování, nebo předat jinému objektu, nebo prostě odmítnout (to pak vede k běhové chybě).

Ve zdrojovém kódu budeme pro zaslání zprávy používat hranaté závorky a konstrukci [<příjemce> <zpráva>]; příjemcem může být libovolný objekt (tj. libovolný výraz, jehož výsledek je typu id). Pro zprávy používáme syntaxi zhruba převzatou ze Smalltalku: jménem zprávy může být jakýkoli identifikátor, obsahující libovolné množství dvojteček; dvojtečky representují parametry. Dvojtečky mohou ve zprávě stát kdekoli, a parametry se píší hned za ně (takže jméno zprávy je "roztrhané", parametry jsou uvnitř něj); díky tomu jsou i velmi složité zprávy snadno čitelné:

id obj;
[obj zpravaBezParametru];
[obj zpravaSJednimParametrem:1];
[obj zpravaSParamteremX:1 aParametremY:2];
// takhle nějak by vypadala reálná zpráva:
[obj drawCircleWithCentreX:10 Y:10 radius:12 title:"Terč"];
// zpráva může vracet hodnotu:
int suma=[obj intValue]+23;
// vrací-li hodnotu typu id, tedy objekt, můžeme mu hned zaslat další zprávu:
[[obj dejMiObjekt] zpravaProVracenyObjekt];

Uvědomme si dobře ten nejdůležitější rozdíl mezi zasíláním zpráv, využívaným v objektovém prostředí, a mezi voláním funkcí, používaném v jazycích C, C++ a podobných: při zasílání zpráv to, jaká operace bude na základě přijetí zprávy provedena, rozhodne až přijímající objekt ve chvíli, kdy zprávu dostal. Protože se takto vazba mezi požadavkem toho, kdo zprávu odesílá, a reakcí toho, kdo ji přijímá, naváže co nejpozději to je možné (tedy až v okamžiku faktického odesílání zprávy za běhu programu), nazývá se tento systém někdy také pozdní vazba (late binding).

Výhodou pozdní vazby je nesmírná flexibilita: představme si třeba naprosto triviální funkci pro výpočet průměru:

double average(id *o) { // pole objektů, končí hodnotou nil
  double cnt=0,sum=0;
  while (*o) {
    sum+=[o doubleValue];
    cnt++; o++;
  }
  return sum/cnt;
}

Pokud bychom něco podobného napsali v plain C, byla by pro každý typ hodnot zapotřebí nová implementace: funkce, která počítá průměr 'intů' by neuměla spočíst průměr 'floatů', o ostatních variantách ani nemluvě. C++ je o něco málo flexibilnější — tam by bylo možné jedinou funkcí počítat průměr různých objektů, pokud by ovšem programátor původní třídy nezapomněl označit metodu doubleValue jako virtual a/nebo rozumně využít templates (programátoři v C++ vědí o čem mluvím, ostatní buďte rádi, že to nevíte, jde o pěknou prasárnu) — jinak na tom není C++ o nic lépe, než plain C. Speciálně jednou přeložený kód v C++ už je pevně vázán na konkrétní třídu (a její podtřídy), a pokud bychom jej zavolali na objekt třídy jiné (a tomu žádný překladač nikdy při uvedené deklaraci zabránit nemůže), dokonce ani nemusí dojít k běhové chybě: může se stát cokoli, od naprostého zhroucení programu až po (samozřejmě mnohem horší) běh bez ohlášení jakékoli chyby, jenže také s generováním nesprávných výsledků...

V dynamickém objektovém prostředí je ale funkce totálně flexibilní: je úplně jedno, jaké objekty dostane, jeden může representovat třeba celé číslo, druhý číslo v pohyblivé řádové čárce, a třetí matici (přičemž po přijetí zprávy doubleValue spočte a vrátí její determinant). Čtvrtým objektem může být zase něco úplně jiného — třeba textové pole v uživatelském rozhraní, jež vrací svůj obsah interpretovaný jako číslo... Funkce bude pořád korektně pracovat! A co by se stalo, kdybychom funkci předali objekt, který zprávě doubleValue nerozumí? Inu, to nejkorektnější řešení, jež lze nalézt: program by byl ukončen, a do konsole by se vypsala přesná příčina chyby ("objekt třídy X dostal zprávu doubleValue, již nedokáže zpracovat").

Dosáhnout takové univerzálnosti — a s ní spojené přenositelnosti kódu z jednoho projektu do druhého — v jazycích, jež nemají zprávy, prostě není možné. Mimochodem, ten fakt, že libovolnému objektu můžeme poslat libovolnou zprávu a on si ji korektně zpracuje po svém se nazývá polymorfismus, a je považován za zcela základní rys objektových systémů (a to dokonce i autory jazyků typu C++ nebo Java, jež samy polymorfismus mají jen v krajně omezené podobě).

Stojí za to si uvědomit, že až dosud jsme se vůbec nebavili o tom, co to vlastně objekt "doopravdy je". Co obsahuje? Co je zapsáno v tom místě v paměti, kam ukazuje pointer typu id? Správná odpověď zní: nevíme, a nic nám do toho není! Právě tím je zajištěna nesmírná flexibilita objektového systému: s objekty komunikujeme výhradně prostřednictvím systému zpráv; objekty samy se postarají o jejich korektní interpretaci. Dokonce není bezvýhradně pravda ani to, že by objekty "representovaly data": jistě, velmi často tomu tak skutečně je, ale nutné to není. Můžeme mít třeba objekt, který po přijetí zprávy doubleValue vždy vygeneruje a vrátí náhodnou hodnotu...

Druhá věc, již jsme prozatím přeskočili, je způsob, jakým programátor, který objekt vytváří, určí jeho chování (tj. to, jak bude objekt na které zprávy reagovat) — k tomu se dostaneme za chvilenku.

Třídy, tvorba objektů a dědičnost

Na základě vlastností, popsaných v minulém odstavci, by již bylo možné vytvořit docela slušný objektový systém (jen by se v něm programovalo trochu nešikovně, ale šlo by to: podobné API existovalo např. pro na svou dobu skvělé kapesní počítače Psion Serie 3). Pro pohodlné programování se však vyplatí zavést ještě dvě novinky: tzv. třídy (classes), representující objekty stejného nebo podobného druhu, a dědičnost (inheritance), sloužící pro pohodlnou tvorbu nových tříd.

Požadavek na využití tříd vychází vlastně z praxe: obvykle se setkáváme s množstvím objektů stejného druhu. V programu je řada textových řetězců; v databázovém systému knihovny je množství "oddělení" a ještě více "knih". Každý objekt "kniha" se přitom podobá všem ostatním objektům "kniha" v tom smyslu, že reaguje stejným způsobem na stejné zprávy — jen vrací jiné konkrétní hodnoty. Libovolnému objektu "kniha" tedy můžeme například poslat zprávu autor, a dozvíme se, kdo knihu napsal; nikdy nedostaneme počet stránek. Na přijetí zprávy title bude kterýkoli objekt "kniha" vždy reagovat vrácením názvu, a určitě ne třeba vyřazením z katalogu... a tak podobně.

Bylo by tedy nanejvýš nepraktické, kdyby měl programátor systému určovat způsob reakce třeba na zprávu autor pro každý objekt "kniha" zvlášť. Namísto toho programátor sestaví třídu "kniha", a v jejím rámci naprogramuje obecnou reakci na kteroukoli zprávu, již objekty "kniha" mají zpracovávat. Každý konkrétní objekt pak ví, které třídě patří; dostane-li objekt nějakou zprávu, vyhledá si mechanismus zpracování zprávy ve své třídě.

Dědičnost

Pro další usnadnění práce programátora je k dispozici dědičnost. Jedná se o jednoduchoučkou záležitost, opět odpovídající praxi: obvykle jsou si objekty různých druhů (různých tříd) více či méně podobné. Chceme-li popsat třeba křeslo, řekneme pravděpodobně něco jako "to je vlastně židle s těmito několika drobnými rozdíly:...". Analogicky v objektovém prostředí: vytváříme-li novou třídu, můžeme využít kteroukoli z již existujících tříd a popsat pouze rozdíly mezi nimi.

Třídy nejen representují "typy objektů"; zároveň nám samy mohou nabídnout řadu služeb. Základní z nich je samozřejmě tvorba nových objektů: dosud jsme se vůbec nezabývali tím, jak vznikají nové objekty (ani tím, jak zanikají objekty již nepotřebné, ale to si necháme až na příští díl tohoto seriálu). Tvorba objektů je ale jednoduchá: jestliže třída "ví všechno" o objektech, jež representuje, je nanejvýš přirozené, aby sama tyto objekty podle potřeby vytvářela.

Třídy jsou také objekty!

Ovšem, ouha: máme tady další "novou věc", měli bychom podobně, jako jsme přidali do jazyka objekt (a operace nad ním, tj. zasílání zprávy) přidat do jazyka nová klíčová slova pro třídu a nějaké operace nad ní? Samozřejmě, bylo by to možné, a např. C++ to tak dělá; existuje však daleko elegantnější a zároveň nesrovnatelně praktičtější řešení. Uvědomme si, že objekty jsme zavedli natolik obecně, že mohou dělat prakticky cokoli — proč by tedy třídy samy nemohly být objekty jako každé jiné? Pro komunikaci s třídami pak můžeme použít naprosto standardní mechanismus zpráv. Jen opět pro lepší čitelnost budeme pro třídy používat namísto typu id typ Class a namísto hodnoty nil hodnotu Nil. Znovu ovšem připomeňme, že to děláme jen pro sebe, aby se nám lépe četly zdrojové texty; překladači to je jedno, a vše by fungovalo stejně dobře i kdybychom používali kdekoli kterýkoli z trojice typů (včetně void*) a hodnot (včetně NULL).

Pokud tedy budeme mluvit o objektech, může to znamenat jak "normální" objekty, o nichž jsme se bavili až dosud, tak i třídy. Chceme-li někde zdůraznit, že máme na mysli pouze "normální objekty", budeme používat pojem instance (neboť "normální objekty" nejsou ničím jiným, než právě instancemi tříd). Jinými slovy, v Objective C pracujeme s objekty, jimiž mohou být buďto třídy, nebo instance.

(Poznamenejme, že třídy jsou zcela standardními objekty až na jednu výjimku: samy již nemají žádnou "třídu tříd", metatřídu. Bylo by možné ji zavést, a některé objektové systémy to skutečně dělají; praktické výhody jsou však minimální.)

Přece jen o něco ale jazyk rozšířit musíme: o prostředky pro tvorbu tříd, a pro popis toho, jak budou jim odpovídající objekty zpracovávat zprávy.

Rozhraní, properties, implementace a metody

Popis třídy má dvě jasně oddělené části: rozhraní, jež obsahuje informace o tom že třída vůbec existuje, jaké má jméno, koho je dědicem, a jak se s jejími instancemi i s ní samotnou pracuje (a z technických důvodů i něco málo o vnitřní struktuře instancí, ačkoli to samozřejmě teoreticky v rozhraní vůbec nemá co dělat), a implementaci, jež určuje jak objekty budou zpracovávat zprávy. Ve zdrojových textech Objective C pro rozhraní a implementaci tříd slouží direktivy @interface, @implementation a @end.

Rozhraní

Nejjednodušší rozhraní prostě jen určí jméno nově vytvářené třídy. Pokud využíváme dědičnosti (což je v praxi téměř vždy), zapíšeme za jméno nové třídy dvojtečku, a za ni jméno již existující třídy, od níž chceme novou děděním odvodit (budeme jí někdy říkat nadtřída):

@interface MyClass:NSObject @end 

Nic jiného už pro použití třídy a jejích instancí není zapotřebí: pokud překladač zpracuje tento jediný řádek, "ví" o existence třídy MyClass a je schopen jí i jejím instancím zasílat libovolné zprávy. (Samozřejmě, aby třída doopravdy existovala, musíme ji – třeba v úplně jiném modulu – skutečně vytvořit pomocí direktivy @implementation; k tomu se dostaneme za chvilku.)

V naprosté většině případů potřebujeme, aby každá instance třídy obsahovala nějaké vlastní proměnné (často se jim říká anglicky properties), jež tak či onak definují její obsah: objekt "kniha" by asi měl proměnné "autor", "název", "počet stran", "vydavatel" a podobně. Všechna objektová prostředí proto umožňují v rámci třídy takové proměnné definovat. Je celkem zřejmé, že se obsah těchto proměnných stane součástí toho "něčeho v paměti", co — jak víme z prvého odstavce — representuje objekt; právě v tomto (a pouze v tomto) smyslu se objekt trochu podobá klasické céčkové struktuře. Ve zdrojovém textu můžeme takové proměnné definovat ve složených závorkách hned za jménem třídy a nadtřídy; syntaxe přesně odpovídá "vnitřku" standardní céčkové deklarace struct:

@interface MyClass2:NSObject {
  // každý objekt třídy MyClass2 bude mít vlastní...
  int i,j; // ...dvě proměnné typu int...
  double d; // ...jednu typu double...
  id o1,o2,o3; // ...a tři (odkazy na) objekty.
}
@end 

(K těm "odkazům na": připomeňme, že id je vlastně ukazatel, obyčejný void*; mezi např. proměnnou i a o2 je tedy určitý rozdíl, zřejmý zkušeným programátorům v C: číslo i leží skutečně uvnitř instance třídy MyClass, zatímco objekt o2 je někde venku — uvnitř instance třídy MyClass je jen odkaz na něj.)

Pokud měla nějaké vlastní proměnné nadtřída, budou v definované třídě k dispozici samozřejmě také. Jinými slovy, vlastní proměnné kterékoli třídy zahrnují nejen ty, jež jsou deklarovány v jejím rozhraní, ale také všechny deklarované v její nadtřídě, v nadtřídě nadtřídy, a tak dále, až po "nejvyšší" třídu, jež již žádnou nadtřídu nemá.

Pečlivý čtenář prvního odstavce, kde jsme popisovali zprávy, se možná zarazil: zpráva intValue vracela číslo typu int, zpráva doubleValue vracela číslo typu double; tři argumenty zprávy drawCircleWithCentreX:Y:radius:title: byly typu int, a čtvrtý char*: jak to má překladač poznat? Snadno: poslední standardní součástí rozhraní je totiž deklarace zpráv a jejich typů. Syntaxe je jednoduchá — před každou zprávu napíšeme znak '-', argumenty označíme libovolnými identifikátory, a před ně i před celou zprávu napíšeme patřičné typy v závorkách:

@interface MyClass3:NSObject { ... }
-(int)intValue;
-(double)doubleValue;
-(void)drawCircleWithCentreX:(int)x Y:(int)y radius:(int)r title:(char*)tt;
@end 

Je důležité mít na paměti, že se jedná jen o informaci pro překladač a o nic jiného! Za běhu pak díky pozdní vazbě může libovolný objekt dostat libovolnou zprávu, zcela bez ohledu na to, jestli je zapsaná v jeho rozhraní nebo ne. Stejně snadno také můžeme používat i zprávy, jež nejsou zapsané v žádném rozhraní: překladač pak považuje jejich návratové hodnoty i jejich případné argumenty za objekty (tedy typy id) – což znamená, že to stejně dobře může být cokoli, co se dá na id bez ztráty převést (typicky int či obecný ukazatel na cokoli). Totéž platí pro návratové hodnoty nebo argumenty, u kterých žádný typ v závorce neuvedeme. (Pro pohodlí a zabezpečení proti překlepům překladač v takovýchto případech obvykle zobrazuje varování; přeložený kód je však opět naprosto korektní.)

Vidíme, že Objective C je, alespoň pokud zůstaneme jen u objektů a tříd a ponecháme stranou klasické C, v zásadě beztypový jazyk: všechny objekty jsou téhož typu (totiž id/Class); to, že mohou být instancemi různých tříd či třídami samotnými, se projeví až za běhu, a to jen a jenom tím, kterým zprávám rozumějí a kterým ne. Skutečně tomu přesně tak je; pro pohodlí programátorů však existuje možnost, jak překladači sdělit, že objekt je instancí některé konkrétní třídy: deklarujeme jej jako "ukazatel na třídu":

MyClass *obj; // v podstatě totéž, jako "id obj"

Překladač – a teď pozor – v podstatě přeloží vše přesně stejně, jako kdybychom použili obecnou deklaraci id obj; jediný rozdíl je v tom, že hlídá, zda neposíláme objektu obj zprávy, jež nejsou v rozhraní třídy MyClass, a pokud ano, vypíše varování (ale opět vše přeloží korektně, a pokud objekt ve skutečnosti dané zprávě rozumí, vše také korektně poběží).

Jediný důvod, proč je v minulém odstavci to "v podstatě", spočívá v tom, že mohou existovat dvě totožné zprávy s různými parametry či návratovými hodnotami: představme si, že v jedné třídě jsme deklarovali zprávu

-(int)value;

a ve druhé

-(double)value;

To je samozřejmě špatný návrh, ovšem Objective C to umožňuje: pokud pak pošleme zprávu value nějakému objektu, o němž překladač neví, jaké je třídy, vybere prostě náhodně jednu z možností a vrácenou hodnotu buď interpretuje jako int nebo jako double (neboť obojí najednou samozřejmě nejde) – a ovšem, vypíše varování.

Pokud ovšem překladač díky deklaraci JménoTřídy* ví, že zprávu posílá právě té třídě, v níž je třeba prvá z deklarací, nemusí hádat, a prostě vrácenou hodnotu interpretuje jako int. Proto je obecně vhodné tam, kde to jde (tedy tam, kde víme, které třídy objekt bude, již při překladu), používat raději tento typ deklarací nežli pouhé id – přestože s ním by programy běžely stejně dobře.

Implementace

Implementace z hlediska programátora vlastně není nic jiného, než naprogramování několika tzv. metod. Metoda je v zásadě zcela standardní "céčková" funkce; namísto hlavičky funkce však použijeme hlavičku, jež přesně odpovídá deklaraci zprávy v rozhraní (jen není zakončena středníkem). Překladač pak udělá dvě věci: (a) přeloží kód metody, (b) umístí do třídy informaci, že dostane-li kterákoli její instance zprávu, jejíž identifikátor odpovídá hlavičce metody, bude vyvolána právě tato metoda.

Na rozdíl od deklarací v rozhraní tedy metody v implementaci skutečně popisují chování objektu: dostane-li instance zprávu, jíž neodpovídá žádná z jejích metod, odmítne ji, a dojde k běhové chybě (pro úplnost poznamenejme, že jsou samozřejmě k dispozici i prostředky, jak programovat plně dynamické zpracování zpráv, tj. takové, že objekt může zpracovávat např. libovolnou zprávu, jejíž jméno začíná na "a" a má sudý počet písmen tak, že vrátí délku identifikátoru zprávy násobenou počtem jejích parametrů; prozatím si však takovými šílenostmi nebudeme komplikovat život).

@implementation MyClass3
-(int)intValue {
  return 1;
}
-(double)doubleValue {
  return 1.0;
}
-(char)charValue {
  return 'a';
}
@end 

Povšimněme si, že metody v implementaci neodpovídají přesně zprávám z rozhraní. To, že v implementaci je metod více, je naprosto běžné: odpovídající zprávy z toho či onoho důvodu nejsou součástí rozhraní, ale instance třídy MyClass3 je přesto dokáží zpracovat. Opačný případ (zpráva uvedená v rozhraní nemá metodu v implementaci) je méně obvyklý (a překladač proto v jeho případě opět vypíše varování), ale také možný a zcela korektní.

Uvnitř implementace metod jsou přímo přístupné všechny vlastní proměnné, stačí uvést jejich jméno (takže kdybychom např. implementovali metodu třídy MyClass2, mohli bychom vracet hodnotu proměnné d příkazem "return d;").

Nakonec je třeba říci, že s odmítnutím zprávy a běhovou chybou jsem malinko lhal: pokud totiž 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 (teprve pak tedy dojde k té zmíněné chybě). To pohodlně a automaticky zajišťuje dědění zpráv: jestliže např. v implementaci třídy NSObject je metoda name, můžeme odpovídající zprávu posílat např. objektům třídy MyClass3 bez obavy, že by byla odmítnuta.

Metody tříd

Připomeňme, že třída sama je objektem, a sama tedy dokáže přijímat a zpracovávat zprávy. Proto můžeme v rozhraní kromě deklarace zpráv, určených pro instance této třídy, deklarovat i zprávy určené pro třídu samotnou. Podobně v implementaci můžeme definovat "třídní" metody – tedy takové, jež budou vyvolány v případě, že třída sama dostane zprávu, odpovídající hlavičce metody. V obou případech je deklarace i definice takřka stejná jako minule; jen znak '-' na začátku je nahrazen znakem '+':

@interface MyClass4:NSObject
+(char*)myName; // pro samotnou třídu
-(char*)myName; // pro její instance
@end
@implementation MyClass4
+(char*)myName {
  return "Třída MyClass4";
}
-(char*)myName {
  return "Instance MyClass4";
}
@end 

Poslední informace, která nám chybí k tomu, abychom mohli začít opravdu programovat, je jak se dostaneme k "objektu třída" z programu. To je ale prosté: pokud jméno třídy použijeme v hranatých závorkách jako příjemce zprávy, representuje právě požadovaný "objekt třída". Takže malé cvičení pro pozorné čtenáře: je jasné, co vypíše následující funkce, je-li použita po deklaraci a definici třídy MyClass4?

void printout(void) {
  id o=[MyClass4 new]; // zpráva 'new', zděděná z NSObject-u,
                       // vytvoří novou instanci
  printf("%s, %s\n",[MyClass4 name],[o name]);
} 

Pokud ne, můžete si to samozřejmě hned vyzkoušet v XCode, stačí trochu upravit náš prográmek Hello World z prvního dílu. Jen pozor na české znaky a případné "nezlomitelné mezery": těm samozřejmě překladač nerozumí, takže po copy&paste z webového prohlížeče je musíme odstranit ručně.

Ještě snad poznámku – samozřejmě, že metody tříd se dědí analogickým způsobem, jako metody instancí: jestliže dostane třída zprávu, pro niž nenajde ve vlastní implementaci žádnou "plusovou" metodu, hledá metodu v implementaci své nadtřídy. Zde pak ještě existuje jedna speciální výjimka: pokud se třídní metoda nenajde ani v nejvyšší třídě, hledá se v ní ještě jednou, ale tentokrát jako "mínusová" (tedy určená pro instanci, nikoli třídu). Na první pohled to vypadá trochu zmateně, ovšem má to železnou logiku: jsou-li třídy samy objekty, je přece zřejmé, že by samy měly rozumět "objektovým" metodám kořenové třídy.

A to je skorem celé Objective C!

Příště si ukážeme těch několik málo (skutečně málo, a poměrně nevýznamných) prvků jazyka Objective C na něž se dnes nedostalo. Pak se už začneme bavit o skutečných vlastnostech prostředí Cocoa: ukážeme si mechanismus tvorby a zániku objektů a podobně.

Ještě jednou "Hello, World"

Vyplatí se se znalostmi, jež jsme právě nabrali, se znovu podívat na příklad programu Hello World z předminulého článku: rozepíšeme si jej řádek po řádku, a vysvětlíme si, co se ve kterém z nich přesně děje:

#import <Foundation/Foundation.h>

Tak o tomhle jsme se bavili minule, na samém konci, v souvislosti s direktivou include z klasického C: tento řádek prostě do zdrojového programu vloží jakýsi soubor Foundation.h ze složky Foundation – v něm překladač najde všechny potřebné deklarace.

@implementation HelloWorld:NSObject

Tohle jsme se naučili dnes: vytváříme třídu jménem HelloWorld, jejíž nadtřídou je jakási třída NSObject. Protože jsme si nechali od cesty @interface (v takhle jednoduchém programu to není zapotřebí), musíme nadtřídu uvést v implementaci; normálně by to nebylo třeba.

static BOOL print=NO;

O typu BOOL jsme se zatím nezmínili, stejně jako o konstantě NO. Ono také jde jenom o nová, snadno čitelná jména pro starý dobrý int a hodnotu 0 – stejně dobře jsme tedy mohli napsat "static int print=0": překladači je to naprosto fuk, ale pro nás je program s BOOL a NO určitě přehlednější.

Klíčové slovo static jsme si zde také mohli odpustit: je to trik, který řekne překladači, že tato proměnná je v tomto modulu "privátní". Je tedy dobrým zvykem všechny proměnné vyjma těch, jež musí být veřejné, deklarovat jako static prostě proto, aby se náhodou jiný modul "netrefil" do téhož veřejného jména a nedošlo ke kolisi.

+(void)parser:p didStartElement:element namespaceURI:u qualifiedName:q attributes:a {
    print=[element isEqual:@"description"];
}

Definujeme metodu nové třídy: dostane-li třída zprávu parser:didStartElement:namespaceURI:qualifiedName:attributes:, vyvolá tuto metodu. V ní objektu, který dostaneme v jejím druhém argumentu, pošleme zprávu isEqual: s argumentem @"description". To je něco jako řetězcová konstanta; jen se namísto ukazatele na char v paměti vytvoří statický textový objekt (samozřejmě, všechny tyto triky se naučíme a vysvětlíme si je podrobně v dalších dílech).

Jak naznačuje název zprávy isEqual:, objekt, který ji dostane, ověří, zda je nebo není ekvivalentní argumentu zprávy – jinými slovy, do proměnné print uložíme, zda XML element, jenž jsme dostali ve druhém argumentu, je či není element "description".

+(void)parser:p didEndElement:e namespaceURI:u qualifiedName:q {
    if (print) printf("\n");
    print=NO;
}
+(void)parser:p foundCharacters:string {
    if (print) printf("%s",[string UTF8String]);
}

Zbytek je poměrně jasný: pokud dostaneme (libovolný) koncový tag elementu, proměnnou print opět vynulujeme. Pokud dostaneme nějaký text a proměnná print obsahuje "pravdu" (tedy nenulovou hodnotu), text vypíšeme. Celkem je zřejmé, že pokud naší třídě někdo bude posílat zprávu parser:didStartElement:namespaceURI:qualifiedName:attributes: pro každý element XML, zprávu parser:didEndElement:namespaceURI:qualifiedName: pro každý koncový tag elementu, a zprávu parser:foundCharacters: pro každý text v XML, vypíšeme takto obsah všech elementů "description" – což je pro stream RSS přesně to, co jsme chtěli.

@end

Konec implementace třídy HelloWorld.

int main (int argc, const char * argv[]) {

Minule jsme se zmínili, že "speciálním způsobem" deklarovaná funkce main je vstupním bodem každého programu v C. Tak je tomu i zde: právě odsud se program spustí.

    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

Jakési třídě NSAutoreleasePool pošleme zprávu alloc. Zpráva zřejmě vrátí objekt, jemuž pošleme zprávu init. Ta vrátí objekt třídy NSAutoreleasePool (jemuž pak na samém konci pošleme zprávu release). Co to celé znamená se dozvíme později; zatím to můžeme klidně ignorovat (v našem jednoduchém prográmku je to skutečně celkem zbytečné).

    printf("Hello, here are world news:\n\n");

Neprogramátorům v C jsem vlastně zapomněl říci, že standardní funkce printf píše text na standardní výstup... ale už si to mezitím určitě sami domysleli ;).

    NSXMLParser *xml=[[NSXMLParser alloc] initWithContentsOfURL:[NSURL URLWithString:
	   @"http://www.bbc.co.uk/syndication/feeds/news/ukfs_news/world/rss091.xml"]];

Takže: třídě NSURL pošleme zprávu URLWithString: s argumentem, jímž je (textový objekt obsahující) adresu streamu RSS stanice BBS... a tak dále, tomu už určitě rozumíte sami. Důležité je, že vytvoříme objekt třídy NSXMLParser, který zná adresu, z níž lze získat BBS RSS (to se to hezky rýmuje).

    [xml setDelegate:[HelloWorld class]];

Objekt třídy NSXMLParser bude o průběhu prohlížení XML informovat svého delegáta – zde pomocí zprávy setDelegate: určíme, že jím bude objekt, který vrátí naše třída HelloWorld po přijetí zprávy class. Samozřejmě, je to třída sama – trik se zprávou class je zapotřebí jen proto, že jméno třídy lze pro její identifikaci použít pouze na místě příjemce zprávy a nikde jinde (takže nemůžeme napsat rovnou "[xml setDelegate:HelloWorld]").

    [xml parse];

Po přijetí zprávy parse se objekt třídy NSXMLParser pustí do práce: rozebere RSS stream ze stanice BBS, a o průběhu informuje svého delegáta – takže naše třída HelloWorld dostává přesně ty zprávy, jež jsme v ní před chvílí naprogramovali.

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

Tématické zařazení:

 » Rubriky  » Informace  

 » Rubriky  » Agregator  

 » Rubriky  » Začínáme s  

Poslat článek

Nyní máte možnost poslat odkaz článku svým přátelům:

Váš e-mail:

(Není povinný)

E-mail adresáta:

Odkaz článku:

Vzkaz:

Kontrola:

Do spodního pole opište z obrázku 5 znaků:

Kód pro ověření

 

 

 

 

 

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

Uživatelské jméno:

Heslo: