Po několika málo dílech, které se spíše zabývaly novinkami v jazyku C#, se dnes podíváme na jednu novinku z oblasti nástrojů.
reklama
Po několika málo dílech, které se spíše zabývaly novinkami v jazyku C#, se dnes podíváme na jednu novinku z oblasti nástrojů.
Pozn.: Ačkoli nástroj Code Contracts pravděpodobně bude součástí finální verze VS 2010, nyní (Beta 1) je třeba jej doinstalovat separátně např. z http://msdn.microsoft.com/en-us/devlabs/dd491992.aspx.
Napadlo vás někdy, kolik řádek obsahuje váš projekt, které jsou vztažené pouze na validaci vstupů? A nemyslím tím validaci vstupů od uživatele, ale mezi metodami. Zvláště pokud na projektu pracuje více vývojářů, je třeba zajistit, aby metoda pracovala pouze s očekávanými hodnotami. Většina metod tak začíná sérií „if (…) throw new …“. Případně jsou pro tyto případy připraveny metody. Ačkoli takovéto postupy jsou rozhodně správné a podobně jako např. unit testy pomáhají držet vše ve správných mantinelech, přeci jen dalo by se ještě pokročit. Stejně jako máme typovou kontrolu během kompilace, nebylo by skvělé mít tuto možnost i pro tyto podmínky?
Ano, můžete namítnout, že jednoznačné určení není možné a máte samozřejmě pravdu. Avšak zbývá stále velká podmnožina, kde tyto testy je možné během kompilace provést. A právě na tuto cestu se vydal projekt, původem z Microsoft Research, nazvaný Code Contracts.
Velmi vágně řečeno vám umožní zapsat k metodám podmínky a následně jejich platnost testovat, částečně již během kompilace. Tedy něco jako vylepšený Debug.Assert.
Pokud se podíváme blíže na jednotlivé skupiny, najdeme:
- Podmínky platící před vstupem do metody
- Podmínky platící při ukončení metody (úspěšném i neúspěšném)
- Invarianty objektu – podmínky platící vždy
- Asserty apod.
Veškeré potřebné metody se nacházejí ve třídě Contract, která vlastně není žádnou implementací, ale slouží jako metadata pro následné ověření podmínek. Všechny podmínky se zapisují na začátek metody a během kompilace jsou přeskládány na správná místa. V případě „release“ sestavení mohou být pak tyto podmínky odstraněny úplně (pro zrychlení kódu).
Vstupní podmínky (pre-conditions)
Vstupní podmínky jsou většinou kladeny na množinu vstupních hodnot a týkají se jejich povolených rozsahů.
Připraveny máme dvě základní metody. Contract.Requires a Contract.RequiresAlways. Obě dělají to samé, pouze druhá je do kódu zahrnuta vždy – i v případě „release“ sestavení. Parametrem je podmínka, která musí platit před započetím vykonávání metody a volitelná chybová zpráva.
Vezmeme-li například jednoduchou metodu pro dělení, mohl by její zápis vypadat:
public static double VydelNove(double a, double b)
{
Contract.Requires(b != 0, "Nelze delit nulou.");
return a / b;
}
Oproti původnímu:
public static double VydelPostaru(double a, double b)
{
if (b == 0)
throw new ArgumentException("b");
return a / b;
}
Výhodou tohoto přístupu není jen ověření podmínky při běhu, ale také možnost ji ověřit během kompilace a dokonce i při psaní kódu. A nejen to. Díky těmto metainformacím je možné si představit např. velmi jednoduché generování programátorské dokumentace.
Pokud by se nyní někdo pokusil zavolat metodu s druhým parametrem nula (a při zapnutém testování na pozadí), dostane chybu ještě dříve, než dojde k běhu aplikace.

Pro kolekce je pak připravena metoda ForAll, se kterou otestujete, že všechny prvky v dané kolekci vyhovují zadané podmínce. Podobně jako například Enumerable.All případně Enumerable.Any.
public static double SumaProKladnaCisla(int[] cisla)
{
Contract.Requires(Contract.ForAll(cisla, i => i > 0));
return cisla.Sum();
}
Poslední metodou pro vstupní podmínky je EndContractBlock. Ta se může zdát trochu podivná, neboť neověřuje přímo žádnou podmínku. Nicméně slouží v základu pro dvě věci. Pokud již nějaké podmínky máte a chcete je zahnout do testovacího bloku, umístíte je před volání této metody. A samozřejmě pokud vyjadřovací prostředky třídy Contract nestačí, napíšete vlastní podmínku, taktéž před volání. Code Contracts pak tento kód rozpozná a bude se k němu odpovídajícím způsobem chovat.
Výstupní podmínky (post-conditions)
Pro výstupní podmínky máme k dispozici dvě jednoduché metody. Ensures a EnsuresOnThrow. Metoda Ensures říká, jaká podmínka bude vždy platit při opuštění metody. Je možné tak ověřit, že se metoda nedostala do neočekávaného stavu a je též možné na základě očekávaného výsledku volit třeba datové typy ve volající metodě. Můžeme tedy vylepšit naši metodu pro dělení o přidání podmínky:
public static double VydelNove(double a, double b)
{
Contract.Requires(b != 0, "Nelze delit nulou.");
Contract.Ensures(Contract.Result<double>() <= a);
return a / b;
}
Jistě jste si všimli, že jsem použil zatím nepopsanou metodu Result. Pozorné čtenáře jistě napadlo, jak ověřit výstupní podmínku, v případě, že nemusí být jednoduché se k finálnímu výsledku (opravdu na konci) dostat. Právě pro tento případ je metoda Result určena. Díky ní můžete napsat podmínku vztaženou k výsledku, aniž by bylo třeba jej někde ukládat a tím teoreticky zanést chybu. Mimo to je možné ještě využít OldValue a ValueAtReturn. První jmenovaná vrátí hodnotu proměnné tak, jak byla na začátku (pozor, nejedná se však o hlubokou (deep) kopii). A druhá umožňuje získat hodnotu „out“ parametru při výstupu z metody.
Invarianty objektu
Podobně jako pro invarianty algoritmů je možné specifikovat invarianty objektů – stavy resp. podmínky nad vlastnostmi objektu, které platí vždy a jejich porušení signalizuje chybný kód a možné narušení integrity objektu.
Invarianty objektu jsou definovány v metodě (metodách) označených atributem ContractInvariantMethod.
Jako ukázku uvedu třídu Clovek, která má svůj věk. Je celkem přirozené mít věk pouze nezáporný. Proto je definován invariant, který tuto podmínku zajistí (ačkoli by ji bylo možné definovat i pro setter vlastnosti Vek).
public class Clovek
{
[ContractInvariantMethod]
protected void ObjectInvariant()
{
Contract.Invariant(Vek >= 0);
}
public Clovek()
{
this.Vek = 0;
}
public int Vek { get; private set; }
public void Zestarni(int kolik)
{
this.Vek += kolik;
}
public void Omladni(int kolik)
{
this.Vek -= kolik;
}
}
Zavoláte-li nyní new Clovek().Omladni(99); dostane se objekt do nesprávného stavu a bude zobrazen dialog s popisem chyby.

Poslední dvě metody, které zmíním, jsou: Assert, jejíž použití vám nebude cizí, pokud znáte Debug.Assert; a Assume, která umožňuje vnutit svoji „pravdu“ statickému testeru i pokud si myslí opak.
Jak jsem se zmínil výše, je možné ověřování podmínek různě nastavit. Můžete nechat testovat podmínky na pozadí (podobně jako při syntaktické analýze) nebo během kompilace. A samozřejmě za běhu aplikace. Dále je možné zapnout statické testování. Tento nemusí (a ani nemůže) být stoprocentní, neboť úplnou kontrolu správnosti kódu není možné obecně staticky provést. Nicméně poměrně velkou část podmínek ověřit možné je.

Vzhledem k faktu, že nástroj Code Contracts je stále ve vývoji, je možné, že některé schopnosti budou přidány, případně změněny. Proto výše zobrazený dialog berte s rezervou.
Code Contracts je alespoň z mého pohledu skvělý nástroj, který nahradí mnoho mých extension metod pro validaci vstupu – především mé oblíbené (not-)null hodnoty při ukládání do databáze. A to vše je ještě částečně možné ověřit staticky. Doporučuji stáhnout, nainstalovat a zkoušet různé podmínky a jejich ověřování, případně různá nastavení. Není nad kód, o který se můžete „opřít“.
Pokud vás tato problematika zajímá více, doporučuji podívat se též na rozsáhlejší projekt nazvaný Pex v Microsoft Research, věnující se taktéž testování podmínek v kódu.
Na závěr obligátní otázka: Používáte ve svém kódu podmínky u metod?