Я прочитал следующий пост о противоречии и ответе Лассе В. Карлсена:
Понимание ковариантного и контравариантного интерфейсов в C #
Хотя я понимаю концепцию, я не понимаю, почему она полезна.
Например, зачем кому-то составлять список только для чтения (как в посте: List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal>
)
Я также знаю, что параметры переопределяющих методов могут быть противоречивыми (концептуально. Насколько я знаю, это не используется в C #, Java и C ++).
Какие есть примеры, в которых это имеет смысл?
Я был бы признателен за простые примеры из реальной жизни.
(Я думаю, что этот вопрос больше касается ковариации, а не контравариантности, поскольку приведенный пример касается ковариации.)
List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal>
Вне контекста это немного вводит в заблуждение. В приведенном вами примере автор намеревался передать идею, что технически, если List<Fish>
на самом деле ссылаясь на List<Animal>
Это было бы быть в безопасности, чтобы добавить Fish
к этому.
Но, конечно, это также позволит вам добавить Cow
к этому — что явно неправильно.
Поэтому компилятор не позволяет вам назначить List<Animal>
ссылка на List<Fish>
ссылка.
Так когда же это будет безопасно и полезно?
Это безопасное назначение, если коллекция не может быть изменена. В C #, IEnumerable<T>
может представлять неизменяемую коллекцию.
Так что вы можете сделать это безопасно:
IEnumerable<Animal> animals = GetAccessToFishes(); // for some reason, returns List<Animal>
потому что нет возможности добавить не-Рыбу в animals
, У него нет методов, позволяющих вам сделать это.
Так когда же это будет полезно?
Это полезно всякий раз, когда вы хотите получить доступ к некоторому общему методу или свойству коллекции, которые могут содержать элементы одного или нескольких типов, производных от базового класса.
Например, у вас может быть иерархия, представляющая различные категории акций для супермаркета.
Допустим, что базовый класс, StockItem
, имеет свойство double SalePrice
,
Скажем так, у вас есть метод, Shopper.Basket()
который возвращает IEnumerable<StockItem>
который представляет предметы, которые покупатель имеет в своей корзине. Предметы в корзине могут быть любого конкретного вида, полученного из StockItem
,
В этом случае вы можете добавить цены на все предметы в корзине (я написал это от руки без использования Linq, чтобы прояснить, что происходит. Реальный код будет использовать IEnumerable.Sum()
конечно):
IEnumerable<StockItem> itemsInBasket = shopper.Basket;
double totalCost = 0.0;
foreach (var item in itemsInBasket)
totalCost += item.SalePrice;
контрвариация
Пример использования контравариантности — когда вы хотите применить какое-либо действие к элементу или коллекции элементов через тип базового класса, даже если у вас есть производный тип.
Например, у вас может быть метод, который применяет действие к каждому элементу в последовательности StockItem
вот так:
void ApplyToStockItems(IEnumerable<StockItem> items, Action<StockItem> action)
{
foreach (var item in items)
action(item);
}
С использованием StockItem
Например, предположим, что у него есть Print()
метод, который вы можете использовать, чтобы распечатать его на кассовый чек. Вы можете позвонить, а затем использовать его так:
Action<StockItem> printItem = item => { item.Print(); }
ApplyToStockItems(shopper.Basket, printItem);
В этом примере типы товаров в корзине могут быть Fruit
, Electronics
, Clothing
и так далее. Но потому что все они происходят от StockItem
код работает со всеми из них.
Надеюсь, полезность такого рода кода понятна! Это очень похоже на то, как работают многие методы в Linq.
Ковариантность полезна для репозиториев только для чтения; контравариантность для репозиториев только для записи.
public interface IReadRepository<out TVehicle>
{
TVehicle GetItem(Guid id);
}
public interface IWriteRepository<in TVehicle>
{
void AddItem(TVehicle vehicle);
}
Таким образом, экземпляр IReadRepository<Car>
также является примером IReadRepository<Vehicle>
потому что, если вы достанете машину из хранилища, это тоже автомобиль; Тем не менее, случай IWriteRepository<Vehicle>
также является примером IWriteRepository<Car>
потому что, если вы можете добавить автомобиль в хранилище, вы можете записать автомобиль в хранилище.
Это объясняет причины out
а также in
ключевые слова для ковариации и контравариантности.
Что касается того, почему вы можете захотеть сделать это разделение (используя отдельные интерфейсы только для чтения и только для записи, которые, скорее всего, будут реализованы одним и тем же конкретным классом), это поможет вам поддерживать четкое разделение между командами (операциями записи) и запросами. (операции чтения), которые, вероятно, будут иметь различные требования в отношении производительности, согласованности и доступности, и, следовательно, потребуются разные стратегии в вашей кодовой базе.
Если вы на самом деле не понимаете концепцию контравариантности, то такие проблемы могут (и будут) возникать.
Давайте построим самый простой пример:
public class Human
{
virtual public void DisplayLanguage() { Console.WriteLine("I do speak a language"); }
}
public class Asian : Human
{
override public void DisplayLanguage() { Console.WriteLine("I speak chinesse"); }
}
public class European : Human
{
override public void DisplayLanguage() { Console.WriteLine("I speak romanian"); }
}
Хорошо, вот пример противоположной дисперсии
public class test
{
static void ContraMethod(Human h)
{
h.DisplayLanguage();
}
static void ContraForInterfaces(IEnumerable<Human> humans)
{
foreach (European euro in humans)
{
euro.DisplayLanguage();
}
}
static void Main()
{
European euro = new European();
test.ContraMethod(euro);
List<European> euroList = new List<European>() { new European(), new European(), new European() };
test.ContraForInterfaces(euroList);
}
}
До .Net 4.0 метод ContraForInterfaces не был разрешен (поэтому они фактически исправили ошибку :)).
Хорошо, теперь, что означает метод ContraForInterfaces?
Это просто, когда вы составили список европейцев, объекты внутри всегда ВСЕГДА европейцы, даже если вы передаете их методу, который принимает IEnumerable<>. Таким образом, вы всегда будете вызывать правильный метод для вашего объекта. Ковариация и ContraVariance просто параметрический полиморфизм (и не более того) теперь применяется и к делегатам и интерфейсам. 🙂