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 { }
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
@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!
ładnie