Kowariancja i kontrawariancja

Deti

Wstęp

Kowariancja (covariance) i kontrawariancja (contravariance) w języku C# (i prawdopodobnie innych językach) opisuje relacje klas. Aby wyjaśnić te hasła, użyję następujących klas:

class Organism { }
class Animal: Organism { }
class Cat: Animal { }

animalcat.png

W języku C# wartość zwracana z funkcji jest kowariancją. Oznacza to, że typem dla zwracanego obiektu może być zarówno typ tego obiektu jak i każdy obiekt bazowy.

static Animal GetAnimal() {
    return new Animal();
}

Animal animal=GetAnimal(); //ok
Organism organism=GetAnimal(); //ok
Cat cat=new Animal(); // błąd!

Zarówno pierwsze odwołanie jest prawidłowe, jak i drugie - ponieważ typ Animal dziedziczy po typie Organism. Zatem każda instancja klasy Animal jest też instancją klasy Organism. Trzecie odwołanie jest nieprawidłowe - zwrócony obiekt niekoniecznie musi być typem Cat.

Z drugiej strony, wszystkie parametry funkcji są kontrawariancją (relacją odwrotną do kowariancji). Innymi słowy, parametrem funkcji może być obiekt o typie takim samym jak argument funkcji lub każdym, który dziedziczy po nim.

static void Method(Animal animal) {
}

Method(new Animal()); // ok
Method(new Cat()); // ok
Method(new Organism()); //błąd!

Pierwsze odwołanie przekazuje typ, który jest identyczny z typem argumentu. Drugie odwołanie również jest dozwolone, ponieważ instancja klasy Cat jest też instancją klasy Animal. Jednak trzecie odwołanie jest zabronione - nie każdy Organism to Animal.

Typy generyczne i C# 4.0

Typy generyczne w wersji C#<4.0 miały jedną wadę - nie używały ani kowariancji ani kontrawariancji.

interface IInvariant<T> {
    T Get();
}

class Tester<T>:IInvariant<T> {
    public T Get() {
        return default(T);
    }
}

Mamy do czynienia ze zwykłym interfejsem generycznym, i klasą która dziedziczy po nim. Rozpatrzmy taki oto kod:

IInvariant<Animal> animalTester=new Tester<Animal>();
IInvariant<Organism> organismTester=animalTester; //błąd!

Kompilator nie pozwala na taki kod zgłaszając niezgodność typów. Uparty programista jednak twierdzi, że z typami jest wszystko w porządku ponieważ każdy Animal jest też instancją Organism. I ma rację, jednak zapomina, że relacja ta (kowariancja) jest możliwa tylko z danymi wyjściowymi, a kompilator nie wie jak używamy typu generycznego.

Nowością w C# 4.0 jest możliwość powiadomienia kompilatora czy chcemy użyć relacji kowariancji czy kontrawariancji (za pomocą słów kluczowych in i out).

Ważne! Pomimo, że występuje tu słowo kluczowe out, jest to kompletnie inne zastosowanie niż wyjściowy parametr funkcji. Programiści C# stwierdzili, że nie ma sensu dodawać nowego słowa kluczowego, skoro można użyć już istniejący.

Zmieńmy zatem kod wg specyfikacji C# 4.0:

interface ICovariance<out T> {
    T Get();
}

class Tester<T>:ICovariance<T> {
    public T Get() {
        return default(T);
    }
}

.. oraz kod testowy:

ICovariance<Animal> animalTester=new Tester<Animal>();
ICovariance<Organism> organismTester=animalTester;

Rezultat: "Build succeeded"!.

Kontrawariancja analogicznie:

interface IContravariance<in T> {
    void Set(T obj);
}

class Tester<T>:IContravariance<T> {
    public void Set(T obj) {
    }
}

....

IContravariance<Animal> animalTester=new Tester<Animal>(); //ok
IContravariance<Cat> catTester=animalTester; //ok

3 komentarzy

@Deti czy to co pisze @neves to prawda?

Chciałbym zauważyć, że wstęp jest błędny, zarówno parametry zwracane jak i przesyłane do metody są kowariancyjne, bo do ogólnego przypisujemy szczegółowy.
Regułka że wartość zwracana jest kowariancją, a parametry kontrawariancją dotyczy przypisywania metod do delegatów (Func<in T,out TResult>), a nie bezpośredniego wywoływania tychże metod!