c # — Каково частое использование в программировании противоречий?

Я прочитал следующий пост о противоречии и ответе Лассе В. Карлсена:

Понимание ковариантного и контравариантного интерфейсов в C #

Хотя я понимаю концепцию, я не понимаю, почему она полезна.
Например, зачем кому-то составлять список только для чтения (как в посте: List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal> )

Я также знаю, что параметры переопределяющих методов могут быть противоречивыми (концептуально. Насколько я знаю, это не используется в C #, Java и C ++).
Какие есть примеры, в которых это имеет смысл?

Я был бы признателен за простые примеры из реальной жизни.

6

Решение

(Я думаю, что этот вопрос больше касается ковариации, а не контравариантности, поскольку приведенный пример касается ковариации.)

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.

2

Другие решения

Ковариантность полезна для репозиториев только для чтения; контравариантность для репозиториев только для записи.

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 ключевые слова для ковариации и контравариантности.

Что касается того, почему вы можете захотеть сделать это разделение (используя отдельные интерфейсы только для чтения и только для записи, которые, скорее всего, будут реализованы одним и тем же конкретным классом), это поможет вам поддерживать четкое разделение между командами (операциями записи) и запросами. (операции чтения), которые, вероятно, будут иметь различные требования в отношении производительности, согласованности и доступности, и, следовательно, потребуются разные стратегии в вашей кодовой базе.

1

Если вы на самом деле не понимаете концепцию контравариантности, то такие проблемы могут (и будут) возникать.

Давайте построим самый простой пример:

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 просто параметрический полиморфизм (и не более того) теперь применяется и к делегатам и интерфейсам. 🙂

0