Statická vs. dynamická typová kontrola

24. 6. 2004
Doba čtení: 12 minut

Sdílet

V dnešní době mají programátoři k dispozici nepřeberné množství jazyků, v nichž mohou pracovat. Tyto jazyky se mnohdy velmi výrazně liší a jedním z hlavních rozdílů bývá i to, jak se staví k typové kontrole. V tomto článku se pokusíme porovnat vlastnosti statické a dynamické typové kontroly.

„Sme co sme a programy só co sme my“

Na Rootu čas od času vychází články a seriály o programovacích jazycích, které jsou přístupem k typové kontrole naprosto odlišné. V poslední době např. Lisp, Smalltalk, Python,Mercury nebo Flex. Především vývojáři pracující se staticky typovanými jazyky se na ty dynamické často dívají s nedůvěrou. Tento článek se pokusí pokud možno co nejobjektivněji statickou a dynamickou typovou kontrolu porovnat.

Všichni programátoři chtějí psát co nejjednoduššeji a nejrychleji co nejkvalitnější programy a typová kontrola je prostředek, který jim to má usnadnit. Samozřejmě bez typové kontroly lze psát programy rovněž, nicméně v tom případě je třeba klást naprosto jiné nároky na kvalitu jejich návrhu a testování. V první řadě je třeba si uvědomit, že dynamická typová kontrola nerovná se žádná typová kontrola. Problematiku statické a dynamické kontroly nelze také zaměňovat se silnou a slabou typovou kontrolou.

Poznamenejme, že pokud budeme hovořit o dynamické typové kontrole, půjde ve většině případů o typovou kontrolu smalltalkovského typu. Kromě Smalltalku se k dynamicky typovaným jazykům řadí ještě například Common Lisp, Python či Ruby. Mezi nimi se generalizuje poměrně těžko.

Statická typová kontrola

Idea statické typové kontroly je prostá. U každé deklarované proměnné či parametru je nutné uvádět typ. Typ pak definuje množinu hodnot, kterých může proměnná nabývat, a množinu operací, které mohou být s danou proměnnou provedeny. V každém okamžiku pak musí být naprosto jasné, jakého typu je proměnná, s níž právě pracujeme.

Statická typová kontrola se vyznačuje velikou bezpečností vygenerovaných programů a má jednu obrovskou výhodu – již v době překladu můžeme prohlásit, že výsledná aplikace nemá žádnou typovou chybu, tedy že se nesnažíme pracovat s daty, která mají nepovolený rozsah hodnot ani se nad nimi nesnažíme vykonávat nepovolené operace.

Na první pohled to sice nemusí být tak zřejmé, ale vytvořit bezpečný pouze staticky typovaný jazyk je velmi obtížný úkol, obzvláště pokud chceme, aby byl snadno použitelný. Moderní staticky typované jazyky tak nezřídka zavádí některé prvky dynamického typování v podobě implicitních běhových kontrol.

Dynamická typová kontrola

Zástupce dynamicky typovaných jazyků můžeme nalézt především mezi objektově orientovanými jazyky. To, že nějaký jazyk je dynamicky typovaný, většinou ještě neznamená, že není velmi silně typovaný. Zpravidla je tomu právě naopak. U čistě objektově orientovaných jazyků v podstatě nemá cenu hovořit o typové množině hodnot, protože objekty spolu komunikují pouze a jenom prostřednictvím zasílání zpráv. Většinový přístup je takový, že množina zpráv, které daná skupina objektů rozumí, je určena pomocí třídy, jež definuje jejich sdílené chování. Nicméně objekty se chovají jako uzavřené entity, a je tedy naprosto jedno, jakým způsobem si omezují množinu přípustných zpráv. Není proto vůbec nutné využívat třídy. Např. jazyk Self a jemu podobné třídy vůbec nepotřebují a využívají principu delegace zpráv na jiné objekty.

Tento přístup typové kontroly je velmi jednoduchý, ale nese s sebou některé problémy. Vše bude jistě patrné z následujícího příkladu napsaného ve Smalltalku.

| var |
var := 'string' factorial.
[ var Smalltalk ] System.

Na začátku jsem si deklarovali proměnnou var. Jak je vidět, nikde jsme u ní neuvedli typ. Poté jsme do ní přiřadili výsledek volání zprávy factorial řetězci. Jistě není třeba nijak dlouze rozvádět to, že smalltalkovská třída String pro řetězce vůbec zprávufactorial nezná, protože tato operace nad řetězci nedává smysl a je definovaná ve třídě Integer.

Poté jsme v bloku (Céčkaři si místo hranatých závorek mohou představit složené) proměnné var poslali zprávu. Předchozí operace vůbec neměla smysl, takže nevíme, co je proměnná var vlastně zač. Přesto jí pošleme zprávu jménem Smalltalk. Jenže taková zpráva vůbec ve Smalltalku neexistuje, je to jméno globální proměnné.

Na dovršení vší té hrůzy jsme výslednému bloku poslali zprávu System. To prosím není už dokonce ani globální proměnná, je to prostě jen symbol, tedy speciální druh řetězce (proto se nemusí umisťovat do apostrof).

Věřte nebo ne, tento kód je ve Smalltalku přeložitelný.

Možnosti statické kontroly dynamicky typovaného jazyka jsou v době překladu programu velmi omezené. Existují studie, které se touto problematikou zabývají, a funkční projekty, které umožňují pomocí velice sofistikovaných postupů určovat typy smalltalkovských výrazů. Existují dokonce varianty Smalltalku se statickým typovým systémem (Strongtalk). Ovšem ani tak si nemůžeme být nikdy v době kompilace zcela jisti, s jakým konkrétním typem objektu se bude ve skutečnosti pracovat.

V praxi vám proto kompilátor pouze zkontroluje, jestli v systému existuje symbol, který se mu snažíte předhodit jako selektor zprávy. Navíc můžete zprávu, jejíž selektor v době překladu vůbec neznáte a ani symbol toho jména v systému neexistuje, zaslat explicitně pomocí metody #perform:

Je tedy zřejmé, že Smalltalk dává programátorovi skutečně až nebezpečně velkou volnost. Naštěstí je nutné deklarovat dopředu krom jmen parametrů metod alespoň jména proměnných. Ani to ovšem není u všech jazyků pravidlem.

Na první pohled se tak musí jistě zdát, že ve Smalltalku a podobných jazycích programují jen nezodpovědní hazardéři, kterým je jedno, jak co jejich program bude dělat.

Omezení statické typové kontroly

Statická typová kontrola je mocný nástroj, naráží ale na některá poměrně zásadní omezení.

Učebnicovým příkladem je omezení hodnot. Řekněme, že máme definován celočíselný typ s rozsahem 0 až 255. Statická typová kontrola nám v tom případě samozřejmě zakáže napsat

var = 256;

Ovšem většinou už jí nebude vadit, když jí předhodíme třeba

var = 255;
var = var + 1;

V tomto případě dojde k překročení omezení rozsahu daného typu a program vygeneruje chybu nebo dojde k přetečení. Je zřejmé, že například pro bezpečnou operaci sčítání musí být prováděna kontrola parametrů v době běhu programu. To je ovšem mimo síly čistě statické typové kontroly.

Např. C# vám kód

int i = int.MaxValue;
i = i+1;

bez hlášení problémů přeloží a při jeho provedení se dočkáme výsledku –2147483648 Pokud chceme přetečení kontrolovat, musíme použít speciální jazykovou konstrukci zapínající běhovou kontrolu.

checked { i = i+1; };

Další problém představuje například tvorba abstraktních datových typů. Při programování se totiž typ nesmí měnit. Z principu představuje každá operace přetypování riziko. Je zřetelná snaha omezit v programovacích jazycích počet implicitních přetypování a donutit programátora konkretizovat svoje požadavky. Připouští se totiž možnost, že přetypování nebude možné, ale zjistit to půjde až za běhu programu. Pokud se spolehneme jen na statickou kontrolu, můžeme pracovat s daty zcela nepřípustným způsobem a dál děj se vůle boží.

Přetypování se ale v některých situacích vyhneme jen stěží. V případě, že programujeme nějakou třídu pro kolekci (seznam, pole, hashovací tabulku, strom…) a nechceme ji předělávat pro každý existující typ, budeme muset přistoupit k deklaraci jejích položek na bázi nějakého velmi obecného typu. Při vybírání dat z kolekce poté musíme data opět přetypovávat zpět. Při té příležitosti by měla být za běhu programu prověřena korektnost provedeného přetypování.

Například tento prográmek v C# jde bez problémů přeložit.

namespace TypeControl
{
   class MyClass1
   {
      public virtual void Method()
      {
         System.Console.WriteLine("MyClass1");
      }
   }
   class MyClass2 : MyClass1
   {
      public override void Method()
      {
         System.Console.WriteLine("MyClass2");
      }
   }
   class MainProgram
   {
      static void Call(MyClass1 param)
      {
         param.Method();
      }
      [System.STAThread]
      static void Main(string[] args)
      {
         MyClass1 var = new MyClass1();
         Call((MyClass2)var);
      }
   }
}

O tom, že provedené přetypování je chybné a náš prográmek zhavaruje, se dozvíme až ve chvíli, kdy je prováděno, tedy v okamžiku, kdy hotová aplikace třeba vesele pracuje již několik měsíců u zákazníka.

Architekti jazyků se podobným nepříjemnostem snaží vyhnout, a proto se v nových verzích Javy či C# můžeme setkat s generickými typy. Ty se snaží možnosti statické kontroly rozšířit i na tyto situace. Ada je obsahuje již od počátku.

Neméně problematická je existence nedefinované hodnoty. Vezměme si tento příklad ze C#.

MyClass var;
var = null;
var.Method();

Je bez problémů přeložitelný. Při jeho provedení ovšem dojde k selhání, protože se snažíme zavolat metodu nedefinovaného objektu, což zaregistruje běhová kontrola. Tato situace není nijak zvlášť tragická. Mnohem horší je, pokud se spolehneme pouze na statickou typovou kontrolu, jakou používá C++. V jeho případě je při provedení obdobného kódu metoda Method spuštěna, jako by se nechumelilo. Problémy jsou registrovány až v okamžiku, kdy tato metoda začne pracovat s instančními proměnnými. Pokud k tomu ovšem nedojde, tato metoda se provede bez sebemenšího varování a program pokračuje vesele dál. Milé, že? Je zřejmé, že skutečně bezpečný program musí v masivní míře využívat běhových kontrol.

Dynamická typová omezení

„if it walks like a duck, and talks like a duck, then it is a duck“

Při překladu dynamicky typovaných jazyků se provádí jen poměrně triviální kontroly a spoléháme se na běhovou validaci. V případě omezení rozsahů to znamená explicitní kontroly při provádění potenciálně nebezpečných operací. Bezpečnost je tedy stejná jako u staticky typovaných jazyků s rozšířenou běhovou kontrolou.

U silně typovaných dynamických jazyků ke klasickému přetypování běžně nedochází. Bývá nahrazeno polymorfním chováním objektů. Není podstatná příslušnost objektu k nějaké třídě, ale to, jakým zprávám objekt rozumí. Ve Smalltalku například není problém vytvořit objekt, který bude rozumět úplně všem zprávám. Toho lze občas s výhodou využít. Takovýto objekt všeználek můžeme podstrčit místo jiného objektu, jemu zaslané zprávy zaznamenávat, a rychle tak zjistit, jaké metody jsou po původním objektu vyžadovány.

Silně typované dynamické jazyky by neměly operaci obdobnou přetypování vůbec poskytovat. Pokud chceme vytvořit z nějakého objektu objekt jiného typu, explicitně jej požádáme, aby nám nový objekt na základě svého stavu sám vytvořil. Například pokud číslu zašleme zprávu asString, vrátí instanci třídy String. Typová bezpečnost je tak prakticky celá v rukou programátora a nespoléhá se na částečně implicitní převody. Když to jde, využívá se pouze polymorfismus těžící ze shodnosti části rozhraní různých tříd.

Problém nedefinované hodnoty se řeší tak, že se vlastně vůbec neřeší. Nedefinovaný objekt je objekt jako každý jiný, a pokud mu zašleme zprávu, vykoná ji naprosto stejně jako kterýkoliv jiný objekt. Jediný rozdíl je v tom, že pro drtivou většinu zpráv zahlásí, že jim nerozumí, a vyvolá výjimku.

Přínosy dynamického typování

Dynamické typování s sebou nese celou řadu pozitiv. Výsledným programům pak dodává na flexibilitě a obecnosti. Nad staticky typovaným jazykem je například prakticky nemyslitelné postavit inkrementálně budovaný systém se všemi výhodami, které přináší. Ač některé jazyky (Ada, Flex) umožňují výrazně upravovat existující třídy, za běhu programu nemohou být ve statických jazycích vytvářeny nové třídy či modifikováno rozhraní stávajících.

U dynamického typování smalltalkovského typu může být každá metoda překládána zcela samostatně nezávisle na ostatních. Díky tomu je kompilace velmi rychlá a jednoduchá. Uživatel má možnost modifikovat program za běhu. Celý návrh může být pojat mnohem abstraktněji a obecněji.

U programovacích jazyků založených na prototypování lze pak k výhodám přičíst i vlastnosti, jako je dynamická dědičnost, která je u staticky typovaných jazyků naprosto nemyslitelná.

Je patrné, že s přechodem od statického na dynamické typování je nutné změnit přístup k vývoji softwaru. Pokud člověk zůstane u návyků získaných ve staticky typovaných jazycích, velmi brzy narazí. Daleko větší důraz je kladen na návrh a testování programu. Samotné kódování je podružné.

Dynamicky typovaný jazyk musí nezbytně přinést rozšířené a pohodlnější možnosti ladění programů, než je to nutné u staticky typovaných jazyků. Musí dát možnost účinně testovat samostatné celky programů. Část kódování se provádí přímo v ladících nástrojích. Programátor by měl mít možnost si správnost svého napsaného programu co nejrychleji ověřit. Fáze návrhu, kódování a testování splývají dohromady a vývoj programů tak bývá často rychlejší a výsledné programy nezřídka bezpečnější.

Dynamicky typované jazyky bývají zpravidla nesrovnatelně jednodušší než staticky typované. Ke změnám v jejich návrhu dochází pouze zřídka a mají nadčasový charakter. Je snazší se je naučit a bývají jednodušší na použití.

V neposlední řadě bývá zápis programů v nich napsaných podstatně kratší než u staticky typovaných jazyků. Díky tomu se automaticky snižuje počet možných chyb a přehmatů. Pokud si vezmete staticky typovaný program, spočítáte si v něm chyby související se statickou typovou kontrolou a zhrozíte se, kolik by jich v dynamicky typovaném jazyce kompilátorem prošlo, je váš postoj zcela lichý. S dynamickým typováním totiž jde ruku v ruce i mnoho koncepčních změn, díky nimž k řadě chyb vůbec nedochází.

Závěr

Z uvedených příkladů je snad dostatečně patrné, že pouhá statická kontrola je pro tvorbu skutečně bezpečných programů zcela nedostačující. Pokud v programech napsaných v jazycích s čistou statickou typovou kontrolou, jako je C/C++, neprovádíte zcela důsledně explicitní běhové kontroly, tedy nekontrolujete rozsahy, neprovádíte kontroly korektnosti přetypování, takřka u každé metody nekontrolujete validitu ukazatele this apod., vaše programy naprosto neodpovídají nárokům na bezpečné produkty. Je to jako jízda na motorce s namontovaným airbagem. Despekt k dynamicky typovaným jazykům zde rozhodně není na místě. Jediné, čeho si skutečně můžete cenit, je možnost volby, kdy je jen na vás, jestli se rozhodnete běhové kontroly provádět, či je třeba kvůli optimalizacím pominete. V žádném případě se nesmí podcenit fáze testování.

Podstatně lépe na tom jsou uživatelé staticky typovaných jazyků s implicitními běhovými kontrolami, jako je Java, C#, Ada apod. V nich lze tvořit velmi kvalitní programy, ovšem v žádném případě se nelze uchlácholit pocitem bezpečí. Je nutné počítat s tím, že vaše programy se v době běhu mohou potýkat s velmi vážnými problémy. Programy musí tyto problémy umět spolehlivě řešit například formou kvalitní sítě odchytávání výjimek. Ani ve vašem případě nesmíte zapomínat na důsledné testování. To, že je váš program přeložitelný, rozhodně neznamená, že je kvalitní. Programátoři v těchto jazycích mají bohužel tendenci spoléhat se na statickou typovou kontrolu více, než je zdrávo.

Programátoři v dynamicky typovaných jazycích by na důkladné testování neměli zapomínat v žádném případě. Velká škála chyb vyplyne na povrch až době běhu programu. Pokud svůj produkt důsledně neotestujete, tedy např. u jednotlivých metod neprověříte všechna možná větvení, setkáte se jistě s problémy. Tohle ovšem platí i pro předchozí dvě skupiny, takže na tom určitě nejste o moc hůře. Na rozdíl od nich máte ovšem všechna rizika více na očích. Můžete se těšit z daleko bohatších dynamických možností vašich jazyků a z lepší podpory pro ladění a testování.

bitcoin_skoleni

Jak je vidět, vzájemné pohrdání tím či oním typem typové kontroly není rozhodně na místě, protože bezpečnost programů stejně nakonec záleží na pečlivosti programátora a na kvalitě testování. Za všechno se platí. Efektivitou, elegancí, nároky na testování, možnostmi jazyka, dobou vývoje nebo bezpečností. Ve výsledku ale vždy penězi.

Samozřejmě nemám patent na rozum, i když si o něj brzy určitě zažádám, a neznám všechny programovací jazyky a jejich detaily. Takže pokud máte k této problematice co říct, diskuse pod článkem je vám k dispozici.

Autor článku