Приклад коваріації та противаріантності реального світу


162

У мене виникають невеликі проблеми з розумінням того, як я використовував би коваріацію та протиріччя в реальному світі.

Поки що єдиними прикладами, які я бачив, був той самий старий приклад масиву.

object[] objectArray = new string[] { "string 1", "string 2" };

Було б добре побачити приклад, який дозволив би мені використовувати його під час моєї розробки, якби я міг бачити, як він використовується в іншому місці.


1
Я досліджую коваріацію у цій відповіді на (власне) запитання: типи коваріації: на прикладі . Я думаю, вам це буде цікаво і, сподіваюся, повчальним.
Крістіан Діаконеску

Відповіді:


109

Скажімо, у вас є вчитель класу і клас, який виходить з нього, вчитель. У вас є деякі операції, які беруть IEnumerable<Person>аргумент. У вашому класі школи є метод, який повертає IEnumerable<Teacher>. Коваріація дозволяє безпосередньо використовувати цей результат для методів, які приймають IEnumerable<Person>, замінюючи більш похідний тип на менш похідний (більш загальний) тип. Контраваріантність, протиінтуїтивно зрозуміла, дозволяє використовувати більш загальний тип, де вказано більш похідний тип.

Дивіться також Коваріація та суперечливість у Generics на MSDN .

Класи :

public class Person 
{
     public string Name { get; set; }
} 

public class Teacher : Person { } 

public class MailingList
{
    public void Add(IEnumerable<out Person> people) { ... }
}

public class School
{
    public IEnumerable<Teacher> GetTeachers() { ... }
}

public class PersonNameComparer : IComparer<Person>
{
    public int Compare(Person a, Person b) 
    { 
        if (a == null) return b == null ? 0 : -1;
        return b == null ? 1 : Compare(a,b);
    }

    private int Compare(string a, string b)
    {
        if (a == null) return b == null ? 0 : -1;
        return b == null ? 1 : a.CompareTo(b);
    }
}

Використання :

var teachers = school.GetTeachers();
var mailingList = new MailingList();

// Add() is covariant, we can use a more derived type
mailingList.Add(teachers);

// the Set<T> constructor uses a contravariant interface, IComparer<in T>,
// we can use a more generic type than required.
// See https://msdn.microsoft.com/en-us/library/8ehhxeaf.aspx for declaration syntax
var teacherSet = new SortedSet<Teachers>(teachers, new PersonNameComparer());

14
@FilipBartuzi - якщо, як я, коли я писав цю відповідь, ти був працевлаштований в університеті, який є дуже реальним прикладом світу.
tvanfosson

5
Як це можна позначати відповіддю, коли він не відповідає на питання і не дає жодного прикладу використання дисперсії co / contra у c #?
barakcaf

@barakcaf додав приклад протиріччя. не впевнений, чому ви не бачили приклад коваріації - можливо, вам потрібно було прокрутити код вниз - але я додав деякі коментарі навколо цього.
tvanfosson

@tvanfosson код використовує co / contra, тобто, він не показує, як це оголосити. У прикладі не показано використання введення / виводу у загальній декларації, тоді як інша відповідь.
barakcaf

Отже, якщо я правильно визнаю, коваріація - це те, що дозволяє принцип заміщення Ліскова в C #, чи правильно?
Мігель Велосо

136
// Contravariance
interface IGobbler<in T> {
    void gobble(T t);
}

// Since a QuadrupedGobbler can gobble any four-footed
// creature, it is OK to treat it as a donkey gobbler.
IGobbler<Donkey> dg = new QuadrupedGobbler();
dg.gobble(MyDonkey());

// Covariance
interface ISpewer<out T> {
    T spew();
}

// A MouseSpewer obviously spews rodents (all mice are
// rodents), so we can treat it as a rodent spewer.
ISpewer<Rodent> rs = new MouseSpewer();
Rodent r = rs.spew();

Для повноти…

// Invariance
interface IHat<T> {
    void hide(T t);
    T pull();
}

// A RabbitHat…
IHat<Rabbit> rHat = RabbitHat();

// …cannot be treated covariantly as a mammal hat…
IHat<Mammal> mHat = rHat;      // Compiler error
// …because…
mHat.hide(new Dolphin());      // Hide a dolphin in a rabbit hat??

// It also cannot be treated contravariantly as a cottontail hat…
IHat<CottonTail> cHat = rHat;  // Compiler error
// …because…
rHat.hide(new MarshRabbit());
cHat.pull();                   // Pull a marsh rabbit out of a cottontail hat??

138
Мені подобається цей реалістичний приклад. На минулому тижні я просто писав якийсь код осла, і я був такий радий, що зараз у нас є коваріація. :-)
Ерік Ліпперт

4
Цей коментар вище, але @javadba розповідає Еріку Ліпперту, що таке коваріація та протиріччя - це реалістичний коваріантний приклад того, що я розповідаю своїй бабусі, як смоктати яйця! : p
iAteABug_And_iLiked_it

1
У запитанні не було запитання про те, що можуть робити протиріччя та коваріантність , воно запитувало, навіщо вам це потрібно використовувати . Ваш приклад далеко не практичний, тому що він цього не вимагає. Я можу створити QuadrupedGobbler і ставитись до нього як до самого себе (призначити його IGobbler <Quadruped>), і він все ще може збивати Donkeys (я можу передати осла методом Gobble, який вимагає чотириногих). Не потрібно протиріччя. Це здорово, що ми можемо ставитися до QuadrupedGobbler як до DonkeyGobbler, але навіщо нам це потрібно, у такому випадку, якщо QuadrupedGobbler вже може погубити ослів?
wired_in

1
@wired_in Тому, що коли ти дбаєш лише про ослів, то бути більш загальним може заважати. Наприклад, якщо у вас є ферма, яка постачає ослів, щоб їх пограбували, ви можете це виразити як void feed(IGobbler<Donkey> dg). Якщо ви замість цього взяли параметр IGobbler <Quadruped>, ви не могли перейти дракона, який їсть лише ослів.
Марсело Кантос

1
Ваай запізнився на вечірку, але це про найкращий письмовий приклад, який я бачив навколо SO. Створює повний сенс, будучи смішним. Мені доведеться доповнити свою гру відповідями ...
Джессі Вільямс

121

Ось що я зібрав, щоб допомогти зрозуміти різницю

public interface ICovariant<out T> { }
public interface IContravariant<in T> { }

public class Covariant<T> : ICovariant<T> { }
public class Contravariant<T> : IContravariant<T> { }

public class Fruit { }
public class Apple : Fruit { }

public class TheInsAndOuts
{
    public void Covariance()
    {
        ICovariant<Fruit> fruit = new Covariant<Fruit>();
        ICovariant<Apple> apple = new Covariant<Apple>();

        Covariant(fruit);
        Covariant(apple); //apple is being upcasted to fruit, without the out keyword this will not compile
    }

    public void Contravariance()
    {
        IContravariant<Fruit> fruit = new Contravariant<Fruit>();
        IContravariant<Apple> apple = new Contravariant<Apple>();

        Contravariant(fruit); //fruit is being downcasted to apple, without the in keyword this will not compile
        Contravariant(apple);
    }

    public void Covariant(ICovariant<Fruit> fruit) { }

    public void Contravariant(IContravariant<Apple> apple) { }
}

tldr

ICovariant<Fruit> apple = new Covariant<Apple>(); //because it's covariant
IContravariant<Apple> fruit = new Contravariant<Fruit>(); //because it's contravariant

10
Це найкраще, що я бачив досі, це зрозуміло і стисло. Великий приклад!
Роб Л

6
Як можна звести плід до яблуні (у Contravarianceприкладі), коли Fruitце батько Apple?
Тобіас Маршалл

@TobiasMarschall, це означає, що вам доведеться більше вивчати "поліморфізм"
snr

56

Ключові слова вхідних та вихідних елементів керують правилами листування компілятора для інтерфейсів та делегатів із загальними параметрами:

interface IInvariant<T> {
    // This interface can not be implicitly cast AT ALL
    // Used for non-readonly collections
    IList<T> GetList { get; }
    // Used when T is used as both argument *and* return type
    T Method(T argument);
}//interface

interface ICovariant<out T> {
    // This interface can be implicitly cast to LESS DERIVED (upcasting)
    // Used for readonly collections
    IEnumerable<T> GetList { get; }
    // Used when T is used as return type
    T Method();
}//interface

interface IContravariant<in T> {
    // This interface can be implicitly cast to MORE DERIVED (downcasting)
    // Usually means T is used as argument
    void Method(T argument);
}//interface

class Casting {

    IInvariant<Animal> invariantAnimal;
    ICovariant<Animal> covariantAnimal;
    IContravariant<Animal> contravariantAnimal;

    IInvariant<Fish> invariantFish;
    ICovariant<Fish> covariantFish;
    IContravariant<Fish> contravariantFish;

    public void Go() {

        // NOT ALLOWED invariants do *not* allow implicit casting:
        invariantAnimal = invariantFish; 
        invariantFish = invariantAnimal; // NOT ALLOWED

        // ALLOWED covariants *allow* implicit upcasting:
        covariantAnimal = covariantFish; 
        // NOT ALLOWED covariants do *not* allow implicit downcasting:
        covariantFish = covariantAnimal; 

        // NOT ALLOWED contravariants do *not* allow implicit upcasting:
        contravariantAnimal = contravariantFish; 
        // ALLOWED contravariants *allow* implicit downcasting
        contravariantFish = contravariantAnimal; 

    }//method

}//class

// .NET Framework Examples:
public interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable { }
public interface IEnumerable<out T> : IEnumerable { }


class Delegates {

    // When T is used as both "in" (argument) and "out" (return value)
    delegate T Invariant<T>(T argument);

    // When T is used as "out" (return value) only
    delegate T Covariant<out T>();

    // When T is used as "in" (argument) only
    delegate void Contravariant<in T>(T argument);

    // Confusing
    delegate T CovariantBoth<out T>(T argument);

    // Confusing
    delegate T ContravariantBoth<in T>(T argument);

    // From .NET Framework:
    public delegate void Action<in T>(T obj);
    public delegate TResult Func<in T, out TResult>(T arg);

}//class

Якщо припустити, що риба - це підтип Тварина. Чудова відповідь до речі.
Раджан Прасад

48

Ось простий приклад використання ієрархії спадкування.

З огляду на просту ієрархію класів:

введіть тут опис зображення

І в коді:

public abstract class LifeForm  { }
public abstract class Animal : LifeForm { }
public class Giraffe : Animal { }
public class Zebra : Animal { }

Інваріантність (тобто параметри загального типу * не * прикрашені inабо outключові слова)

Здається, такий метод, як цей

public static void PrintLifeForms(IList<LifeForm> lifeForms)
{
    foreach (var lifeForm in lifeForms)
    {
        Console.WriteLine(lifeForm.GetType().ToString());
    }
}

... повинен прийняти неоднорідну колекцію: (що вона робить)

var myAnimals = new List<LifeForm>
{
    new Giraffe(),
    new Zebra()
};
PrintLifeForms(myAnimals); // Giraffe, Zebra

Однак передача колекції більш похідного типу не вдається!

var myGiraffes = new List<Giraffe>
{
    new Giraffe(), // "Jerry"
    new Giraffe() // "Melman"
};
PrintLifeForms(myGiraffes); // Compile Error!

cannot convert from 'System.Collections.Generic.List<Giraffe>' to 'System.Collections.Generic.IList<LifeForm>'

Чому? Оскільки загальний параметр IList<LifeForm>не є коваріантним - IList<T>є інваріантним, тому IList<LifeForm>приймає лише колекції (які реалізують IList) там, де Tповинен бути параметризований тип LifeForm.

Якщо реалізація методу PrintLifeFormsбула шкідливою (але має такий же підпис методу), причина, через яку компілятор перешкоджає передачі, List<Giraffe>стає очевидною:

 public static void PrintLifeForms(IList<LifeForm> lifeForms)
 {
     lifeForms.Add(new Zebra());
 }

Оскільки IListдозволяє додавати або видаляти елементи, будь-який підклас з LifeFormцього приводу може бути доданий до параметра lifeFormsі порушує тип будь-якої колекції похідних типів, переданих методу. (Тут шкідливий метод намагатиметься додати Zebraдо var myGiraffes). На щастя, компілятор захищає нас від цієї небезпеки.

Коваріація (загальна з параметризованим типом, прикрашена out)

Коваріація широко використовується для незмінних колекцій (тобто там, де нові елементи не можуть бути додані або вилучені з колекції)

Рішенням прикладу вище є забезпечення коваріантного загального типу колекції, наприклад IEnumerable(визначеного як IEnumerable<out T>). IEnumerableне має методів зміни до колекції, і в результаті outковаріації тепер будь-яка колекція з підтипом LifeFormможе бути передана методу:

public static void PrintLifeForms(IEnumerable<LifeForm> lifeForms)
{
    foreach (var lifeForm in lifeForms)
    {
        Console.WriteLine(lifeForm.GetType().ToString());
    }
}

PrintLifeFormsтепер може бути викликаний Zebras, Giraffesі будь-який IEnumerable<>з будь-якого підкласуLifeForm

Протиріччя (загальне із типом, параметризованим типом, прикрашене in)

Протилежність часто використовується, коли функції передаються як параметри.

Ось приклад функції, яка приймає Action<Zebra>як параметр і викликає його у відомій екземплярі Zebra:

public void PerformZebraAction(Action<Zebra> zebraAction)
{
    var zebra = new Zebra();
    zebraAction(zebra);
}

Як і очікувалося, це працює чудово:

var myAction = new Action<Zebra>(z => Console.WriteLine("I'm a zebra"));
PerformZebraAction(myAction); // I'm a zebra

Інтуїтивно це не вдасться:

var myAction = new Action<Giraffe>(g => Console.WriteLine("I'm a giraffe"));
PerformZebraAction(myAction); 

cannot convert from 'System.Action<Giraffe>' to 'System.Action<Zebra>'

Однак це вдається

var myAction = new Action<Animal>(a => Console.WriteLine("I'm an animal"));
PerformZebraAction(myAction); // I'm an animal

і навіть це також вдається:

var myAction = new Action<object>(a => Console.WriteLine("I'm an amoeba"));
PerformZebraAction(myAction); // I'm an amoeba

Чому? Тому що Actionвизначається як Action<in T>, тобто це contravariant, означає, що для Action<Zebra> myAction, що myActionможе бути якнайбільше "a" Action<Zebra>, але менш похідні суперкласи Zebraтакож є прийнятними.

Хоча це може бути спочатку не інтуїтивно зрозумілим (наприклад, як можна Action<object>передавати його як параметр, що вимагає Action<Zebra>?), Якщо ви розпакуєте етапи, ви зауважите, що сама викликана функція ( PerformZebraAction) відповідає за передачу даних (у цьому випадку Zebraекземпляр ) до функції - дані не надходять з коду виклику.

Через інвертований підхід використання функцій вищого порядку таким чином, до моменту Actionвиклику це більш похідний Zebraекземпляр викликається zebraActionфункцією (передається як параметр), хоча сама функція використовує менш похідний тип.


7
Це чудове пояснення для різних варіантів дисперсії, оскільки він розповідає на прикладі, а також уточнює, чому компілятор обмежує чи дозволяє без ключових слів
введення

Де використовується inключове слово для протиріччя ?
javadba

@javadba у наведеному вище Action<in T>і Func<in T, out TResult>є протирівноважним у вхідному типі. (Мої приклади використовують існуючий інваріантний (Список), коваріантний (IEnumerable) та противаріантний (Action, Func) типи
StuartLC

Добре, я цього не роблю, я C#б не знав цього.
javadba

Він досить схожий у Scala, просто інший синтаксис - [+ T] був би коваріантним у T, [-T] був би протилежним у T, Scala також може примусово застосовувати обмеження "між" та підрозряд "Нічого", що C # не має.
StuartLC

32
class A {}
class B : A {}

public void SomeFunction()
{
    var someListOfB = new List<B>();
    someListOfB.Add(new B());
    someListOfB.Add(new B());
    someListOfB.Add(new B());
    SomeFunctionThatTakesA(someListOfB);
}

public void SomeFunctionThatTakesA(IEnumerable<A> input)
{
    // Before C# 4, you couldn't pass in List<B>:
    // cannot convert from
    // 'System.Collections.Generic.List<ConsoleApplication1.B>' to
    // 'System.Collections.Generic.IEnumerable<ConsoleApplication1.A>'
}

В основному всякий раз, коли у вас була функція, яка займає перелік одного типу, ви не можете передати в Enumerable похідний тип без явного його введення.

Просто щоб попередити вас про пастку:

var ListOfB = new List<B>();
if(ListOfB is IEnumerable<A>)
{
    // In C# 4, this branch will
    // execute...
    Console.Write("It is A");
}
else if (ListOfB is IEnumerable<B>)
{
    // ...but in C# 3 and earlier,
    // this one will execute instead.
    Console.Write("It is B");
}

Це все-таки жахливий код, але він існує, і зміна поведінки в C # 4 може ввести непомітні та важко знайти помилки, якщо ви використовуєте подібну конструкцію.


Тож це впливає на колекції більше ніж на що-небудь, тому що в c # 3 ви можете передати більш похідний тип у метод менш похідного типу.
Бритва

3
Так, велика зміна полягає в тому, що IEnumerable зараз підтримує це, тоді як цього не було раніше.
Майкл Стум

4

Від MSDN

Наступний приклад коду показує підтримку коваріації та противаріантності груп методів

static object GetObject() { return null; }
static void SetObject(object obj) { }

static string GetString() { return ""; }
static void SetString(string str) { }

static void Test()
{
    // Covariance. A delegate specifies a return type as object, 
    // but you can assign a method that returns a string.
    Func<object> del = GetString;

    // Contravariance. A delegate specifies a parameter type as string, 
    // but you can assign a method that takes an object.
    Action<string> del2 = SetObject;
}

4

Суперечність

У реальному світі завжди можна використовувати притулок для тварин замість притулку для кроликів, оскільки кожен раз, коли у притулку для тварин розміщується кролик, це тварина. Однак якщо ви використовуєте притулок для кроликів замість притулку для тварин, його персонал може з'їсти тигра.

У коді, це означає , що якщо у вас є , IShelter<Animal> animalsви можете просто написати , IShelter<Rabbit> rabbits = animals якщо ви обіцяєте і використовувати Tв IShelter<T>тільки як параметри методу наступним чином:

public class Contravariance
{
    public class Animal { }
    public class Rabbit : Animal { }

    public interface IShelter<in T>
    {
        void Host(T thing);
    }

    public void NoCompileErrors()
    {
        IShelter<Animal> animals = null;
        IShelter<Rabbit> rabbits = null;

        rabbits = animals;
    }
}

і замініть елемент на більш загальний, тобто зменшіть дисперсію або введіть контра- дисперсію.

Коваріація

У реальному світі ви завжди можете використовувати постачальника кроликів замість постачальника тварин, оскільки кожен раз, коли постачальник кроликів дає вам кролика, це тварина. Однак якщо ви використовуєте постачальника тварин замість кролика, ви можете з'їсти тигра.

У коді, це означає , що якщо у вас є , ISupply<Rabbit> rabbitsви можете просто написати , ISupply<Animal> animals = rabbits якщо ви обіцяєте і використовувати Tв ISupply<T>тільки в якості значення, що повертається методу наступним чином:

public class Covariance
{
    public class Animal { }
    public class Rabbit : Animal { }

    public interface ISupply<out T>
    {
        T Get();
    }

    public void NoCompileErrors()
    {
        ISupply<Animal> animals = null;
        ISupply<Rabbit> rabbits = null;

        animals = rabbits;
    }
}

і замініть елемент на більш похідний, тобто збільшити дисперсію або ввести спів- дисперсію.

Загалом, це лише обіцяна від вас обіцянка часу, коли ви будете поводитись із загальним типом певним чином, щоб зберегти безпеку типу та не їсти когось.

Ви можете дати це для читання двічі обгорнути голову навколо цього.


ви можете з'їсти тигра, який коштував нагороди
javadba

Ваш коментар contravarianceцікавий. Я читаю це як вказівку на оперативну вимогу: щоб більш загальний тип підтримував випадки використання всіх типів, похідних від нього. Тому в цьому випадку притулок для тварин повинен мати можливість підтримувати притулок для кожного типу тварин. У такому випадку додавання нового підкласу може порушити суперклас! Тобто - якщо ми додамо підтип Tyrannosaurus Rex, це може зруйнувати наше притулок для тварин .
javadba

(Продовження). Це різко відрізняється від коваріації, яка чітко описана структурно : всі більш конкретні підтипи підтримують операції, визначені в супертипі, але не обов'язково однаковим чином.
javadba

3

Делегат перетворювача допомагає мені візуалізувати обидві концепції спільної роботи:

delegate TOutput Converter<in TInput, out TOutput>(TInput input);

TOutputявляє собою коваріацію, де метод повертає більш конкретний тип .

TInputявляє собою противаріантність, коли метод передається менш конкретним типом .

public class Dog { public string Name { get; set; } }
public class Poodle : Dog { public void DoBackflip(){ System.Console.WriteLine("2nd smartest breed - woof!"); } }

public static Poodle ConvertDogToPoodle(Dog dog)
{
    return new Poodle() { Name = dog.Name };
}

List<Dog> dogs = new List<Dog>() { new Dog { Name = "Truffles" }, new Dog { Name = "Fuzzball" } };
List<Poodle> poodles = dogs.ConvertAll(new Converter<Dog, Poodle>(ConvertDogToPoodle));
poodles[0].DoBackflip();
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.