Oto prosty przykład wykorzystujący hierarchię dziedziczenia.
Biorąc pod uwagę prostą hierarchię klas:
A w kodzie:
public abstract class LifeForm { }
public abstract class Animal : LifeForm { }
public class Giraffe : Animal { }
public class Zebra : Animal { }
Niezmienność (tj. Parametry typu ogólnego * nie * ozdobione słowami kluczowymi in
lub out
)
Pozornie metoda taka jak ta
public static void PrintLifeForms(IList<LifeForm> lifeForms)
{
foreach (var lifeForm in lifeForms)
{
Console.WriteLine(lifeForm.GetType().ToString());
}
}
... powinien akceptować zbiór heterogeniczny: (co robi)
var myAnimals = new List<LifeForm>
{
new Giraffe(),
new Zebra()
};
PrintLifeForms(myAnimals); // Giraffe, Zebra
Jednak przekazanie kolekcji bardziej pochodnego typu kończy się niepowodzeniem!
var myGiraffes = new List<Giraffe>
{
new Giraffe(), // "Jerry"
new Giraffe() // "Melman"
};
PrintLifeForms(myGiraffes); // Compile Error!
cannot convert from 'System.Collections.Generic.List<Giraffe>' to 'System.Collections.Generic.IList<LifeForm>'
Czemu? Ponieważ parametr ogólny IList<LifeForm>
nie jest kowariantny -
IList<T>
jest niezmienny, więc IList<LifeForm>
akceptuje tylko kolekcje (które implementują IList), w których T
musi znajdować się sparametryzowany typ LifeForm
.
Jeśli implementacja metody PrintLifeForms
była złośliwa (ale ma tę samą sygnaturę metody), powód, dla którego kompilator uniemożliwia przekazanie, List<Giraffe>
staje się oczywisty:
public static void PrintLifeForms(IList<LifeForm> lifeForms)
{
lifeForms.Add(new Zebra());
}
Ponieważ IList
zezwala na dodawanie lub usuwanie elementów, każda podklasa klasy LifeForm
mogłaby zostać dodana do parametru lifeForms
i naruszyłaby typ dowolnej kolekcji typów pochodnych przekazanych do metody. (W tym przypadku złośliwa metoda spróbuje dodać Zebra
do var myGiraffes
). Na szczęście kompilator chroni nas przed tym niebezpieczeństwem.
Kowariancja (ogólna z typem sparametryzowanym ozdobiona out
)
Kowariancja jest szeroko stosowana w niezmiennych kolekcjach (tj. Gdy nie można dodawać ani usuwać nowych elementów z kolekcji)
Rozwiązaniem powyższego przykładu jest zapewnienie, że używany jest kowariantny ogólny typ kolekcji, np. IEnumerable
(Zdefiniowany jako IEnumerable<out T>
). IEnumerable
nie ma metod zmiany kolekcji, a w wyniku out
kowariancji dowolny zbiór z podtypem LifeForm
może być teraz przekazany do metody:
public static void PrintLifeForms(IEnumerable<LifeForm> lifeForms)
{
foreach (var lifeForm in lifeForms)
{
Console.WriteLine(lifeForm.GetType().ToString());
}
}
PrintLifeForms
można teraz nazywa się Zebras
, Giraffes
a każdy IEnumerable<>
z dowolnej podklasyLifeForm
Kontrawariancja (ogólna z parametryzowanym typem ozdobiona in
)
Kontrawariancja jest często używana, gdy funkcje są przekazywane jako parametry.
Oto przykład funkcji, która przyjmuje Action<Zebra>
parametr jako parametr i wywołuje go na znanym wystąpieniu Zebry:
public void PerformZebraAction(Action<Zebra> zebraAction)
{
var zebra = new Zebra();
zebraAction(zebra);
}
Zgodnie z oczekiwaniami działa to dobrze:
var myAction = new Action<Zebra>(z => Console.WriteLine("I'm a zebra"));
PerformZebraAction(myAction); // I'm a zebra
Intuicyjnie to się nie powiedzie:
var myAction = new Action<Giraffe>(g => Console.WriteLine("I'm a giraffe"));
PerformZebraAction(myAction);
cannot convert from 'System.Action<Giraffe>' to 'System.Action<Zebra>'
Jednak to się udaje
var myAction = new Action<Animal>(a => Console.WriteLine("I'm an animal"));
PerformZebraAction(myAction); // I'm an animal
i nawet to się udaje:
var myAction = new Action<object>(a => Console.WriteLine("I'm an amoeba"));
PerformZebraAction(myAction); // I'm an amoeba
Czemu? Ponieważ Action
jest zdefiniowane jako Action<in T>
, tj. Jest contravariant
, co oznacza, że dla Action<Zebra> myAction
, które myAction
może wynosić „co najwyżej” a Action<Zebra>
, ale mniej pochodne nadklasy Zebra
są również dopuszczalne.
Chociaż na początku może to być nieintuicyjne (np. Jak można Action<object>
przekazać parametr jako wymagający Action<Zebra>
?), Jeśli rozpakujesz kroki, zauważysz, że sama wywoływana funkcja ( PerformZebraAction
) jest odpowiedzialna za przekazywanie danych (w tym przypadku Zebra
instancja ) do funkcji - dane nie pochodzą z kodu wywołującego.
Ze względu na odwrotne podejście polegające na używaniu funkcji wyższego rzędu w ten sposób, w momencie Action
wywołania funkcji jest ona bardziej pochodną Zebra
instancją, która jest wywoływana względem zebraAction
funkcji (przekazywana jako parametr), chociaż sama funkcja używa mniej pochodnego typu.