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: Nemohu se ubránit pocitu, že se Java stala něčím jako náboženstvím 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... 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ší. 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: Přetížený přiřazovací operátor umožňuje přenos všech prvků z jednoho kontejneru do jiného. 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. ...vzpomenu si na eleganci zápisu 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í: ...řekněme si několik slov o polymorfizmu jako takovém. 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?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ů? 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í. ...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í. 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 reálná čísla jsou zvláštním případem komplexních čísel 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: Kdybychom opravdu někde použili komplexní čísla odvozená od reálných čísel, znamenalo by to, že 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í metodyDefinujeme-li v předkovi: 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í: 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.╥ 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: 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ě. 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í parametryzpůsob, který se používá v JBuilderu, umožňuje ... mj. sdílení handlerů ╨ několik komponent může při různých událostech volat týž handler 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á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... 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: K 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. Quod erat demonstrandum. Copyright © O.Čada <ocs@ocs.cz>, Chip 2000 |