Zpět | Obsah | Další |
V minulém textu jsme se seznámili se systémem objektů a ukázali jsme si všechny základní služby jazyka Objective C. Nyní se seznámíme se zbytkem konstrukcí, jež Objective C nabízí; ačkoli žádná z nich není pro programování bezpodmínečně nutná -- jednoduché testovací prográmky jste si snadno mohli vyzkoušet už s využitím služeb, popsaných minule -- dokáží programátorovi výrazně usnadnit život.
Systém objektů a základy Objective C si popíšeme v několika odstavcích. Nejprve pro úplnost zopakujeme odkazy na prvky, popsané minule:
Zbývající služby jazyka Objective C jsou popsány v dnešním dílu v následujících odstavcích:
Jazyk Objective C je určen především pro práci s objekty; neobjektových rozšíření v něm proto mnoho nenalezneme. Ta, jež zde jsou, jsou však velmi příjemná. Prvým z nich je možnost používat komentář "//" stejně jako v C++ (to již nepřímo vyplynulo z příkladů v minulém dílu, kde byly takové komentáře používány). Druhým je standardizace typu a hodnot pro logické (boolovské) proměnné: aniž by byl narušen standardní přístup jazyka C, slouží jako logický typ typ BOOL a odpovídající hodnoty jsou YES a NO. Standardní headery prostě definují
typedef int BOOL;
#define YES 1
#define NO 0
případně v jazyce C ekvivalentní typedef enum {NO, YES} BOOL, jehož výhodou je, že konstanty YES a NO jsou známy i na úrovni debuggeru.
Velmi šikovným rozšířením je direktiva #import. Ta funguje téměř stejně, jako klasická direktiva #include; překladač ale zajistí, že každý zdrojový soubor se bude překládat nejvýše jednou. V Objective C si proto můžeme ušetřit nepohodlné obkládání každého hlavičkového souboru direktivami typu
#ifndef _STDIO_H_
#define _STDIO_H_
...
#endif
Je snad trochu sporné, zda mezi neobjektová rozšíření můžeme řadit nové typy, hodnoty a identifikátory: kromě typů id a Class a hodnot nil a Nil, jež již známe z minulého dílu, nabízí Objective C následující typy a identifikátory:
Typy | |
---|---|
SEL | vnitřní representace zprávy |
IMP | metoda (přímý ukazatel na metodu, používaný pro statický přístup) |
Identifikátory | |
id self | v implementaci metody reprezentuje objekt, který metodu zpracovává |
id super | ditto, ale jeho metody jsou vyhledávány v rodičovské třídě |
SEL _cmd | v implementaci metody reprezentuje zprávu, jež metodu vyvolala |
Typ SEL reprezentuje zprávu a je definován jako celočíselná hodnota, na kterou je zpráva interně přeložena. Spolu s direktivou @selector, jež zprávy převádí na tento typ, umožňuje zprávy ukládat do proměnných, vzájemně porovnávat a podobně. Typ IMP vlastně není ničím jiným, než ukazatelem na funkci, a využívá se v těch zcela výjimečných případech, kdy potřebujeme volat metodu rychleji, než prostřednictvím mechanismu zpráv. Ukázky praktického použití naleznete ve čtvrtém a osmém příkladu.
Poznamenejme, že pro dosažení statické typové kontroly srovnatelné s C++ nabízí Objective C možnost používat na místě typu id konstrukci "ukazatel na třídu" s významem "objekt dané třídy nebo jejího dědice". Je vhodné zdůraznit, že jde pouze o statickou, překladovou kontrolu -- na výsledný program to nemá vůbec žádný vliv, ten bude fungovat stejně dobře (nebo stejně špatně) jako kdybychom všude důsledně používali id. Praktickou ukázku najdete v příkladu 2.
Díky tomu že self, super a _cmd jsou identifikátory a ne klíčová slova (jako je tomu např. v nedomyšleném C++), můžeme je bez jakýchkoli problémů předefinovat; překladač Objective C proto bez problémů přeloží 'obyčejný céčkový' program, ve kterém je použita např. proměnná jménem self.
Proměnné objektu mohou být k dispozici pouze jeho vlastním metodám, nebo i metodám všech jeho dědiců, nebo -- ve výjimečných případech, kdy z nějakého důvodu musíme rezignovat na objektové programování a využívat statické programátorské techniky -- mohou být proměnné přístupné z jakéhokoli úseku kódu. Možnosti přístupu k proměnným jsou určeny použitím jedné ze tří direktiv:
@private | proměnné jsou přístupné pouze metodám objektu samotného; |
@protected | proměnné jsou přístupné i dědicům (tento přístup je standardní nepoužijeme-li žádnou z direktiv); |
@public | proměnné jsou přístupné komukoli. |
Jestliže z nějakého důvodu musíme rezignovat na objektový přístup, můžeme také získat neomezený přístup k proměnným kteréhokoli objektu pomocí direktivy @defs; ukázka jejího použití (i ukázka direktivy @public) je v příkladu 8.
Primární účel kategorií je umožnit rozložení implementace jedné složité třídy do několika zdrojových souborů. Kategorie má interface i implementaci obdobné standardním, avšak na místě nadřízené třídy je jméno kategorie v závorkách. Kategorie samozřejmě nemůže definovat vlastní proměnné; má však volný přístup k proměnným, definovaným v základním rozhraní třídy.
Dejme tomu, že máme následující třídu:
@interface Xxx:Yyy
-aaa;
-bbb;
-ccc;
@end
včetně odpovídající implementace
@implementation Xxx
-aaa { ... }
-bbb { ... }
-ccc { ... }
@end
Pokud by pro nás bylo z jakéhokoli důvodu výhodné oddělit od sebe implementace těchto tří metod do samostatných celků, mohli bychom stejně dobře použít základní třídy a dvou kategorií -- z hlediska práce s třídou Xxx a jejími objekty by se nezměnilo vůbec nic:
@interface Xxx:Yyy // základní třída
-aaa;
@end
@interface Xxx (KategorieProMetoduB)
-bbb;
@end
@interface Xxx (AProMetoduCcc)
-ccc;
@end
Obdobně by samozřejmě byla rozdělena i implementace:
@implementation Xxx // základní třída
-aaa { ... }
@end
@implementation Xxx (KategorieProMetoduB)
-bbb { ... }
@end
@implementation Xxx (AProMetoduCcc)
-ccc { ... }
@end
Samozřejmě, že každá kategorie může být v samostatném zdrojovém souboru; navíc můžeme do projektu zahrnout jen ty kategorie, jež v konkrétním případě skutečně potřebujeme.
Kategorie navíc umožňují doplňovat nebo měnit již existující třídy: dejme tomu, že bychom chtěli, aby libovolný objekt dokázal reagovat na zprávu where jménem počítače, na kterém běží proces, v rámci něhož objekt existuje. V Objective C není nic jednoduššího -- prostě implementujeme kategorii
@interface NSObject (ReportWhere)
-(NSString*)where;
@end
@implementation NSObject (ReportWhere)
-(NSString*)where
{
return [[NSProcess Info processInfo] hostName];
}
@end
Jakmile máme kategorii hotovou, můžeme novou službu zcela volně používat u kteréhokoli objektu v každém projektu, ve kterém je tato kategorie (ať již přímo, nebo např. v rámci sdílené knihovny) k dispozici.
Protokol v zásadě není ničím jiným, než seznamem metod; používá se jako společný prvek pro specifikaci tříd, které mají mít společné metody, ale nejsou strukturálně příbuzné (čímž nahrazuje implementačně i programátorsky obtížnou vícenásobnou dědičnost C++ v tom jediném případě, kdy měla jakýsi smysl).
Syntaxe protokolu je téměř totožná syntaxi rozhraní (interface); liší se od něj pouze v tom, že nemůže obsahovat žádné vlastní proměnné (properties), a že se namísto direktivy @interface použije direktiva @protocol:
@protocol DoubleValue
-(double)doubleValue;
@end
Tento jednoduchý protokol specifikuje zprávu doubleValue; mohli bychom jej tedy využít například pro formální popis toho, co stačí pro objekt abychom jej mohli použít ve funkci average z ukázky flexibility objektů v minulém dílu.
Protokoly mají vlastní systém "dědičnosti", jež samozřejmě nemusí nikterak souviset s děděním tříd. "Dědění" protokolu prostě znamená "chci do nového protokolu zahrnout všechny zprávy z toho starého"; protokol Values z příštího příkladu tedy specifikuje dvě zprávy -- intValue a doubleValue. Povšimněte si mírně odlišné syntaxe pro určování "nadprotokolů", než jakou používáme pro dědění tříd -- za malou chvilinku si ukážeme, jaký to má smysl:
@protocol Values <DoubleValue>
-(int)intValue;
@end
Zatímco vícenásobná dědičnost tříd, již nabízí např. C++, není k ničemu dobrá a vede leda k implementačním problémům a nesrozumitelným programům, vícenásobná "dědičnost" protokolů je samozřejmě žádoucí -- dává naprostý smysl, vytvořit třeba nový protokol, který shrnuje všechny zprávy z protokolů P1, P2, P3 a Values (mohli bychom do seznamu klidně přidat i DoubleValue, ale bylo by to zbytečné -- jeho obsah se do výsledku promítne díky "dědění" přes protokol Values):
@protokol Kombinace <P1, P2, P3, Values> @end
Hlavní důvod, proč se syntaxe "dědění" protokolů liší od dědění tříd spočívá v tom, že pomocí lomených závorek můžeme libovolnou skupinu protokolů specifikovat při deklaraci rozhraní:
@interface MyClass:NSObject <Values>
...
@end
To má dva důsledky: předně, o třídě MyClass "je známo", že implementuje protokol Values (a tedy samozřejmě i protokol DoubleValue); za běhu si to můžeme dynamicky ověřit a vyhnout se tak běhové chybě -- příklad z ukázky flexibility objektů bychom tedy mohli spolehlivěji přepsat takto:
double average(id *o) // pole objektů, končí hodnotou nil
{
double cnt=0,sum=0;
while (*o) {
if (![o conformsToProtocol:@protocol(DoubleValue)])
continue;
sum+=[o doubleValue];
cnt++; o++;
}
return sum/cnt;
}
Tato funkce bude fungovat korektně i v případě, že ji někdo omylem zavolá na objekty, mezi nimiž jsou i takové, které protokol DoubleValue neimplementují a metodu doubleValue neznají. Původní verse z minulého dílu by v takovém případě skončila běhovou chybou; této se to stát nemůže. Podobné zabezpečení je už v jazycích typu C++ zhola nemožné...
Druhý důsledek je kontrola, již zajišťuje překladač: pokud ani třída MyClass, ani žádná z jejích nadříd neobsahuje implementaci metod z protokolu Values, zobrazí překladač varování (samozřejmě ne chybu, což je nešvar, páchaný v podobných příkladech např. překladači C++ -- programátor přece potřebuje ladit i nedokončené třídy, takže slušný překladač musí být ochoten takovou věc přeložit, jen programátora varuje že to není korektní).
Poslední záležitost, o které se zde v souvislosti s protokoly zmíníme, je také jen kontrola při překladu: deklarací
id<Values> o;
(nebo samozřejmě obdobnou s jinou sadou protokolů v lomených závorkách) vytvoříme proměnnou, o níž překladač ví, které protokoly odpovídající objekt implementuje. Na rozdíl od (stejně dobře fungující) deklarace s pouhým id se v tomto případě formou varování ihned dozvíme, pokusíme-li se někde objektu poslat zprávu, již žádný ze specifikovaných protokolů neobsahuje.
Sedmý příklad ukazuje využití protokolu v jednoduché sestavě klient/server.
Objective C nabízí ještě dvě direktivy, @class a @encode. Prvá z nich prostě informuje překladač o existenci třídy daného jména, a slouží pro dopředné reference:
@class Xxx;
@interface Yyy
-(Xxx*)xxx;
@end
@interface Xxx
-(Yyy*)yyy;
@end
Direktiva @encode slouží pro dynamickou specifikaci typu, a v praxi se téměř vůbec nepoužívá (protože plně objektový systém dynamické typy vlastně nepotřebuje -- namísto nich se používají objekty, jež si typovou informaci nesou implicitně v sobě); její podrobný popis si proto můžeme odpustit.
Pro lepší ilustraci uvedeme několik bohatě komentovaných příkladů jednoduchých programů; s výjimkou příkladů 1 a 7, jež ukazují využití hotové knihovní třídy, nevyžadují příklady nic jiného než překladač Objective C se standardními knihovnami.
Příklad 1: využití předdefinovaných tříd (knihovny tříd NeXTstepu);Dokončili jsme stručný popis 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) v něm mohou psát libovolné testovací programy.
Příště 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 (c) Chip, O. Čada 2000