Generics - typy ogólne

Typy ogólne to nowa cecha języka wprowadzona w C# i VB.NET pozwalająca na parametryzowanie typu innym typem - podobnie jak ma to miejsce w szablonach w C++. W .NET typ ogólny nie jest jednak makrodefinicją, a odrębnym typem. Wprowadzenie typów ogólnych przy okazji pomogło rozwiązać inny problem - obsługi po stronie aplikacji wartości null zwracanych przez bazy danych.

Typy ogólne to nowa cecha języka wprowadzona w C# i VB.NET pozwalająca na parametryzowanie typu innym typem - podobnie jak ma to miejsce w szablonach w C++. W .NET typ ogólny nie jest jednak makrodefinicją, a odrębnym typem. Wprowadzenie typów ogólnych przy okazji pomogło rozwiązać inny problem - obsługi po stronie aplikacji wartości null zwracanych przez bazy danych.

Jedną z cech C# jest tzw. mocna kontrola typów. W trakcie kompilacji następuje sprawdzenie, czy przypisywane sobie wartości są "zgodnych" typów. Jeżeli nie, zgłaszany jest btąd w czasie kompilacji. Wyjątkiem są jednak kolekcje, czyli pewnego rodzaju pojemniki na wiele elementów, na przykład ArrayList (tablica, która dynamicznie zwiększa swój rozmiar w miarę dodawania kolejnych elementów). Przechowują one zwykle typ Object, żeby pomieścić dowolne elementy, ale tym samym pozwalają umieścić w jednej kolekcji elementy o różnych typach, co zwykle jest błędem w aplikacji albo (jeżeli zostało zamierzone) wymaga uważnego sprawdzania typów i rzutowania podczas pracy z taką strukturą.

Oczywiście można generować klasy realizujące kolekcje z mocną kontrolą typów, ale to nie jest zbyt wygodne, bo wymaga albo zewnętrznego generatora (jak CodeSmith), albo pracowitego wykonywania operacji Znajdź i zamień.

Załóżmy, że mamy klasę Customer, zawierającą podstawowe informacje o kliencie:

class Customer {

public string name;

public string surName;

public Customer(string name, string surName) {

this.name = name;

this.surName = surName;

}

public override string ToString() {

return name + " - " + surName;

}

}

(Metoda ToString jest przydatna, ponieważ m.in. korzysta z niej debuger, generując podgląd zmiennej - warto ją zawsze dodawać do definicji klasy). W .NET 2.0 (tu przykłady pokazane są w C#) wprowadzono nową strukturę o nazwie generics - co można przetłumaczyć jako "typy ogólne". Taki element jest parametryzowany i może być np. listą przechowującą tylko elementy danego typu. W .NET Framework zdefiniowano kilkanaście gotowych klas ogólnych, głównie w przestrzeniach nazw System.Collection.Generics i System.Collections.ObjectModel.

Comparer<T>

Porównuje dwa obiekty (implementuje IComparer), uwzględniając informacje o globalizacji. Używany do sprawdzania "równości" i sortowania.

Collection<T>

ReadOnlyCollection<T> (tylko do odczytu) Bazowa klasa dla kolekcji, odpowiednik ICollection.

Dictionary<K, V>

Słownik wartości. K jest typem-kluczem, V - przechowywaną wartością. Dictionary pozwala szybko znaleźć wartość na podstawie danego klucza. Odpowiednik IDictionary i ew. Hashtable (tablicy haszującej). W .NET 2.0 Dictionary ma dwie wygodne kolekcje - KeyCollection i ValueCollection, które (odpowiednio) pozwalają na traktowanie kluczy/wartości jak zwykłych kolekcji.

IEnumerable<T>, IEnumerator<T>

Implementuje mechanizmy niezbędne do wyliczania kolejnych elementów w kolekcji przy użyciu pętli foreach. Cały mechnizm jest opisany w artykule Enumeratory .NET 2.0)

KeyedCollection<T, U>

Struktura przypominająca słownik, ale wartość klucza jest częścią wartości przechowywanej w kolekcji. Innymi słowy, dodając łańcuch do kolekcji KeyedCollection<string,string>, można określić, Że np. do "Tomasz Kopacz" kluczem będzie "Kopacz". Jest to klasa abstrakcyjna (wymagana jest implementacja metody GetKeyForItem, która określa sposób wybrania wartości klucza).

LinkedList<T>

Podwójna lista (dwukierunkowa, czyli taka, której każdy węzeł ma wskaźnik do poprzedniego i następnego elementu). Węzeł to typ LinkedListNode<T>.

List<T>

Lista pojedyncza (jednokierunkowa). Co ciekawe, lista ta jest wewnętrznie zrealizowana w postaci tablicy, której rozmiar jest dynamicznie zwiększany wraz z dodawaniem do listy nowych elementów.

SortedList<K, V>

Lista przechowująca parę K,V, która w miarę dodawania elementów jest sortowana po typie k (kluczu). Wymaga, żeby typ miał zaimplementowany interfejs IComparer.

Queue<T>

Kolejka, struktura typu FIFO. Jeżeli do kolejki wstawiamy elementy w kolejności 1,2,3, to wyjmujemy je w kolejności 1,2,3.

Wyspecjalizowane kolekcje

W przestrzeni nazw System. Collections .Specialized znajdują się (dostępne też w .NET 1.1) pewne predefiniowane klasy kolekcji, dostosowane do danego typu - na przykład StringCollection (kolekcja łańcuchów) czy StringDictionary (słownik, w którym kluczem i wartością jest łańcuch).

Stack<T>

Stos, struktura typu LIFO. Jeżeli na stos kładziemy elementy w kolejności 1,2,3, to zdejmujemy je w kolejności 3,2,1.

W poniższym listingu pokazany jest sposób wykorzystania typu ogólnego listy:

LinkedList<Customer> custList = new LinkedList<Customer>();

custList.AddLast( new Customer("Tomasz", "Kopacz"));

custList.AddLast( new Customer("Lech", "Zdanowicz"));

//custList.AddLast("AAA" ); - Błąd kompilacji

Console.WriteLine(custList.First.Value.name);

Proszę zauważyć, że można się zwyczajnie odwoływać do składowej name klasy - właściwość First w danym typie ogólnym to referencja do typu Customer. Tak skonstruowana lista uniemożliwia dodanie do niej wartości innego typu. Dla porównania podobny kod, ale odwołujący się do ArrayList, ma postać:

ArrayList al = new ArrayList();

al.Add(new Customer("Tomasz", "Kopacz"));

al.Add(new Customer("Lech", "Zdanowicz"));

//al.Add("AAA"); - To można wykonać, kolekcja

//nic nie wie o typie przechowywanych wartości

Console.WriteLine((al[0] as Customer).name);

W tej sytuacji nie ma prawie żadnej kontroli nad tym, co wkładamy do kolekcji - może to być nasz typ Customer, ale równie dobrze prosty łańcuch. Przy wyjmowaniu elementów z kolekcji dostajemy object, który trzeba zrzutować na właściwy typ (a naprawdę wcześniej trzeba by sprawdzić, czy to, co wyjmujemy, jest odpowiedniego typu i czy rzutowanie nie spowoduje zgłoszenia wyjątku).

Warto też przyjrzeć się wygenerowanemu kodowi MSIL (można to zrobić np. narzędziem ILDASM z pakietu .NET SDKalbo bardzo wygodną przeglądarką .NET Reflector Lutza Roedera - http://www.aisto.com/roeder/ ). W wypadku typu ogólnego przy dodawaniu nowego elementu mamy po prostu:

ldstr "Tomasz"

ldstr "Kopacz"

newobj instance void

LangTest.Customer::.ctor(string, string)

callvirt instance [System]System.Collections.Generic.LinkedListNode'1<!0>

[System]System.Collections.Generic.LinkedListvl<LangTest.Customer>::AddLast(!0

potem, gdy chcemy odczytać pole name pierwszego elementu listy:

callvirt instance [System]System.Collections.Generic.LinkedListNode"1<!0>

[System]System.Collections.Generic.LinkedList*l<LangTest.Customer>::get_First()

callvirt instance !0 [System]System.Collections.Generic.LinkedListNode'1

<LangTest.Customer>::get_Value()

ldfld string LangTest.Customer::name


Zobacz również