Подтвердить что ты не робот

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

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

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

Изменить для пояснения:

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

interface IBibble<out T>
.
.

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

interface IBibble<in T>
.
.
4b9b3361

Ответ 1

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

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

Позвольте мне объяснить это на более английском языке.

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

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

Однако, если у вас есть только список рыб, но нужно относиться к ним как к животным, это работает? Интуитивно, он должен работать, но в С# 3.0 и ранее этот фрагмент кода не будет компилироваться:

IEnumerable<Animal> animals = GetFishes(); // returns IEnumerable<Fish>

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

Другими словами, компилятор не может гарантировать, что это не разрешено:

animals.Add(new Mammal("Zebra"));

Таким образом, компилятор просто категорически отказывается компилировать ваш код. Это ковариация.

Посмотрим на контравариантность.

Поскольку наш зоопарк может обрабатывать всех животных, он, безусловно, может обрабатывать рыбу, поэтому попробуйте добавить рыбу в наш зоопарк.

В С# 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 в С# 4.0 специально маркируют интерфейс как тот или другой. С помощью in вы можете поместить общий тип (обычно T) в позиции ввода, что означает аргументы метода и свойства только для записи.

С помощью out вы можете поместить общий тип в выходные позиции, которые являются значениями возврата метода, свойствами только для чтения и параметрами метода 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

Ответ 2

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

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