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