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:
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):
- Nastal čas na kakao...
- Tak nejdřív kakao ochutnáme...
- Programovací jazyk C: velmi, velmi stručně
- Objective C: to si vysvětlíme podrobněji
- Co jsme si o Objective C ještě neřekli...
- Nastal čas na kakao - Vznik a zánik objektů
- Nastal čas na kakao - Kopírování objektů
- Nastal čas na kakao - Skryté podtřídy
- Nastal čas na kakao - Základní služby objektů
- Nastal čas na kakao - Jak správně psát v Objective C
- Nastal čas na kakao - Jak správně importovat
- Nastal čas na kakao - Podtřídy, delegáti, vkládání, jak se to rýmuje?
- Nastal čas na kakao - Využití kategorií namísto dědičnosti
- Nastal čas na kakao - Vkládání objektů a přesměrování zpráv
- Nastal čas na kakao - Inicializace a rušení objektů
- Nastal čas na kakao - Metody initWith... a designovaný inicializátor
- Nastal čas na kakao - Inicializace: tipy a triky
- Nastal čas na kakao - Accesory: přístup k proměnným instancí
- Nastal čas na kakao - Šedá je teorie, zelený je strom života...
- Nastal čas na kakao - Více o XCode: inspektory
- Nastal čas na kakao - Aplikace RSS2: datový model
- Nastal čas na kakao - Aplikace RSS: implementace datového modelu
- Nastal čas na kakao - Aplikace RSS: parsování XML
- Nastal čas na kakao - Interface Builder a uživatelské rozhraní
- Nastal čas na kakao - Interface Builder: atributy objektů
- Nastal čas na kakao - Interface Builder: atributy objektů
- Nastal čas na kakao - Druhý kontrolér a dokončení aplikace
- Nastal čas na kakao - Drobná vylepšení a zdokonalení...
- Nastal čas na kakao - Ladění
- Nastal čas na kakao - Třídy Foundation Kitu
- Nastal čas na kakao - Třídy Foundation Kitu (2)
- Nastal čas na kakao - Textové řetězce: NS(Mutable)String
- Nastal čas na kakao - Čísla, binární data a další...
- Nastal čas na kakao - Archivace objektů
- Nastal čas na kakao - Trocha magie, aneb distribuované objekty
- Nastal čas na kakao - Málem bychom zapomněli: NSAutoreleasePool
- Nastal čas na kakao - Zpracování výjimek: NSException
- Nastal čas na kakao - NSInvocation a černá magie
- Nastal čas na kakao - Kakao v Tygrovi
- Nastal čas na kakao - Notifikace: nepřímé předávání zpráv
- Nastal čas na kakao - NSUserDefaults
- Nastal čas na kakao - Co nového ve Foundation Kitu
- Nastal čas na kakao – s Intelem, s Intelem, jedeme do...
- Co nového v Xcode
- Začínáme s AppKitem
- Jak MVC v Kakau vypadá doopravdy?
- Jak MVC v Kakau vypadá doopravdy: dokončení
- Přehled tříd AppKitu
- Nastal čas na kakao - Přehled tříd AppKitu 2
- Přehled tříd AppKitu 3: zbývající třídy GUI
- Přehled tříd AppKitu 4: textový systém
- Nastal čas na kakao - Přehled tříd AppKitu 5: hlavně grafika
- Přehled tříd AppKitu 6: dokumentový systém
- Přehled tříd AppKitu 7: dokončení
- Pojmenované vlastnosti objektů
- Pojmenované vlastnosti objektů: implementace
- Pojmenované vlastnosti objektů: relace 1:N
- Pojmenované vlastnosti objektů: řazení jmen a agregační funkce
- Sledování změn objektů
- Sledování změn objektů – ukázka
- Sledování změn objektů – zdrojový kód
- Sledování změn objektů: kód modelu
- Sledování změn objektů: přímý přístup
- Kontroléry a vazby
- Vázání vazeb
- Další vazby s jednoduchým kontrolérem
- Implementace a použití převodu hodnot
- Validace hodnot
- Validace a chyby, a jedna hezká vazba...
- Práce s polem objektů
- Základní vazby NSArrayControlleru
- Převodníky, přepínače, placeholdery
- Mírná vylepšení v mezích zákona
- Objective C 2.0 - novinky z Leoparda
- NSTreeController
- Programování v Cocoa - Pár tipů a triků
- Programování v Cocoa - Základy kreslení
- Kterak nakreslit modrý obdélník...
- Další služby pro kreslení
- Obrázky a písmenka...
- Události a myš
- Lepší práce s myší
- Události klávesnice
- Input Management
- Příkazy a schránka
- Další události
- Táhni a padni
- Byli jsme na tahu; nyní padneme.
- Zvolme si, jak vhodit
- Drobnosti a chybičky
- Speciální případy tahání či házení
- Kterak táhnout něco, co neexistuje?
- Jak na sítě...
- NSURLConnection
- Safari za minutu
- Služby WebKitu
- Kakao v Leopardu
- Druhé Objective C
- Druhé Objective C: různé drobnosti
- Druhé Objective C: kategorie a protokoly
- Druhé Objective C: nový příkaz cyklu
- Druhé Objective C: atributy a accesory
- Druhé Objective C: atributy a accesory
- 64 je dvakrát 32
- Ubicumque dulce est, ibi et acidum invenies...
- Irbis: že prý žádné novinky?
- Blok sem, blok tam, nám už je to všechno jasné...
- Bloky jsou i v AppKitu
- Irbis a Foundation Kit
- Kde jsou má data?
- Kde jsou má data? V NSCache!
- Soubor, jméno, URL, jak se to rýmuje...
- Další podpora NSURL
- Zabíjení!
- A máme tady i...OS!
- Systémové prvky GUI
- Programování pro iOS 1. díl - Rozdíly mezi "i" a "Mac"
- Programování pro iOS - 2. Začínáme programovat
- Programování pro iOS - 3. základní ovladače a propojení GUI s kódem
- Programování pro iOS - 4. Varovná hlášení
- Programování pro iOS - 5. Rámce a jejich řídicí objekty
- Programování pro iOS - 6. Ukládání dat
- Programování pro iOS - 7. Správa paměti a starý restík
- Programování pro iOS - 8. Dokončení aplikace
- Programování pro iOS - 9. Jak dostat aplikaci do iPhone
- Programování pro iOS - 10. Instalace aplikace do cizího iPhone
- Programování pro iOS - 11. Jak dostat aplikaci do libovolného iPhone
- Programování pro iOS - 12. Touching!
- Programování pro iOS - 13. Kreslíme na iPhone
- Programování pro iOS - 14. Udělejme gesto
- Programování pro iOS - 15. Další gesta
- Programování pro iOS - 16. Více prstů, více zábavy
- Programování pro iOS - 17. Podpora standardních gest
- Programování pro iOS - 18. Recognizery v iOS
- Programování pro iOS - 19. Další standardní recognizery
- Programování pro iOS - 20. Co nového v iOSu
- Programování pro iOS - 21. "Multitasking"
- Programování pro iOS - 22. Nulla est honesta avaritia nisi temporis
- Programování pro iOS - 23. Jak se aktivovat, jsme-li v pozadí
- Programování pro iOS - 24. Zbývající drobnosti
- Programování pro iOS - 25. Řídicí objekty rámců
- Programování pro iOS - 26. Jak se dělá UIViewController
- Programování pro iOS - 27. Kde vzít rámce
- Programování pro iOS - 28. Základní služby
- Programování pro iOS - 29. Práce s rámci
- Programování pro iOS - 30. Rotace zařízení
- Programování pro iOS - 31. Správa paměti v rámcích
- Programování pro iOS - 32. Řídicí objekt pro tabulky
- Programování pro iOS - 33. Řídicí objekt pro strom
- Programování pro iOS - 33. Více o UINavigationControlleru
- Programování pro iOS - 35. Ještě jednou UINavigationController
- Programování pro iOS - 36. Po navigátoru taby
- Programování pro iOS - 37. Více o UITabBarControlleru
- Programování pro iOS - 38. Dokončení UITabBarControlleru
- Programování pro iOS - 39. UIPopoverController
- Programování pro iOS - 40. Další triky UIPopoverControlleru
- Programování pro iOS - 41. Zbývající služby UIPopoverControlleru
- Programování pro iOS - 42. UISplitViewController
- Programujeme v
iTunesXcode 4 - Programování pro iOS - 44. Předvolby Xcode 4
- Programování pro iOS - 45. Práce v Xcode 4
- Xcode 4: projekt a cíle
- Xcode 4: práce s cíli
- Xcode 4: Build Settings
- Xcode 4: Build Phases
- Xcode4: Build Phases podruhé
- Xcode 4: Co jsou to Build Rules?
- Xcode4: taje editoru
- Xcode4: automatické doplňování v editoru
- XIBy chyby
- Více o XIBech
- Editor XIBů
- Inspektory pro XIBy
- Vazby mezi objekty v XIBech
- Vazby mezi objekty v kódu
- Paletky Xcode pro XIBy
- Xcode 4: levý sloupec
- Xcode 4: okno Organizer
- Xcode 4: okno Organizer, část druhá
- Xcode 4: co je to Workspace?
- Xcode 4: základy schémat
- Xcode 4: akční schémata