GenericsCzęść NR.17

Generics, Typy generyczne referują się do definicji metody, klasy i interfejsów, które operują zależnie od tego, co podasz jako parametr generyczny.

Typy generyczne mają wiele zalet jedną z nich jest wykrywanie ich działania już na poziomie pisania kodu. Dzięki nim też możemy uniknąć konwersji typów.

Generyczne klasy

Generyczne klasy pozwalają elementom klasy użyć nieokreślonych na tym etapie parametrów typu.

Aby utworzyć generyczną klasę w Javie i C# trzeba po nazwie klasy dopisać nazwę parametru generycznego typu pomiędzy nawiasami.

Konwencja nazewnicza mówi by ten parametr zawierał tylko 1 pojedynczą literę pisaną z dużej litery.

Standardowo litera “T” jest używana. Poniżej znajduje się przykład utworzenia generycznej klasy.

class JavaMagicBox<T> { public T box; }

Tworząc instancje tej klasy musimy pomiędzy nawiasami określić konkretny typ. W tym wypadku jest to typ Integer.

JavaMagicBox<Integer> iBox = new JavaMagicBox<Integer>();

Klasa generyczna pozwala nam utworzyć wiele różnych klas w zależności co umieścimy jako parametr T.

JavaMagicBox<Integer> jBox = new JavaMagicBox<Integer>();
JavaMagicBox<String> jBox3 = new JavaMagicBox<String>();
JavaMagicBox<Double> jBox4 = new JavaMagicBox<Double>();

W Javie 7 dalsza deklaracja typu generycznego nie wymaga podawania parametrów generycznych pod warunkiem, że kompilator potrafi sam się domyślić o jaki typ chodzi.

JavaMagicBox<Integer> jBox2 = new JavaMagicBox<>();

Kiedy instancja pudełka została utworzona możemy do niej umieścić zmienną pod warunkiem, że jej typ jest zgodny z tym typem, który podaliśmy, gdy tworzyliśmy instancje tej klasy.

jBox miał parametr generyczny Integer, co oznacza, że pole też jest typu Integer, a to pozwala mi umieścić liczbę całkowitą do tego pola.

jBox.box = 5;
Integer aa = jBox.box;

Tworzenie klasy generycznej w C# jest identyczne. Nawet zasady nazewnictwa parametrów generycznych są takie same.

public class CSharpMagicBox<T>
{
    public T box;
}

Pole box będzie miało typ zgodny z parametrem generycznym.

CSharpMagicBox<int> mbox = new CSharpMagicBox<int>();
CSharpMagicBox<string> mbox2 = new CSharpMagicBox<string>();

mbox.box = 5;
mbox2.box = "5";

Generyczne metody

Metoda też może być generyczną.

W przykładzie poniżej znajduje się metoda, która łączy napisy dwóch parametrów.

static string JoinString(string a, string b)
{
    return a + b;
}

Możemy tę metodę przerobić na metodę generyczną, oznacza to, że będzie ona mogła przyjąć parametry o dowolnym typie określonym wcześniej w nawisach,

static string JoinString<T>(T a, T b)
{
    return a.ToString() + b.ToString();
}

Metoda też może zwracać typ generyczny.

static T Return<T>(T a, T b)
{
    if (a.GetHashCode() > b.GetHashCode())
        return a;
    else
        return b;
}

W Javie deklaracja metody generycznej różni się trochę od tej C#. Parametr generyczny jest deklarowany przed typem zwracanym przez metodę.

public static <T> String JoinString(T a, T b)
{
    return a.toString() + b.toString();
}

Klasa może zawierać metody generyczne, niezależnie od tego czy sama klasa, interfejs jest generyczna.

Oto kolejny przykład metody generycznej, która wydrukuje wszystkie elementy tablicy typu T.

public static <T> void printOutArray(T[] array)
{
    for (T element : array)
    System.out.print(element + " ");
}

Analogiczna metoda napisana w C#.

public static void PrintOutArray<T>(T[] array)
{
    for (int i = 0; i <  array.Length; i++)
    {
        Console.WriteLine(array[i]);
    }
}

Jeśli metoda generyczna znajduje się w klasie generycznej i planujemy użyć tą samą zmienną genryczną, to nie musimy jej deklarować w metodzie.

image

Te twierdzenie w C# jest prawdziwe nie zależnie czy metoda jest statyczna czy nie.

public class CSharpMagicBox<T>
{
    public T box;

    public static void printOutArray(T[] array)
    {
        for (int i = 0; i <  array.Length; i++)
        {
            Console.WriteLine(array[i]);
        }
    }
}

W Javie metody statyczne, nawet jeśli są w klasie generycznej muszą mieć deklarację parametru generycznego w składni metody.

image

Jeśli metoda nie jest statyczna takiego wymogu nie ma.

class JavaMagicBox<T> 
{ 
    public T box; 

    public void printOutArray(T[] array)
    {
        for (T element : array)
        System.out.print(element + " ");
    }
}

Wywołanie generyczne metody

public class CSharNotGenericpMagicBox
{
    public static void printOutArray<T>(T[] array)
    {
        for (int i = 0; i <  array.Length; i++)
        {
            Console.WriteLine(array[i]);
        }
    }
}

class JavaNotGenericMagicBox 
{ 
    public  static <T> void printOutArray(T[] array)
    {
        for (T element : array)
        System.out.print(element + " ");
    }
}

W Javie i w C# generyczna metoda jest wywoływana normalnie jak zwykła metoda, bez podawania argumentu typu.

Integer[] cArray = { 1, 2, 3 };
JavaNotGenericMagicBox.printOutArray(cArray);

int[] carray = new []{ 1, 2, 3, 4 };
CSharpNotGenericMagicBox.printOutArray(carray);

W większości przypadków kompilator Javy i C# potrafi domyśleć się, jaki typ jest potrzebny w wywołaniu, więc nie wymagana jest jego jawna deklaracja. Istnieją jednak przypadki, gdy taką operację trzeba wykonać, a wygląda to tak:

Integer[] cArray = { 1, 2, 3 };
JavaNotGenericMagicBox.<Integer>printOutArray(cArray);

int[] carray = new []{ 1, 2, 3, 4 };
CSharpNotGenericMagicBox.printOutArray<int>(carray);

Ten przypadek dotyczy jednak metod generycznych nie znajdujących się w klasach generycznych,

Jeśli niestatyczna metoda w Javie i w C# jest częścią klasy generycznej nie musimy deklarować typu przy wywołaniu metody, gdyż ten został już ustalony przy tworzeniu klasy.

Integer[] cArray = { 1, 2, 3 };
JavaMagicBox<Integer> cBox = new JavaMagicBox<Integer>();
cBox.printOutArray(cArray);
int[] carray = new []{ 1, 2, 3, 4 };
CSharpMagicBox<int> c = new CSharpMagicBox<int>();
c.printOutArray(carray);

W statycznych metodach w Javie jest identycznie - bez względu na to czy klasa jest generyczna czy nie -nie trzeba pisać na poziomie klasy definicji typu.

Jest on definiowany na poziomie metody, a tam kompilator domyśla się jaką wartość umieścić.

JavaMagicBox<Integer> cBox = new JavaMagicBox<Integer>();
JavaMagicBox.printOutArray(cArray);

Jeśli klasa jest generyczna w statycznych metodach w C# typ musi być określany na poziomie nazwy klasy. W przeciwnym wypadku definicja jest na poziomie deklaracji wywołania metody, a tam kompilator potrafi się domyśleć jaką wartość ma wstawić.

int[] carray = new []{ 1, 2, 3, 4 };
CSharpMagicBox<int>.printOutArray(carray);

Dlatego tak jest tylko w C#. Wynika to z mechanizmu kompilowania kodu. Statyczne klasy w zależności od typu generycznego utworzą w bibliotece oddzielne i różne klasy statyczne.

Przykładowo statyczna klasa CSharpMagicBox<int> i CSharpMagicBox<string> utworzą dwie różne klasy statyczne podczas kompilacji w DLL będą one określone jako klasy statyczne CSharpMagicBoxWithInt i CSharpMagicBoxWithString.

W Javie ten mechanizm jest zupełnie inny. Na końcu wpisu podaję więcej szczegółów.

Generyczne interfejsy

Generyczne interfejsy są deklarowane podobnie jak generyczne klasy. Generyczne interfejsy spełniają te same dwa cele, co zwykłe interfejsy.

Gdy generyczny interfejs jest implementowany klasa może, albo podobnie jak interfejs stać się klasą generyczną, albo uzupełnić typ generyczny konkretnym typem.

interface MyGenericStorage<T>
{
    public void store(T t);
}

//Nie generyczna klasa implementuje generyczny interfejs
class IntegerStorage implements MyGenericStorage<Integer>
{
    public Integer number;
    public void store(Integer i) { number = i; }
}

//generyczna klasa implementuje generyczny interfejs
class GenericStorage<T> implements MyGenericStorage<T>
{
    public T something;
    public void store(T t) { something = t; }
}
interface IBase { }
interface IBaseGeneric<T> { }
class Gen1<T> : IBase { } 
class Gen2<T> : IBaseGeneric<int> { }
class Gen3<T> : IBaseGeneric<T> { } 

Klasa implementująca generyczny interfejs nie musi być klasą generyczną.

Generyczne parametry typu

Możemy mieć więcej parametrów generycznych.

W Javie typem generycznym nie mogą być typy proste. Dlatego we wcześniejszych przykładach używałem typu Integer, a nie int.

Stosujemy przecinek by dodać kolejne parametry. Każdy parametr musi być unikatowy w swojej nazwie.

class JavaClass<T, U> {}

Przy deklaracji klasy musimy uzupełnić wszystkie parametry.

JavaClass<Integer, Float> javaClass = new JavaClass<Integer,Float>();

Metody też mogą mieć wiele parametrów typów generycznych.

static void Method1<T1, T2>() {}
static void Method2<T1>() {}

Dziedziczenie po klasach generycznych

Analogicznie do interfejsów.

class Base { }
class BaseGeneric<T> { }
class Gen1<T> : Base { } 
class Gen2<T> : BaseGeneric<int> { }
class Gen3<T> : BaseGeneric<T> { } 

Wartość domyślna T : Tylko c#

C# oferuje słowo kluczowe “default”, które jest przydatne przy typach generycznych. Zwróci on wartość domyślną określonego typu.

Rozwiązuje to problem z przypisaniem domyślnej wartości do typu, gdy na tym etapie nie wiemy co to jest.

static void ResetValue<T>(ref T a)
{
    a = default(T);
}

Java nie posiada słowa kluczowego default.

Użycie zmiennych generycznych i różnice kompilacji Javy i C#

Istnieją pewne ograniczenia co do tego, jak zmienne generyczne mogą być używane.

Przykładowo w Javie nie możemy sprawdzić jakiej instancji jest typ generyczny.

class JClass<T>
{
    public void jMethod(Object o)
    {
        T t1; // allowed
        t1 = null; // allowed
        System.out.print(t1.toString()); // allowed
        if (o instanceof T) {} // invalid
        T t2 = new T(); // invalid
    }
}

Kompilator Javy konstruuje na poziomie kompilacji typ generyczny. Po kompilacji sprawdza, czy wszystkie typy wewnątrz zmiennych się zgadzają. Jeśli tak jest, kasuje on wszystkie informacje na temat parametrów i umieszcza odpowiedzenie rzutowanie.

Oznacza to, że kod generyczny Javy nie daje żadnych przyspieszeń, zalet w kodzie od kodu niegenerycznego opartego na rzutowaniach obiektów.

Co tłumaczy dlaczego słowo kluczowe instance of nie działa bo w trakcie działania aplikacji informacje o typie są tracone.

One istnieją tylko w procesie kompilacji.

Kod instance of chce sprawdzić czy coś jest zgodne z typem T, ale na poziomie działania programu typ T będzie obiektem.

Można ten problem rozwiązać, ale różnice w kompilacji są.

Class<T> type; 

if ( type.isInstance(obj) ) {
   T t = type.cast(obj);
   // ...
}

Dla Javy typ generyczny w trakcie działania aplikacji jest więc obiektem, który jest rzutowany w kluczowych sytuacjach na określony przez nas typ.

image

Brzmi to strasznie głupio bo w teorii oznacza to, że powinienem wrzucić typ inny niż Integer do tej generycznej tablicy i jedyne, co mnie powstrzymuje to kompilator. Pod koniec jednak i tak to będzie kolekcja obiektów rzutowanych niejawnie na Integer w pewnych kluczowych momentach.

Tłumaczy to też dlaczego typy generyczne w Javie nie mogą przechowywać typów prostych.

W C# sprawy się trochę różnią.

class CClass<T>
{
    public void XMethod(Object o)
    {
        T t1; // allowed

        t1 = null; // invalid
        t1 = default(T) ;// approved

        Console.WriteLine(t1.ToString()); // allowed

        if (o is T) { } // allowed
        bool result = o.GetType().IsAssignableFrom(t1.GetType()); // allowed

        T t2 = new T(); // invalid without constrain
    }
}

Kompilator w .NET tworzy oddzielną klasę na podstawie naszego szablonu klasy generycznej.

Jeśli więc utworzymy List<Person> i List<Game> to kompilator w plikach DLL tak naprawdę tworzy dla nich oddzielne klasy jak ListOfPerson i ListOfGame.

Zaletą tego jest szybkość. Nie ma rzutowania i ponieważ informacje o typie są w DLL można przy pomocy odpowiednich metod sprawdzić, jaki typ generyczny jest używany wewnątrz klasy i poza nią.

Jedyna wada takiego rozwiązania polega na tym, że C# 1.0 nie miał typów generycznych, więc pewne kolekcje z tego okresu nie rozumieją pewnych poleceń generycznych. Zdarzy się jednak to tylko wtedy, gdy będziesz migrował kod z C# 1.0 do C# 2.0

Do pola typu generycznego nie można przypisać wartości null. Wynika to z tego, że w C# typy generyczne potrafią przechowywać wszystkie typy, także te niereferencyjne.

Na szczęście na pomoc przychodzi słowo kluczowe default.

Warto też zaznaczyć, ze niektóre problemy mogą być rozwiązane za pomocą mechanizmów ograniczeń typów generycznych constrain w C#...o tym w następnym wpisie z tego cyklu.