Zpět Obsah Další

Objekty a Objective C


Nastala chvíle, kde se pustíme do vývojového prostředí takříkajíc od podlahy: začneme tím, že se seznámíme se základy objektového prostředí, na kterém je celé prostředí Cocoa postaveno. V tomto článku budeme předpokládat, že čtenář má orientační znalosti o jazyce C: to proto, že občas pár řádků kódu objasní situaci mnohem lépe, než několik odstavců textu.

Snad je to pro řadu čtenářů zbytečné; přesto si tento objektový úvod nemůžeme odpustit. Vzhledem k nešťastnému rozšíření jazyka C++ a — v rámci systému Delphi — také ObjectPASCALu má totiž řada programátorů o objektech naprosto nesprávnou představu. Dobrá orientace v objektovém prostředí je ale pro správné pochopení vývojového systému, který je nad objekty postaven, dost důležitá. Na druhou stranu, účelem tohoto článku není vytvořit kompletní učebnici objektového progamování (jakkoli taková věc v našich knihkupectvích tragicky chybí); popíšeme si jen to, co je potřebné pro práci v prostředí Cocoa. Ukážeme si proto také přímo konkrétní prostředky jazyka Objective C, který je základním jazykem pro Cocoa, a v němž je napsána většina jeho knihoven a aplikací. V ostatních podporovaných objektových jazycích (Java, WebScript) jsou služby až na výjimky totožné, liší se jen v drobnostech a v syntaxi.

Systém objektů a základy Objective C si popíšeme v několika odstavcích:

Objekty a zprávy

V jazycích typu C++ je objekt jen jakýmsi poměrně logickým rozšířením proměnné typu struct. Ve skutečně objektovém prostředí tomu tak ale není: objekt je naprosto nová záležitost, která se chová o hodně jinak, než kterýkoli z ostatních typů. Abychom proto mohli objekty pohodlně zapojit do systému typů jazyka C, aniž by proto bylo zapotřebí tento jazyk zásadně předělat, použijeme pro ně nejobecnější z typů: ukazatel. Každý objekt pak bude prostě representován "něčím v paměti":

void *obj1,*obj2;
...
if (obj1==NULL) printf("Objekt 1 neexistuje");
if (obj1==obj2)
  printf("obj1 i obj2 representují tentýž objekt")

Pro lepší přehlednost programů si nadefinujeme nový typ id; ten budeme pro objekty používat namísto typu void*. Podobně namísto NULL budeme používat 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");
if (obj1==obj2)
  printf("obj1 i obj2 representují tentýž objekt")

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 případné parametry. 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 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 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 převzatou ze SmallTalku: jméno zprávy může být libovolný 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;

Uvědomme si 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é, nazývá se tento systém někdy také pozdní vazba ('late binding').

Výhodou pozdní vazby je nesmírná flexibilita: představme si 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 objektů, odvozených od jedné pevně dané třídy a jejích dědiců, pokud ovšem programátor původní třídy nezapomněl označit metodu doubleValue jako virtual (programátoři v C++ vědí o čem mluvím, ostatní to nepotřebují) — jinak na tom není C++ o nic lépe, než plain C. V 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ý float, 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 přečtený jako číslo... Funkce bude pořád korektně pracovat! Dosáhnout takové univerzálnosti — a s ní spojené přenositelnosti kódu z jednoho projektu do druhého — v jazycích typu C++ prostě není možné.

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 vrátí náhodnou hodnotu... Druhá věc, již jsme prozatím přeskočili, je způsob, jakým programátor určí chování objektu (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. Pro pohodlné programování se však vyplatí zavést ještě dvě novinky: třídy, representující objekty stejného nebo podobného druhu, a dědičnost, 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; na odeslání zprávy title bude kterýkoli objekt "kniha" reagovat vrácením názvu, a 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ě.

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

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ň 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).

(Poznamenejme, že třídy jsou standardními objekty až na jednu výjimku: samy již nemají žádnou "třídu tříd" (čili 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řeci jen ale jazyk o něco rozšířit musíme: o prostředky pro tvorbu tříd, a pro popis toho, jak budou 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 jak se s jejími objekty pracuje (a kvůli dědičnosti i něco málo o jejich vnitřní struktuře), a implementaci, jež určuje jak objekty budou zpracovávat zprávy. Ve zdrojových textech pro jejich popis slouží direktivy @interface, @implementation a @end.

Nejjednodušší rozhraní prostě 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í říkat nadtřída):

@interface MyClass:NSObject @end

Velice často by se nám hodilo, aby každý objekt třídy obsahoval nějaké vlastní proměnné ('properties'), jež tak či onak definují jeho obsah: objekt "kniha" by asi měl proměnné "autor", "název", "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. 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 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

(Připomeňme, že id je vlastně ukazatel; 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ř objektu třídy MyClass, zatímco objekt o2 je někde venku — uvnitř objektu 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 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ž 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č vědět? 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 identifikátory, a před ně i před celou zprávu v závorkách napíšeme typy:

@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č! Za běhu pak díky pozdní vazbě může libovolný objekt dostat libovolnou zprávu, bez ohledu na to, jestli je zapsaná v jeho rozhraní nebo ne. Můžeme, mimochodem, používat i zprávy, jež nejsou zapsané v žádném rozhraní: jejich návratové hodnoty i jejich případné argumenty pak budou typu id. Totéž platí pro návratové hodnoty nebo argumenty, u kterých žádný typ v závorce neuvedeme.

Implementace z hlediska programátora vlastně není nic jiného, než naprogramování několika metod. Metoda je v zásadě 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í objekt zprávu, odpovídající 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 objekt zprávu, jíž neodpovídá žádná z jeho metod, odmítne ji, a dojde k běhové chybě (pro úplnost poznamenejme, že jsou k dispozici 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; prozatím si však takovými věcmi 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 objekty 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ý, ale také možný.

Uvnitř implementace metod jsou přístupné všechny vlastní proměnné objektu (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 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. To pohodlně a automaticky zajišťuje dědění zpráv: jestliže v implementaci třídy NSObject byla 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 si, že třída sama je objektem, a sama dokáže přijímat a zpracovávat zprávy. Proto můžeme v rozhraní kromě deklarace zpráv, určených pro objekty, deklarovat i zprávy určené pro samotnou třídu. Podobně v implementaci můžeme definovat metody, 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 stejná jako minule; jen znak '-' na začátku je nahrazen znakem '+':

@interface MyClass4:NSObject
+alloc; // vrátí nový objekt této třídy
+(char*)name; // pro třídu
-(char*)name; // pro objekty
@end
@implementation MyClass4
+alloc { ... }
+(char*)name
{
  return "Třída MyClass4";
}
-
(char*)name
{
  return "Objekt 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 alloc];
  printf("%s, %s",[MyClass4 name],[o name]);
}

Samozřejmě, že metody tříd se dědí analogickým způsobem, jako metody objektů: jestliže dostane třída zprávu, pro niž nenajde ve vlastní implementaci žádnou "plusovou" metodu, hledá metodu v implementaci své nadtřídy. To je, mimochodem, jedna z podstatných výhod Objective C oproti C++, ve kterém "třídní" (v jeho terminologii statické) metody dědit nejdou vůbec.

Shrnutí

Ukázali jsme si základní přístup k objektům a principy jejich používání. V rámci příkladů jsme se přitom seznámili s nejdůležitějšími součástmi jazyka Objective C; ti, kdo mají jeho překladač k dispozici (jako GNU C je k dispozici na libovolné platformě, od Mac OS X přes všechny varianty unixu až po DOS či Windows) skutečně již mohou začít programovat.

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


Zpět Obsah Další

Copyright © Chip, O. Čada 2000-2003