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

162

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

До сих пор я видел только один и тот же старый пример массива.

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

Было бы неплохо увидеть пример, который позволил бы мне использовать его во время моей разработки, если бы я мог видеть, что он используется в другом месте.

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

Ответы:

109

Допустим, у вас есть класс Person и класс, производный от него, Учитель. У вас есть некоторые операции, которые принимают в IEnumerable<Person>качестве аргумента. В вашем классе школы у вас есть метод, который возвращает IEnumerable<Teacher>. Ковариантность позволяет вам напрямую использовать этот результат для методов, которые IEnumerable<Person>заменяют более производный тип менее производным (более универсальным) типом. Контравариантность, напротив, позволяет вам использовать более общий тип, где указывается более производный тип.

См. Также Ковариантность и Контравариантность в обобщениях на 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());
tvanfosson
источник
14
@FilipBartuzi - если, как и я, когда я писал этот ответ, вы работали в университете, который является очень реальным примером.
tvanfosson
5
Как это может быть помечено как ответ, если оно не отвечает на вопрос и не приводит ни одного примера использования дисперсии co / contra в c #?
barakcaf
@barakcaf добавил пример контравариантности. не знаю, почему вы не видели пример ковариации - возможно, вам нужно было прокрутить код вниз - но я добавил несколько комментариев по этому поводу.
tvanfosson
@tvanfosson код использует co / contra, то есть он не показывает, как его объявить. Пример не показывает использование in / out в обобщенном объявлении, в то время как другой ответ делает.
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>), и он все еще может сожрать ослов (я могу передать осла методу Gobble, для которого требуется Quadruped). Нет необходимости в контравариантности. Это здорово, что мы можем рассматривать QuadrupedGobbler как DonkeyGobbler, но зачем нам, в этом случае, если QuadrupedGobbler уже может сожрать ослов?
wired_in
1
@wired_in Потому что, когда вы заботитесь только о ослах, помешать может быть более общее. Например, если у вас есть ферма, которая поставляет ослов для сожжения, вы можете выразить это как void feed(IGobbler<Donkey> dg). Если вместо этого вы взяли IGobbler <Quadruped>, вы не сможете передать дракона, который ест только ослов.
Марсело Кантос
1
Вау, опаздываю на вечеринку, но это примерно лучший письменный пример, который я видел в SO. Имеет полный смысл, будучи нелепым. Я собираюсь дополнить свою игру ответами ...
Джесси Уильямс,
122

Вот что я собрал, чтобы помочь мне понять разницу

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
CSharper
источник
10
Это лучшее, что я видел до сих пор, это ясно и кратко. Отличный пример!
Роб Л
6
Как фрукты могут быть понижены до яблока (в Contravarianceпримере), когда Fruitродитель Apple?
Тобиас Маршалл
@TobiasMarschall , что означает , что вы должны изучить более над «полиморфизм»
ОСШ
56

Ключевые слова in и out управляют правилами приведения компилятора для интерфейсов и делегатов с общими параметрами:

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
разъем
источник
Предполагая, что рыба является подтипом животных. Отличный ответ, кстати.
Раджан Прасад
49

Вот простой пример использования иерархии наследования.

Учитывая простую иерархию классов:

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

И в коде:

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

Contravariance (универсальный с параметризованным типом, украшенным 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может быть не больше, чем «а» Action<Zebra>, но менее производные суперклассы Zebraтакже приемлемы.

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

Из-за перевернутого подхода использования функций более высокого порядка таким образом, к тому времени, когда Actionвызывается, это более производный Zebraэкземпляр, который вызывается против zebraActionфункции (передаваемой как параметр), хотя сама функция использует менее производный тип.

StuartLC
источник
7
Это отличное объяснение различных вариантов дисперсии, поскольку в нем рассматривается пример, а также разъясняется, почему компилятор ограничивает или разрешает без ключевых слов in / out
Vikhram
Где используется inключевое слово для контравариантности ?
Джавадба
@javadba в приведенном выше, Action<in T>и Func<in T, out TResult>являются контравариантными в типе ввода. (В моих примерах используются существующие инвариантные (List), ковариантные (IEnumerable) и контравариантные (Action, Func) типы)
StuartLC
Хорошо, я не делаю, C#поэтому не знал бы этого.
Джавадба
Это довольно похоже в Scala, просто другой синтаксис - [+ T] будет ковариантным в T, [-T] будет контравариантным в T, Scala также может применять ограничение «между» и беспорядочный подкласс «Nothing», который 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 одного типа, вы не можете передать 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;
    }
}

и заменить элемент более производным, то есть увеличить дисперсию или ввести ко дисперсию.

В общем, это всего лишь проверяемое вами во время компиляции обещание, что вы будете обращаться с универсальным типом определенным образом, чтобы сохранить безопасность типов и никого не съесть.

Возможно, вы захотите прочитать это, чтобы обернуть голову вокруг этого.

Иван Рыбалко
источник
Вы можете быть съедены тигром, который стоил подъема
Джавадба
Ваш комментарий contravarianceинтересен. Я читаю это как указание на эксплуатационное требование: более общий тип должен поддерживать сценарии использования всех типов, производных от него. Таким образом, в этом случае приют для животных должен быть в состоянии обеспечить укрытие для всех типов животных. В этом случае добавление нового подкласса может нарушить суперкласс! То есть - если мы добавим подтип Tyrannosaurus Rex, то это может разрушить наш существующий приют для животных .
Джавадба
(Продолжение). Это резко отличается от ковариации, которая четко описана структурно : все более конкретные подтипы поддерживают операции, определенные в супертипе - но не обязательно таким же образом.
Джавадба
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();
woggles
источник