Понимание Ковариантных и контравариантных интерфейсов в языке Си#


Я столкнулся с ними в учебнике, который читаю на C#, но мне трудно понять их, вероятно, из-за отсутствия контекста.

Есть ли хорошее краткое объяснение того, что они собой представляют и для чего они полезны?

Изменить для уточнения:

Ковариантный интерфейс:

interface IBibble<out T>
.
.

Контравариантный интерфейс:

interface IBibble<in T>
.
.
2 77

2 ответа:

С помощью <out T> можно рассматривать ссылку на интерфейс как одну из вершин иерархии.

С помощью <in T>, вы можете рассматривать ссылку на интерфейс как один вниз в hiearchy.

Позвольте мне попытаться объяснить это более английскими терминами. Предположим, вы извлекаете список животных из своего зоопарка и собираетесь обработать их. Все животные (в вашем зоопарке)имеют имя и уникальный идентификатор. Некоторые животные-млекопитающие, некоторые-рептилии, некоторые-амфибии, некоторые-рыбы, и т.д. но все они-животные. Итак, с вашим списком животных (который содержит животных разных типов), вы можете сказать, что все животные имеют имя, поэтому, очевидно, было бы безопасно получить имя всех животных. Однако, что делать, если у вас есть список только рыб, но нужно относиться к ним как к животным, это работает? Интуитивно это должно работать, но в C# 3.0 и раньше этот фрагмент кода не будет компилироваться:
IEnumerable<Animal> animals = GetFishes(); // returns IEnumerable<Fish>

Причина этого в том, что компилятор не "знает", что вы намерены или можете сделать с коллекцией животных после того, как вы ее восстановите. Для всего, что он знает, может быть способ через IEnumerable<T> поместить объект обратно в список, и это потенциально позволит вам поместить животное, которое не является рыбой, в коллекцию, которая должна содержать только рыбу.

Другими словами, компилятор не может гарантировать, что это не разрешено:
animals.Add(new Mammal("Zebra"));
Поэтому компилятор просто напросто отказывается компилировать ваш код. Это и есть ковариация. Давайте посмотрим на контравариантность. Поскольку наш зоопарк может обрабатывать всех животных, он, безусловно, может обрабатывать рыбу, поэтому давайте попробуем добавить немного рыбы в наш зоопарк.

В C# 3.0 и ранее это не компилируется:

List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal>
fishes.Add(new Fish("Guppy"));

Здесь компилятор может разрешить этот фрагмент кода, хотя метод возвращает List<Animal> просто потому, что все рыбы-животные, поэтому, если мы просто изменим типы на это:

List<Animal> fishes = GetAccessToFishes();
fishes.Add(new Fish("Guppy"));

Тогда это сработало бы, но компилятор не может определить, что вы не пытаетесь сделать это:

List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal>
Fish firstFist = fishes[0];
Так как список на самом деле является списком животных, это не допускается. Таким образом, контра - и ко-дисперсия-это то, как вы относитесь к ссылкам на объекты и что вам разрешено с ними делать.

Ключевые слова in и out В C# 4.0 специально помечают интерфейс как один или другой. С помощью in, вы можете поместить универсальный тип (обычно T) в input - позиции, что означает метод Аргументы и свойства только для записи.

С помощью out можно поместить универсальный тип в output-positions, то есть возвращаемые значения метода, свойства только для чтения и параметры метода out.

Это позволит вам сделать то, что намеревались сделать с кодом:

IEnumerable<Animal> animals = GetFishes(); // returns IEnumerable<Fish>
// since we can only get animals *out* of the collection, every fish is an animal
// so this is safe

List<T> имеет как входное, так и выходное направления на T, поэтому это не ко-вариант и не контрвариант, а интерфейс, который позволяет добавлять объекты, например:

interface IWriteOnlyList<in T>
{
    void Add(T value);
}

Позволит вы должны сделать это:

IWriteOnlyList<Fish> fishes = GetWriteAccessToAnimals(); // still returns
                                                            IWriteOnlyList<Animal>
fishes.Add(new Fish("Guppy")); <-- this is now safe

Вот несколько видео, которые показывают концепции:

Вот пример:

namespace SO2719954
{
    class Base { }
    class Descendant : Base { }

    interface IBibbleOut<out T> { }
    interface IBibbleIn<in T> { }

    class Program
    {
        static void Main(string[] args)
        {
            // We can do this since every Descendant is also a Base
            // and there is no chance we can put Base objects into
            // the returned object, since T is "out"
            // We can not, however, put Base objects into b, since all
            // Base objects might not be Descendant.
            IBibbleOut<Base> b = GetOutDescendant();

            // We can do this since every Descendant is also a Base
            // and we can now put Descendant objects into Base
            // We can not, however, retrieve Descendant objects out
            // of d, since all Base objects might not be Descendant
            IBibbleIn<Descendant> d = GetInBase();
        }

        static IBibbleOut<Descendant> GetOutDescendant()
        {
            return null;
        }

        static IBibbleIn<Base> GetInBase()
        {
            return null;
        }
    }
}

Без этих меток можно было бы составить следующее:

public List<Descendant> GetDescendants() ...
List<Base> bases = GetDescendants();
bases.Add(new Base()); <-- uh-oh, we try to add a Base to a Descendant

Или вот это:

public List<Base> GetBases() ...
List<Descendant> descendants = GetBases(); <-- uh-oh, we try to treat all Bases
                                               as Descendants

Этот пост - лучшее, что я читал на эту тему

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