ZpětObsahDalší

Více o Objective C


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:

Neobjektová rozšíření

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
SELvnitřní representace zprávy
IMPmetoda (přímý ukazatel na metodu, používaný pro statický přístup)
Identifikátory
id selfv implementaci metody reprezentuje objekt, který metodu zpracovává
id superditto, ale jeho metody jsou vyhledávány v rodičovské třídě
SEL _cmdv 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.

Přístup k proměnným

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:

@privateproměnné jsou přístupné pouze metodám objektu samotného;
@protectedproměnné jsou přístupné i dědicům (tento přístup je standardní nepoužijeme-li žádnou z direktiv);
@publicpromě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.

Kategorie

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.

Protokoly

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)]) {
      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.

Ostatní

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.

Příklady

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);
Příklad 2: tvorba vlastní třídy;
Příklad 3: dědičnost a vkládání objektů;
Příklad 4: dynamické rozpoznání třídy;
Příklad 5: skládání objektů a dynamické rozpoznání zpráv;
Příklad 6: skládání objektů a dynamické rozpoznání zpráv -- jiná varianta;
Příklad 7: mechanismus klient/server;
Příklad 8: statický přístup k objektům.

Shrnutí

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ětObsahDalší

Copyright (c) Chip, O. Čada 2000