Covariant

Obiekt może przetrzymywać jakąkolwiek wartość bądź referencje do każdego typu.

Poniższy kod jest OK

 

 

int myNumber = 12;
string myString = "Moto-myszy z Marsa";
object myObject = myNumber;
object myObject2 = myString;

Pamiętaj, że klasa string dziedziczy po klasie Object, więc wszystkie klasy String są obiektami. Struktura nie może dziedziczyć po klasie, ale istnieją pewne wyjątki na poziomie wbudowanej kompilacji. W pewnym sensie struktura int dziedziczy po object, więc ona też może być umieszczona w object. Jednakże, w “pewnym sensie”nie jest to, tak do końca prawdą, gdyż int jest typem wartościowym i ta różnica później będzie istotna.

Tyle wstępu.

Aby powoli przejść do tematyki tego wpisu, stworzę interfejs generyczny i zaimplementuję go w klasie.

interface IInformer<T>
{
    void SetInfo(T info);
    T GetInfo();
}

class Informer<T> :IInformer<T>
{
    private T info;

    void IInformer<T>.SetInfo(T info)
    {
        this.info = info;
    }

    T IInformer<T>.GetInfo()
    {
        return this.info;
    }
}

 

Klasa Informer od T przechowuje jedną informację o nieokreślonym typie. Interfejs IInformer definiuje metody, które klasa Informer implementuje. Służą one do obsługiwania informacji, pobierania i zapisywania.

Mogę stworzyć instancje tej klasy i przechowywać w niej informacje o typie string.

Informer<string> stringInformer = new Informer<string>();
IInformer<string> storedStringInformer = stringInformer;
storedStringInformer.SetInfo("Wojownicze programistczne Żółwie");
Console.WriteLine("Informacja: {0}",storedStringInformer.GetInfo());

Kod tworzy instancje Informer<string>. Do obiektu referuje się poprzez interfejs IInformer<string> .Wywołuje metodę SetInfo().

Informer<T> ma zaimplementowany interfejs jawnie (explicitly), więc metody mogą być wywołane poprzez odpowiednią referencje do interfejsu.

Kod ten wywołuje także metodę GetInfo przez interfejs. Jeżeli uruchomisz ten kod, to w konsoli wyświetli się:

image

Teraz spójrz na poniższy kod. Jak myślisz, co można zrobić z tym wyrażeniem?

IInformer<object> storedObjectInformer = stringInformer;
IInformer<object> storedObjectInformer = stringInformer;

Wyrażenie to jest zbliżone do wyrażenia, które stworzyło IInformer od string. Różnicą jest typ parametru. Jest to object.

Czy ten kod jest poprawny?

Wszystkie napisy string są obiektami, więc w teorii to może działać.

Jednak tak nie jest. Próba napisania tego wyrażenie skończy się błędem kompilacji. Błąd informuje, że przy tej operacji wymagane jest rzutowanie jawne.

object interfejs

W takim razie wykonajmy rzutowanie, to powinno rozwiązać problem.

IInformer<object> storedObjectInformer = 
    (IInformer<object>)stringInformer;

Ten kod się skompiluje ,ale…

image

…wywoła on wyjątek “InvalidCastException”, w czasie działania programu. Problem polega na tym, że, pomimo iż wszystkie napisy string są obiektami w odwrotnym przypadku, to nie jest to prawdą. Nie wszystkie obiekty są napisami. Jeżeli takie wyrażenia byłoby dozwolone, to byłoby możliwe przechowywanie np. obiektu Cuboid (prostopadłościan) w polu int.

Informer<int> intInformer = new Informer<int>();
IInformer<object> storedObjectInformer = (IInformer<object>)intInformer;
Cuboid cu = new Cuboid(1, 2, 3);
storedObjectInformer.SetInfo(cu); // jQuery15206245398391038179_1340132466930?

Interfejs IInformer od T jest niezmienny. Nie możesz przypisać obiektu IInformer od X do referencji typu IInformer od Y, nawet jeśli typ X dziedziczy po typie Y.

C# domyślnie tego pilnuje, aby mieć pewność, że istnieje bezpieczeństwo typów w twoim kodzie.

Covariant Interfejs

Zdefiniuję teraz interfejsy “IStoreInformer” od T i “IRetrieveInformer” od T. W połączeniu oba interfejsy wykonują to samo zadanie, co IIformer. Implementacja tych interfejsów wygląda tak.

interface IStoreInformer<T>
{
    void SetInfo(T info);
}

interface IRetrieveInformer<T>
{
    T GetData();
}

class Informer<T> :IStoreInformer<T>,IRetrieveInformer<T>
{
    private T info;

    void IStoreInformer<T>.SetInfo(T info)
    {
        this.info = info;
    }

    T IRetrieveInformer<T>.GetData()
    {
        return this.info;
    }
}

Funkcjonalnie, klasa Informer jest taka sama jak wcześniej, poza tym, że dostęp do tych dwóch metod jest wykonywany przez dwa różne interfejsy.

Informer<string> stringInformer = new Informer<string>();
IStoreInformer<string> storeStringInformer = stringInformer;
storeStringInformer.SetInfo("121");
IRetrieveInformer<string> retriveStringInformer = stringInformer;
Console.WriteLine("Informacyjna liczba: {0}",
    retriveStringInformer.GetInfo());

Czy teraz poniższy kod nie wywoła błędu kompilacji?

IRetrieveInformer<object> retriveObjectInformer = stringInformer;

Nie. Kompilacja zawiedzie jak wcześniej.

Zauważ, że kompilator twierdzi, iż wyrażenie to nie jest bezpieczne typowo, ale powody tego błędu już nie występują. Interfejs IRetrieveInformer od T pozwala tylko na przeczytanie informacji poprzez metodę “GetInfo()” , ale interfejs nie ma żadnego sposobu na zmianę tej informacji. W tej sytuacji typ zwracany ma znaczenie tylko przy zwracanej wartości przez metodę, w tym generycznym interfejsie. Istnieje sposób na poinformowanie kompilatora, że pewne niejawne konwersje danych są legalne i nie jest tu wymagana dokładniejsza polityka bezpieczeństwa typów.

Aby to zrobić wystarczy przed deklaracją typu T użyć słowa kluczowego out, w ten sposób.

interface IRetrieveInformer<out T>
{
    T GetInfo();
}

Ta funkcjonalność nazwa się “covariance”. Teraz możesz przyrównać obiekt IRetrieveInformer od typu X do IRetrieveInformer od typu Y, tak długo, aż istnieje niejawna konwersja pomiędzy typem X i Y, bądź typ X jest pochodny od Y. Poniższy kod nie wywołuje już błędu.

// klasa string dziedziczy po object więc wszystko jest OK 
IRetrieveInformer<object> retriveObjectInformer = stringInformer;

Słowo kluczowe out działa na typ parametru T tylko wtedy, gdy typ parametru spełnia swój cel jako typ zwracany w metodzie. Jeżeli, użyjesz tego typu parametru do określenia typu jakiejkolwiek metody z parametrami, słowo kluczowe out będzie nielegalne i wywoła błąd w czasie kompilacji.

Kowariancja (Covariance) działa też tylko z typami referencyjnymi. Struktury czy typy wartościowe nie mają drzewa dziedziczenia. Dlatego poniższy kod się skompiluje.

Informer<int> intInformer = new Informer<int>();
IStoreInformer<int> storeIntInformer = intInformer;
storeIntInformer.SetInfo(121);
// OK 

IRetrieveInformer<object> retriveObjectInformer = intInformer;
//int nie są obiektami 

W .NET istnieją interfejsy, które obsługują kowariancje jest to np. jeden z ważniejszych interfejsów “IEnumerable od T” . Wypadałoby zrobić o nim wpis.

Interfejs Contravariant

Kontrawariancja jest następstwem kowariancji. Pozwala ona na użycie generycznego interfejsu, jako referencji do obiektu typu Y przez referencje do typu X , tak długo, jak typ Y wywodzi się od X. Brzmi to skomplikowanie dlatego też przyda się porządny przykład.

W przestrzeni nazw System.Collections.Generics istnieje interfejs IComparer, który wygląda tak:

ipublic interface IComparer<in T>
{
    int Compare(T x, T y);
}

Klasa, która implementuje ten interfejs definiuje także metodę Compare, która służy do porównywania dwóch obiektów o określonym typie T. Metoda Compare ma zwracać zero, gdy oba obiekty reprezentują tą samą wartość, negatywną liczbę, jeżeli x jest mniejszy od y i dodatnią liczbę, gdy x jest większe od y.


Poniższy kod pokazuje przykład implementacji tej metody wraz z użyciem metody GetHashCode().

iclass ObjComparer :IComparer<Object>
{
    int IComparer<object>.Compare(object x, object y)
    {
        int xHas = x.GetHashCode();
        int yHas = y.GetHashCode();

        if (xHas == yHas)
            return 0;

        if (xHas < yHas)
            return -1;

        return 1;
    }
}

Mogę stworzyć instancje tej klasy i wywołać wewnątrz metodę Compare przez interfejs IComparer<Object>.

iobject x = 12;
object y = 12;
ObjComparer comp = new ObjComparer();

IComparer<Object> objecCom = comp;
int res = objecCom.Compare(x, y);


ZamyślenieSpokojnie powoli przechodzę do kontrawariancji.

Bardzo interesujący jest fakt, że mogę użyć referencji do tego samego obiektu poprzez inną wersję interfejsu IComparer, która np. porównuje stringi.

iobject x = 12;
object y = 12;
ObjComparer comp = new ObjComparer();

IComparer<Object> objecCom = comp;

// co tutaj się dzieje?
IComparer<String> stringCom = objecCom;
int res = objecCom.Compare(x, y);

Na pierwszy rzut oka wydaje się, że to stwierdzenie wywoła błąd, w końcu nie łamie to bezpieczeństwa typu. Jednak jeśli pomyślisz o tym, co ten interfejs robi, ma to więcej sensu. Celem metody Compare jest zwrócenie wartości, bazując na operacji porównywaniu argumentów dodanych do metody.

Skoro mogę porównywać obiekty, to powinienem też porównywać napisy string, ponieważ wszystko, co string potrafić zrobić, obiekt również potrafi (magia dziedziczenia).

Wciąż brzmi to niewiarygodnie, skąd kompilator wie, że nie zostanie wywołany wyjątek w wyniku wywołania metody Compare() , gdy spróbujemy jej użyć przez interfejs o innym typie.

Odwiedzając ponownie definicję interfejsu Compare, można zobaczyć użycie słowa kluczowego in w deklaracji typu T.

ipublic interface IComparer<in T>
{
    int Compare(T x, T y);
}

Słowo kluczowe “in” mówi kompilatorowi, że możesz przesłać typ T jako parametry metody lub też przesłać jakikolwiek typ, który wywodzi się od T. Nie możesz tutaj użyć typu T, jako typu, który ma być zwrócony.
Umożliwia to odwoływanie się do obiektu poprzez referencje w generycznym interfejsie, który wywodzi się od innego typu. Czyli jeśli X potrafi wykonać dane operacje i ma właściwości, czy pola, wtedy typ Y, który wywodzi się od typu X też posiada te same operacje (chociaż jeśli są one nadpisane ich działanie jest inne), pola i właściwości. Konsekwentnie nie powinno być żadnego niebezpieczeństwa, w wyniku użycia substytutu obiektu typu Y dla obiektu typu X.

Kowariancja i Kontrawariancja mogą wydawać się dziwną tematyką w świecie generycznym, ale są użyteczne. Na przykład List od T jest generyczną kolekcją, która używa IComparer od T aby zaimplementować sortowanie i inne metody. Lista obiektów będzie zawierać kolekcje obiektów każdego typu, więc metody sortujące muszą zawierać logikę, która pozwoli na posortowanie każdego obiektu, po każdy typie.

Bez użycia kontrawariancji metody sortujące musiałby dodać logikę określającą prawdziwy typ obiektu, który jest sortowany, a potem zaimplementować specyficzny mechanizm sortujący dla tego typu. Brzmi jak długi ciąg if i else if.

Kontrawariancja i kowariancja na pewno ma swoje zastosowanie w metodach, w których można umieścić zgodnie z tymi zasadami dowolny interfejs od T.
Czym jest kowariancja i kontrawariancja?. Czy nie są to definicje matematyczne? Tak, są to definicje matematyczne, ale nie każdy programista pamięta, co one znaczą w matematyce. Definicja kowariancji i kontrawariancji, bazując na przykładach w tym wpisie, jest następująca.

Kowariancja– Jeżeli metody w generycznym interfejsie mogą zwracać string, to mogą zwracać też obiekty, ponieważ wszystkie napisy string są obiektami.

Kontrawariancja- Jeżeli metody w generycznym interfejsie pobierają parametry jako obiekty, to mogą też przyjmować napisy string jako parametry. Jeżeli wykonujesz operacje używając typu obiekt, możesz wykonać te same operacje na typie string, ponieważ wszystkie napisy string są obiektami.

Tylko interfejsy i delegaty mogą deklarować typ T konwariancyjnie i kontrawariancyjnie. Nie możesz użyć słów kluczowych in i out w klasach generycznych.