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.
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.