Вот простой пример использования иерархии наследования.
Учитывая простую иерархию классов:

И в коде:
public abstract class LifeForm { }
public abstract class Animal : LifeForm { }
public class Giraffe : Animal { }
public class Zebra : Animal { }
Инвариантность (т.е. параметры общего типа * не * украшены inилиout ключевыми словами)
По-видимому, такой метод, как этот
public static void PrintLifeForms(IList<LifeForm> lifeForms)
{
foreach (var lifeForm in lifeForms)
{
Console.WriteLine(lifeForm.GetType().ToString());
}
}
... должен принять гетерогенную коллекцию: (что он делает)
var myAnimals = new List<LifeForm>
{
new Giraffe(),
new Zebra()
};
PrintLifeForms(myAnimals); // Giraffe, Zebra
Однако передать коллекцию более производного типа не удается!
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>'
Зачем? Поскольку универсальный параметр IList<LifeForm>не является ковариантным -
IList<T>он инвариантен, поэтому IList<LifeForm>принимает только те коллекции (которые реализуют IList) там, где Tдолжен быть параметризованный тип LifeForm.
Если реализация метода PrintLifeFormsбыла вредоносной (но имеет такую же сигнатуру метода), причина, по которой компилятор предотвращает передачу, List<Giraffe>становится очевидной:
public static void PrintLifeForms(IList<LifeForm> lifeForms)
{
lifeForms.Add(new Zebra());
}
Так как IListдопускает добавление или удаление элементов, любой подкласс LifeFormможет, таким образом, быть добавлен к параметру lifeFormsи будет нарушать тип любой коллекции производных типов, передаваемых методу. (Здесь злонамеренный метод попытается добавить Zebraк var myGiraffes). К счастью, компилятор защищает нас от этой опасности.
Ковариантность (универсальный с параметризованным типом, украшенным out)
Ковариантность широко используется с неизменяемыми коллекциями (т. Е. Когда новые элементы не могут быть добавлены или удалены из коллекции)
Решение приведенного выше примера состоит в том, чтобы гарантировать использование ковариантного универсального типа коллекции, например IEnumerable(определяется как IEnumerable<out T>). IEnumerableне имеет методов для изменения коллекции, и в результате outковариации любая коллекция с подтипом LifeFormтеперь может быть передана методу:
public static void PrintLifeForms(IEnumerable<LifeForm> lifeForms)
{
foreach (var lifeForm in lifeForms)
{
Console.WriteLine(lifeForm.GetType().ToString());
}
}
PrintLifeFormsтеперь может быть вызван Zebras, Giraffesи любой IEnumerable<>из любого подклассаLifeForm
Contravariance (универсальный с параметризованным типом, украшенным in)
Контравариантность часто используется, когда функции передаются в качестве параметров.
Вот пример функции, которая принимает Action<Zebra>в качестве параметра и вызывает ее в известном экземпляре Zebra:
public void PerformZebraAction(Action<Zebra> zebraAction)
{
var zebra = new Zebra();
zebraAction(zebra);
}
Как и ожидалось, это работает просто отлично:
var myAction = new Action<Zebra>(z => Console.WriteLine("I'm a zebra"));
PerformZebraAction(myAction); // I'm a zebra
Интуитивно понятно, что это не удастся:
var myAction = new Action<Giraffe>(g => Console.WriteLine("I'm a giraffe"));
PerformZebraAction(myAction);
cannot convert from 'System.Action<Giraffe>' to 'System.Action<Zebra>'
Тем не менее, это удается
var myAction = new Action<Animal>(a => Console.WriteLine("I'm an animal"));
PerformZebraAction(myAction); // I'm an animal
и даже это тоже удается
var myAction = new Action<object>(a => Console.WriteLine("I'm an amoeba"));
PerformZebraAction(myAction); // I'm an amoeba
Зачем? Потому Actionчто определяется как Action<in T>, то есть contravariant, это означает, что для Action<Zebra> myAction, которое myActionможет быть не больше, чем «а» Action<Zebra>, но менее производные суперклассы Zebraтакже приемлемы.
Хотя вначале это может быть не интуитивно понятно (например, как можно Action<object>передать как параметр, требующий Action<Zebra>?), Если вы распакуете шаги, вы заметите, что вызываемая функция ( PerformZebraAction) сама отвечает за передачу данных (в данном случае это Zebraэкземпляр ) к функции - данные не поступают из вызывающего кода.
Из-за перевернутого подхода использования функций более высокого порядка таким образом, к тому времени, когда Actionвызывается, это более производный Zebraэкземпляр, который вызывается против zebraActionфункции (передаваемой как параметр), хотя сама функция использует менее производный тип.