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

 

Jaký fotograf/ka získal/a cenu za nejpopulárnější příspěvek v Nikon Photo Contest?

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  

Diskuse k článku

 

Vložit nový příspěvek   Sbalit příspěvky

 

patek?

Autor: Slza Muž

Založeno: 07.05.2004, 03:34
Odpovědí: 0

eh, nic proti Objective C, ale patecni seznam her byl prece jen vhodnejsi...

Odpovědět na příspěvek

pár poznámek

Autor: Jiří Volejník Muž

Založeno: 07.05.2004, 11:51
Odpovědí: 0

Neřekl bych že programátoři, kteří umějí C++, prožívají nějake zmatení při učení Objective-C. Prostě něco uvítají, něco ocení, z něčeho budou zklamáni. Tak to alespoň bylo u mne. Mimochodem, dobrý článek o rozdílech mezi Objective-C a C++ je např. zde: http://www.mactech.com/ar
ticles/mactech/Vol.13/13.
03/CandObjectiveCCompared
/

Zapouzdření je skutečně jedním ze základních kamenů objektového programování, ale není to úplně to co píšete. Zaměňujete jej (jako mnoho programátorů) za skrývání informací (information hiding). Zapouzdření je prostě uzavření více prvků do jednoho celku. Tedy například i pouhá struktura je jednotkou zapouzdření, přestože její obsah je zvenku viditelný. Objekty navíc umožňují zapouzdřit i funkce, a teprve pomocí skrývání informací nechat viditelné jen to, co má tvořit rozhraní. Zajímavý rozbor této problematiky naleznete např. zde: http://www.toa.com/pub/ab
straction.txt

Polymorf
ismus v OOP není fakt, že libovolnému objektu můžeme zaslat libovolnou zprávu a on si ji zpracuje po svém. To je jen jedna z jeho forem. Polymorfismus je širší pojem, a různé jazyky jej podporují různě. Něco jako "úplný" či "skutečný" polymorfismus neexistuje. Nejen C++, ale i Objective-C tedy podporují polymorfismus jen "v omezené míře". Zaguglujte si sami na téma polymorphism a oop a uvidíte...

Možnost zaslat libovolnou zprávu libovolnému objektu je na první pohled velmi působivá, ale takovéto posílání zpráv "naslepo", tedy bez základní znalosti objektu, je velmi nebezpečné, a mělo by se využívat spíše výjimečně. Je to zároveň silná a zároveň nebezpečná vlastnost. Ne že by snad samotné zaslání zprávy způsobilo nějaké problémy, ale snadno se může stát, že příjemce příslušnou metodu sice implementuje, ale ve zcela jiném kontextu. Prostě náhodou dostala stejné jméno. Co se v takovém případě stane, nelze odhadnout. Je sice možné například v rámci firmy domluvit nějaké konvence pro pojmenovávání tříd a metod, ale se všemi na světě, jejichž knihovny používáte, se nedomluvíte.

Šablony (templates) jsou v C++ velmi mocným nástrojem. Rozhodně není možné označit je termínem "prasárna". Jejich nevhodným použitím je samozřejmě možné "prasárnu" udělat, ale to platí o mnoha věcech.

To že nesmíte v C++ "zapomenout" na slovo "virtual", není nijak na závadu. Umožňuje to totiž nepoužívat polymorfismus tam kde není potřeba, nebo tam kde není vhodný. Run-time polymorfismus vede například ke snížení výkonu (příslušná metoda se musí najít až za běhu), a vždy bude dost aplikací kde je výkon na prvním místě.

Omlouvám se za tak rozsáhlý příspěvek, ale mazat už to nebudu ;-) Hezý víkend všem...

Odpovědět na příspěvek

Céčko

Autor: Tomáš Muž

Založeno: 07.05.2004, 14:38
Odpovědí: 0

Zdravím, Ondřeji,

děkuji za seriál o programování, mám z toho fakt radost. Mám k Vám ale i prosbu:

Rád bych se nejdříve trošku rozkoukal v C (i na zaklade Vaseho minuleho clanku). Mam dve knihy o C od pana Herouta, nevim ale, jak a kam zapisovat v Xcode kód... Můžete mi prosím nějak poradit, případně mě odkázat někam, kde bych si to mohl nastudovat?

Děkuji.

Tomáš R.

Odpovědět na příspěvek

RE: Céčko

Autor: OC Muž

Založeno: 07.05.2004, 15:52

Ten podrobný postup z prvého dílu, jak udělat a spustit aplikaci Hello World, nestačí? Tam je fakt vše popisováno krok za krokem... kdyžtak napište, ve kterém momentě jste se "ztratil".

Odpovědět na příspěvek

RE: RE: Céčko

Autor: Tomáš Muž

Založeno: 07.05.2004, 20:15

Už to chodí.

Díky.
Tom

Odpovědět na příspěvek

Dobra ucebnice C, polymorfismus, mozna oprava (?)

Autor: Marek Zada Muž

Založeno: 07.05.2004, 15:39
Odpovědí: 0

Zdravím,

mám výbornou učebnici jazyka C, prastařičkou "Learn C on Machintosh" jako PDF (v angličtině), mohu jen doporučit. Lze ji použít pro XCode i příkazovou řádku (vlastně všude tam, kde máte textový editor a kompilátor) Zájemci nechť se obrací na můj email.

Nechci se nijak vměšovat do diskuse na téma polymorfismu, ale fakt je, že Ondřejovo krátké a velmi výstižné vysvětlení odpovídá významu, které je možné najít na DICT serveru The Free On-line Dictionary of Computing:

In object-oriented programming, the term (polymorphism) is used to describe a variable that may refer to objects whose class is not known at compile time and which respond at run time according to the actual class of the object to which they refer.

Mám připomínku k příkladu uvedeného v sekci "Metody tříd". Myslím (a v praxi i vyzkoušel), že [MyClass4 name] způsobí varování kompilátoru, protože metoda "name" pro NSObject vrací NSString, kdežto v příkladě je očekáváno char.

Problém lze vyřešit tím, že "name" nahradíte "myName".

V konečném smyslu mi ale žádná z možností nevypsala na stdout NIC. ????

Odpovědět na příspěvek

RE: Dobra ucebnice C, polymorfismus, mozna oprava (?)

Autor: OC Muž

Založeno: 07.05.2004, 15:55

Jejda, za to "name" se fakt MOC omlouvám!!!! To je tím, že jsem zvyklý používat name jež je NSString*, ale protože zatím si s NSStringy moc netykáme, použil jsem char* myName... a v té funkci printout jsem na to zapomněl jak na smrt. Mea culpa, mea maxima culpa!

Jinak ale nevím v čem je problém -- po opravě toho nešťastného "name" to funguje jak má. Jelikož do této zprávy by se špatně přidávaly snímky Xcode, připojuji přímý výsledek překladu a spuštění v Terminálu; to je totéž, jen méně pohodlné:

16 /tmp> #import

@interface MyClass4:NSObject
+(char
*)myName; // pro samotnou tdu
-(char*)myNa
me; // pro jej instance
@end
@implemen
tation MyClass4
+(char*)myName {
return "Trida MyClass4";
}
-(char*)my
Name {
return "Instance MyClass4";
}
@end
void printout(void) {
id o=[MyClass4 new];
printf("%s, %s\n",[MyClass4 myName],[o myName]);
}
int main() { printout(); return 0; }
17 /tmp> cc -Wall q.m -framework Foundation && ./a.out
Trida MyClass4, Instance MyClass4
18 /tmp>

Odpovědět na příspěvek

RE: RE: Dobra ucebnice C, polymorfismus, mozna oprava (?)

Autor: Marek Zada Muž

Založeno: 07.05.2004, 16:21

Tak nevim, kde je problém... asi v kódóvání. Když to přepíšu v Terminálu, tak to funguje. Když mám to samé v Xcode, tak akorát zařádkuje, ale vypíše nic. No nic, je to taková blbinka, takže se tím nebudu zdržovat.

Odpovědět na příspěvek

pre xxoc: syquest

Autor: jozko pucik Muž

Založeno: 09.05.2004, 23:44
Odpovědí: 0

bola by tu moznost zohnat syquest 275MB [ak sa nemylim mechaniku] rozhranie tusim fast scsi vo funkcnom stave.snad by potom slo obnovit zalohy...
ak je zaujem reply sem a ja sa uz nejak nakontaktujem :)

Odpovědět na příspěvek

RE: pre xxoc: syquest

Autor: OC Muž

Založeno: 10.05.2004, 02:42

No... o mechaniku jako takovou spíše asi nestojím díky moc. Ale kdyby bylo možné převést asi čtyři nebo pět médií SyQuest 3.5" 105MB na CD, byl bych velice vděčný.

Odpovědět na příspěvek

RE: RE: pre xxoc: syquest

Autor: jozko pucik Muž

Založeno: 10.05.2004, 21:46

su tie 105mb media kompatibilne s tym 275 mb syquestom? myslim, ze je lepsie poslat mechaniku ako medium [dobre zabalena mechanika transport prezije, ale kto vie kadial by ot medium slo a v akom stave by doslo.]. ak je mechanika kompatibilna, som ochotny ju poslat.

Odpovědět na příspěvek

jen abyste čtenáře neodradil...

Autor: Jan Volf Muž

Založeno: 12.05.2004, 16:05
Odpovědí: 0

Obávám se, že jste čtenářům hned nazačátku pěkně zamotal hlavu s objekty. Já si vzpomínám, když jsem začínal sám s objektovým programováním, měl jsem takovou pěknou tlustou knížku o C++, kde ten přístup k problematice byl víceméně fylozofický. To nebylo nic pro mě, tu knížku jsem nakonce prodal. A ač vy jste se snažil i s jednoduchými příklady, až příliš mi to připomělo tu knížku. Vysvětlit začátečníkům, hned na začátku takové věci, ač klíčové, jako polymorfismus, dedičnost, rozdíl mezi instancí objektu a třídou a pod. může spoustu lidí odradit, tak jako tehdy mě. Jasné je, že každý preferuje jiný způsob učení, ale nemůžu se ubránit dojmu, že váš článek byl napůl demonstrace toho, že Objective C je prostě lepší (a v tom s vámi souhlasím) než C++.
Abych jen nekritizoval, tak jsem se trochu přemýšlel, jak by mohl podobný článek vypadat a myslím, že cesta by byla třeba takové, že by se použila jako příklad aplikace, která užívá alespoň základní věci z foundations (NSString, NSDictionary, NSArray, NSNumber,NSEvent, NSNotification), plus některá specifika (mutable vs. imutable, alokace objektů, retain count), prostě to co programátor s největší pravděpodobností použije a čemu se nevyhne. N ní by se vysvětlilo jak taková aplikace funguje, co se vlastně děje, když se spustí a když běží (což asi chystáte v následujících kapitolách) a přitom po troškách odkrývat problematiku objektového programování, která je až na některé věci univerzální a která by na kokrétním příkladě byla velmi snadno pochopitelná a demonstrovatelná. Toť můj názor, víc by se měli vyjádřit čtenáři.

Odpovědět na příspěvek

jen drobna chybka

Autor: poke Muž

Založeno: 24.11.2008, 17:04
Odpovědí: 0

V poznamce (psane malymi pismeny) pod definici interface tridy MyClass2.

By mel byt misto odkazu na tridu MyClass uveden odkaz na tridu MyClass2.
;-)

Odpovědět na příspěvek

 

 

Vložit nový příspěvek

Jméno:

Pohlaví:

,

E-mail:

Předmět:

Příspěvek:

 

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: