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 inlub 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 Tmusi znajdować się sparametryzowany typ LifeForm.
Jeśli implementacja metody PrintLifeFormsbył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ż IListzezwala na dodawanie lub usuwanie elementów, każda podklasa klasy LifeFormmogłaby zostać dodana do parametru lifeFormsi naruszyłaby typ dowolnej kolekcji typów pochodnych przekazanych do metody. (W tym przypadku złośliwa metoda spróbuje dodać Zebrado 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>). IEnumerablenie ma metod zmiany kolekcji, a w wyniku outkowariancji dowolny zbiór z podtypem LifeFormmoże być teraz przekazany do metody:
public static void PrintLifeForms(IEnumerable<LifeForm> lifeForms)
{
foreach (var lifeForm in lifeForms)
{
Console.WriteLine(lifeForm.GetType().ToString());
}
}
PrintLifeFormsmożna teraz nazywa się Zebras, Giraffesa 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ż Actionjest zdefiniowane jako Action<in T>, tj. Jest contravariant, co oznacza, że dla Action<Zebra> myAction, które myActionmoże wynosić „co najwyżej” a Action<Zebra>, ale mniej pochodne nadklasy Zebrasą 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 Zebrainstancja ) 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 Actionwywołania funkcji jest ona bardziej pochodną Zebrainstancją, która jest wywoływana względem zebraActionfunkcji (przekazywana jako parametr), chociaż sama funkcja używa mniej pochodnego typu.