Вот простой пример использования иерархии наследования.
Учитывая простую иерархию классов:
И в коде:
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
функции (передаваемой как параметр), хотя сама функция использует менее производный тип.