GenericsCzęść NR.21

Podczas kursu była mowa o tablicach, które są stworzone do przechowywania typów wartościowych. Przy typach referencyjnych wykonuje się pakowanie i używanie tablic ich traci sens. Tablice mają określoną wielkość i aby ją zmienić trzeba stworzyć ją od nowa.

Klasy z System.Collection jak ArrayList czy HashTable przechowują wszystko jako obiekty więc pakowanie i wypakowywanie zawsze zachodzi bez względu na to, czy jest to typ wartościowy, czy też nie. Mimo, iż są elastyczne z powodu przechowywania wszystkiego jako obiekt możemy zderzyć się z błędem przy rzutowaniu.

Od C# 2.0 mamy do dyspozycji jeszcze inne klasy kolekcji i są one najczęściej używane.

Problem z obiektami

Aby zrozumieć dlaczego te klasy są tak użyteczne opiszę tutaj problemy z jakimi człowiek może się spotkać.

Typ “object” referuje się do wartości o jakimkolwiek typie. Wszystkie typy referencyjne dziedziczą automatycznie po klasie System.Object także wbudowane klasy w .NET .

Daje to możliwość tworzenia metod czy zmiennych, które mogą przechowywać różne wartości. Klasy takie jak ArrayList, HashTable są kolekcjami, które mogą przechowywać wszystko. Opisałem je w tym wpisie.

Możesz tworzyć kolejki, stosy, które mogą zawierać w sobie każdy typ. Problem polega na tym, że skoro te kolekcje mogą przechowywać wszystko to mogą w nich znaleźć się nieodpowiednie obiekty. Co prawda mamy do dyspozycji słowa kluczowe jak “as” czy “is” , które mogą sprawdzać czym tak naprawdę jest dany “object” , ale wciąż nie zmienia to faktu, że jest to uciążliwe.

Kolejną poważną wadą tego zastosowania jest pakowanie i wypakowywanie danych. Umieszczając np. liczbę do ArrayList musi być ona spakowana do obiektu , a gdy chcemy ją wyciągnąć musi być ona wypakowywana.

int zlo = 42;

ArrayList al = new ArrayList();

al.Add(zlo); //pakowanie 
zlo = (int)al[0] + 1; //wypakowywanie

Problem nie aż taki drastyczny ale jeśli ArrayList ma przechowywać 1000 elementów to już nie jest ciekawie.

int zlo = 42;
Queue kolejka = new Queue();

kolejka.Enqueue(zlo); //wrzucanie do kolejki 

zlo = (int)kolejka.Dequeue(); //wyciąganie z kolejki 

Zachowania kolejki i stosu są takie unikalne, dlaczego muszą one przechowywać zawsze obiekty?. Otóż sytuacja się zmieniła.

Generics

Od C# 2.0 mamy do dyspozycji kolekcie, które są wolne od częstego pakowania i wypakowywania danych i są bezpieczne typem . W klasach tych musimy podać typ przechowywania danych, na których dana kolekcja ma operować.

using System.Collections.Generic;

Te klasy znajdują się w przestrzeni nazw System.Collections.Generics. W przestrzeni tej nazwy znajduje się inna wersja kolejki i stosu, które mogą przechowywać określony typ. Nie wiem, od której wersji tak jest ale to teraz bez znaczenia.

int dobro = 0;

Queue<int> kolejka = new Queue<int>();
kolejka.Enqueue(dobro);

dobro = kolejka.Dequeue();

Jak widać w kodzie typ jest określony pomiędzy znakami <int> przy deklaracji kolejki. Nie ma tutaj też żadnego rzutowania ponieważ kolejka może przechowywać tylko typ int.

Dobra , ale o co chodzi z tym symbolem znakowym <Typ> ,a raczej <T>. Spójrzmy na deklaracje klasy kolejki i stosu od T.

public class Queue<T> : //...
public class Stack<T> : //...


Znak “T” gra jako parametr, który symbolizuje dany typ i ten typ jest zawsze określony w czasie kompilacji.

W ten sposób nie ma problemu z utworzeniem kolejki czy stosu <int>, <string> i tak dalej. To samo dotyczy metod tych kolekcji, w końcu one też działają na parametrze T.

public void Enqueue(T item);
public T Dequeue();

public void Push(T item);
public T Pop();

Parametr T zastępuje dany typ i jest zastąpiony prawdziwym typem w czasie deklaracji danej klasy Generics.

Do tych klas nie można więc już umieścić innych typów od tych, które zadeklarowaliśmy zamiast <T>.

Mechanizm T jest bardziej skomplikowany niż się wydaje. Jego działanie nie polega dosłownie na zastąpieniu tego elementu jakby kod to był jakiś notatnik, czy word. Kompilator dokonuje dokładną i kompletną substytucje w taki sposób ,aby T mogło referować się do każdego typu.

Jednak przykładowo Queue<string> i Queue<int> powinny być traktowane jak dwa oddzielne typy danych. Specyficzne typy klas Generics nazywane są zbudowanymi typami “constructed types” .

Myślę że zrobię o tym wpis. Tworzenie swoich kolekcji od T nie jest takie proste. Nie mówiąc o tym , że jest to ciekawe zagadnienie.

Krótki pogląd klas z System.Collection.Generics

Klas z tej przestrzeni jest wiele, co zatem idzie nie widzę sensu objaśniania wszystkich tych klas. Inna sprawa jest taka ,że te kolekcje mają metody LINQ . Nie chcę na razie mówić o LINQ. Na to można zrobić oddzielny kurs.

Większość z tych kolekcji ma metodę Clear() do czyszczenia zwartości jak i właściwość “Count” oznaczającą liczbę elementów w danej kolekcji.

Najpopularniejszą kolekcją jest List<T>. Jeśli zaczynasz swoją przygodę z tymi kolekcjami jest to dobry start. Jest to zwyczajna lista elementów. Elementy można dodawać za pomocą metody (.Add). Naturalnie zawartość kolekcji może być wyświetlona przez pętle foreach.

List<string> lista = new List<string>();
lista.Add("Amiga");
lista.Add("Zdzisław");

foreach (var item in lista)
{
    Console.WriteLine(item);
}

Do zawartości listy można odwołać się za pomocą indeksów. Jeśli jeszcze nie korzystałeś z listy to wiedz, że w większości wypadków ona wypiera tablice jednowymiarowe.

List<string> lista = new List<string>();
lista.Add("Napis");
string zabiore = lista[0];

O ile dobrze pamiętasz kolekcje, które posiadają metodę Add() (kolejka i stos odpadają) mogą mieć zadeklarowane wartości przy inicjacji.

List<string> lista = new List<string>() 
{ "A", "Obiekt", "Ciałem", "Się", "Stał" };

Klasy Generics mają też wiele parametrów od T np. Dictionary czyli słownik. W nim trzeba zadeklarować klucz i daną wartość pod ten klucz.

public class Dictionary<TKey, TValue>

Słownik przechowuje kolekcie par kluczy/wartości. Każda wartość w miejscu “TValue” jest powiązana z danym kluczem “TKey”. Możesz wyciągać dane wartości przy użyciu danego klucza.

Poniższy przykład ilustruje użycie słownika. Kluczami są stare gry na Amigę , a wartości reprezentują rok wydania danej gry.

Dictionary<string, int> slownik = new Dictionary<string, int>();
slownik.Add("Goblins", 1992);
slownik.Add("Mortal Kombat", 1993);
slownik.Add("Defender Of Crown", 1988);

int rok = slownik["Mortal Kombat"];

Słownik w działaniu jest zbliżony do kolekcji, którą omówiłem wcześniej jak HashTable.

Slownik
Słownik posiada też swoje własne ciekawe metody i szczerze większość z nich wyjaśnia swoje działanie swoją nazwą.

W System.Collection.Generics mamy jeszcze do dyspozycji: SortedDictionary<TKey, TValue> , SortedList<TKey, TValue> są to też słowniki ale posiadają swoje własne dodatkowe zachowania.

SortedList<TKey, TValue> to słownik, który ma posortowane klucze zgodnie z metodą pochodzącej od interfejsu IComparer<T>. Dla klasy string będzie to kolejność alfabetyczna.

SortedDictionary<TKey, TValue> to słownik, który też ma posortowane klucze ale robi tę operacje trochę w inny sposób. Potrzebuje więcej pamięci niż SortedList ale działa szybciej. Jednak jeśli kolekcja jest poszerzona za jednym zamachem to SortedList działa lepiej.

Istnieje jeszcze wiele ciekawych kolekcji Generics w C# ale na początek zabawy spokojnie wystarczy List<T> i Dictionary<TKey, TValue> .

Co dalej:

Tworzenie własnej kolekcji od zostawię na inny wpis.
Kurs ten jest już trochę za długi dlatego następny wpis o "Czasie obiektów" będzie przedostatni, a na koniec zrobię podsumowanie tego kursu w pigułce.