12. Generické datové typy

Abstrakt

V této kapitole se budeme zabývat generickými datovými typy. Nejprve si uvedeme motivaci, proč používat generické datové typy a na jakých základech jsou postaveny. V další části pak bude ukázáno jak tyto konstrukce implementovat.

12.1. Teorie generik

Základní myšlenkou rozšíření jazyka C# 2.0 byla podpora elegantního a rychlého typově bezpečného kódu. Také zjednodušení některých základních úkolů programátora, které se velmi často opakovali. Tyto důvody, pro rozšíření jazyka, dále navrhnout a zrealizovat s velkým důrazem na zpětnou kompatibilitu s předchozí verzí jazyka (C# 1.0). Než pochopíme využití generik musíme navázat na předešlou problematiku psaní kódu.

12.1.1. Univerzální kód

Univerzální kód využívá programátor pro všestrannou práci nad libovolnými daty. Z toho důvodu knihovny často využívají základní a hierarchicky nejvyšší typ object, který umožňuje uchovávat libovolná data. Tudíž jak hodnotového, tak i referenčního typu. Lze jej také využít pro univerzální položky, parametry a návratové hodnoty funkcí. S použitím univerzálního kódu však vzniká řada základních problémů:

  • Nejzásadnějším problémem je chybějící typová kontrola při překladu, kdy nemůžeme zjistit, zda přiřazení konkrétního typu do typu object je správné, anebo zdali není tak závadné, aby způsobilo během provádění aplikace chybu. Tuto chybu lze způsobit použitím nevhodných nebo nekompatibilních typů.

  • Další problém nastává u použití hodnotových typů, a to operací pro zaobalení hodnot do objektové reprezentace (boxing) a následující rozbalení zpět na jejich konkrétní hodnotu (unboxing). Během těchto operací se musí neustále provádět typová kontrola za běhu aplikace, což má za důsledek časové a výkonnostní zpomalení aplikace.

  • Také použití referenčních typů není bezproblémové. Nedochází u nich k předešlým zbytečným operacím, ale při převodu zpět z proměnné typu object na konkrétní typ (unboxing), musí dojít k explicitně zadané typové konverzi. Zde právě může dojít k chybě, pokud konverze neodpovídá uloženému typu.

  • Poslední problém může nastat v nepřehlednosti, kdy častým využitím univerzálního kódu získáme obtěžující a nevzhledné typové konverze.

12.1.2. Popis generik

Mluvíme-li o generickém kódu, máme namysli kód, který využívá parametrizované typy. Parametrizované typy jsou nahrazeny konkrétními typy v době použití kódu. Generický typ T se uvádí za název definovaného objektu (třídy, rozhraní, metody, atd.) a je ohraničen špičatými závorkami <T>. Princip generik je znám také z jiných jazyků jako Eiffel, Ada (Java) a především z jazyku C++. Generika mají podobnou syntaxi jako šablony v programovacím jazyku C++, avšak realizují hodně odlišnou implementaci. V jazyce C# 2.0 lze pomocí generik definovat třídy, struktury, rozhraní, metody a delegáty. Použitím generik získáme několik výhod:

  • V prvé řadě dosáhneme silné typové kontroly v době překladu zdrojového kódu, a tedy možného nalezení všech případů, které by mohly způsobit havárii aplikace způsobenou nesprávným využitím použitých typů.

  • Odstraníme také časté a z časového hlediska zbytečné operace boxing a unboxing.

  • Omezení explicitních typových konverzí, které jsou nutné v době překladu, kdy neznáme typ objektu uloženého v univerzální kolekci.

Díky těmto výhodám dosáhneme nejen čistějšího, rychlejšího výsledného kódu, ale především náš kód bude hlavně bezpečnější.

12.1.3. Omezení

C# 2.0 dále poskytuje tzv. omezení, které můžeme aplikovat na zmíněná generika. Specifikace požadavků na typové parametry:

  1. Zabezpečují, že typy poskytují požadovanou funkcionalitu.

  2. Umožňují silnější typové kontroly při překladu.

  3. Omezují potřeby explicitních typových konverzí.

  4. Poskytují možnost omezení použitelnosti výsledného generika.

V případě psaní obecného kódu nad obecným typem předpokládáme jeho jistou funkcionalitu. U konkrétního typu budeme proto požadovat také jeho určitou funkcionalitu. Pro zajištění této funkcionality existují tři typy omezení:

  • Omezení třídy (Class constraint)

    Konkrétní typ, který v generickém kódu použijeme, musí být odvozen od specifické základní třídy. Tím zajistíme, že máme definovanou jistou funkcionalitu.

  • Omezení rozhraní (Interface constraint)

    Zajišťuje, že rozhraní, které použijeme, je implementováno od konkrétního specifického rozhraní.

  • Omezení konstruktoru (Constructor constraint)

    Požadavek, aby třída nebo struktura, kterou používáme, obsahovala veřejný implicitní konstruktor.

12.1.4. Generika ve srovnání s kolekcemi

  1. Kolekce jsou nevýhodné z časových důvodů a možného výskytu programových chyb

    O základních prvcích kolekcí typů object víme:

    • jsou dynamicky napsané => programové chyby jsou odhaleny pouze až v době běhu aplikace

    • jsou potřebná přetypování za běhu programu => což ovšem má za důsledek snížení celkové rychlosti programu

    • hodnoty jednoduchých datových typů (např. int) musí být zabalené (jako Integer) => to zabere volné místo a potřebný čas

    Příklad deklarace kolekce typu Map:

    [ukázka kódu]
    Map  map = new HashMap();
                                

    Z takto nadefinované proměnné nejsme schopni určit s jakými typy bude kolekce pracovat. Možným řešením pro dokumenty s netriviálním využitím kolekcí je vhodné vložení komentáře k deklaraci funkce příslušné kolekce, jak je uvedeno níže. Komentáře jsou samozřejmě ignorovány překladačem.

    [ukázka kódu]
    Map /* from Integer to Map from String to Integer */ map = new HashMap();
                                 

    Generiky mohou udělat výsledný kód typově bezpečným. S generickými kolekcemi využívajícími parametrický polymorfismus (neboli generika), lze předešlou deklaraci zapsat takto:

    [ukázka kódu]
    IMap<int,IMap<string,int>> map = new HashMap<int,IMap<string,int>>();
                                

    Takto nadefinovanou proměnnou získáme nejen typové bezpečí, ale také lépe pochopíme smyl použití dané proměnné.

  2. Kolekce je nutné přetypovávat

    Společnost Microsoft přidala k verzi frameworku .NET 2.0 nový prostor jmen pro kolekce nacházející se v System.Collections. Tento prostor jmen obsahuje několik odlišných rozhraní a tříd, které definují různé typy pomocných kontejnerových seznamů, slovníků a hašovacích tabulek. Každý zmíněný typ má jinou implementaci a slouží ke konkrétnímu účelu. Každá položka musí být přetypována během procesu zpřístupnění z kolekce. Toto povinné přetypování může způsobit chybu, která by vyplývala z dalšího jiného typu objektu, který byl někdy předtím vložen do kolekce.

    Pomocí vytvoření nové kolekce zajistíme typovou bezpečnost, avšak kolekce není znovu použitelná pro další typy. Navíc pokud máme více specifických tříd, pak se údržba na nich stává tak těžkopádnou, že ani nemá cenu ji realizovat za účelem poskytnutí typového bezpečí. Řešením jsou tedy generika.

    Generika byli vytvořeny proto, aby nabízely spojení typového bezpečí, výkonu a všeobecných zásad v definovaném typu. Generika je tudíž typově bezpečná třída, která je deklarovaná bez konkrétního typu a aplikovaná v definici. Spíše lze říci, že typ je konkretizovaný až v době, kdy je objekt užívaný.

    System.Collections.Generics je nový prostor jmen (namespace) určený pro generické kolekce, který obsahuje několik předběžně sestavených tříd, které jsou navrhnuty pro reprezentaci běžných typů, včetně těchto: Zřetězený seznam, Seznam, Fronta, Zásobník

    [příklad ke stažení]

    Ukázka využití existujícího generického vzorového kódu, namísto vytváření vlastního generického kódu zde.

12.2. Generika v příkladech

12.2.1. Proč potřebujeme generika

Prozatímní účel datových struktur je ve využití typu object k uložení dat různého typu. Například, zde vidíme objektově založenou třídu Stack reprezentující zásobník:

[ukázka kódu]
public class Stack
{
    object[] items;
    int count;
    public void Push(object item) {...}
    public object Pop() {...}
}
                    

Využitím typu object je tato třída velmi flexibilní, avšak má i stinné stránky. Například, pokud vložíme na zásobník hodnotu nějakého hodnotového typu metodou Push, třeba typu int, dojde k automatickému zabalení hodnoty do objektové reprezentace (boxingu). Při pozdějším použití metody Pop, pro získání této hodnoty ze zásobníku, musí dojít k explicitnímu přetypování do správného konkrétního typu (unboxing).

[ukázka kódu]
Stack stack = new Stack();
stack.Push(3);
int i = (int)stack.Pop();
                    

Explicitní přetypování je pro programátora únavné a společně s operací zabalení (boxing) přidává výkonovou režii aplikace, neboť potřebují dynamickou alokaci paměti a typové kontroly za běhu aplikace. Také je nevýhodou, že nelze zjistit jaký je typ objektu vloženého do zásobníku. Dále pak může dojít k tomu, že objekt uložený v zásobníku může být po vyjmutí explicitně přetypován do jiného typu. Jako zde na ukázce:

[ukázka kódu]
Stack stack = new Stack();
stack.Push(new Customer());	// class for customer
string s = (string)stack.Pop(); // bad explicit cast, but not error
                    

Zatímco výše uvedený kód je založen na nevhodně použité třídě Stack, je kód po technické stránce správný a proto není oznámena chyba během provádění kompilace. Problém není zřejmý do té doby, než je kód spuštěný. Poté se už chyba projeví vyvoláním výjimky pro špatné přetypování InvalidCastException.

12.2.2. Konstrukce a využití generik

Generika dovedou vytvořeným typům přiřadit typové parametry . Typový parametr je specifikován v lomených závorkách za jménem definovaného objektu (<T>). Genericky vytvořené objekty přijímají pouze typ, pro který byly vytvořeny a ukládají data tohoto typu bez zbytečných konverzí, které využívají objektově založené struktury. Všimněte si rozdílů v implementaci předešlého zásobníku a generického zásobníku s typovým parametrem, jehož implementace je zde naznačena:

[ukázka kódu]
public class Stack<T>
{
    T[] items;
    int count;
    public void Push(T item) {...}
    public T Pop() {...}
}
                    

Při použití generické třídy Stack<T> se aktuální typ nahradí za již specifikovaný typ T. V následující ukázce je typ int předán jako typový argument pro T:

[ukázka kódu]
Stack<int> stack = new Stack<int>();
stack.Push(3);
int x = stack.Pop();
                    

Typ Stack<T> je nazýván zkonstruovaným typem (constructed type). V objektu typu Stack<int> je každý výskyt typového parametru T nahrazen typem v argumentu, zde tedy typem int. Když je vytvořena instance objektu generického typu Stack<int>, její položky jsou uloženy jako pole typu int[], což je rozhodně lepší než, kdyby byly tyto položky uloženy v typu object[]. Důvodem je značné využití paměti u negenerického zásobníku Stack.

Generika poskytují přísnou kontrolu typů, což například znamená, že je chybou vložení proměnné typu int do generického zásobníku typu Customer. Stack<Customer> je omezený pouze na objekty typu Customer, proto překladač zahlásí chybu v následujícím příkladu u posledních dvou řádků:

[ukázka kódu]
Stack<Customer> stack = new Stack<Customer>();
stack.Push(new Customer());
Customer c = stack.Pop();
stack.Push(3);		            // Type mismatch error
int x = stack.Pop();		// Type mismatch error
                    

12.2.3. Generické typy

Generické typy mohou mít více než jeden parametrický typ, jak tomu bylo v předchozím příkladu. Na dalším příkladu je možné vidět využití generické třídy Dictionary, která obsahuje jeden typový parametr pro klíče a druhý pro typ vkládaných hodnot slovníku:

[ukázka kódu]
public class Dictionary<K, V>
{
    public void Add(K key, V value) {...}
    public V this[K key] {...}
}
                    

V případě, že použijeme instanci objektu Dictionary<K, V>, pak mu musíme dodat příslušné argumenty:

[ukázka kódu]
Dictionary<string,Customer> dict = new Dictionary<string,Customer>();
dict.Add("Peter", new Customer());
Customer c = dict["Peter"];
                    

Generické typy programátorům dovolí vytvořit a testovat kód pouze jednou a znovu jej použít pro jakékoliv typy. Navíc v případě hodnotových typů je použití generického uložení dat mnohem výkonnější, protože se vyhneme operacím boxing, unboxingu a také přetypování.

Budeme chtít srovnat rychlost provádění genericky vytvořené datové struktury s objektově založenou datovou strukturou v závislosti na použitém typu.

Zjistíme, že generika jsou na tom podstatně lépe, samozřejmě při implementaci stejného problému. Jak blíže určuje dolní tabulka.

Tabulka 4.4. Tabulka porovnání rychlostí generických objektů s negenerickými

Typ objektuČas provedení kódu [s]Časový rozdíl
negenerickéhogenerického[s]
Seznam typu string0,5620,4380,125
Seznam typu int0,9840,3430,641
Zásobník typu string0,4060,4060,000
Zásobník typu int0,7810,2650,516

Tabulka poskytuje srovnání generického a negenerického objektu, který je vždy naplněn 10 000 prvky a každý prvek je přiřazen do proměnné příslušného typu. V tabulce můžeme vidět snížení času provedení kódu při využití generického objektu. Tabulka dále ukazuje na větší časový rozdíl při práci s typem int. Dokonce v případě typu string vkládaného do zásobníku je čas u jeho negenerické i generické implementace stejný.

12.2.4. Generické typy a IL

Podobně jako u negenerického typu je zkompilovaná reprezentace generického typu složená z instrukcí IL a metadat. Samozřejmě také zakóduje existenci a použití typových parametrů.

Nejprve dojde k vytvoření instance zkonstruovaného generického typu jako Stack<int>. Následně pak překladač JIT (just-in-time) běhového prostředí .NET CLR přemění generický IL a metadata do nativního kódu. Mezitím dochází také k dosazení aktuálních typů za typové parametry. Pozdější odkazy na zkonstruovaný generický typ pak využívají stejný nativní kód. Proces vytvoření konkrétního zkonstruovaného typu z generického typu se nazývá konkretizace generického typu.

Běhové prostředí CLR (Common Language Runtime) platformy .NET vytváří speciální kopii nativního kódu pro každou konkretizaci generického typu s hodnotovým typem. Avšak pro všechny odkazové typy sdílí unikátní kopii nativního kódu. Je tomu tak, protože v nativní úrovni kódu jsou odkazy (reference) jen ukazateli stejné reprezentace.

12.2.5. Důvody a ukázky omezení

Generická třída toho obvykle udělá více než jen ukládání dat založených na typových parametrech. Často budeme chtít vyvolat metody na objektech, jejichž typ je dán typovým parametrem. Blíže na ukázce příkladu, kdy metoda Add v generickém slovníku třídy Dictionary<K,V> bude potřebovat porovnat užívané klíče K metodou CompareTo:

[ukázka kódu]
public class Dictionary<K, V>
{
    public void Add(K key, V value)
    {
        ...
        if (key.CompareTo(x) < 0) {...}    // Error, no CompareTo method
        ...
    }
}
                    

U metody CompareTo ovšem nastane chyba během provádění kompilace, neboť typový argument K je předepsán pro jakýkoliv typ. Proto jediní členové, kteří můžou být součástí parametru key, jsou deklarovány typem object. Tak jako metody Equals, GetHashCode a ToString. Řešením je přetypování parametru key na takový typ, který obsahuje metodu CompareTo, například IComparable:

[ukázka kódu]
public class Dictionary<K, V>
{
    public void Add(K key, V value)
    {
        ...
        if (((IComparable)key).CompareTo(x) < 0) {...}
        ...
    }
}
                    

Toto řešení je už funkční, avšak vyžaduje dynamickou typovou kontrolu za běhu programu, což přidává na režii. Dále může vyvolat chybové hlášení InvalidCastExeption, pokud typový parametr key neimplementuje IComparable.

Z důvodů silnější typové kontroly za běhu aplikace a snížení přetypování, nabízí jazyk C# jakýsi nepovinný seznam omezení dodán ke každému typovému parametru. Dále určuje, že typ má být používaný jako argument pro typový parametr. Omezení jsou deklarována slovem where následované typovým parametrem, za ním dvojtečkou, pak čárkami odděleným seznamem třídních typů, typů rozhraních a typových parametrů (nebo také speciálním odkazovým typem, hodnotovým typem, ale i omezeným konstruktorem).

Aby třída Dictionary<K,V> zabezpečila to, že klíče budou vždy implementovány jako IComparable, musí deklarace třídy obsahovat omezení pro typový parametr:

[ukázka kódu]
public class Dictionary<K, V> where K: IComparable
{
    public void Add(K key, V value)
    {
        ...
        if (key.CompareTo(x) < 0) {...}
        ...
    }
}
                    

Touto deklarací zajistíme, že překladač doplní za typový argument K pouze takový typ, který implementuje rozhraní IComparable. Nemusíme tudíž ani explicitně přetypovávat, neb všichni členové typu, daného omezením typového parametru, jsou přímo dosažitelní z objektu.

12.2.6. Omezení typovým parametrem

Pro daný typový parametr je možné specifikovat jako omezení několik rozhraní a typových parametrů, ale pouze jednu třídu. Každý omezený typový parametr má oddělovač where. V dolním příkladu má typový parametr K dvě omezení rozhraní, zatímco typový parametr E má omezení třídním typem a konstruktorem:

[ukázka kódu]
public class EntityTable<K, E>
    where K: IComparable<K>, IPersistable
    where E: Entity, new()
{
    public void Add(K key, E entity)
    {
        ...
        if (key.CompareTo(x) < 0) {...}
        ...
    }
}
                    

Omezení konstruktorem new() zajistí, že typ použitý jako typový argument pro E má veřejný bezparametrový konstruktor. Tím povolí generické třídě použití new E() k vytvoření instance jejího typu.

Omezení typových parametrů je nutné používat obezřetně, neboť také můžeme omezit možné vhodné využití generického typu.

12.2.7. Generické metody

V některých případech není typový parametr potřebný pro celou třídu, ale pouze uvnitř konkrétní metody. Příkladem je metoda, která vezme generický typ jako parametr. Například, budeme chtít vložit několik hodnot v řádku jedním zavoláním metody s využitím dříve popsané generické třídy Stack<T>. Metoda pro specificky zkonstruovaný typ by vypadala takto:

[ukázka kódu]
void PushMultiple(Stack<int> stack, params int[] values) {
    foreach (int value in values) stack.Push(value);
}
                    

Vícenásobné vložení dat

Tuto metodu můžeme použít k vícenásobnému vložení hodnot typu int do Stack<int>:

[ukázka kódu]
Stack<int> stack = new Stack<int>();
PushMultiple(stack, 1, 2, 3, 4);
                    

Metoda ovšem pracuje pouze pro konkrétní zkonstruovaný typ Stack<int>. Pro práci se všemi typy musí být napsána jako generická metoda.

Generická metoda má jeden nebo více typových parametrů zapsaných v hranatých závorkách < a > za jménem metody. Typové parametry mohou být použity uvnitř seznamu parametrů jako návratový typ a v těle metody. Generická metoda PushMultiple() by poté vypadala:

[ukázka kódu]
void PushMultiple<T>(Stack<T> stack, params T[] values) {
     foreach (T value in values) stack.Push(value);
}
                    

Při následném volání metody jsou typové argumenty zadány v hranatých závorkách v místě, kde dochází k vyvolání metody:

[ukázka kódu]
Stack<int> stack = new Stack<int>();
PushMultiple<int>(stack, 1, 2, 3, 4);
                    

V mnoha případech může překladač odvodit správný typový parametr z argumentů příslušné metody. Tento proces se nazývá typové odvozování (type inferencing). Z výše uvedeného příkladu, přesněji z prvního argumentu typu Stack<int> a následujících argumentů metody typu int, může překladač rozpoznat typový parametr. Ten musí tedy být typu int. Proto může být generická metoda PushMultiple() volána bez deklarujícího typového parametru:

[ukázka kódu]
Stack<int> stack = new Stack<int>();
PushMultiple(stack, 1, 2, 3, 4);
                    
12.2.7.1. Volání generických metod

Volání generické metody může explicitně určit programátor seznamem typových argumentů. Nebo může seznam typových argumentů při volání metody opomenout, nevypsat a spolehnout se na typové odvozování, které zajistí určení správných typových argumentů. V následujícím příkladu si ukážeme, jak dojde k rozhodnutí přetížení po typovém odvozování a po tom, co jsou typové argumenty nahrazeny v parametrovém seznamu:

[ukázka kódu]
class Test
{
   static void F<T>(int x, T y) {
       Console.WriteLine("one");
   }
   static void F<T>(T x, long y) {
       Console.WriteLine("two");
   }
   static void Main() {
       F<int>(5, 324);            // Ok, prints "one"
       F<byte>(5, 324);           // Ok, prints "two"
       F(5, 324);                       // Ok, prints "one"
       F<double>(5, 324);         // Error, ambiguous
       F(5, 324L);                      // Error, ambiguous
   }
}
                        

U prvních tří volání generických metod ve funkci Main() nedojde k chybě, neboť se u prvních dvou funkcí explicitně určí seznam typových argumentů. U třetí metody je určen typový argument pomocí typového odvozování. Avšak u posledních dvou generických metod dojde k chybě způsobené nejednoznačným výběrem generické metody podle typových argumentů.

12.2.7.2. Signatura generických metod

Omezení jsou u signatur generických metod ignorována. Významný je počet generických typových parametrů jako i seřazení pozic typových parametrů. Následné srovnání signatur jednotlivých metod v příkladu ukáže více:

[ukázka kódu]
class A {}
class B {}
                            
interface IX
{
    T F1<T>(T[] a, int i);	              // Error
    void F1<U>(U[] a, int i);	            // Error
                                    
    void F2<T>(int x);     	              // Ok 
    void F2(int x);         	                  // Ok
                                        
    void F3<T>(T t) where T: A;	          // Error
    void F3<T>(T t) where T: B;	          // Error 
}
                        

U obou funkcí F1 dojde k chybě, neboť obě deklarace mají stejnou signaturu. A také z důvodu, že návratový typ a jméno typového parametru není u druhé funkce F1 stejný. U funkcí F2 je vše v pořádku, neboť počet typových parametrů je součástí signatury. Chyba u funkcí F3 je ukázkou výše zmíněného pravidla, že omezení jsou v signaturách ignorována.

12.2.8. Generická třída

Deklarace generické třídy je stejná jako deklarace třídy, akorát vyžaduje seznam typových parametrů k vytvoření skutečných typů. Deklarace třídy může libovolně definovat typové parametry:

[ukázka kódu]

Deklarace třídy:

atributy modifikátory class identifikátor <seznam typových parametrů> bázi_třídy omezení_typových_parametrů tělo_třídy ;

Všimněte si v předpisu částí deklarace generické třídy napsaných kurzívou. Tyto části nemusí být definovány při deklaraci, neboť jsou nepovinné.Povinné části deklarace třídy jsou zvýrazněny tučně.

Tento předpis je podobný i u ostatních generických struktur. Základní rozdíl je v použití klíčového slova, které specifikuje vytváření příslušného objektu (jako interface, struct, atd.).

Deklarace třídy nemůže poskytovat omezení_typových_parametrů, pokud rovněž nedodává seznam_typových_parametrů. Deklarace generické třídy navíc mohou být vložené uvnitř deklarace negenerické třídy.

12.2.8.1. Zkonstruovaný typ

Generická třída je odkázaná na používání zkonstruovaných typů. Deklarace generické třídy:

[ukázka kódu]
class List<T> {} 
                        

Příklady zkonstruovaných typů mohou být List<T>, List<int> a List<List<string>>. Zkonstruovaný typ, který využívá jeden nebo více typových parametrů, jako List<T>, se nazývá otevřený zkonstruovaný typ. Naopak pokud nevyužívá typové parametry, jako List<int>, tak se nazývá uzavřený zkonstruovaný typ.

Generické typy mohou být přetěžovány v závislosti na počtu typových parametrů. Například, pokud jsou dvě deklarace ve stejném jmenném prostoru nebo vnější deklaraci, mohou používat stejný identifikátor v deklaraci tak dlouho, dokud mají různý počet typových parametrů.

[ukázka kódu]
class C {}
class C<V> {}
struct C<U,V> {}	  // Error, C with two type parameters defined twice
class C<A,B> {}	    // Error, C with two type parameters defined twice
                        

U posledních dvou deklarací nastane chyba porušením výše zmíněné podmínky o počtu typových parametrů.

12.2.8.2. Typové parametry

Typové parametry mohou být dodávány do deklarace třídy. Každý typový parametr je identifikátor sloužící k udržení místa pro později použitý typový argument. Při vytvoření zkonstruovaného typu se za typový parametr dosadí aktuální typ, představovaný typovým argumentem.

Typový parametr nemůže mít stejné jméno jako jiný typový parametr obsažený v té samé deklaraci anebo jméno člena deklarovaného ve třídě. Nemůže mít také stejné jméno jako je jméno jeho typu.

Rozsah typového parametru určuje základní třída, omezení typových parametrů a tělo třídy. Rozsah se nerozšiřuje k odvozeným třídám narozdíl od členů třídy. Uvnitř rozsahu může být typový parametr užíván jako: hodnotový typ, referenční typ, typový parametr.

Typové parametry mohou být konkretizovány mnoha různými aktuálními typovými argumenty, avšak mají lehce odlišné operace a omezení než ostatní typy. Typové parametry jsou navíc záležitostí pouze kompilační úrovně. Během provádění aplikace jsou nahrazeny typem, jenž je specifikován typovým argumentem.

12.2.8.3. Vložené typy

Deklarace generické třídy může obsahovat také deklarace vložených typů (nested types). Uvnitř vloženého typu mohou být použity typové parametry z příslušné třídy. Deklarace vloženého typu může obsahovat další typové parametry, které platí pouze vzhledem k vloženému typu.

Pro každou deklaraci uvnitř deklarace generického typu platí, že je implicitně deklarací generického typu. Pokud se budeme odkazovat na typ vložený uvnitř generického typu, pak současně zkonstruovaný typ musí být pojmenovaný, včetně jeho typových argumentů. Vložený typ může být použit bez kvalifikace, tedy bez pojmenování. Instance typu vnější třídy může být implicitně použita při sestavení vloženého typu. V příkladu níže si ukážeme tři různé, avšak správné, způsoby jak se odkázat na zkonstruovaný typ vytvořený z vnitřní třídy Inner:

[ukázka kódu]
class Outer<T>
{
    class Inner<U>
    {
        public static void F(T t, U u) {...}
    }
    static void F(T t) {
        Outer<T>.Inner<string>.F(t, "abc");	     // These two statements have
        Inner<string>.F(t, "abc");		                 // the same effect
        Outer<int>.Inner<string>.F(3, "abc");    // This type is different
        Outer.Inner<string>.F(t, "abc");		           // Error, Outer needs type arg
    }
}
                        

První dvě inicializace jsou ekvivalentní, třetí je odlišná. V posledním čtvrtém příkladě nastane chyba, neboť třída Outer potřebuje typový argument. Ačkoliv to není zrovna správný programovací styl, typový parametr ve vloženém typu může ukrýt člena typového parametru deklarovaného ve vnějším typu:

[ukázka kódu]
class Outer<T>
{
    class Inner<T>	    // Valid, hides Outer’s T
    {
        public T t;		   // Refers to Inner’s T
    }
}
                        
12.2.8.4. Statická pole

U deklarace generické třídy je statická proměnná sdílena mezi všemi instancemi stejného uzavřeného zkonstruovaného typu. Ovšem mezi různými instancemi uzavřeného zkonstruovaného typu není statická proměnná sdílena. Zmíněné pravidlo platí bez ohledu na to, zda-li typ statické proměnné zahrnuje či nezahrnuje typové parametry.

Příklad deklarace generické třídy využívající statické pole:

[ukázka kódu]
class C<V>
{
    static int count = 0;
    public C() {
        count++;
    }
    public static int Count {
        get { return count; }
    }
}
                         
[příklad ke stažení]

Celý příklad lze nalézt zde

Nový uzavřený zkonstruovaný třídní typ je inicializovaný poprvé, když budˇ:

  • je vytvořena instance uzavřeného zkonstruovaného typu

  • jsou zmínění někteří ze statických členů uzavřeného zkonstruovaného typu

Statický konstruktor je vykonán přesně jednou pro každý uzavřený zkonstruovaný třídní typ. To je vhodné místo k vynucení si ověření typového parametru za běhu programu, neboť nemůže být kontrolován během kompilace skrze omezení. Například, následující typ užívá statického konstruktoru k vynucení si toho, že typový argument je typu enum:

[ukázka kódu]
class Gen<T> where T: struct
{
    static Gen() {
        if (!typeof(T).IsEnum) {
            throw new ArgumentException("T must be an enum");
        }
    }
}
                        
12.2.8.5. Statický konstruktor

Generická třída využívá statický konstruktor k inicializaci dříve zmíněného statického pole. Dále jej můžeme využívat k dalším inicializacím pro jakýkoliv různý uzavřený zkonstruovaný typ, který je vytvořen z této deklarace generické třídy. Typové parametry deklarace generického typu mohou být používány uvnitř těla statického konstruktoru.

12.2.8.6. Přetěžování

Uvnitř deklarace generické třídy můžeme přetěžovat:

  • metody

  • konstruktory

  • indexery

  • operátory

Deklarované signatury musí být jedinečné. Není ovšem vyloučeno, že náhrada typových argumentů může mít za výsledek totožné signatury. V takových případech budou pravidla o rozlišení přetížení vybrána pro nejvíce specifického člena. Následující příklad ukáže přetížení, která jsou platná či neplatná dle tohoto pravidla:

[ukázka kódu]
                            
interface I1<T> {...}
interface I2<T> {...}
 
class G1<U>
{
    int F1(U u);			        // Overload resulotion for G<int>.F1
    int F1(int i);			      // will pick non-generic
    void F2(I1<U> a);		// Valid overload
    void F2(I2<U> a);
}

class G2<U, V>
{
    void F3(U u, V v);	         	  // Valid, but overload resolution for
    void F3(V v, U u);		          // G2<int,int>.F3 will fail
    void F4(U u, I1<V> v);	  // Valid, but overload resolution for	
    void F4(I1<V> v, U u);	  // G2<I1<int>,int>.F4 will fail
    void F5(U u1, I1<V> v2);	// Valid overload
    void F5(V v1, U u2);
    void F6(ref U u);		            // valid overload
    void F6(out V v);
}
                        

12.2.9. Implicitní hodnoty

Implicitní hodnoty využívají klíčového slova default . Vrací implicitní hodnotu konkrétního typového parametru.

  • null pro odkazové typy

  • 0 pro číselné typy

  • false pro booleovské typy

  • '\0' znakové typy

  • strukturu inicializovanou implicitní hodnotou

Ukázka použití klíčového slova default pro typové parametry:

[ukázka kódu]
public class C<T>
{
    private T value;
    public T M() {
        return (condition) ? value : default(T);
    }
}
                

Metoda M() na základě vyhodnocení regulárního výrazu vrací, buď proměnnou value anebo implicitní hodnotu typového parametru přes výraz default(T).

Využití implicitních hodnot není omezeno jenom na generika. Lze je využít v jakémkoliv kódu. Implicitní hodnoty patří k dalším rozšířením jazyka C# 2.0

12.2.10. Výhody generik

  • program se stává staticky napsaným => takže chyby jsou odhaleny v době provádění kompilace a nikoliv přímo před uživatelem za běhu aplikace

  • běhová přetypování nejsou potřebná => čímž je program rychlejší

  • hodnoty jednoduchých datových typů (jako např. int) nemusí být zabalené => program je rychlejší a zabírá méně místa

12.2.11. Negenerické objekty

Vlastnosti, události, indexery, operátory, konstruktory a destruktory nemohou sami mít typové parametry. Mohou se ovšem vyskytovat v generických typech a využívat typového parametru. Ukázka kódu z již dříve probraného příkladu, kde se vyskytuje použití indexeru, který využívá předaný typový parametr. Na příkladu vidíme využití generické třídy Dictionary pro práci se slovníkem, která obsahuje jeden typový parametr pro klíče a druhý pro typ vkládaných hodnot slovníku:

[ukázka kódu]
public class Dictionary<K, V>
{
    public void Add(K key, V value) {...}
    public V this[K key] {...}                    
}