Omawiając algorytmy sortowania
skupiłem się na jedynie jednym typie danych - mianowicie liczbach typu int. A co w przypadku gdy mamy do
posortowania tablicę zawierającą ciąg znaków typu char? Nic prostszego, przeciążamy metodę odpowiedzialną za
sortowanie. Podobnie postępujemy w przypadku innych typów danych aż w końcu
nasz program nieprzyjemnie puchnie z powodu redundancji kodu. A co w przypadku
własnych struktur danych które nie mają zaimplementowanych metod
odpowiedzialnych za porównywanie czy zamianę wartości? Z pomocą przychodzą nam
dwa mechanizmy: generyczność oraz delegaci.
Generyczność
Jak wiemy, C# jest językiem
programowania który cechuje się silną kontrolą typów. Oznacza to, że w gdy miejscu
użycia wartości typu bool wstawimy
np. int (tak jak to jest możliwe w instrukcji
if w C/C++) kompilator powie nam parę niemiłych rzeczy o tym co zrobiliśmy. Więc
aby zmaksymalizować wydajność, zapewnić zgodność typów i zmniejszyć redundancję
kodu wprowadzono typy generyczne. Mechanizm ten pozwala na definiowanie metod
bez podania typu danych jakimi będzie ona operować. Typ danych z jakimi
procować będzie metoda jest ustalany na poziomie deklaracji metody. Przykład
użycia tego mechanizmy znajduje się w kodzie poniżej:
void
swap<T>(ref T
a, ref T b)
{
T
tmp = a;
a = b;
b = tmp;
}
Używając znaczników <>
definiujemy typ generyczny. Duża litera T jest typem zmiennej, który w momencie
deklaracji metody zostanie zastąpiony właściwym. Użycie takiej funkcji odbywa
się poprzez wstawienie w znaczniki <> typu docelowego. Przykład:
int
a = 2, b = 6;
swap<int>(ref a, ref b)
Delegaci
Delegaci to swego rodzaju
referencyjny typ danych który odnosi się do metod. Mówiąc krótko, delegat to
odnośnik do metody i taki odnośnik możemy przekazać jako argument funkcji.
Dzięki temu możemy z wnętrza funkcji wywołać daną metodę. Do delegata przypisać
możemy dowolną metodę która pasuje do jego sygnatury. Deklaracja delegata
wygląda następująco:
delegate void Swapper<T>(ref
T a, ref T b);
Deklarację zaczynamy, jak w wypadku
każdej zmiennej, od słowa kluczowego: delegate.
Następnie określamy sygnaturę delegata, która wygląda identycznie jak
definiowanie funkcji: podajemy typ zwracanych danych, nazwę i w nawiasach
argumenty. Przypisanie do delegata metody wygląda w następujący sposób:
Swapper<int>
swap = new Swapper<int>(fun);
Najpierw
deklarujemy delegata o nazwie swap. Jak
już wspomniałem, delegaci to typy referencyjne i dlatego posługujemy się tutaj
słówkiem new a jako argument
przekazujemy samą nazwę funkcji. Wywołanie funkcji poprzez delegata wygląda jak
zwyczajne wywołanie:
swap(ref zmiennaA, ref zmiennaB);
Przykład użycia: szybkie sortowanie
Listing poniżej zawiera deklarację
szybkiego sortowania jakie napisaliśmy wcześniej oraz z użyciem delegatów i
typów generycznych.
void
qSort(int[] arr, int
size);
delegate
int Comparer<T>(ref T a, ref T b);
delegate
void Swapper<T>(ref T a, ref T b);
void
qSort<T>(T[] array, int size, Comparer<T> compare, Swapper<T> swap);
Na pierwszy rzut oka może to wydawać się bardzo
skomplikowane ale po kolei. Po cóż nam delegaci? Podczas sortowania często
porównujemy i zamieniamy elementy - o ile w przypadku typów wbudowanych (np. int, double)
nie stanowi to problemu, to dla typów zdefiniowanych przez użytkownika
zaczynają się schody. Dlatego też będziemy potrzebować dwóch funkcji
pomocniczych: jednej która będzie odpowiedzialna za porównywanie ze sobą
elementów i drugiej, która zamieni je miejscami.