Programowanie w języku C#

Kowariancja i kontrawariancja

  • 0 komentarzy
  • 10770 odsłon
  • Oceń ten tekst jako pierwszy

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