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

86

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

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

Отредактируйте для пояснения:

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

interface IBibble<out T>
.
.

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

interface IBibble<in T>
.
.
NibblyPig
источник
3
Это короткое и хорошее объяснение ИМХО: blogs.msdn.com/csharpfaq/archive/2010/02/16/…
digEmAll
Может быть полезно: Сообщение в блоге
Крунал,
Хм, это хорошо, но не объясняет, почему именно это меня действительно сбивает с толку.
NibblyPig

Ответы:

144

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

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

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

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

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

Однако что, если у вас есть только список рыб, но нужно обращаться с ними как с животными, это работает? Интуитивно это должно работать, но в 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) в позиции ввода , что означает аргументы метода и свойства только для записи.

С помощью 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
Лассе В. Карлсен
источник
Хм, не могли бы вы объяснить цель ковариации и контравариантности? Это могло бы помочь мне понять это больше.
NibblyPig
1
Посмотрите последний бит, который компилятор предотвращал раньше, цель входа и выхода - сказать, что вы можете делать с безопасными интерфейсами (или типами), чтобы компилятор не мешал вам делать безопасные вещи .
Лассе В. Карлсен
Превосходный ответ, я смотрел видео, они были очень полезны, и в сочетании с вашим примером я сейчас отсортировал его. Остается только один вопрос: почему требуются «выход» и «вход», почему Visual Studio автоматически не знает, что вы пытаетесь сделать (или в чем причина этого)?
NibblyPig
Automagic «Я вижу, что вы пытаетесь там сделать» обычно не одобряется, когда дело доходит до объявления таких вещей, как классы. Лучше, чтобы программист явно отмечал типы. Вы можете попробовать добавить «in» в класс, у которого есть методы, возвращающие T, и компилятор пожалуется. Представьте, что бы произошло, если бы он просто удалил «в», который ранее автоматически добавил для вас.
Лассе В. Карлсен
1
Если для одного ключевого слова требуется такое длинное объяснение, что-то явно не так. На мой взгляд, C # в данном конкретном случае пытается быть слишком умным. Тем не менее, спасибо за крутое объяснение.
rr-
7

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

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

Бен Джи
источник
5
Линк кажется мертвым. Вот заархивированная версия: web.archive.org/web/20140626123445/http://adamnathan.co.uk/…
si618,
1
мне даже больше нравится это объяснение: codepureandsimple.com/…
volkit