Розуміння коваріантних та контраваріантних інтерфейсів у C #


86

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

Чи є гарне стисле пояснення того, що вони є і для чого вони корисні?

Змінити для уточнення:

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

interface IBibble<out T>
.
.

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

interface IBibble<in T>
.
.

3
Це коротке та гарне розкриття IMHO: blogs.msdn.com/csharpfaq/archive/2010/02/16/…
digEmAll

Може бути корисним: Допис у блозі
Krunal

Хм, це добре, але це не пояснює, чому саме це насправді бентежить.
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, вам дозволено розміщувати загальний тип у вихідних- позиціях, що є значеннями повернення методу, властивостями лише для читання та параметрами методу 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> має як вхідні, так і вихідні вказівки на Т, тому це не ко-варіант, не контра-варіант, а інтерфейс, який дозволив вам додавати об’єкти, наприклад:

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
Дивіться останній біт, який попереджав компілятор раніше, мета входу та виходу - сказати, що ви можете зробити з безпечними інтерфейсами (або типами), щоб компілятор не заважав вам робити безпечні дії .
Лассе В. Карлсен,

Чудова відповідь, я переглянув відео, вони були дуже корисними, і в поєднанні з вашим прикладом я відсортував зараз. Залишається лише одне питання, і ось чому потрібно "виходити" та "входити", чому візуальна студія автоматично не знає, що ви намагаєтесь зробити (або яка причина цього стоїть)?
NibblyPig

Автоматичне "Я бачу, що ти там намагаєшся робити", як правило, не дотримується, коли йдеться про оголошення таких речей, як класи, краще, щоб програміст чітко позначив типи. Ви можете спробувати додати "in" до класу, який має методи, які повертають T, і компілятор скаржиться. Уявіть, що сталося б, якби він мовчки просто видалив "в", який раніше автоматично додав для вас.
Лассе В. Карлсен,

1
Якщо одне ключове слово потребує цього довгого пояснення, щось явно не так. На мій погляд, C # намагається бути надто розумним у цьому конкретному випадку. Тим не менше, дякую за круте пояснення.
rr-

7

Цей допис - найкраще, що я прочитав з цього питання

Коротше кажучи, коваріація / контраваріація / інваріантність стосується автоматичного відливання типу (від основи до похідної та навпаки). Типи цих типів можливі лише за умови дотримання деяких гарантій щодо дій читання / запису, що виконуються на залитих об'єктах. Докладніше читайте у дописі.


5
Посилання здається мертвим. Ось заархівована версія: web.archive.org/web/20140626123445/http://adamnathan.co.uk/…
si618

1
мені це пояснення подобається ще більше: codepureandsimple.com/…
volkit
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.