niedziela, 16 grudnia 2012

Generyczność i delegaci


            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. 

Brak komentarzy:

Prześlij komentarz