Přátelské nedorozumění nad kávouNemohu se ubránit pocitu, že se Java stala něčím jako náboženstvím. Jestliže se o ní nevyjádříte dostatečně uctivě, vrhnou se na vás její příznivci jako na kacíře. Není to nic nového, podobný osud potkal před časem Pascal, C++, Forth a jiné jazyky. Jedním z prvních z nich byl nepochybně Fortran: Kdo by se nepamatoval na článek o skutečných programátorech, kteří používají právě tento jazyk, a pojídačích koláčů, kteří používají Pascal. Problém je, že řada argumentů v podobných diskusích vychází z neporozumění nebo z neznalosti čehokoli jiného kromě zbožňovaného jazyka. Proto jsem velice rád, že se argumentace ve prospěch Javy ujal pan Čada, který velmi dobře zná nejen Javu a C++, o které nám zde půjde, ale i řadu dalších souvislostí, a to jak po teoretické, tak i po praktické stránce. O co vlastně jdeČlánky „Jak jsem potkal Javu“ a „Co má Java proti C++“ vznikly na základě poznámek, které jsem si dělal, když jsem se s tímto jazykem seznamoval. Nekladl jsem si jiný cíl než poukázat na místa, která mohou být pro céčkaře z nějakých důvodů zajímavá nebo nebezpečná. Nic víc a nic méně. Článek pana Čady se vyjadřuje k řadě problémů; já se zde pokusím na některé z nich odpovědět. O přetěžování operátorůTvrzení, že přetěžování operátorů má smysl jen pro matice a podobné matematické struktury a pro aritmetické operátory, vypadá na pohled přesvědčivě, skutečnost je ale složitější. Prohlédneme-li si pozorně standardní knihovnu jazyka C++, zjistíme že téměř každou z tříd v ní provází skupina přetížených operátorů. Některé z nich jsou metody, jiné obyčejné funkce. Uveďme si alespoň několik příkladů: n Přetížený operátor indexování slouží k přístupu k prvkům dvoustranné fronty (deque) a asociativního pole (map). n Přetížený přiřazovací operátor umožňuje přenos všech prvků z jednoho kontejneru do jiného. n Přetížené operátory == a != slouží pro porovnávání obsahu kontejnerů. (Např. dva seznamy jsou si rovny, jestliže obsahují stejné prvky ve stejném pořadí.) n Díky tomu, že můžeme přetěžovat operátory ==, !=, ++, -- a * (dereferencování), lze v C++ snadno definovat iterátory, tedy pomocné datové struktury, které umožňují zacházet jednotným způsobem s kontejnery obsahujícími „posloupnosti“, tedy s poli, seznamy, frontami ap. Pak můžeme snadno vytvořit např. šablonu funkce, která bude umět setřídit jak „obyčejné“ céčkovské pole tak seznam nebo dvoustrannou frontu. Za vyloženou lahůdku považuji použití přetížených operátorů pro vstupní a výstupní operace. Kdykoli mám v Javě napsat něco jako System.out.println(a); vzpomenu si na eleganci zápisu cout << a << endl << b << endl; a tiše (nebo i nahlas) zanadávám. Hlavní síla operátorů pro vstupy a výstupy v C++ ale spočívá v tom, že si je mohu rozšířit – přetížit – i na své vlastní datové typy a zacházet s nimi zcela stejně jako s typy vestavěnými. Samozřejmě, operátory lze přetěžovat i nesmyslným způsobem. Lze definovat operátor *, který bude sčítat, lze definovat operátor =, který bude přenášet data zleva doprava atd. Ovšem skutečnost, že něco lze zneužít nebo použít nesprávným způsobem, ještě přece není důvod k zákazu. Logika „lze to zneužít, tedy se to zneužívá, a proto to zakážeme“, mi připomíná logiku zákona, který zakazoval vypalovat soukromě alkohol a za provinilého považoval každého, kdo měl destilační přístroj nebo jeho „podstatnou část“. To bychom totiž museli zakázat i metody: Kdo nám zabrání definovat metodu Plus(), která bude dělit? Dovedeme-li tuto logiku do absurdity, měli bychom zakázat programování jako celek, neboť jen tak lze zabránit psaní počítačových virů, trojských koňů a jiných lahůdek. Virtuální a nevirtuální metodyDovolím si oponovat názoru, že nevirtuální metody jsou něco příšerného až zrůdného. Je to jen další vyjadřovací prostředek, který C++ programátorovi nabízí, a lze jej velice elegantně využít. Než se ale pustíme do úvah na toto téma, řekněme si několik slov o polymorfismu jako takovém. Za tímto označením se skrývá jedno z pravidel objektového programování, které říká, že na místě, kde očekáváme instanci předka (bázové třídy), lze použít instanci potomka (odvozené třídy), neboť odvozená třída by měla představovat zvláštní případ (specializaci) bázové třídy. To znamená, že při použití objektu ve skutečnosti nemusíme znát jeho třídu; prostě mu pošleme zprávu (zavoláme jeho metodu) a on zareaguje správným způsobem. Představme si např., že pracujeme s grafickými objekty, které jsou instancemi tříd odvozených od společného předka – třídy GrafickyObjekt. Máme jich plný zásobník a chceme je všechny nakreslit. Pošleme jim tedy všem po řadě zprávu „nakresli se“, tj. zavoláme odpovídající metodu, a očekáváme, že objekt, který představuje čtverec, se nakreslí jako čtverec, zatímco objekt, který představuje kruh, se opravdu nakreslí jako kruh, aniž se o to musíme zvlášť starat. Syntakticky bude odpovídající příkaz v Javě i v C++ pokaždé stejný, bude to volání metody se stejným identifikátorem (např. Nakresli()) a se stejnými parametry. Je věcí programu, aby se postaral o volání správných metod odpovídajících skutečnému typu instance. Technický prostředek, kterým se toho dosahuje, se označuje jako „pozdní vazba“. Tolik na úvod, ze kterého by mělo být jasné, že polymorfismus má smysl pouze v případě, že je ve hře dědičnost. V Javě tvoří všechny třídy jedinou dědickou hierarchii, proto je tak trochu logické, že všechny metody využívají pozdní vazbu. Na druhé straně v C++ mohou existovat zcela samostatné třídy, tedy třídy, které neleží v žádné dědické hierarchii. Typickým příkladem může být knihovní třída complex<T> reprezentující komplexní čísla vytvořená z dvojice čísel typu T. Proč by taková třída měla obsahovat nějaké společné vlastnosti všech objektů? Protože tato třída není potomkem žádné třídy a protože nepředpokládáme, že by se mohla stát předkem nějaké třídy, není nejmenší důvod používat pro její metody pozdní vazbu – tedy deklarovat je jako virtuální. Nicméně i v polymorfních třídách mohou mít nevirtuální metody své opodstatnění. Ostatně neuškodí, podíváme-li se na možnosti, které nám v souvislosti s virtuálními a nevirtuálními metodami C++ nabízí a jaký mají význam. Definujeme-li v předkovi: n čistě virtuální metodu bez implementace (v Javě a občas i v C++ se nazývá „abstraktní“), definujeme tím její rozhraní a přikazujeme potomkům, že musí definovat její implementaci; n čistě virtuální metodu s implementací (to je konstrukce, která v Javě nemá analogii), definujeme tím její rozhraní a nabízíme potomkům implicitní implementaci, kterou ale nelze volat pomocí pozdní vazby – potomek ji musí zavolat s plnou kvalifikací; n virtuální metodu („obyčejnou“), definujeme tím její rozhraní a implementaci, kterou mohou potomci změnit, je-li to potřebné; n nevirtuální metodu, definujeme tím její rozhraní a implementaci, která je pro potomky závazná. K poslednímu bodu je třeba poznamenat, že nejde o závaznost syntaktickou (jako je klíčové slovo final v Javě), neboť překladač se nevzbouří, jestliže nevirtuální metodu v potomkovi předefinujeme. Existují i situace, kdy to má smysl, většinou tím ale způsobíme chybu – program se bude chovat jinak, než jsme si přáli. Vícenásobná dědičnost a rozhraníVícenásobná dědičnost se v C++ opravdu používá především k implementaci rozhraní. Lze pro ni nalézt i jiná použití, neboť představuje zajímavou alternativu ke skládání objektů – například standardní vstupně-výstupní proud iostream je společným potomkem tříd istream a ostream, které představují samostatný vstupní, resp. samostatný výstupní proud. To ale není příliš podstatné; podívejme se na příklad, na kterém pan Čada předvádí, k jakým problémům může vícenásobná dědičnost vést. Pro pohodlí čtenáře – ale i své – ho zde zopakuji. Nejprve definujeme abstraktní třídu Interface, která nahrazuje javské rozhraní: class
Interface {public: Pak definujeme třídu Object, která by měla sloužit jako společný předek všech tříd v našem programu: class Object { public: Nakonec definujeme třídu Xxx, která implementuje rozhraní Interface a která je potomkem třídy Object: class
Xxx: Nakonec se podíváme na funkci use(), která pracuje s rozhraním Interface a která působí problémy: void use(Interface* o) Předáme-li této funkci jako parametr ukazatel na instanci třídy Xxx, zavolá se v příkazu, označeném vykřičníkem, metoda metoda(), nikoli metoda xxx(). To je velice nepříjemná chyba, jejíž hledání v programu může trvat velmi dlouho. Co se vlastně stalo? Přetypování (Object*)o překladači vlastně říká: „Zde máš ukazatel o na třídu Interface; buď tak laskav a zacházej s ním jako s ukazatelem na třídu Object.“ Tyto dvě třídy spolu nijak nesouvisí a v době překladu nelze obecně zjistit, zda o náhodou nebude za běhu obsahovat ukazatel na instanci odvozené třídy, který je zároveň potomkem třídy Object. Uvedený postup tedy představuje jedinou alespoň trochu rozumnou interpretaci požadovaného přetypování. Výsledkem je, že volání metody xxx()překladač přeloží jako volání první virtuální metody třídy Object, tj. metody metoda(). My jsme si ale přáli něco jiného: Chtěli jsme zacházet s ukazatelem na rozhraní jako s ukazatelem na instanci nějaké třídy, která je potomkem třídy Object, a zavolat její zděděnou metodu xxx(). To samozřejmě jde, musíme ale zapomenout na analogii s Javou a říci si o to tak, jak se sluší a patří v C++. Především bychom měli se měli rozloučit s přetypovacím operátorem (typ), převzatým z jazyka C. Jeho úkoly – a něco navíc – si mezi sebe rozdělily operátory dynamic_cast, static_cast, const_cast a reinterpret_cast. Protože nám jde o přetypování mezi ukazateli na polymorfní třídy v rámci jedné dědické hierarchie, použijeme operátor dynamic_cast: Object *uo = dynamic_cast<Object*>(o); Nyní již program bude fungovat bez problémů. Poznamenejme, že zatímco operátor (typ) se v C++ vyhodnocuje už v době překladu, operátor dynamic_cast se volá za běhu programu. Tento operátor nejprve zjistí, zda má požadované přetypování smysl, a pokud ano, provede jej. Pokud ne, vrátí 0. Proto jsme vrácenou hodnotu uložili do pomocné proměnné a před použitím testovali její hodnotu. Použitím operátoru dynamic_cast lze vyřešit i další problémy v příkladech, které pan Čada uvádí ve svém, článku a které obsahují přetypování ukazatelů na objekt. Existují v C++ třídy?To je otázka, na kterou není vůbec snadné odpovědět – záleží na tom, co si pod tím představujeme. Položme si několik otázek, které by nám mohly pomoci najít odpověď. n Můžeme za běhu programu poslat třídě zprávu? – Můžeme; v C++ existují metody tříd, nazývané podobně jako v Javě „statické“. n Má třída svou datovou reprezentaci v programu? – Může ji mít. Skládá z datových složek třídy („statických“ dat) a popřípadě z tabulky virtuálních metod a informací nezbytných pro dynamické určování typu za běhu programu (ovšem pokud má naše třída alespoň jednu virtuální metodu). n Chová se instance objektového typu v C++ jako „černá skříňka“, která přijímá zprávy a reaguje na ně? – Odpověď zní ano, pokud samozřejmě s instancí zacházíme korektně. Například nekorektní přetypování může způsobit problémy, ale o tom hovořím v oddílu „Vícenásobná dědičnost a rozhraní“. n Může program vytvořit instanci třídy, která nebyla v době psaní programu známa, kterou naprogramoval někdo jiný, kterou a kterou umístil např. do dynamické knihovny? – Může, ale není to zdaleka tak jednoduché jako v Javě nebo v jiných čistě objektových jazycích. Musí být splněny jisté dodatečné a částečně omezující předpoklady, např. že půjde o třídu odvozenou od pevně daného předka, že odpovídající dynamická knihovna bude obsahovat určité pomocné funkce atd. n Stačí tři obvykle uváděné principy objektového programování – zapouzdření, dědičnost, polymlorfizmus – k tomu, abychom uznali, že jazyk podporuje objektové programování? – Na tuto otázku nechť si odpoví čtenář sám. V zájmu objektivity je ale nutno dodat, že nepolymorfní třídy v C++, tedy třídy, které neobsahují žádné virtuální metody, se opravdu podobají spíše strukturám s metodami než objektům – nefunguje pro ně mj. dynamická identifikace typů (RTTI). Lze si ovšem jen těžko představit, že se v jakékoli rozumné dědické hierarchii vyskytují nepolymorfní třídy. SuperHlavní účel klíčového slova super opravdu není potlačení pozdní vazby, jak snad z mé poznámky v článku „Jak jsem potkal Javu“ mohlo vyplývat. Jak známo, toto super zpřístupňuje složky předka, nadtřídy. Mimochodem, příklad, na kterém pan Čada použití super ukazuje, není právě nejšťastnější. I když technicky lze třídu komplexních čísel zavést jako potomka třídy reálných čísel, logicky to není nejlepší, neboť v matematice je jejich vztah přesně opačný – reálná čísla jsou zvláštním případem komplexních čísel (s nulovou imaginární částí). Kdybychom opravdu někde použili komplexní čísla odvozená od reálných čísel, znamenalo by to, že n v místech, kde se očekává pouze reálné číslo, nám program dovolí použít i jakékoli komplexní číslo, n v místech, kde se očekává komplexní číslo, nám program nedovolí použít číslo reálné. Obojí je nesmyslné, ovšem hlavního tématu diskuze, významu klíčového slova super, se to v podstatě netýká. Vraťme se tedy ke klíčovému slovu super a k polymorfismu. Java obecně neumožňuje potlačit pozdní vazbu, tj. předepsat, že chceme volat zděděnou metodu, nikoli metodu implementovanou v potomkovi. Jedinou situaci, kterou takto lze označit, ukazuje následující příklad: class
Predek { class
Potomek extends Predek { Kdybychom v metodě g() potomka nepoužili klíčové slovo super, volala by se metoda f() třídy Potomek. Takto zavoláme metodu předka, tedy vyřadíme (alespoň částečně) pozdní vazbu. Tento trik ovšem umožňuje pouze využít metodu předka k implementaci metody potomka, neumožňuje programově zavolat metodu předka pro instanci potomka. Neumožňuje také volat metodu ze vzdálenějšího předka. Reference a garbage collectorAutomatická správa paměti, garbage collector, je velice pohodlné zařízení. Když si na něj zvyknete, nebude se vám chtít vracet se k systémům, které ho neobsahují. Nicméně v C++ jej lze poměrně snadno nahradit. V běžných případech stačí zapouzdřit ukazatel na dynamicky alokovaný objekt do automatického ukazatele, instance pomocného objektového typu, jehož destruktor se postará o uvolnění paměti objektu. Takový automatický ukazatel si můžeme snadno naprogramovat sami; standardní knihovna jazyka C++ ale obsahuje šablonu třídy auto_ptr<T>, která implementuje ukazatel na typ T. Tento automatický ukazatel obsahuje i příznak vlastnictví a o uvolnění paměti se pokusí pouze ukazatel, který objekt „vlastní“. Ve složitějších případech se pro dynamicky alokované objekty používá tzv. počítání referencí. Objekt se v tom případě stará o svém uvolnění z paměti sám – obsahuje celočíselnou proměnnou, ve které si počítá, kolik ukazatelů na něj existuje, a v případě, že tento počet klesne na nulu, spáchá sebevraždu např. příkazem delete this; Je jasné, že jde o způsob poměrně nepohodlný a náchylný k chybám, na druhé straně ale o způsob velice efektivní. Funkční parametryV článku „Jak jsem potkal Javu“ jsem jako způsob předávání funkčního parametru předvedl automaticky generovaný kód z JBuilderu, ve kterém se předávaná metoda s daným prototypem „zabalí“ do anonymní třídy. To je zcela standardní javská konstrukce, nejde o žádné rozšíření. Souhlasím s tím, že způsob, který pan Čada ukazuje, je na první pohled elegantnější. Nicméně způsob, který se používá v JBuilderu, umožňuje, aby handler, podprogram, který se stará o odezvu na nějakou událost, byl metodou okna nebo obecně objektu, který představuje součást grafického rozhraní, ve kterém je komponenta umístěna. To umožňuje mj. sdílení handlerů – několik komponent může při různých událostech volat týž handler. (Můžeme např. požadovat, aby se při dvojkliku myší na vstupní řádku stalo totéž jako při stisknutí tlačítka vedle této řádky.) Nepsat stejný kód vícekrát, to je přece stará programátorská zásada. Překladač a programátorJedním ze základních rysů jazyka C++ je, že jeho překladač pokládá programátora za myslícího, samostatného a svéprávného člověka, který ví, co chce. Proto se mu snaží vyhovět, nehledá důvod, proč jeho program označit za chybný, a dovoluje mu řadu „potenciálně nebezpečných“ operací, jako je přímé zacházení s ukazateli, přetypování ukazatele na jednu třídu na ukazatel na jinou třídu, předefinování nevirtuálních metod v odvozené třídě apod. V tomto přístupu k programátorovi se C++ výrazně odlišuje nejen od Javy, ale i od Pascalu, Ady a řady jiných programovacích jazyků. Proto také v případě mnoha potenciálně nebezpečných konstrukcí překladač hlásí pouze varování nebo ani to ne – spoléhá na kázeň a znalosti programátora. Java zdaleka tak liberální není, i když ji v žádném případě nelze označit za vyloženě restriktivní. To není odsudek, to je prosté konstatování skutečnosti. Podle mých zkušeností jsou mezi programátory dvě výrazné skupiny. Jedna z nich dává přednost jazykům, které poskytují volnost výrazových prostředků a možnost zvolit si vlastní osobitý styl, a to i za cenu, že mohou udělat chybu. K tomu jsou ochotni dodržovat jistou vlastní disciplínu, která je při práci s takovým nástrojem nezbytná. Druhá skupina preferuje spíše jazyky, které programátora vedou, nutí ho pracovat určitým předem daným stylem, ale na druhé straně umožňují snáze navrhovat strukturu programu a zamezují některým druhům chyb. Obojí má své výhody a nevýhody a příslušnost ke kterékoli z těchto skupin podle mých zkušeností naprosto nevypovídá o kvalitě programátora. Možná, že souvisí s jeho postojem ke světu, ale obávám se, že to je spíše filozofický problém. Myslet ve svém jazykuK tomu, abychom mohli úspěšně programovat v nějakém jazyku – a nemusí to být jen C++ nebo Java – nestačí znát klíčová slova a základní knihovní funkce nebo třídy. Každý jazyk má svou vnitřní logiku, a tu je při návrhu programu třeba brát v úvahu. Jinak se dostaneme do situace, budeme dělat z Javy C++, z C++ Pascal a podobně – a budeme náramně překvapeni, že se v našem programu objevují „záhadné“ a „nesmyslné“ chyby. Miroslav Virius (virius@km1.fjfi.cvut.cz)
|
Silná káva pro objektového programátoraKdyž jsem si v sedmém čísle přečtl článek pana Viriuse "Jak jsem potkal Javu", měl jsem nejprve pocit, odpovídající titulku: vždyť to tak není?!? Po druhém přečtení jsem si musel názor opravit: ono to tak většinou je, pan Virius je příliš dobrý profesionál, než aby – až na drobnosti – napsal něco, co není pravda. Jenže... jenže jako programátor v neobjektovém a špatně navrženém C++ prostě neví, v čem jsou silné a slabé stránky skutečně objektového programovacího jazyka, takže sice skvěle popíše drobné syntaktické a sémantické odlišnosti, ale vůbec se nezmíní o podstatných rozdílech. Rád bych proto tímto článkem jeho seriálek ne opravil – protože na víceméně bezchybném textu není co opravovat – ale doplnil tak, aby čtenář získal celkový přehled o tom, co mu Java nabízí oproti C++ nového, a naopak, co starého není nadále k dispozici. Většinou budu reagovat na konkrétní údaje a informace z článků v sedmém a v osmém čísle; někdy však se rozepíši samostatně o věcech, jimž pan Virius nevěnoval ani zmínku. Interpretovaný? Ano, ale...Java skutečně je standardně interpretovaná, což zvyšuje její přenositelnost a snižuje efektivitu. Ovšem, není to jediné řešení! Pro případy, kdy přenositelnost není tak zásadní, ale efektivita ano, se ještě pořád Javy nemusíme vzdávat: skvělý systém překladačů EGCS (nástupce známého GNU C) Javu dokáže překládat do strojového kódu téměř libovolného mikroprocesoru! Navíc, jako všechny GNU produkty, je k dispozici pro skoro jakékoli prostředí, a zcela zdarma. Čistě objektová znamená něco jiného!"Čistá objektovost" Javy neznamená, že by "úplně všechno byly objekty" – což je sice hezká idea, ale v praxi trochu problém, ale to, že její objekty jsou skutečnými objekty, tj. uzavřenými černými skříňkami, se kterými můžeme podle potřeby komunikovat, ale které teprve samy naše pokyny interpretují – a pokud to rozumně nejde, vyvolají dobře definovanou chybu. V C++ je tomu právě naopak: zde je "objekt" vlastně pořád proměnná typu struct s řadou speciálních služeb navíc, a její nekorektní použití vede k nepředpověditelnému zhroucení programu, či ještě hůř k jeho nesprávné funkci... Nejlépe je to vidět na typické situaci, kdy prostě voláme metodu: NejakaTrida *oc=new NejakaTrida; Tyto zápisy v C++ i Javě jsou si syntakticky velmi podobné, a sémanticky jsou – na první pohled – totožné: v obou případech se vytvoří nová instance třídy NejakaTrida, odkaz na ni se uloží do proměnné, a pak se prostřednictvím proměnné vyvolá její metoda nejakaMetoda. Zásadní rozdíl ovšem spočívá v tom, že v Javě každý objekt ví, co je zač a podle toho sám interpretuje to, co po něm někdo jiný chce. Ukažme si konkrétní ilustrace těchto rozdílů, opět na – na první pohled – prakticky totožném kódu: // pokračování minulého příkladu Příklad v C++ je zřejmý: prostě zkusíme vyvolat metodu z jiné třídy, přičemž program zcela nepředpověditelným způsobem spadne, nebo dokonce poběží dál, ale po provedení naprosto neočekávané a nesmyslné akce!!! Jistě, lze namítnout, že přetypování ('typecast') samo o sobě je "nedoporučeníhodná" věc, která si říká o problémy, jenže v praxi bez něj programovat prostě nelze. Můžeme se v extrémním případě vyhnout explicitním přetypováním, ovšem stále nám zůstanou implicitní: např. všechny metody všech knihovních tříd jsou přece implicitně otypovány podle toho, jaké headery použijeme – pokud jsou třeba náhodou novější, s novými či prostě jinými virtuálními metodami oproti headerům, jež byly použity při tvorbě knihoven, máme v C++ závažný problém. V Javě nic podobného nehrozí. Především, už samotný překladač by poznal, že instance třídy NejakaTrida a UplneJinaTrida navzájem přetypovat nelze, a program by vůbec nepřeložil; proto musíme hodnotu nejprve přetypovat na universální třídu Object. Pak se program korektně přeloží, ovšem při spuštění nedojde k žádné nepředpověditelné chybě: namísto toho se vyvolá jasně definovaná výjimka; v Javě už ve chvíli, kdy nekorektní přetypování provádíme. Ukažme si zcela konkrétní příklad: 29 ~/Library/Java\> cat Test.java Možná stojí za další ukázku to, že nejde o nějaké specifikum Javy – takto korektní chování je dáno rozumnou podporou objektů. Např. obdobný program v Objective C – až na oddělená rozhraní a implementace tříd je téměř přesně stejný, jako minulý příklad v Javě -- také ohlásí chybu (v tomto případě při pokusu volat metodu metoda): 50 /tmp\> cat test.m Můžeme jít ještě dál: v Javě vůbec není zapotřebí znát třídu objektu k tomu, abychom mohli korektně volat jeho metodu (nebo, protože objektová terminologie skutečně je pro Javu dalsko vhodnější, abychom mu mohli zaslat zprávu, na základě níž sám objekt vhodnou metodu provede). Podobně jako u metod tříd, zmíněných níže, zde narážíme na umělé omezení – samotný jazyk Java to prostě neumí (překladač dělá podobnou hloupost jako C++, že korektní, byť podezřelé akce hlásí jako chyby, ačkoli by mělo jít o warningy). Proto musíme využít knihovní služby; zde je využit package apple.com.foundation, který nabízí velmi pohodlnou třídu NSSelector. Pokud bychom tento package neměli k dispozici, museli bychom použít standardní package java.lang.reflect, který nabízí v zásadě tytéž služby, ale s daleko méně šikovným API. Program je ve třech zdrojových souborech jen proto, aby třídy Ttt a Sss mohly být public: 279 ~/Library/Java\> cat Ttt.java Sss.java Test.java Zásadní rozdíl spočívá v tom, že C++ svůj "objekt" používá zvenku: překladač vychází ze znalosti vnitřní struktury objektu, a podle toho konstruuje volání metody. Pokud skutečná vnitřní struktura objektu představám překladače neodpovídá – a to se ve skutečném, rozsáhlém, distribuovaném a často upgradovaném systému stává s železnou pravidelností – může se stát cokoli: zavolá se kód na nesmyslné adrese, nebo se dokonce volají data jako kód, případně se zavolá úplně jiná metoda... Java naopak využívá skutečné vnitřní struktury existujícího objektu; proto v ní k podobným problémům z principu nikdy a nijak nemůže dojít – ať již jsou objekty získány jakkoli, s přetypováním či bez něj, z knihoven nebo třeba z jiného procesu. Mimochodem, to je také jeden z důvodů, proč je v Javě každá třída vždy dědicem základní třídy Object. Funkce tam být nemohou......protože prostě tvůrci Javy nechtěli poměrně přehledný jazyk komplikovat další syntaktickou kategorií, nadto kategorií prakticky nepotřebnou. V žádném případě tedy nejde o "snahu po objektovosti na úkor zdravého rozumu", ale právě naopak: zdravý rozum tvůrcům Javy správně napověděl, že jazyk, v němž jsou k dispozici statické metody tříd, už žádné funkce nepotřebuje: darmo by komplikovaly jeho syntaxi i sémantiku, a nepřinesly by naprosto nic nového. Co se týká nutnosti jména "funkcí" kvalifikovat jménem třídy... nu, v 99.99% případů je to jen dobře, a naopak v klasických jazycích tato možnost vždy nepříjemně chyběla. Matematicky náročné algoritmy ostatně nikdo – alespoň nikdo, kdo má onen zmíněný zdravý rozum – nebude psát v Javě, ale použije native rutinu v plain C nebo ve Fortranu, a z Javy ji jen zavolá. A konečně, dejme tomu, že jednou za dlouhý čas je opravdu zapotřebí psát v Javě "sin(PI)" – pak stačí použít universální preprocesor z jazyka C, a vše funguje jak má: 115 ~/Library/Java\> cat Test.java Jednoduchoučké a zcela funkční... a to je ještě příkazový řádek zbytečně komplikovaný tím, že můj překladač javac se neumí chovat slušně, a nedostane-li vstupní soubor, překládat standardní vstup – jinak by stačilo pouhé "cc -E -P Test.java | javac". Vlk nemá hlad, a koza je v pořádkuMožnost kvalifikovaných bloků pro násilné ukončení příkazy break a continue namísto příkazu goto opět není nějaká z nouze ctnost, jak naznačuje původní článek; naopak, jde o perfektní řešení problémů, které příkaz goto přináší. Protože to nemá s objekty nic společného, jen velmi stručně: téměř vždy je možné použití goto přepsat lépe a elegantněji – s jedinou výjimkou, a tou je právě výskok z vnořených bloků, asi takto: for (...) { Přesně tuto situaci Java dokonale (a přehledněji a s menším risikem chyb) řeší – můžeme zde napsat ekvivalentní, ale mnohem elegantnější for (...) Hlavni: { // nebo Hlavni: for..., podle libosti Chyba příkazu goto totiž není v tom, že jde o příkaz skoku jako takový; konečně, skok je nutně skryt i v příkazech cyklu nebo v příkazu if. Jde především o nepřehledný kód, kdy se skáče sem a tam (v dobách BASICu jsme takový zdroják nazývávali "špagetovým", protože linie provádění kódu byla díky příkazům goto propletená jako špagety na talíři). Třídy: ale tohle je opravdu jinak!Především, v Javě jsou třídy. Už to je pro programátora v C++ novinkou – C++ totiž třídy nemělo, deklarace class v něm vytvořila pouze nový typ, který však za běhu neměl žádnou skutečnou representaci. V Javě je naproti tomu třída objekt jako každý jiný. Velmi typické využití této možnosti je objekt, který sám vytváří podle potřeby jiné objekty; přitom třídu, jejíž instance bude vytvářet, se dozví až za běhu: 152 ~/Library/Java\> cat
Test.java Tento jednoduchý příklad samozřejmě vypadá uměle; v praxi je však u dynamických systémů možnost uchovat třídu často velmi šikovná. Např. právě nyní pracuji na systému konverzních filtrů, v němž objekt "filtr" podle potřeby a podle typu vstupních dat vytváří jiný objekt, který provede konverzi dat. Filtry i konverze jsou samozřejmě dynamicky zaveditelné z nezávisle vytvářených DLL knihoven. V jazyce typu C++ by bylo nutné pro každou, i tu nejtriviálnější konverzi vždy vytvářet novou podtřídu filtru; v Javě (resp. jiném objektovém jazyce – zmíněný projekt píši v Objective C) tento problém není: pro jednoduché případy si prostě jediný, standardní filtr zapamatuje třídu, od níž bude nové konverze odvozovat, a je hotovo... PolymorfismusCo se týká metod: je jistě pravda, že pan Virius je z C++ zvyklý, že metody standardně nejsou polymorfní; tuto příšernou a nesmyslnou zrůdnost jazyka C++ však naštěstí Java odstranila. Nic takového jako nevirtuální metoda jazyka C++ v Javě není, a díky bohu za to! Klíčové slovo super, o kterém se pan Virius v tomto kontextu zmiňuje, docela určitě neslouží k potlačení pozdní vazby; jeho význam je jiný, a dostaneme se k němu později. Java ovšem umožňuje v opodstatněných případech dosáhnout stejné efektivity, již v C++ nabízejí nevirtuální metody, ale bez jejich strašlivých nevýhod: umožňuje to klíčové slovo final. Deklarujeme-li metodu jako final, říkáme překladači, že ji již nikdo v případných podtřídách nesmí reimplementovat (chcete-li, "přetížit" – podle mého názoru je tento kalk z angličtiny dost zrůdný, asi jako kdybychom chtěli rozhraní říkat "meziobličej"). Překladač proto může optimalizovat až do extrémů (např. může takovouto metodu přeložit jako inline). Přitom zůstává zcela korektně zachováno základní paradigma objektového systému, tj. zpracování požadavku stále závisí na objektu – není tedy naštěstí již možná zrůdnost jazyka C++, kdy zcela korektní přetypování na obecnější třídu mohlo vést k volání zcela odlišných metod: // v Javě se objekty chovají rozumně, takže... Jelikož objekt sám se nikterak nezměnil, nemůže se změnit v objektovém prostředí ani volání jeho metod – jako se to děje v C++: // v C++ se mohou dít divné věci, takže... To je samozřejmě obecně špatně – voláme-li jednu a tu samou metodu jednoho a toho samého objektu, nemůže se nám zavolat něco jiného podle toho, víme-li zrovna náhodou o objektu více nebo méně (tj. známe-li jeho skutečnou třídu NejakaTrida, nebo jen některou z jejích nadtříd JinaTrida)! Vícenásobná dědičnost a rozhraníJe možné diskutovat o tom, nakolik je nebo není vícenásobná dědičnost šikovná; mé zkušenosti říkají, že vždy přináší více problémů než výhod, ovšem zkušenosti jiných programátorů mohou být odlišné. Faktem ale je, že nejčastější a určitě nejrozumnější využití vícenásobné dědičnosti vždy bylo právě definování rozhraní pomocí plně abstraktních tříd. V C++ to však vedlo k problémům s identifikací objektů, a k nepříjemným a obtížně odstranitelným chybám, jež pak z toho vyplývaly – v následujícím příkladu se zavolá úplně jiná metoda, než jsme chtěli!!! 180 ~/Library/Java\> cat smazat.cpp V Javě právě díky tomu, že pro definici rozhraní využívá jinou syntaxi (a pro jeho implementaci samozřejmě i jinou sémantiku) než pro normální "třídní" dědičnost tento problém nemůže nastat: 188 ~/Library/Java\> cat Test.java Ono je používání odlišné syntaktické kategorie i čistší, protože se jedná o odlišné věci: dědičnost tříd postihuje strukturální podobnost objektů, zatímco rozhraní postihují jejich podobnost funkční; zatímco jeden směr implikace plati (jsou-li si dva objekty podobné strukturálně, budou mít alespoň nějakou podobu i funkčně), opačný nikoli: z funkční podobnosti ještě nemusí plynout podobnost struktury. To by však bylo téma pro rozsáhlou diskusi o základních principech objektového programování; pokud si vzpomínám, před lety jsme ji s panem Viriusem i vedli na stránkách Softwarových novin, avšak nevyřešila nic... doufejme, že tato polemika bude plodnější. Na co je dobré super?Výraz super naprosto neslouží k potlačení polymorfismu, ale umožňuje programově volat implementaci z nadtřídy. Představme si hypotetickou třídu Complex, která bude implementovat komplexní čísla, a sama bude dědicem třídy Real tak, že zděděna bude reálná složka, zatímco imaginární bude v nové proměnné im; implementace metody setZero, jež číslo vynuluje, by pak mohla vypadat např. takto: public void setZero() { Nejde tedy o potlačení polymorfismu, protože neustále platí, že metoda setZero je zcela polymorfní: volá se metoda právě toho objektu, se kterým pracujeme, bez ohledu na případné přetypování nebo jiné vnější triky. Klíčové slovo super pouze umožňuje v rámci volání metody definované v určité třídě využít implemetaci z předka jako součást nové implementace. V C++ bylo v zásadě možné totéž, museli jsme však nadtřídu explicitně kvalifikovat jejím jménem (v našem příkladu bychom museli napsat Real::setZero()), což bylo nepohodlné, a při změnách hierarchie tříd to vedlo k nepříjemným a obtížně odhalitelným chybám. Metody tříd jsou omezeny jako v C++Přeci jen pár hloupostí Java ale z C++ naneštěstí přebrala; patří mezi ně např. to, že v metodách tříd (tj. metodách, deklarovaných jako static) není k dispozici ani výraz this (který by zde samozřejmě měl representovat třídu, jejíž metodu právě provádíme), ani výraz super (jenž by měl volat metodu nadtřídy). Podívejme se nejprve na ukázku jak to má správně vypadat; protože Java to neumí, bude ukázka v Objective C, s detailními komentáři pro lepší porozumění: 54 /tmp> cat test.m Uvědomme si, co se děje: používáme zde metody tříd přesně stejně, jako (virtuální) metody objektů: obě třídy mají nějak implementovánu metodu metoda; třída Xxx prostě vrátí "this" (které se v Objective C jmenuje self), zatímco třída Yyy využije implementaci ze své nadtřídy. Stojí za to zdůraznit, že vše je naprosto logické:
Bohužel, do Javy tento program přepsat nelze, ačkoli všechna potřebná primitiva v ní jsou k dispozici, sémantika by byla zřejmá a bezproblémová, a syntaxe se nabízí: // toto bohužel NENÍ korektní program v Javě!!! Stejně je nesmyslné, že rozhraní nemohou obsahovat metody tříd. Je to škoda, protože na rozdíl od C++ kompletní podpoře metod tříd nebrání nic než zcela umělé syntaktické omezení. Můžeme jen doufat, že vyšší verse Javy tuto možnost, samozřejmou v objektových jazycích, přinesou. Reference a garbage collectorJe vhodné si uvědomit, že to, že objektové typy jsou ve skutečnosti reference, spolu s garbage collectorem především umožňuje cosi, co bylo v C++ krajně problematické: sdílení objektů. Mají-li dva různé moduly sdíleně pracovat s jedním společným objektem, je zřejmé, že alespoň jeden z nich musí používat referenci; v klasickém prostředí typu C++ navíc vznikají téměř neřešitelné problémy s tím, kdo a kdy má objekt uvolnit. Má to snad být ten modul, který jej vytvořil? V dynamickém systému je ale velmi dobře možné, že ten, kdo objekt vytvořil, sám zanikne dávno předtím, než ostatní moduly, jež s objektem pracují, skončí... Abych ušetřil prostor v tomto článku, odkážu čtenáře na minulý díl seriálu o programování v Cocoa, který tuto problematiku podrobně rozebírá a vysvětluje i její řešení (na příkladu poloautomatického garbage collectoru, který nabízí Cocoa pro Objective C; plně automatický garbage collector v Javě je samozřejmě ještě daleko pohodlnější). "Přetížení" operátorů není nebezpečné, nekázeň ale ano!Především, obecně určitě platí, že je-li nějaká služba k dispozici, je to lepší, než naopak: mohu si přece vybrat, jestli ji použiji nebo ne... Jenže, toto obecně zcela rozumné a nevyvratitelné stanovisko naráží na praxi programátorů: přiznejme si to na rovinu, většinou jsme my programátoři pěkná prasata. Příklad srovnávající "a*b+c" s "Plus(Krat(a,b),c)" je proto hezký, ale ve skutečném životě poněkud k ničemu: jaké objekty – kromě čísel, a ta jsou v Javě representována neobjektovými typy – má smysl násobit a sčítat? Jistě, pár příkladů se najde – matice, posloupnosti, s přimhouřenýma očima množiny. Algebry. Jak často jste ale právě tyto objekty implementovali? Musel jsem upravovat a portovat řadu cizích programů v C++; s maticemi nebo posloupnostmi čísel jsem se dosud nikdy nesetkal, zato jsem v praxi zažil, mimo jiné: - operátor '*', vyhledávající substringy (takže, pokud s1 a s2
byly stringy, s1*s2 byl index začátku s2 v s1); Jistěže to jsou ukázky nekorektního programování, ale zřejmě mám nějakou divnou smůlu, že jsem se až dosud setkával téměř výhradně s podobnými případy. Přesto vcelku souhlasím s tím, že by overloading v Javě byl příjemný – sám skřípu zuby, kdykoli v Objective C musím psát něco jako [string1 stringByAppendingString:string2] – avšak naprosto chápu, proč se jej tvůrci Javy rozhodli nepodporovat. Funkční parametryZde je asi trochu zapotřebí uvést na pravou míru tvrzení, že "legrace nastane ve chvíli, kdy chceme předat jako parametr funkci". Především, jak dobře víme, v Javě žádné funkce nejsou – máme zde jen metody objektů (a tříd, jež jsou samy také objekty). Díky tomu ovšem také nikde žádné funkce předávat nepotřebujeme, a příklad, který pan Virius uvádí, je proto trochu umělý – zřejmě jej navrhoval nějaký programátor v C++, který s objektovým programováním neměl moc zkušeností (nebyl to pan Virius, byl to nějaký nešťastník přímo ve firmě Sun). Vyřešit situace, jež v C++ vyžadují callbacky nebo předávání funkcí, je totiž v objektovém systému nesmírně jednoduché: stačí předat objekt, který má být upozorněn když ta či ona událost nastane; nic jiného není zapotřebí. Ukažme si nejprve nejjednodušší variantu s využitím specifického rozhraní; v praxi se to dělá lépe a pohodlněji s využitím selektorů, avšak tento příklad je na první pohled srozumitelnější, a proto jím začneme: interface ButtonObserver { Kterákoli instance třídy, jež implementuje rozhraní ButtonObserver, pak může být bez nejmenších problémů použita jako "callback objekt" – tj. ten, kdo je automaticky informován o události, a může na ni nějak zareagovat. Odpovídající implementace by mohla vypadat například takto: class MyController implements ButtonObserver { To je vše. Naprosto žádný automaticky generovaný kód není zapotřebí – dokonce ani pro vytvoření instance třídy MyController a vyvolání metody setObserver odpovídajícího tlačítka s touto instancí jako argumentem. O to se totiž v rozumném prostředí (jakým je např. Cocoa, ale patrně nikoli JBuilder, a zřejmě ani standardní rozhraní Swing) postarají standardní knihovní funkce při načítání objektové sítě uživatelského rozhraní, vytvořené prostředky visuálního programování (v systému Cocoa je to universální InterfaceBuilder, schopný připravit objektovou síť pro libovolný objektový programovací jazyk). (Ostatně, co se JBuilderu týká: se zájmem jsem si přečtl i druhý článek, věnovaný tomuto prostředí. Rozsah tohoto článku mi bohužel neumožňuje reagovat; v příštím čísle Chipu se však seriál "Programování v Cocoa" dostane k popisu jeho standardního vývojového prostředí ProjectBuilder; rád bych proto tento článek všem čtenářům doporučil. ProjectBuilder nejenže umožnuje programovat v Javě s větším luxusem, než jaký je k dispozici v JBuilderu; navíc ale podporuje i další programovací jazyky, takže každou část projektu můžeme snadno psát v jazyce, který se pro ni ideálně hodí.) Nepříjemným omezením implementace podle minulého příkladu by byla (a) nutnost mít pro každou akci, která v UI může nastat, samostatné rozhraní, (b) nemožnost navázat více akcí na jednu společnou metodu, (c) navázat akci na již existující metodu již hotové třídy, aniž bychom kvůli tomu museli psát další kód. (Je zřejmé proč? Ne-li, pročtěte si implementaci znovu!) Skutečně používané řešení proto využívá selektory; implementace třídy Button by pak v praxi mohla vypadat nějak takto: class Button { Zde patrně bude potřebné podrobnější vysvětlení: proměnná target a odpovídající metoda setTarget pro její nastavení jsou snad zřejmé, můžeme takto prostě určit libovolný objekt, který má být informován když došlo ke stisknutí tlačítka. Zajímavější je proměnná action a odpovídající metoda setAction. Zde je opět využit package apple.com.foundation, který nabízí velmi pohodlnou třídu NSSelector; pokud bychom tento package neměli k dispozici, museli bychom použít standardní package java.lang.reflect, který nabízí v zásadě tytéž služby, ale s daleko méně šikovným API. V každém případě, proměnná action obsahuje signaturu metody; přesný ekvivalent v C++ neexistuje, nejblíž je patrně ukazatel na metodu. V Objective C nebo SmallTalku je přesným ekvivalentem selektor. Zde se znovu dostáváme k tomu, že v Javě – stejně jako v jiných objektových jazycích – každý objekt ví co je zač a sám interpretuje metody (namísto toho, aby to dělal překladač "zvenku" jako v C++). Díky tomu je snadno možné (a) do proměnné action uložit informaci "Jde o metodu se jménem XYZ a jedním argumentem typu Object" – což je přesně to, co v implementaci setAction děláme; a (b) vyvolat takto specifikovanou metodu libovolného objektu s tím, že pokud objekt žádnou takovou metodu nemá, dojde k výjimce. To je zase jádrem implementace getEvent (služba action.invoke). Je tedy zřejmé, že takto připravená třída Button dokáže informovat libovolný objekt o tom, že tlačítko representované její instancí bylo stisknuto, a že mu tuto informaci dokáže předat prostřednictvím libovolné metody (jež má jediný argument typu Object; z příkladu však snad je vidět, jak by bylo možné obejít i toto omezení, kdyby to bylo zapotřebí). Praktické použití pak je triviální: stačí korektně nastavit "target" a "action" – což za normálních okolností není třeba dělat programově, ale postará se o to systém visuálního programování, v rozumném systému aniž by bylo třeba generovat jakýkoli kód (pak je totiž takový systém nezávislý na programovacím jazyce!). My si zde ukážeme několik příkladů s programovým nastavením, jež lze snadno ukázat v textu: class MyController { Vidíme, že jedna konkrétní akce performSomeAction bude vyvolána kdykoli uživatel stiskne dané tlačítko, nebo kděkoli zvolí danou položku z menu, nebo kdykoli zavře jisté okno. Jak by vypadala možná implementace tříd MenuItem a Window a odpovídajících metod je snad již zřejmé. Za samostatnou ukázku stojí ještě to, že stejně dobře můžeme jako "target" využít instanci některé knihovní třídy; není zde žádné omezení na třídy, jež sami píšeme. Dejme tomu, že funkce tlačítka z minulého příkladu se může dynamicky měnit. Jistě, bylo by možné napsat si na to extra metodu, ve které by se vždy nová akce přiřadila všem třem objektům (tlačítku, položce menu a oknu)... tak by se to asi dělalo v C++. Jde to ale mnohem pohodlněji: stačí přeci nastavit ... a vše funguje automaticky: tlačítku můžeme přiřadit kdykoli jakkoli libovolnou akci, a položka menu a zavření okna ji vždy korektně vyvolají – protože jejich skutečnou akcí je vlastně "říci tlačítku, že má udělat přesně to samé, jako by bylo stisknuto". Tento systém navíc umožňuje řadu snadných, logických, a přitom nesmírně luxusních rozšíření. Uveďme pouze jeden příklad: v metodě performClick je podmínka "target!=null && action!=null" vlastně zbytečně redundantní: pro chování "žádná akce" stačí nastavit na null jen "action". Toho lze využít – a např. Cocoa toho také využívá – tak, že pro danou metodu ("action"), ale neexistující "target" se metoda automaticky posílá tomu objektu uživatelského rozhraní, který je v rámci aplikace právě aktivní (s trochou zjednodušení můžeme říci, že je to ten, ve kterém momentálně je kursor). Jestliže pak – ať již programově jako zde, nebo pomocí prostředků visuálního programování – nastavíme např. ... zajistili jsme, že tlačítko vyvolá kopírování do schránky v kterémkoli objektu uživatelského rozhraní (v textovém okně, nebo v textovém políčku dialogu, nebo prostě kdekoli), ve kterém je právě kursor! Jistě, aby to mohlo fungovat, vyžaduje to polymorfismus knihovních objektů (tj. aby kterýkoli objekt UI, který může uložit svůj označený obsah do schránky, to udělal když dostane zprávu copy) – to je však v rozumně navržených knihovnách samozřejmostí. Opravdu úplně "jiný kafe"V závěru se opět s panem Viriusem dokonale shodnu: ano, Javu je opravdu třeba se naučit, a zvlášť programátorům, kteří dosud neznali nic než C++, to dá dost práce. Ne kvůli nevýznamným syntaktickým a nepříliš zásadním sémantickým rozdílům, ale pro zásadní rozdíl ve filosofii: Java je objektový jazyk, zatímco C++ ne. Copyright © O.Čada <ocs@ocs.cz>, Chip 2000
|
Nad kávou bychom se nakonec shodli, ale já mám raději kakao...Rád bych pokračoval v debatě, rozpoutané mým článkem "Silná káva pro objektového programátora"; dovolím si zde navázat na reakci pana Viriuse "Přátelské nedorozumění nad kávou". Ačkoli původní články pana Viriuse, na něž reagovala má "Silná káva", vyšly v "papírové" podobě, dnešní pokračování (stejně jako všechna budoucí) již se objeví jen v HTML formátu na Chip CD. Dovolím si proto využít většího prostoru, jež CD dává, a napíši tento článek přímo jako polemiku: budu citovat ty úryvky textu, se kterými nesouhlasím, nebo k nimž bych rád to či ono doplnil -- asi takto:
Nu, pro někoho snad -- můj případ to rozhodně není. Naopak, já Javu moc rád nemám: vadí mi její tečková syntaxe, podle mého nesrovnatelně méně šikovná a přehledná než smalltalkový "dvojtečkový" zápis, na který jsem zvyklý z Objective C; vadí mi řada jejích umělých (a podle mého soudu zhola zbytečných) omezení, vadí mi řada jejích standardních knihoven, jež -- zhýčkán OpenStepem -- považuji za nedomyšlené, a našlo by se toho ještě víc... Důvodem k tomu, že jsem spáchal "Silnou kávu", a tak vlastně zahájil tuto debatu, tedy rozhodně nebylo "že mi pan Virius sahal na Javu". Jde mi o něco jiného: Java, jako plně objektový jazyk s podporou selektorů, pozdní vazby, tříd jako objektů a podobně umožňuje -- a zároveň alespoň do jisté míry vyžaduje -- trochu odlišný programátorský styl, než víceméně statické C++. Kdo bude v Javě programovat "jako v C++", spláče nad výdělkem: na jedné straně mu bude citelně chybět řada služeb, které jsou pro statický "C++ styl" mimořádně šikovné (např. šablony), a na straně druhé se zbytečně zbaví řady velmi luxusních možností, jež objektové jazyky na rozdíl od C++ nabízejí (typicky práce s "neznámými" objekty a s rozhraními, beztypové kontejnery a podobně). Přitom ani tak nejde právě o Javu a právě o C++ -- mohli bychom zvolit třeba SmallTalk a Adu, nebo jinou dvojici jazyků. Jsem přesvědčen, že zatímco přechod mezi dvěma statickými (nebo mezi dvěma dynamickými) jazyky je jednoduchý a nevyžaduje o mnoho více, než seznámit se s klíčovými slovy a základní sémantikou, mezi statickými a dynamickými jazyky je poměrně hluboká propast podstatně odlišného optimálního designu. Hlavním účelem této debaty je podle mého názoru pomoc těm, kdo zmíněnou propast chtějí nebo musí překonat. Jsem rád, že právě tímto směrem se polemika rozvíjí: od jen marginálně zajímavé debaty o konkrétním programovacím jazyce se posunuje k obecné diskusi o objektovém programování. OverloadingOverloading (či chcete-li přetěžování -- jak jsem již psal, považuji tento kalk z angličtiny za velmi nepodařený) operátorů není rozhodně zásadní součástí diskuse o objektovém programování. I přesto -- když už jsme to jednou nakousli, pokusme se i tuto část diskuse dovést k závěru...
Jak jsem uvedl již v původním textu, existuje řada případů, kdy je overloading operátorů vhodný; sám bych jej v Objective C (kde není k dispozici vůbec) velice ocenil pro práci se stringy. Znovu bych však rád zdůraznil, že na druhou stranu takových případů je o poznání méně, než se většina -- i velmi dobrých -- programátorů domnívá. Troufl bych si říci, že mi sám pan Virius -- který patří mezi programátory nejlepší -- přesně takový příklad poskytl:
jistěže v C++ takové přetížení může dávat dobrý smysl. My jsme ale v Javě (třeba v Objective C by tomu ale nebylo jinak); zde jsou důsledně všechny objekty jen a jedině reference. I kdyby tak byl k dispozici overloading, takovéto jeho využití by bylo katastrofální. Kdybychom změnili význam třeba operátoru přiřazení pro některý objektový typ (třídu), máme problém -- s objekty toho typu pak prakticky nebude možné vůbec pracovat, protože nebude možnost nijak nastavit hodnotu odpovídající proměnné! Rozlišení jako v C++, kdy "x=y" změní jen hodnotu ukazatele, zatimco "*x=*y" zkopíruje obsah objektu, v Javě možné není... V praxi by tedy pro Javu dávalo smysl používat overloading pouze pro ty operátory, které "normálně" nad objektovými typy (tj. referencemi, de facto ukazateli) nemají význam: takové případy bezpochyby existují (a i proto jsem už minule psal "souhlasím s tím, že by overloading v Javě byl příjemný"), avšak zbývá jich přeci jen o poznání méně, než v C++. Za další, s overloadingem je problém i vinou polymorfismu. V plně objektových jazycích dává velmi dobrý smysl třeba používat proměnné pro obecné, beztypové objekty (id v Objective C, Object v Javě; v C++ to není možné, ale teoretickým ekvivalentem by mohl být void*). Overloading by proto musel být nikoli vlastností překladače (jako je tomu v C++), ale runtime! Navíc je třeba mít na paměti to, že služby jazyka nejsou dány od Boha a my jen nerozhodujeme které zakázat -- je tomu právě naopak: má-li jazyk mít nějakou službu, musí me ji sami implementovat. Jakkoli nejde o zásadní rozdíl, přeci jen je syntaktická analýza zdrojového textu bez možnosti overloadovaných operátorů snazší; ještě větší význam to má u interpretovaných jazyků -- jako je (většinou) právě Java. Domnívám se, že i tento argument vedl návrháře Javy k rozhodnutí overloading operátorů nepodporovat.
Nu, standardní vstup a výstup má Java řešen velmi nešťastně. Ovšem, overloading operátoru << také není velký zázrak ve srovnání s pohodlím jazyka C a jeho printf formátů (zvláště máme-li chytrý překladač jako GNU C, který ověřuje shodu skutečných argumentů s formátem, a vydá varování při nesrovnalostech). Objekty lze přesnadno doplnit -- např. v API Cocoa by výše uvedený příklad vypadal takto: NSLog(@"%@\n%@\n",a,b); A navíc, jak často v dnešním téměř bezvýjimky GUI světě opravdu využijeme standardní výstup? Jen vyjímečně; nesrovnatelně častěji pracujeme se stringy, pro které nám overloading operátoru << je málo platný -- kdežto printf formáty v Cocoa můžeme stejně snadno využívat i tak: NSString *novyString=[NSString stringWithFormat:@"%@\n%@\n",a,b]; V Javě bohužel tato skvělá a nesmírně pohodlná možnost k dispozici není. PolymorfismusZde narážíme, obávám se, na jedno zásadní, ale opravdu zásadní nedorozumění:
Oops, tohle je opravdu úplně jinak. Především, polymorfismus nemá s dědičností naprosto nic společného. V objektovém prostředí je polymorfismus prostě o tom, že mohu nejrůznějším objektům říci "Hej, udělej to a to", a každý z nich to provede po svém a rozumným způsobem (nebo na mě zařve "chyba, tohle neumím" -- jde-li to, ideálně už při překladu, jenže ono to obvykle nejde). Ve skutečnosti je polymorfismus natolik obecný princip, že nemusí mít nic společného ani s objekty! Skutečně -- jen si vzpomeňme na "i/o streamy" v klasických, neobjektových operačních systémech. Bylo možné standardizovaným způsobem pracovat s řadou nejrůznějších "zařízení" -- ať již to byla konsole, soubor, pipe, tiskárna... Přitom nade všemi bylo možné používat společnou sadu operací: to přeci není nic jiného než polymorfismus, bez dědičnosti i bez objektů. Podívejme se do reálného světa, který je koneckonců předlohou pro objektové systémy: můžeme (alespoň zkusit) provést jakoukoli operaci nad jakýmkoli objektem: můžeme nakopnout míč, psa, nebo třeba šéfa do zadku -- každý z těchto objektů na danou zprávu zareaguje po svém, tedy polymorfně. Můžeme nakopnout i žulovou skálu -- ta patrně nezareguje vůbec, zato palec u nohy ohlásí "error"... Vraťme se ale k objektovým systémům: jsem přesvědčen, že polymorfismus patří mezi jejich naprosto základní vlastnosti, a že se dá vyjádřit slovy: libovolnému objektu mohu poslat libovolnou zprávu. Pokud je to na první pohled zřejmý nesmysl, vynadá mi překladač; pokud to překladač nemůže vědět (tj. pracuji-li s proměnnou, jež může za běhu obsahovat různé objekty), je třeba, aby se to korektně vyřešilo za běhu: totiž buď správným vyvoláním odpovídající metody -- existuje-li -- nebo smysluplnou běhovou chybou (mimochodem takovou, již lze programově odchytit a zpracovat). Nyní je asi zřejmé, proč nepovažuji C++ za objektový jazyk: v něm totiž výše uvedené tvrzení neplatí. C++ se sice poctivě snaží při překladu odchytit co nejvíce "nesmyslů", avšak ty, jež by se měly řešit za běhu, řešeny nejsou nijak -- ať si program dělá co chce, zavolá nesmyslný kód, nebo třeba spadne. A, bohužel, týká se to nejen obyčejných, ale i virtuálních metod Je k něčemu polymorfismus pro všechny třídy?
Nu, především proto, aby s jejími objekty bylo možné standardně pracovat! Chci např. vkládat libovolné objekty do kontejnerů, jež vnitřně využívají hashovací tabulku s rychlým přístupem: aby to bylo možné, musí být každý objekt schopen standardně vygenerovat svůj hashovací klíč. Chci mít možnost se při ladění v debuggeru podívat, je-li na adrese XXX objekt, a ano-li, jaký: aby to bylo možné, musí být každý objekt schopen standardně ohlásit svou třídu a svůj momentální stav a obsah. Chci všechny objekty sdílet mezi různými moduly -- nemám-li k dispozici grbage collector, musí každý objekt podporovat nějaké standardní služby pro počítání referencí. Podobných záležitostí je ještě mnohem více... Samozřejmě, v principu není nutné, aby proto všechny třídy měly společného rodiče; stejně dobře bychom mohli použít rozhraní v Javě nebo protokol v Objective C. Společná kořenová třída je však většinou praktičtější řešení, už proto, že obvykle chceme, aby všechny objekty měly alespoň nějaká společná data: přinejmenším odkaz na třídu objektu, a v jazycích, které nemají garbage collector (např. Objective C) také čítač referencí pro korektní sdílení.
Nu, v tom je právě ta chyba: nepředpokládáme to, pravda -- ale třeba za dva roky ta situace nastane! V Epocu, který je kompletně postaven na C++, se mi již mnohokrát stalo, že bych býval potřeboval mírně pozměnit chování některé standardní knihovní třídy... ale ouha, nešlo to. Její programátor totiž "předpokládal", a proto byly všechny odpovídající služby implementovány pomocí nevirtuálních metod. Důsledek pak je to, že tam, kde by v objektovém systému stačilo několik řádků kódu v kategorii nebo odvozené třídě, je nutné psát, ladit a udržovat kód, který z 90% dělá totéž, jako sám operační systém... nebo "slézt" na úroveň strojového kódu a systém "patchnout", ale to je ještě horší. Tertium, bohužel, non datur -- jen a jenom vinou nevirtuálních metod a statického systému. Snad by se chtělo říci "ale ne, vinou nedokonalého návrhu"? Ale kdež -- nikdo není tak dokonalý, aby dokázal zaručeně a spolehlivě myslet na všechno. Vždy, vždy, vždy se vyskytne chyba, dříve nebo později se vždy objeví problém, s nímž původní návrhář nepočítal. Nu, a z tohoto hlediska rozdíl mezi plně dynamickým systémem typu Objective C a víceméně statickým systémem typu C++ spočívá prostě v tom, že v dynamickém systému lze tyto neočekávané problémy řešit nesrovnatelně snáze. Polymorfismus usnadňuje analýzu a design!Když jsem psal příklad, vysvětlující použití super, nijak zvlášť jsem o třídní hierarchii nepřemýšlel -- hierarchie nic neznamenajících tříd A a B by byla stejně dobrá, ale namátkou jsem zvolil reálná a komplexní čísla. Právem mi potom pan Virius vytkl to, že hiearchie tříd není správná -- protože
a nikoli naopak. Proto je zcela smysluplné, aby třída reálných čísel byla potomkem třídy komplexních, a nikoli naopak... ...ale pozor, není to postavené na hlavu? Vždyť jsem připravil hierarchii tříd rozumně a logicky: potomek může k proměnným a službám předka přidat nějaké další proměnné a služby; je tedy velmi smysluplné, aby k jednomu reálnému číslu ve třídě Real bylo přidáno druhé (representující imaginární část) v jejím potomkovi Complex! Tento postup má i "vývojovou logiku" -- v praxi nejspíš nejprve vytvoříme a odladíme třídu Real, a teprve později podle potřeby budeme přidělávat třídu Complex, než naopak... Tohle je problém, se kterým si v rigidním prostředí C++ tak snadno neporadíme: má-li odvozená třída representovat speciální případ a nadřízená třída případ obecný, velice často by nás to nutilo nejprve implementovat velmi komplikovaný obecný systém -- a pak jej v podtřídách jen zčásti využít. Jako zde: nejprve bychom měli implementovat a odladit komplexní čísla, a pak terpve od nich odvodit reálná (což je samozřejmě triviální, a vlastně je to zbytečné -- stejně dobře můžeme používat přímo komplexní čísla s nulovou imaginární částí). Asi každý programátor potvrdí, že běžná -- a rád bych zdůraznil, že naprosto rozumná! -- praxe vede přesně opačným směrem: nejprve implementuji ten nejjednodušší případ a vše odladím. Pak přidám nějaké rozšířené služby, a zase vše odladím... a tak pokračuji, dokud nemám hotové řešení v plné složitosti, která je zapotřebí. Ačkoli je teoreticky představitelné, že při tomto rozšiřování nebudu vytvářet nové podtřídy, ale naopak nové nadtřídy, ve skutečnosti by to moc nešlo: vždy totiž chci využít dříve napsaný kód; kdybych však vytvářel novou nadřízenou třídu, musel bych jej do ní explicitně přenášet -- všechny výhody dědičnosti by byly rázem vepsí. Co s tím? Jak se vyhnout problému, který se zdá být přímým důsledkem tohoto postupu, a který pan Virius naprosto precizně popsal:
Inu, čtenáři je řešení již asi zřejmé: stačí, máme-li k dispozici polymorfismus -- ten opravdický, který nemá s dědičností a s třídní hierarchií nic společného. Klidně si můžeme třeba v Objective C dovolit implementaci typu @interface Real:NSObject a polymorfismus nám bez nejmenších problémů umožní používat jakékoli číslo kdekoli:
...ale pozor, co když někde zkusíme použít imaginární složku zrovna nad objektem třídy Real? Inu, jsou dvě možnosti: pokud náhodou píšeme službu, jež je určena striktně a jednoznačně jen pro komplexní čísla -- třeba takto: double complexAbs(Complex *n) { stane se to nejrozumnější, co se stát může: při volání takovéto služby na objekt třídy Real bude program ukončen s hlášením typu "poslal jsi objektu Real nekorektní zprávu im". Pokud ale víme, že služba má být schopna korektně zpracovat všechna čísla, stojí to jen jeden řádek navíc -- můžeme totiž využít další schopnosti dynamických systémů, ověřit programově třídu objektu: double betterAbs(Real *n) { A je to. Musím se upřímně přiznat, že neznám způsob, jak z této pasti vyklouznout v rigidním jazyku typu C++, kde polymorfismus ani triky typu isKindOfClass nejsou k dispozici. Teoreticky správné by bylo skutečně začít nejobecnější třídou, a pokračovat k jednodušším -- tam, kde to skutečně jde, je to samozřejmě ideální řešení, použitelné v libovolném jazyce. Ono to však v praxi obvykle naneštěstí nejde, protože nejobecnější třída by byla příliš komplikovaná na "jednorázové" naprogramování; navíc se velmi často objeví požadavek na zobecnění později, když už dávno máme "jednodušší" třídy hotové (možné to je i v našem triviálním příkladu -- i kdybychom opravdu začali třídou Complex a od ní odvodili Real, může se nám stát, že za rok bude knihovnu třeba rozšířit o algebru nějaké třídy Vector, jež má více, než dvě reálné složky... a tak dále). Namísto toho bychom mohli začít pohodlně třídou Real, a pokaždé, když nastane potřeba zobecnění, změnit hierarchii tříd: poprvé tedy přidat kořenovou třídu Complex a přeprogramovat Real tak, aby byla jejím dědicem; příště přidat kořenovou třídu Vector a z třídy Complex udělat jejího dědice, a tak dále. Jak jsem se zmínil výše, teoreticky je to možné; v praxi bychom za to ale platili ohromnou pracností každé úpravy, nutností měnit kód již hotových tříd -- a tak zanášet nové chyby do již odladěných programů -- a v neposlední řadě vše znovu překládat, což si můžeme dovolit u malých testovacích prográmků, ale naprosto to nepřipadá v úvahu u rozlehlých systémů, složených z řady spolupracujících knihoven a aplikací. Tudy také cesta nevede. Do třetice, můžeme -- stejně jako v Objective C nebo v Javě -- použít "nesprávnou" hiearachii s kořenovou třídou Real, z ní odvozenou Complex, z ní odvozenou Vector... a tak dále. Ovšem, v tu chvíli tvrdě narazíme na problém popsaný v minulém citátu; a v C++ nemáme ani obecný polymorfismus, ani ověření isKindOfClass, abychom se mu dokázali vyhnout. Opravdu, nevím co s tím... víte to Vy, Miroslave? Nakonec jen pár poznámek pro úplnost a pro "šťouravce":
Základní popis protokolů a kategorií je součástí mého seriálu o programování v API Cocoa, jenž je k dispozici na Chip CD, v dílu "ObjectiveC.html". Virtuální a nevirtuální metody
Zde bych se jen rád optal: jakou situaci taková věc representuje? Pro jaký vztah objektů a služeb je takovéto řešení výhodné (resp. výhodnější, než "obyčejná" virtuální metoda, spojená podle potřeby se zákazem vytvářet přímo instance dané třídy, v Javě formalizovatelným klíčovým slovem abstract)? Vždy jsem tuto možnost měl za jakýsi artefakt, který sám o sobě nemá žádný smysl, ale vznikl jako důsledek kombinace nezávisle navržených rysů jazyka. Možná mi něco ušlo a není tomu tak? Ostatně, když už jsme na to narazili, rád bych zde probral jednu ze svých dávných nejistot: jaký má vůbec smysl volání s plnou kvalifikací?!? Je to snad jen má chyba a omezenost, ale po deseti letech objektového programování jsem zvyklý uvažovat při tvorbě odvozované třídy následujícím způsobem:
Nesetkal jsem se dosud se situací, kdy by bylo zapotřebí tyto dva přístupy smíchat dohromady -- tj. reimplementovat metodu, ale přesto pak tu a tam někdy, když se mi zachce, volat její původní, skrytou implementaci z nadtřídy: tj. přesně to, k čemu slouží plná kvalifikace... Navíc mám pocit, že se jedná o hrubé porušení principu zapozdření (encapsulation): mělo by přeci být věcí objektu, kterou ze svých implementací dané metody mi nabídne; to bych si přeci neměl vybírat "zvenku"? V každém případě, sám jsem plnou kvalifikaci v C++ nikdy nevyužil jinak, než pro simulaci super (jež C++ nemá). Snad se tím připravuji o nějakou nesmírně silnou a šikovnou možnost... zatím si však nějak neumím představit o jakou? Co je vlastně třída a co objekt?Ještě než se podíváme na problémy s přetypováním a (ne)možnost jejich řešení v C++, stojí za to se pozastavit nad jednou formulací:
Oops. My zde přeci s třídami nepracujeme, navíc C++ "ukazatel na třídu" vůbec nezná! Pracujeme však s ukazateli na objekty; ty mohou být instancemi té či oné třídy. Již jsem se setkal s tím, že programátoři v C++ užívali takovéto formulace proto, že jim skutečně nebyl příliš jasný rozdíl mezi třídou a její instancí (mimochodem, programy, jež tito lidé psali, obvykle vypadaly podle toho). U zkušeného odborníka, jakým je pan Virius, samozřejmě takový lapsus nehrozí -- navíc jinde pan Virius zcela korektně hovoří o "ukazateli na instanci třídy". O co tedy jde? Obávám se, že jde o (ze strany pana Viriuse bezpochyby podvědomý a neúmyslný) trik, jak zaobalit dost podivné chování jazyka C++ do takové formulace, v níž vypadá zcela rozumně: přečtěte si to znovu, "...máš ukazatel o na třídu Interface; buď tak laskav a zacházej s ním jako s ukazatelem na třídu Object..." -- už na první pohled je zřejmé, že je to akce velmi, velmi podezřelá, a kdo si ji vyžádá, nesmí se divit... ...ale, je tomu skutečně tak? Pokud situaci přeformulujeme korektně, bude, domnívám se, vypadat malinko jinak: ve skutečnosti totiž zmíněné přetypování říká: "máme ukazatel na objekt, který je instancí dvou tříd zároveň -- třídy Interface a třídy Object. Až dosud jsme s ním pracovali jako s instancí Interface, nadále s ním chceme pracovat jako s instancí Object" To už zní na první pohled rozumněji, že ano? Jak je to s přetypováním?Zřejmě dynamic_cast opravdu vyřeší problémy jednoduchých ukázkových prográmků z mého minulého textu. Ovšem, jsou tu dva problémy: předně, jak "mladý" je tento operátor v C++? Já se C++ učil zrovna z knihy pana Viriuse "Programovací jazyky C/C++" z roku 1992; možná jsem špatně četl, ale s tímto operátorem jsem se v ní nesetkal? Kromě toho, příklady zkouším v GNU C verse 2.7.2.1 v Mac OS X Server -- není to samozřejmě nejnovější verse překladače, ale také to není nic prehistorického! A když jsem řešení s dynamic_cast chtěl vyzkoušet, se zlou jsem se potázal: ... změnil jsem "(Object*)o" na
"dynamic_cast<Object*>(o)" Inu, takové řešení je mi málo platné! Nejde jen o Mac OS X Server -- ten má konečně C++ jen jako lahůdku navíc, standardně se programuje v plně objektovém a bezproblémovém Objective C, Javě či -- pro internetové aplikace -- WebScriptu (zajímavé ovšem je, že jeho editor zdrojových textů zná dynamic_cast jako klíčové slovo). Díval jsem se však i do Epocu, kde je C++ hlavním a víceméně jediným programovacím jazykem, a ani tam dynamic_cast k dispozici není... a přitom Epoc je prakticky jediné prostředí, kde má smysl v C++ programovat, protože všude jinde (snad ještě vyjma BeOSu, ale tím si nejsem jist) jsou k dispozici lepší jazyky! Nadto, i kdybych tuto službu měl k dispozici v každém C++, nutnost použít speciální přepínač při překladu navozuje nepříjemnou nejistotu: co když budu pracovat s objekty z knihoven, jejichž programátor tento přepínač při překladu nepoužil? To je v reálném systému velmi pravděpodobné -- lze očekávat situaci, kdy třídy Interface i Object byly definovány na standardních knihovnách, jež jsou přeloženy bez -frtti, a já se ve svém programu, který se s těmito knihovnami bude spojovat, pokusím použít dynamic_cast<Object*> na ukazatel na instanci třídy Interface: zdalipak to bude fungovat? Nejhorší nakonec: i kdyby to vše bylo v pořádku, kdybychom dynamic_cast skutečně měli standardně k dispozici kdekoli v C++, a fungoval korektně i s knihovními objekty bez ohledu na to, zda knihovna byla nebo nebyla přeložena s přepínačem -frtti, stejně narazíme na mnohem horší a zásadnější problém. Přetypování jsem v příkladech totiž použil jen proto, že je nejjednodušší a chyba je na něm nejlépe vidět. Právě díky tomu však není nejnebezpečnější -- přetypování si můžeme snadno ohlídat. Nejčastějším zdrojem problémů s nesprávnou identitou objektu je ale spojování knihoven. Už obyčejná statická knihovna může snadno zavinit takový "průšvih" -- stačí, aby byla v hlavičkových souborech, jež programátor používá, chyba: použijeme-li např. hlavičkový soubor novější verse, než je verse skutečné knihovny, mohou všechny popsané problémy nastat znovu -- aniž bychom použili jediný operátor přetypování! Samozřejmě, u statických knihoven lze takovýmto problémům zabránit elementární kázní a udržením si přehledu ve versích knihoven a hlavičkových souborů. Jenže každý rozsáhlejší systém dnes využívá dynamické knihovny, často sloužící i pro dodatečná rozšíření: pak ale opět stačí nevhodně zkombinovat jednu versi "pluginu" s jinou versí základní aplikace -- a opět narazíme na tentýž problém. Tentokrát už řešení není tak snadné, protože v různých instalacích takovýchto dynamických systémů velmi často koexistují různé verse knihoven -- v jedné instalaci je nejnovější, v jiné o něco starší... přesto by vše (vyjma aplikací, jež požadují explicitně nové služby nejnovější knihovny), přeci mělo fungovat! Ovšemže je nutné zdůraznit, že dynamické systémy tento problém neodstraní: jen mnohonásobně sníží citlivost systému na takovéto chyby, a výrazně usnadní jejich nalezení, protože
Nejde ostatně jen o implicitní přetypování, podobných problémů je více -- uveďme alespoň jeden příklad: mechanismus virtuálních metod v C++ je citlivý na jejich pořadí v hlavičkových souborech; nové služby proto musíme přidávat vždy na konec, bez ohledu na jejich logické pořadí (jinak ztratíme kompatibilitu mezi různými versemi knihoven). Takovýchto drobností, na které musí programátor myslet -- ač by se o ně měl a mohl postarat překladač zcela automaticky -- je v C++ mnoho; naopak v Javě nebo Objective C jich je velmi, velmi málo. Naopak zase, do C++ pochopitelně lze doplnit služby, jež podobnou robustnost zajistí -- alespoň zčásti, nakolik to je možné; dobrým příkladem je třeba rozhraní COM, nebo knihovna XFoundation z vývojového prostředí pro Epoc firmy X.soft. Ovšem, je to spousta práce navíc a další -- obecně nepřenositelné -- API pro služby, jež by bez problémů mohly být součástí runtime samotného jazyka! Existují v C++ třídy?Inu, ano a ne. V případech, kdy ve třídě není žádná virtuální metoda, jistě ne. V ostatních se zdá že nakonec ano, avšak s řadou omezení: jeden z čtenářů mých článků např. tvrdil, že ve standardním C++ lze napsat ekvivalent příkladu na ukládání tříd do proměnné, jenž jsem uvedl v minulém dílu v Javě s tím, že to v C++ není možné. Příklad, který mi poslal, měl řadu závažných nevýhod; obecně nejhorší z nich byla ta, že to nefungovalo nad jakoukoli třídou, ale jen nad třídami které jsou dědici určité speciálně vytvořené kořenové třídy -- takže např. pro třídy ze standardních knihoven příklad nic neřeší. Navíc se zdá, že toto řešení naráží na stejné problémy, jako výše uvedené dynamic_cast... Mé původní tvrzení že "C++ nemá třídy" se tedy ukázalo být nepřesné. Zdá se, že ve skutečnosti C++ třídy má, ale ne vždy; jejich použití je v nejnovějších překladačích problematické, a ve starších nemožné. Možná bych si dovolil polemizovat s jením konkrétním tvrzením:
Jeho problém totiž je v tom, že zmíněné "korektní zacházení" ve skutečnosti znamená znalost vnitřní struktury konkrétního objektu a tomu odpovídající komunikaci s ním! Speciálně tedy, výraz obj->metoda() se může přeložit pro tisíc různých objektů tisíci různými způsoby, přičemž -- pokud se náhodou (a z důvodů popsaných výše taková náhoda není nikterak vyloučena) použije nesprávný způsob na nesprávný objekt, může se přihodit cokoli... Nezlobte se na mne, Miroslave, ale domnívám se, že takovéto chování neodpovídá popisu "ăčerná skříňkaŇ... přijímá zprávy a reaguje na ně". Je tomu právě naopak: do té skříňky musíme velmi dobře vidět, jinak s ní vůbec nedokážeme komunikovat -- protože s každou z mnoha skříněk, jež jsou v programu psaném v C++, se komunikuje jinak! Funkční parametry
Tuto poznámku pan Virius psal nad starší versí "Silné kávy", v níž byla pro úsporu místa uvedena jen jednodušší varianta s interface ButtonObserver (viz Funkční parametry). Jak je ale vidět z její konečné podoby, tento problém je přehledně a elegantně vyřešen pomocí selektorů; v článku, věnovaném ProjectBuilderu (který je na tomto Chip CD v oddílu, věnovaném API Cocoa) je vidět, jak pohodlný systém visuálmího programování nad tímto mechanismem lze vytvořit. Překladač a programátor
Nu, s tímto tvrzením (ani s dalším, jež necituji, o tom, že Java je poměrně restriktivní) naprosto nelze polemizovat. Domnívám se však, že je velmi podstatné si uvědomit, že míra restriktivnosti jazyka nemá naprosto nic společného s tím, o čem se v této polemice převážně bavíme -- totiž s rozdílem mezi plně objektovými jazyky typu SmallTalku a statickými jazyky typu C++. Skutečně, lze najít příklady všech kategorií: Java je dynamická a restriktivní, Objective C je dynamické a ještě mnohem liberálnější než C++. Naopak, C++ je (relativně) liberální a statické, Ada je ještě "statičtější" -- a restriktivní. Osobně preferuji jazyky (i filosofii) liberální; protože mi zároveň vyhovuje objektové progamování, je pro mne nejlepší Objective C. Jiný by si třeba vybral raději Javu, protože preferuje jazyky restriktivní... To ale -- aspoň podle mého názoru -- není důležité. Já mohu bez problémů programovat v Javě; jen si občas zanadávám, že nemohu, dejme tomu, použít číselnou hodnotu přímo v podmínce (protože není převeditelná na Boolean): řešení je ale vždy triviální, jen na úrovni syntaxe -- musím přidat explicitní podmínku či explicitní přetypování, nic jiného. Naopak, vyznavač Javy bude snadno programovat v Objective C, jen si bude muset dávat větší pozor na "warningy". Co naopak podle mého názoru důležité je, je filosofický rozdíl mezi statickými jazyky typu C++ a plně objektovými jazyky typu SmallTalku: rozdíly jsou zde hluboké, a týkají se sémantiky, ne syntaxe. Např. já v C++ každou chvíli narazím na zcela zásadní problém -- jednou nemohu použít beztypový kontejner; jindy mi chybí možnost ověřit za běhu, zda objekt dokáže zpracovat danou metodu -- a jindy bych potřeboval vůbec poslat objektu, který není znám v době překladu, metodu, která není známa v době překladu... Zkrátka, je to přesně tak:
Quod erat demonstrandum. Copyright © O.Čada <ocs@ocs.cz>, Chip 2000 |
Káva? Nebo C++? Jak je libo...Miroslav Virius Článek pana Čady „Nad kávou bychom se nakonec shodli, ale já mám raději kakao...“ představuje hozenou rukavici; mně nezbývá, než ji zvednout. Ostatně diskuse o vztahu C++ a Javy – a programovacích jazyků vůbec – bude jistě zajímavá nejen pro nás dva. V některých místech budu citovat úryvky z článku pana Čady, ke kterým se chci vyjádřit. Citace budou sázeny kurzívou a odděleny od ostatního textu mezerou. Budou samozřejmě vytržené z kontextu; ovšem článek pana Čady najdete na tomto CD zároveň s mým, takže pro vás nebude problém si odpovídající místo najít a přesvědčit se, zda – a na kolik – jsou mé argumenty k věci. C, C++, Java a učeníZačnu něčím, o čem jsme dosud nehovořili, ale co považuji za velkou výhodu C++ ve srovnání s ostatními potomky jazyka C, jako je Objective C nebo Java. Programátor, který zná jazyk C, může prakticky ihned začít programovat v C++. Neobjektově, samozřejmě, ale objektový přístup se může učit průběžně. K seznámení s tím, co z jazyka C v C++ chybí nebo co může mít jiný význam, postačí přečíst si pětistránkový článek – a to je záležitost na 20 minut i s důkladným přemýšlením, o co jde. Velice podstatné také je, že převážnou většinu knihoven jazyka C lze používat bez problémů v i C++. Vše ostatní, co C++ nabízí, lze zvládat už při práci. Z toho „všeho ostatního“ je samozřejmě nejdůležitější objektový přístup. Zvládnutí základních syntaktických a jiných pravidel pro objekty v C++ není příliš náročné, daleko horší je naučit se objektově myslet, objektově programovat. Zvládnout objektovou analýzu a objektový návrh programu; a k tomu je třeba čas, kterého programátor – pokud ho programování opravdu živí – nemá nikdy dost. V C++ lze programovat na jedné straně zcela neobjektově, na druhé straně prakticky čistě objektově, a mezi těmito dvěma styly je možný téměř plynulý přechod. Proces učení C++ lze popsat zdánlivě paradoxní větou: „Céčkař se C++ naučí za týden, ale učit se ho bude půl roku.“ V onom týdnu je i aktivní zvládnutí šablon, prostorů jmen, výjimek, dynamické identifikace typů a dalších nástrojů, které v jazyce C nebyly. Jestliže posadíme céčkaře k Javě, bude mu adaptace trvat přece jen o něco déle. I když je Java – alespoň na první pohled – jednodušší než C++, přechod k ní je pro céčkaře složitější, neboť programátor musí ihned změnit způsob myšlení. Navíc se musí seznámit s řadou knihovních tříd, a přitom mu zkušenost z jazyka C není k ničemu – s knihovnami začíná prostě od nuly. Je ovšem pochopitelné, že naprosto jinak se bude na Javu a C++ dívat programátor, který zná nějaký objektový jazyk, jako je třeba Smalltalk. Jemu bude javský přístup známý, nanejvýš mu bude v některých ohledech připadat zbytečně omezený. Na závěr tohoto odstavce si dovolím poznamenat, že samozřejmě i v Javě lze programovat neobjektově: Vytvoříme metodu main a další metody jako procedury svého programu a to vše z povinnosti uzavřeme do nějaké třídy. Výsledkem bude ryze procedurální program, i když v téměř čistě objektovém jazyku Java. Některé učebnice Javy takhle začínají. I když se v této souvislosti vtírá vzpomínka na článek o skutečných programátorech a pojídačích koláčů, který tvrdil, že „skutečný programátor dokáže napsat fortranský program v jakémkoli jazyce“, nejde rozhodně o projev profesionality – spíš je to projev nepochopení Javy a její filozofie. PřetěžováníPřetěžování (či chcete-li overloading – ale já dávám přednost českým termínům, protože píšu převážně pro české čtenáře) opravdu není pro diskusi o objektově orientovaném programování rozhodující. Nicméně naše povídání je o především Javě a C++, a proto sem patří. Přetěžování operátorů je nástroj, který bývá v kontextu jazyka C++ s objektovým programováním spojován, ale rozhlédneme-li se po okolí, zjistíme, že ho poskytují i neobjektové jazyky, jako je třeba Fortran 90. Java přetěžování operátorů ve skutečnosti obsahuje. Zamyslíme-li se nad výrazem a + b, zjistíme, že jeho význam se liší podle toho, zda jde o sčítání dvou čísel typu int, dvou čísel typu double nebo o spojování dvou řetězců. Máme tedy stejným symbolem označeno několik různých operací a překladač podle typu operandů určuje, který z operátorů + použije. To znamená, že překladač Javy se s přetěžováním operátorů umí vyrovnat, nedovoluje to však obyčejným programátorům, ale jen tvůrcům jazyka. V tom je C++ vůči programátorovi daleko poctivější. Kdybychom změnili význam třeba operátoru přiřazení pro některý objektový typ (třídu), máme problém – s objekty toho typu pak prakticky nebude možné vůbec pracovat, protože nebude možnost nijak nastavit hodnotu odpovídající proměnné! Vzhledem k tomu, že Java pracuje s objekty pouze prostřednictvím referencí, by opravdu nebylo rozumné umožnit v ní přetěžování přiřazovacího operátoru. Podobně by asi nebylo rozumné dovolit přetěžování operátoru tečka a ještě několika dalších. Jenže to stále není důvod, proč přetěžování jako takové nepodporovat. Koneckonců, C++ také neumožňuje přetěžovat všechny operátory – mezi výjimky patří např. už zmíněná tečka, používaná podobně jako v Javě pro přístup k složkám instancí. ...přece jen je syntaktická analýza zdrojového textu bez možnosti overloadovaných operátorů snazší; ještě větší význam to má u interpretovaných jazyků – jako je (většinou) právě Java. Začnu trochu obecněji: Často se setkávám s výhradami, že přetěžování operátorů je neefektivní nebo že způsobuje složitost překladače. První výhrada je nesmyslná: jsou-li a a b dvě instance objektového typu, pro který je přetížen operátor +, znamená zápis a + b totéž co operator+(a, b) tedy volání funkce (metody). Druhá výhrada je oprávněná, alespoň částečně: Může-li programátor přetěžovat operátory, musí s tím překladač počítat a v případě potřeby umět podle jistých pravidel rozhodnout, který operátor se má použít, tj. kterou metodu má zavolat. Není to ovšem o mnoho složitější než rozlišování přetížených metod nebo vestavěných operátorů, a to Java umí. Skutečnost, že Java je interpretovaný jazyk, s tím nemá nic společného: Víme přece, že zdrojový text se nejprve překládá do bajtového kódu, a teprve ten se interpretuje. Nepochybuji o tom, že rozlišování přetížených metod v Javě řeší už překladač, nikoli interpret JVM, a není důvodu, proč by tomu tak nemohlo být i v případě operátorů. Na složitosti bajtového kódu, stejně jako na složitosti JVM, se tím nemusí nic změnit. A navíc, jak často v dnešním téměř bez výjimky GUI světě opravdu využijeme standardní výstup? Jen výjimečně; nesrovnatelně častěji pracujeme se stringy, pro které nám overloading operátoru << je málo platný... Pomiňme skutečnost, že standardní výstup se používá např. při programování CGI skriptů, při ladění a v mnoha jiných situacích – ono to opravdu není mnoho ve srovnání se záplavou graficky orientovaných aplikací. Jenže objektové datové proudy jazyka C++ se v žádném případě neomezují pouze na standardní vstupy a výstupy. Přetížené operátory >> a << se kromě standardních vstupů a výstupů a práce se soubory dají použít – a také hojně používají – pro práci se znakovými řetězci. Tyto „paměťové“ proudy umožňují formátovat data a výsledek zapisovat do znakových polí. Stejně dobře umožňují číst data ze znakových řetězců naprosto stejným způsobem jako ze souboru. Můžeme se na ně dívat jako na analogii funkcí sprintf() a sscanf() z jazyka C. Existují dokonce ve dvou implementacích: Standardní třídy stringstream, istringstream a ostringstream jsou definovány v hlavičkovém souboru <sstream>, ve starších implementacích jazyka C++ (a kvůli zpětné kompatibilitě také ve většině novějších) najdeme proudy strstream, istrstream a ostrstream, které jsou definovány v hlavičkovém souboru <strstrea.h>. A právě zde poskytují přetížené operátory >> a << pohodlí, které mi v Javě chybí. Navíc, vzhledem k objektové povaze datových proudů umožňují tyto operátory vstup a výstup libovolných datových typů, i typů, které v době návrhu ještě neznáme. Přetěžování a polymorfismusZa další, s overloadingem je problém i vinou polymorfismu. V plně objektových jazycích dává velmi dobrý smysl třeba používat proměnné pro obecné, beztypové objekty (id v Objective C, Object v Javě; v C++ to není možné, ale teoretickým ekvivalentem by mohl být void*). Overloading by proto musel být nikoli vlastností překladače (jako je tomu v C++), ale runtime! No a? Pokusme se ale o seriózní odpověď. V tomto odstavci jde o dvě věci: Za prvé o beztypové kontejnery a za druhé o možnost přetěžování operátorů pro beztypové instance. Začneme druhou z nich, neboť bezprostředně navazuje na předchozí diskusi. Polymorfní chování objektů bývá implementováno jako běhová záležitost, nejinak je tomu i v Javě nebo v C++. Protože však použití přetíženého operátoru není nic jiného než volání metody, mohli bychom tvrdit, že nejde o žádný zvláštní problém. Jenže on tu problém je, a ne malý: Většina operátorů je totiž binárních a my bychom proto při zpracovávání objektů v beztypových kontejnerech potřebovali polymorfismus vzhledem k oběma operandům; přeloženo do jazyka volání metod to znamená, že bychom potřebovali pozdní vazbu jak vzhledem k třídě instance, které patří metoda, tak vzhledem k třídě parametru. To ovšem neposkytuje Java ani C++. Na druhé straně skutečnost, že Java neposkytuje přetížené operátory, problém vícenásobného polymorfismu nikterak neřeší. Jestliže potřebuji provádět nějaké binární operace nad prvky beztypového kontejneru, pak je jedno, zda tyto operace zapíšu pomocí operátoru nebo jako volání metody – vícenásobný polymorfismus si musím naprogramovat sám. Pokud jde o samotné beztypové kontejnery, je celá záležitost také trochu složitější, než by mohlo vyplývat z předchozího citátu. Především, v čistě objektových jazycích je obvyklé, že všechny objekty jsou navzájem příbuzné – mají společného předka, třídu, která se obvykle jmenuje Object a která implementuje společné chování všech objektů. „Beztypový“ kontejner obsahuje odkazy (reference) na instance typu Object, a protože v objektovém programování může potomek vždy zastoupit předka, lze do takového kontejneru ukládat opravdu téměř cokoli – přesněji jakékoli objekty, neboť ty jsou instancemi potomků třídy Object. (To je, alespoň v případě Javy, dost podstatné; do „beztypového“ kontejneru v Javě nemůžeme ukládat hodnoty primitivních typů, např. čísel, pokud je „neobalíme“ do instancí vhodných tříd.) „Beztypovost“ tedy je něco, co v C++ nemá přesnou analogii; třídy mohou být na sobě nezávislé, mohou být členy mnoha různých dědických hierarchií. Na druhé straně málokdy potřebujeme kontejner, do kterého chceme ukládat absolutně cokoli, od čísel přes okna po druhy ptakoještěrů. Typicky potřebujeme polymorfní kontejner, který obsahuje buď různé druhy oken, nebo ty ptakoještěry, ale ne obojí. V C++ můžeme samozřejmě použít kontejner založený na beztypových ukazatelích (void*); není to ale nejlepší řešení, protože do takového kontejneru bychom mohli uložit opravdu cokoli, číslo bot vedle ptakoještěra, a tím bychom se připravili jednak o možnost typové kontroly už při překladu a za druhé o polymorfismus. (Museli bychom si spolu s každým objektem ukládat informaci o jeho typu a podle toho se pak orientovat při jeho zpracovávání.) Ovšem k tomu, abychom dosáhli stejného výsledku jako třeba v Javě, stačí vytvořit si vhodnou dědickou hierarchii s jedním společným předkem, který se může jmenovat třeba také Object, a použít kontejner, který může obsahovat odkazy na instance této třídy. Pokud od takovéhoto společného předka odvodíme jak ptakoještěry, tak okna, budeme je tam smět ukládat vedle sebe, pokud ne, budeme muset mít zvláštní kontejner na okna a zvláštní kontejner na ptakoještěry; obojí může mít svou logiku – záleží na tom, co vlastně potřebujeme. Ostatně řešení založená na objektových hierarchiích se společným předkem byla obvyklou součástí různých knihoven, dodávaných s překladači C++ na počátku devadesátých let. V současné době se ale zpravidla používají kontejnery založené na šablonách. Jejich výhodou je, že umožňují stejně snadno pracovat s objektovými i s neobjektovými typy a přitom v nich lze snadno omezit typ ukládaných hodnot. Navíc jsou součástí standardní šablonové knihovny jazyka C++. Komplexní a reálná číslaPřiznám se, že poznámku o nevhodnosti odvozování komplexních čísel jako potomka čísel reálných jsem napsal jen jaksi na okraj, jako něco, co do ostatního textu vlastně nezapadalo a čemu jsem nepřikládal valnou důležitost. Nicméně: Asi každý programátor potvrdí, že běžná – a rád bych zdůraznil, že naprosto rozumná! – praxe vede přesně opačným směrem: nejprve implementuji ten nejjednodušší případ a vše odladím. Pak přidám nějaké rozšířené služby, a zase vše odladím... To zní krásně, ale pokud si něco podobného vyzkoušíte v případě komplexních čísel a použijete Javu nebo C++, nejspíš se dostanete do problémů. Množina reálných čísel je prostě podmnožinou čísel komplexních, a proto je opačná hierarchie doslova postavená na hlavu. Na druhé straně mi nezbývá, než souhlasit, že po stránce implementace to svádí k uvedenému postupu: Nejprve odladíme reálná čísla, pak přidáme imaginární složku... Představa reálného čísla, které s sebou nosí imaginární část obsahující vždy nulu, se mi vůbec nelíbí a programátor, který něco takového spáchá, nejspíš sklidí, co si zaslouží. Postup, který uvádí pan Čada, rozhodně má jasnou logiku, ale vede v určitých situacích k nesmyslného chování programu. Takže obě cestu se z určitého úhlu sice třpytí, ale zlatá není ani jedna. Naštěstí C++ nabízí jinou cestu – sice na pohled možná méně objektovou, ale o to elegantnější, která není založena na dědičnosti. Stačí ve třídě komplexních čísel definovat konstruktor, který lze volat s jedním parametrem typu reprezentujícího reálné číslo. Tak otevřeme cestu k automatickým konverzím reálných čísel na čísla komplexní a nahradíme dědičnost. Nyní budeme moci na místě komplexního čísla bez problémů použít číslo reálné, zatímco na místě reálného čísla číslo komplexní zapsat nepůjde – tak, jak je to v matematice obvyklé. ...v místech, kde se očekává pouze reálné číslo, budeme moci použít i komplexní (avšak využije se pouze jeho reálná složka) – to není nesmyslné; Zde prosím o příklad: mne žádný rozumný nenapadá. Zjišťování typu instance za běhuMusím se upřímně přiznat, že neznám způsob, jak z této pasti vyklouznout v rigidním jazyku typu C++, kde polymorfismus ani triky typu isKindOfClass nejsou k dispozici. Ale jsou. Polymorfismus je sice omezen na jednotlivé dědické hierarchie, ale to je při šíři možností, které tento jazyk poskytuje, přirozené – ostatně v Javě je tomu také tak, neboť tam všechny objekty tvoří jedinou hierarchii. (Ani Java není plně polymorfní: zkuste poslat nějakou zprávu třeba celému číslu, nezapouzdřenému do žádné obalové třídy.) A pokud jde o zjišťování typu instance za běhu programu (něco jako isKindOfClass), k tomu slouží v C++ standardní operátor typeid, který umožňuje dynamickou identifikaci typů. I tady – podobně jako u operátoru dynamic_cast – narazíme na omezení: aby fungoval opravdu „dynamicky“, musí pracovat s polymorfními třídami. Ovšem to je v případě jakékoli dědické hierarchie víceméně samozřejmost. Vrátíme-li se k předchozímu příkladu, mohli bychom tedy i v C++ postupovat tak, že definujeme nejprve třídu Real a od ní odvodíme jako potomka třídu Complex. Na místě, kde bychom chtěli pouze reálná a nikoli komplexní čísla, bychom zkontrolovali typ předaného parametru a v případě nesouladu vyvolali např. výjimku. Jenže ... nemohu se ubránit dojmu, že to je stejně krkolomné řešení jako odvozovat reálná čísla od komplexních a tahat s sebou pořád nulovou imaginární částí. Dědičnost?Obávám se, že příklad komplexních a reálných čísel ukazuje, že současné pojetí dědičnosti tak, jak je přináší Java nebo C++, je v některých situacích nevhodné. Reálné číslo je zvláštním případem komplexního čísla, ale takovým, kde má jedna složka nulovou hodnotu, a proto bychom ji potřebovali v odvozené třídě vypustit. To je něco, co Java ani C++ neumí – při dědičnosti v jejich pojetí lze datové složky přidávat, nikoli ubírat. Přitom nejde o případ nijak ojedinělý: bod ve dvourozměrném prostoru lze považovat za speciální případ bodu ve třírozměrném prostoru, ve kterém je jedna složka nulová, hada lze považovat za zvíře bez nohou atd. (Málokdo bude programovat obecné zvíře jako hada s nohama.) PoznámkaMyslím, že problém s reálnými a komplexními čísly je také odrazem skutečnosti, že reálná čísla vlastně nejsou zvláštním případem komplexních čísel – alespoň ne v naprosto rigirózním matematickém pojetí. Množina komplexních čísel je množinou uspořádaných dvojic reálných čísel. Samotné reálné číslo v této množině ležet nemůže – jedno číslo není uspořádaná dvojice čísel. Z praktického hlediska je ovšem výhodné prohlásit, že reálná čísla jsou prostě komplexní čísla, která mají imaginární složku nulovou, a proto ji nepíšeme. Toto zjednodušení je natolik výhodné, že se o něm v běžných kurzech matematiky ani nemluví, předkládá se jako samozřejmost – a v běžných matematických aplikacích to funguje. Při objektovém návrhu to ale může způsobit potíže. Virtuální a nevirtuální metodyJazyk C++ nabízí mimo jiné možnost volat metodu „s plnou kvalifikací“, např. u -> A::f(); Je-li f() virtuální metoda, potlačíme tím pozdní vazbu. Zde totiž říkáme: Bez ohledu na to, jakého typu je instance, na kterou ukazuje u, chci metodu, která je definována ve třídě A. (Samozřejmě za předpokladu, že u je ukazatel na potomka třídy A.) To nevypadá příliš objektově, že? Přesto se to může hodit. Objektové hierarchie nemusí být navrženy vždy optimálně; předchozí diskuse o reálných a komplexních číslech to ukázala dost jasně – a to šlo o pouhé dvě třídy, ve skutečných objektových knihovnách jich mohou být stovky. Vedle toho se může stát, že se pokusíme použít objektovou knihovnu k účelu, který neodpovídá přesně jejímu původnímu účelu – i to je poměrně běžná situace. Už to samo je důvod, proč může být rozumné podobnou možnost nabídnout. Podívejme se ale na příklad, který by mohl pocházet z libovolného grafického rozhraní. · Představte si, že máme třídu Okno, která nabízí službu zobraz(). Obecné okno se vykresluje vždy jako bílá, šedě orámovaná plocha. · Potomkem obecné třídy okno bude třída PomocneOkno, které se vykresluje jako šedá neorámovaná plocha. Vedle toho PomocneOkno nabízí řadu dalších služeb, které v obecném okně nejsou – například změnu způsobu zobrazení při stisknutí tlačítka myši. · Od této třídy bude odvozena celá řada ovládacích prvků GUI, jako jsou tlačítka, zaškrtávací políčka ap. Mezi nimi bude i editační pole – třída EditOkno – pro zadávání vstupu. Jenže editační pole chceme nakreslit stejně jako obecné okno, tedy bíle s šedým orámováním. Zde určitě oceníme možnost implementovat metodu zobraz() pomocí metody vzdáleného předka. Jinak bychom měli následující možnosti: n Mohli bychom třídu EditOkno definovat jako přímého potomka třídy Okno. Pak bychom sice zdědili potřebnou implementaci metody zobraz(), ale ztratili bychom všechny speciální vlastnosti, definované ve třídě PomocneOkno (a museli bychom je v ní programovat znovu). Navíc bychom tím třídě EditOkno dali zvláštní postavení v rámci celé hierarchie, a to také není dobré. n Mohli bychom také třídu EditOkno definovat jako potomka třídy PomocneOkno. Pak bychom ale museli předefinovat její metodu zobraz() – a přitom psát znovu totéž co ve třídě Okno. V obou případech by to znamenalo, že musíme určité věci programovat dvakrát, a to nikdy není dobré. (Můžete namítnout, že různé způsoby kreslení lze naprogramovat jako jednu metodu, které předáme barvy plochy a rámečku prostě jako parametry. Složitější příklad by ale zabral příliš mnoho místa.) Lze se samozřejmě přít o tom, zda takovéto řešení porušuje nebo neporušuje zapouzdření. Domnívám se, že ne. n Společný předek – třída Okno – zveřejnil, že má metodu zobraz(), která kreslí bílé, šedě orámované pole. (Nezveřejnil jen název metody, ale i výsledek své činnosti.) n Její potomek, třída PomocneOkno, zveřejnil, má metodu zobraz(), která kreslí šedé, černě orámované pole. Proč by si potomek třídy PomocneOkno nemohl vybrat mezi implementacemi, které nabízí bezprostřední a vzdálený předek? Ani jedno nepředpokládá znalost vnitřní struktury objektů – využíváme pouze to, co o sobě zveřejňují. (Informace o struktuře dědické hierarchie patří, pokud vím, k základním informacím o každé objektové knihovně, a to nejen v C++, ale např. i v Javě.) Je možné samozřejmě namítnout, že lepší by bylo vytvořit pomocnou třídu, kterou použijeme jak v obecném okně, tak v editačním poli. Jistě; ale co když dostanu třídy Okno a PomocneOkno již hotové? To je přece naprosto běžná situace. Čistě virtuální metoda s implementací je věc, která se asi příliš nepoužívá. Ve skutečnosti řada i špičkových programátorů používajících C++ o ní nejspíš ani neví, ale součástí jazyka je, a proto jsem ji uvedl v přehledu možností, které C++ nabízí. Nejde o nic více, než o možnost, jak potomkům nabídnout možnou implementaci a přitom tuto metodu deklarovat jako čistě virtuální (či chcete-li abstraktní). C++ nabízí vedle sebe jak virtuální, tak i nevirtuální metody. Lze se nad tím pozastavovat, lze s tím nesouhlasit, ale to je asi tak vše, co s tím lze dělat – nějak podobně to řekl Jára Cimrman o něčem úplně jiném. Jestliže o nějaké třídě nepředpokládám, že by se mohla stát předkem jiné třídy, použiji v ní nevirtuální metody. Nu, v tom je právě ta chyba: nepředpokládáme to, pravda – ale třeba za dva roky ta situace nastane! V Epocu, který je kompletně postaven na C++, se mi již mnohokrát stalo, že bych býval potřeboval mírně pozměnit chování některé standardní knihovní třídy... ale ouha, nešlo to. To se může stát bohužel i v Javě. Čas od času zjistím, že bych potřeboval předefinovat metodu předka a přizpůsobit ji k obrazu svému, ale ouha – její tvůrce předpokládal, že se měnit již nebude, nebo si to dokonce nepřál – a deklaroval ji jako final. Nebo tak deklaroval celou knihovní třídu – například String. (Nechápu, proč si od ní nesmím odvodit vlastního potomka, který by lépe vyhovoval mým záměrům, ale je to tak.) V obou jazycích, a nejen v nich, se může stát, že se představy tvůrce knihovny a programátora, který ji používá, rozejdou. Bohužel je to vždy programátor, který pak musí psát, psát a psát... Co je vlastně třída a co objekt?V článku „Přátelské nedorozumění nad kávou“ jsem napsal Přetypování (Object*)o překladači vlastně říká: „Zde máš ukazatel o na třídu Interface; buď tak laskav a zacházej s ním jako s ukazatelem na třídu Object.“ Jazyk C++ něco jako „ukazatel na třídu“ ve skutečnosti nezná, a proto si mnoho programátorů zkracuje dlouhý termín a místo „ukazatel na instanci třídy Object“ říká prostě „ukazatel na třídu Object“ nebo „ukazatel na Object“. V diskusi o samotném C++ by to nevadilo, ale my se bavíme také o jazycích, kde ukazatele na třídy skutečně existují a to mohlo způsobit nedorozumění. Pokud k nějakému došlo, omlouvám se. Vraťme se ale k významu uvedeného přetypování. Měli jsme dvě nezávislé třídy, Object a Interface, a ukazatel na jejich společného potomka. Pro lepší orientaci si to zopakujeme: class Interface {public: // První nezávislá třída virtual void
metoda()=0; }; class Object { public: // Druhá nezávislá třída virtual void xxx() { printf("Metoda
vsech objektu...\n"); } }; class Xxx: public Object, public Interface { public: // Společný
potomek virtual void metoda() { printf("metoda
tridy Xxx\n"); }; }; void use(Interface* o) { // Původní implementace z článku „Silná káva“ ((Object*)o)->xxx(); // ! o->metoda(); } Přetypování, o které nám jde, je vyznačeno tučně a vykřičníkem v komentáři. Problém je, že něco takového bude v Javě fungovat, ale v C++ ne. To není chyba C++, ale programátora. V C++ toto přetypování opravdu znamená právě to, co jsem chtěl říci už v článku Přátelské nedorozumění – uvedu poopravené znění svého tvrzení: „Zde máš ukazatel na instanci třídy Interface. Buď tak laskav a zacházej s ním jako s ukazatelem na instanci třídy Object.“ Nic více a nic méně. Operátor (typ) nevyužívá dynamickou identifikaci typů a tak nemůže zjistit, zda je oprávněné přání "máme ukazatel na objekt, který je instancí dvou tříd zároveň – třídy Interface a třídy Object. Až dosud jsme s ním pracovali jako s instancí Interface, nadále s ním chceme pracovat jako s instancí Object" a pokud ano, kde by měl zděděné složky Object vlastně hledat. Takže výše uvedený citát je za dané situace v C++ opravdu jen zbožné přání programátora, který se nechal zmást podobností s Javou. Lze se ptát, proč je to právě tak. Odpověď je jednoduchá – jde o dědictví jazyka C. Takováto interpretace odpovídá logice přetypování ukazatele typu int* na ukazatel typu void* nebo naopak, přetypování ukazatele typu int* na ukazatel typu unsigned* ap. Operátor (typ) v C++ prostě nevyužívá dynamickou identifikaci typů, s tím se musíme smířit, a tam, kde ji potřebujeme, použít operátor dynamic_cast. Problém je, že tvůrci Javy převzali sice syntax jazyků C a C++, ale dali jí nový význam. Proto může totéž přetypování v Javě opravdu vyjadřovat přání uvedené v citátu. Jenže chceme-li něco podobného říci v C++, musíme použít prostředky C++ a nenechat se zmást podobností s Javou. Jak je to s operátorem dynamic_cast?Už minule jsem upozorňoval, že místo (typ) musíme použít operátor dynamic_cast. Přetypování a dynamické identifikaci typů v C++ jsem věnoval samostatný článek, který vyšel v Chipu 10/00 a 11/00, a proto zde pouze odpovím na připomínky z článku pana Čady. ...jak "mladý" je tento operátor v C++? Já se C++ učil zrovna z knihy pana Viriuse "Programovací jazyky C/C++" z roku 1992; možná jsem špatně četl, ale s tímto operátorem jsem se v ní nesetkal? Kromě toho, příklady zkouším v GNU C verse 2.7.2.1 v Mac OS X Server – není to samozřejmě nejnovější verse překladače, ale také to není nic prehistorického! A když jsem řešení s dynamic_cast chtěl vyzkoušet, se zlou jsem se potázal... Přesnou odpověď bohužel neznám. Nicméně mohu se pokusit alespoň o přibližné vymezení doby, kdy se tento operátor objevil v návrhu standardu jazyka C++. Knihu „Programovací jazyky C/C++“ jsem napsal na podzim roku 1991, na trhu se objevila na jaře 1992. V té době se o tomto operátoru ještě nemluvilo. Na druhé straně v knize B. Stroustrupa a M. A. Ellisové „The Annotated C++ Reference Manual“, vydané nakladatelstvím Addison-Wesley v lednu 1994, již najdeme popis tohoto operátoru v dodatcích, které shrnují rozhodnutí standardizační komise. (Tato kniha sloužila dlouhou dobu jako neoficiální norma C++. Poprvé vyšla, pokud vím v r. 1991.) V té době se přetypovací operátory dynamic_cast, static_cast, reinterpret_cast a const_cast spolu s operátorem typeid také začaly objevovat v překladačích. Určitě je obsahoval například překladač Borland C++ 4.0, uvolněný začátkem roku 1994. Přibližně v té době se začal objevovat také v překladačích konkurenčních firem. To znamená, že jeho specifikace je známa již alespoň sedm let a jeho implementace v překladačích je k dispozici jen o něco kratší dobu. Netuším, proč překladač pana Čady neobsahuje tento operátor a hlavičkový soubor <typeinfo>, obávám se ale, že to není vina jazyka: Tento hlavičkový soubor je předepsán standardem (už poměrně dlouhou dobu) a proto by tam měl být – jako <typeinfo>, <typeinfo.h> nebo pod podobným jménem. Ale na něco podobného můžeme narazit i v jiných programovacích jazycích. Jestliže se rozhodnu napsat aplet, může se mi stát, že sice na mém počítači poběží, ale webové prohlížeče většiny uživatelů budou hlásit něco jako Class not found – nebyla nalezena potřebná třída. To proto, že jsem třídu svého apletu odvodil od třídy JApplet, jak se to doporučuje v Javě 2, zatímco většina prohlížečů dosud podporuje pouze Javu 1.1, která třídu JApplet nezná. Mohu ale spadnout i do opačné pasti: Poučen četbou učebnic zasednu k počítači a napíšu class MujAplet extends JApplet {/* ... */} a překladač mi vynadá, že něco takového nezná, protože mám starší verzi Javy. Programovací jazyky se vyvíjejí, a to rychleji, než je nám programátorům milé, a nezbývá, než to vzít na vědomí. Overloading a češtinaZde si dovolím odbočit od hlavního tématu naší diskuse a ocitovat slova pana Čady z článku „Silná káva pro objektového programátora“. ...chcete-li, „přetížit“ — podle mého názoru je tento kalk z angličtiny dost zrůdný, asi jako kdybychom chtěli rozhraní říkat „meziobličej“... Diskuse o české terminologii v programování by vydala na samostatný článek a já se k něčemu takovému už dlouhou dobu chystám, ale zde si dovolím jen několik stručných poznámek. n Přetěžování je doslovný překlad (kalk) anglického termínu overloading, s tím nelze nic dělat. Na druhé straně nevidím důvod, proč by se s tím něco dělat mělo: Logika původního termínu je asi tak „na toto jméno je naloženo několik významů, je tedy přetíženo“. Na anglický (přesněji americký) termín je to logické až moc – neobsahuje to narážku na Alenku v říši divů, vtip ani slovní hříčku. n I když to rozhodně není nejlepší, není to tak špatné a kromě toho, nic jiného se v češtině neujalo. Navíc rozhodneme-li se pro overloading, budeme muset overloadovat a říkat další příšernosti. n Používání původní, tedy anglické terminologie mi připadá jako nejhorší možnost vůbec. Termín by měl pokud možno vysvětlovat, a to i neznalému, a to je možné jen v případě, že použijeme češtinu. Aby bylo jasno, nebráním se používání obecně známých slov, která pocházejí z cizích jazyků a jsou blízká anglickým termínům n Anglická terminologie zní učeně a pomáhá „dělat z toho vědu“. To potřebují filozofové, kteří nechtějí, aby jim někdo rozuměl, protože by mohl přijít na to, že jejich řeči jsou obsahově prázdné. My si na nic podobného hrát nemusíme, programování a počítače mohou být pro řadu lidí složité, i když se budeme snažit to vysvětlit co nejsrozumitelněji, a výklad o něm rozhodně nebude obsahově prázdný, i když budeme mluvit česky. A pokud jde o ten meziobličej: To se opravdu nepoužívá, už před dvaceti nebo více lety někdo vymyslel velice pěkný termín rozhraní, ale sám jistě víte, že meziksicht je poměrně oblíbený programátorský vtípek. Co dodatNa závěr si dovolím parafrázovat výrok, který svého času řekl W. Churchill o něčem úplně jiném: C++ je nejhorší myslitelný jazyk. Bohužel, všechny ostatní jsou ještě horší.
|