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