Typy generyczne

Typy generyczne pozwalają na opóźnienie w dostarczeniu specyfikacji typu danych w elementach takich jak klasy czy metody do momentu użycia ich w trakcie wykonywania programu. Innymi słowy, typy generyczne pozwalają na napisanie klasy lub metody, która może działać z każdym typem danych.

Używając typów generycznych można użyć zastępczego parametru określającego typ danych. Gdy kompilator napotka konstruktora klasy czy wywołanie metody generuje kod do obsługi konkretnego typu danych. Zagadnienie to łatwiej będzie zrozumieć na poniższym przykładzie. Zanim jednak do tego przejdzie, proszę o chwilę uwagi. W generycznej klasie MyGenericArray specjalnie „zdublowałem” deklaracje pól i metod tak, abyście mieli porównanie jak wygląda typowanie oraz generyczna definicja:

using System;
namespace Generics
{
    // Definicja klasy generycznej, w tym przypadku własnej tablicy
    class MyGenericArray<T>
    {
        // definicja typowanej tablicy
        private int[] array;
        // definicja generycznej tablicy
        private T[] genericArray;
        // konstruktor klasy przyjmujący jako parametr rozmiar tablicy
        public MyGenericArray(int size)
        {
            // ustalenie rozmiaru zwykłej tablicy
            array = new int[size + 1];
            // ustalenie rozmiaru tablicy generycznej
            genericArray = new T[size + 1];
        }
        public int getItem(int index)
        {
            return array[index];
        }
        // Powyżej metoda zwracająca typ danych jako int
        // Poniżej metoda pozwalająca na zwrócenie dowolnego typu danych
        public T getGenericItem(int index)
        {
            return genericArray[index];
        }
        public void setValue(int index, int value)
        {
            array[index] = value;
        }
        // Powyżej metoda ustawiająca dane typu całkowitego (int)
        // Poniżej metoda pozwalająca na ustawienie dowolnego typu danych
        public void setGenericValue(int index, T value)
        {
            genericArray[index] = value;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            // Utworzenie tablicy liczb całkowitych oraz jej wypełnienie
            MyGenericArray<int> intArray = new MyGenericArray<int>(5);
            for (int i = 0; i < 5; i++)
            {
                intArray.setGenericValue(i, i * 3);
            }
            // Wypisanie wszystkich danych
            for (int i = 0; i < 5; i++)
            {
                Console.WriteLine("Liczba: {0}", intArray.getGenericItem(i));
            }
            // Używając tej samej generycznej klasy jesteśmy w stanie zadeklarować innym typ danych
            MyGenericArray<char> charArray = new MyGenericArray<char>(5);
            for (int i = 0; i < 5; i++)
            {
                charArray.setGenericValue(i, (char)(i + 97));
            }
            // Wypisanie wszystkich danych
            for (int i = 0; i < 5; i++)
            {
                Console.WriteLine(charArray.getGenericItem(i));
            }
            Console.ReadKey();
            // Wynik działania programu
            // Liczba: 0
            // Liczba: 3
            // Liczba: 6
            // Liczba: 9
            // Liczba: 12
            // a
            // b
            // c
            // d
            // e
        }
    }
}

Cechy typów generycznych

Technika pisania generycznych programów pozwala nam na:

  • pisanie kodu, który może być używany w przyszłości, wpływa na bezpieczeństwo typów oraz wydajność;
  • tworzenie generycznych kolekcji. Platforma .NET zawiera sama w sobie kilka generycznych kolekcji dostępnych w przestrzeni nazw System.Collections.Generic. Możesz używać generycznych kolekcji zamiast kolekcji z przestrzeni nazw System.Collections;
  • tworzenie własnych generycznych interfejsów, klas, metod, zdarzeń oraz delegatów;
  • tworzenie generycznych klas, które pozwalają na dostęp do metod dla poszczególnych typów danych;
  • pobranie informacji dotyczących używanych typów w trakcie wykonywania programu – za pomocą mechanizmu refleksji.

Metody generyczne

W poprzednim przykładzie używaliśmy klas generycznych. Możemy również zadeklarować metody generyczne. Poniższy przykład pokazuje koncepcje używania metod generycznych:

using System;
namespace GenericMethod
{
    class Program
    {
        // Generyczna metoda to zamiany kolejności parametrów
        static void Swap<T>(ref T a, ref T b)
        {
            T temp;
            temp = a;
            a = b;
            b = temp;
        }
        static void Main(string[] args)
        {
            int a, b;
            char c, d;
            a = 10;
            b = 20;
            c = 'A';
            d = 'B';
            // Wartości przed zamianą
            Console.WriteLine("a = {0}, b = {1}", a, b);
            Swap(ref a, ref b);
            // Wartości po zmianie
            Console.WriteLine("a = {0}, b = {1}", a, b);
            // Sprawdzmy teraz innych typ danych
            // Wartości przed zamianą
            Console.WriteLine("a = {0}, b = {1}", c, d);
            Swap(ref c, ref d);
            // Wartości po zmianie
            Console.WriteLine("a = {0}, b = {1}", c, d);
            Console.ReadKey();
            // Wynik działania programu
            // a = 10, b = 20
            // a = 20, b = 10
            // a = A, b = B
            // a = B, b = A
        }
    }
}

Delegaty generyczne

Możemy również przygotować generycznego delegata, który będzie akceptował różne typy danych. Poniżej przykład wraz ze szczegółowym opisem:

using System;
namespace GenericDelegates
{
    // Definicja delegata
    delegate int NumberChangeNormalDef(int i);
    // Definicja generycznego delegate
    delegate T NumberChange<T>(T i);
    // Statyczna klasa testowa z metodami do dodawania, mnożenia oraz wracania liczby
    static class Test
    {
        static int num = 10;
        public static int AddNumber(int a)
        {
            num += a;
            return num;
        }
        public static int MultiplyNumber(int m)
        {
            num *= m;
            return num;
        }
        public static int GetNumber()
        {
            return num;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            // Definicja delegata - dla porównania
            NumberChangeNormalDef normal = new NumberChangeNormalDef(Test.AddNumber);
            // Deklaracja instancji generycznych delegatów
            NumberChange<int> d1 = new NumberChange<int>(Test.AddNumber);
            NumberChange<int> d2 = new NumberChange<int>(Test.MultiplyNumber);
            // Wywołanie metod używając obiektu delegata
            d1(5);
            Console.WriteLine("Liczba: {0}", Test.GetNumber());
            d2(10);
            Console.WriteLine("Liczba: {0}", Test.GetNumber());
            Console.ReadKey();
            // Wynik działania programu
            // Liczba: 15
            // Liczba: 150
        }
    }
}