Contravariance explained

First of, I have read many explanations on SO and blogs about covariance and contravariance and a big thanks goes out to Eric Lippert for producing such a great series on Covariance and Contravariance.

However I have a more specific question that I am trying to get my head around a little bit.

As far as I understand per Eric's explanation is that Covariance and Contravariance are both adjectives that describe a transformation. Covariant transformation is that which preserves the order of types and Contravariant transformation is one that reverses it.

I understand covariance in such a manner that I think most developers understand intuitively.

//covariant operation
Animal someAnimal = new Giraffe(); 
//assume returns Mammal, also covariant operation
someAnimal = Mammal.GetSomeMammal(); 

The return operation here is covariant as we are preserving the size in which both Animal is still bigger than Mammal or Giraffe. On that note most return operations are covariant, contravariant operations would not make sense.

  //if return operations were contravariant
  //the following would be illegal
  //as Mammal would need to be stored in something
  //equal to or less derived than Mammal
  //which would mean that Animal is now less than or equal than Mammal
  //therefore reversing the relationship
  Animal someAnimal =  Mammal.GetSomeMammal(); 

This piece of code of course would not make sense to most developers.

My confusion lies in Contravariant argument parameters. If you had a method such as

bool Compare(Mammal mammal1, Mammal mammal2);

I have always learned that input parameters always force contravariant behavior. Such that if the type is used as an input parameter its behavior should be contravariant.

However what is the difference between the following code

Mammal mammal1 = new Giraffe(); //covariant
Mammal mammal2 = new Dolphin(); //covariant

Compare(mammal1, mammal2); //covariant or contravariant?
//or
Compare(new Giraffe(), new Dolphin()); //covariant or contravariant?

By the same token that you can't do something like this you can't do

   //not valid
   Mammal mammal1 = new Animal();

   //not valid
   Compare(new Animal(), new Dolphin());

I guess what I am asking is, what makes method argument passing a contravariant transformation.

Sorry for the long post, maybe I am understand this incorrectly.

EDIT:

Per some conversation below, I understand that for instance using a delegate layer can clearly show contravariance. Consider the following example

//legal, covariance
Mammal someMammal = new Mammal();
Animal someAnimal = someMammal;

// legal in C# 4.0, covariance (because defined in Interface)
IEnumerable mammalList = Enumerable.Empty();
IEnumerable animalList = mammalList;

//because of this, one would assume
//that the following line is legal as well

void ProcessMammal(Mammal someMammal);

Action processMethod = ProcessMammal;
Action someAction = processMethod;

Of course this is illegal because someone can pass any Animal to someAction, where as the ProcessMammal expects anything thats Mammal or more specific ( lesser than Mammal ). Which is why someAction has to only be Action or anything more specific (Action)

However this is introducing a layer of delegates in the middle, is it necessary that for a contravariant projection to happen there has to be a delegate in the middle? And if we were to define Process as an interface we would declare the argument parameter as a contravariant type only because we wouldn't want someone to be able to do what I had shown above with delegates?

public interface IProcess
{
    void Process(T val);
}

36
задан Community 23 May 2017 в 11:33
поделиться

4 ответа

Обновление: Упс. Как оказалось, я перепутал дисперсию и "совместимость назначений" в своем первоначальном ответе. Ответ отредактирован соответствующим образом. Также я написал статью в блоге, которая, как я надеюсь, должна лучше отвечать на такие вопросы: Часто задаваемые вопросы о ковариационности и противоречивости

Ответ: Полагаю, что ответ на ваш первый вопрос заключается в том, что у вас нет противоречия в этом примере:

bool Compare(Mammal mammal1, Mammal mammal2); 
Mammal mammal1 = new Giraffe(); //covariant - no             
Mammal mammal2 = new Dolphin(); //covariant - no            

Compare(mammal1, mammal2); //covariant or contravariant? - neither            
//or             
Compare(new Giraffe(), new Dolphin()); //covariant or contravariant? - neither

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

В C# дисперсия поддерживается для массивов, делегатов и общих интерфейсов. Как сказал Эрик Липперт в записи своего блога В чем разница между ковариацией и совместимостью присваиваний? заключается в том, что лучше думать о дисперсии как о "проекции" типов.

Ковариацию легче понять, так как она следует правилам совместимости присваивания (массив более производного типа может быть присвоен массиву менее производного типа, "object[] objs = new string[10];"). Противоречие отменяет эти правила. Например, представьте, что можно сделать что-то вроде "string[] strings = new object[10];". Конечно, вы не можете этого сделать по очевидным причинам. Но это было бы противоречие (но опять же, массивы не являются противоречивыми, они поддерживают только ковариацию)

Вот примеры из MSDN, которые, я надеюсь, покажут вам, что на самом деле означает противоречие (сейчас я владелец этих документов, так что если вам кажется, что что-то неясное в документах, не стесняйтесь дать мне обратную связь):

  1. Использование переменной в интерфейсах для общих коллекций

    Employee[] employees = new Employee[3];
    // Вы можете передать PersonComparer, 
    // который реализует IEqualityComparer,
    // хотя метод ожидает IEqualityComparer.
    IEnumerable узелки =
     employees.Distinct(новый PersonComparer());
    
  2. Использование Variance in Delegates

    // Event hander, который принимает параметр типа EventArgs.
    private void MultiHandler(отправитель объекта, System.EventArgs e).
    {
     label1.Text = System.DateTime.Now.ToString();
    }
    публичная форма 1()
    {
     InitializeComponent();
     // Вы можете использовать метод, который имеет параметр EventArgs,
     // хотя событие ожидает параметр KeyEventArgs.
     this.button1.KeyDown += this.MultiHandler;
     // вы можете использовать тот же самый метод 
     // для события, которое ожидает параметр MouseEventArgs.
     this.button1.MouseClick += this.MultiHandler;
     }
    
  3. Использование переменной для функциональных и общих делегатов

     статическая пустота AddToContacts(Person person)
     {
     // Этот метод добавляет объект "Человек
     // к списку контактов.
     }
    
     // Делегат надеется, что 
     // метод, который имеет параметр "Сотрудник",
     // но вы можете назначить ему метод, имеющий параметр Person/
     // потому что работник происходит от Персона/
     Действие addEmployeeToContacts = AddToContacts;
    

Надеюсь это поможет.

27
ответ дан 27 November 2019 в 06:02
поделиться

Насколько я понимаю, ко / контрвариантны не отношения подтипов, а скорее операции (или проекции) между этими типами (такими как делегаты и дженерики). Следовательно:

Animal someAnimal = new Giraffe();

не является ковариантным, это просто совместимость присваивания, поскольку тип Giraffe «меньше» типа Animal. Совместная / контр-дисперсия становится проблемой, когда у вас есть некоторая проекция между этими типами, например:

IEnumerable<Giraffe> giraffes = new[] { new Giraffe() };
IEnumerable<Animal> animals = giraffes;

Это недопустимо в C # 3, однако это должно быть возможным, поскольку последовательность жирафов - это последовательность животных. Проекция T -> IEnumerable сохраняет «направление» отношения типов, поскольку Giraffe и IEnumerable (обратите внимание, что присваивание требует, чтобы тип левой части был как минимум такой же ширины, как и правая).

Контра-дисперсия меняет отношение типов на противоположное:

Action<Animal> printAnimal = a => {System.Console.WriteLine(a.Name)};
Action<Giraffe> printGiraffe = printAnimal;

Это также недопустимо в C # 3 , но это должно быть так, поскольку любое действие, совершаемое животным, может справиться с проходом мимо жирафа. Однако, начиная с Жираф <Животное и Действие <Животное> <Действие <Жираф> , проекция изменила отношения типов на противоположные. Это допустимо в C # 4.

Итак, чтобы ответить на вопросы в вашем примере:

//the following are neither covariant or contravariant - since there is no projection this is just assignment compatibility
Mammal mammal1 = new Giraffe();
Mammal mammal2 = new Dolphin();

//compare is contravariant with respect to its arguments - 
//the delegate assignment is legal in C#4 but not in C#3
Func<Mammal, Mammal, bool> compare = (m1, m2) => //whatever
Func<Giraffe, Dolphin, bool> c2 = compare;

//always invalid - right hand side must be smaller or equal to left hand side
Mammal mammal1 = new Animal();

//not valid for same reason - animal cannot be assigned to Mammal
Compare(new Animal(), new Dolphin());
10
ответ дан 27 November 2019 в 06:02
поделиться

(Отредактировано в ответ на комментарии)

В этой статье MSDN по теме описываются ковариация и контравариантность в применении к сопоставлению функции с делегатом. Переменная типа делегата:

public delegate bool Compare(Giraffe giraffe, Dolphin dolphin);

может (из-за контравариантности) быть заполнена функцией:

public bool Compare(Mammal mammal1, Mammal mammal2)
{
    return String.Compare(mammal1.Name, mammal2.Name) == 0;
}

Насколько я знаю, это не связано с прямым вызовом функции, а связано с сопоставлением функций с делегатами. Я не уверен, что это можно свести к тому уровню, который вы демонстрируете, когда отдельные переменные или назначения объектов являются контравариантными или ковариантными. Но назначение делегата использует контравариантность или ковариацию таким образом, который имеет смысл для меня, согласно связанной статье. Поскольку подпись делегата содержит больше производных типов, чем фактический экземпляр, это называется «контравариантностью»,

1
ответ дан 27 November 2019 в 06:02
поделиться

Ковариативность и Contravariance - это не то, что вы можете наблюдать при работе с классами инстанцирования. Поэтому неправильно говорить об одном из них, когда смотришь на простую инстанцирование класса, как в вашем примере: Animal someAnimal = new Giraffe(); //ковариантная операция

Эти термины не классифицируют операции. Термины Covariance, Contravariance и Invariance описывают связь между определенными аспектами классов и их подклассами.

Covariance
означает, что аспект изменяется аналогично направлению наследования.
Противоречие
означает, что аспект изменяется в направлении, противоположном направлению наследования.
Неопределенность
означает, что аспект не изменяется из класса в его подкласс (подклассы).

Обычно мы рассматриваем следующие аспекты, когда говорим о Ков.., Contrav. и Inv.:

  • Методы
    • Типы параметров
    • Типы возврата
    • Другие аспекты, связанные с сигнатурой, такие как брошенные исключения.
  • Дженерики

Давайте рассмотрим несколько примеров, чтобы лучше понять термины.

class T
class T2 extends T
 
//Covariance: The return types of the method "method" have the same
//direction of inheritance as the classes A and B.
class A { T method() }
class B extends A { T2 method() }
 
//Contravariance: The parameter types of the method "method" have a
//direction of inheritance opposite to the one of the classes A and B.
class A { method(T2 t) }
class B { method(T t) }
В обоих случаях "метод" переопределяется! Далее, приведенные примеры являются единственными правовыми случаями Cov. и Contrav. в объектно-ориентированных языках..:
  • Covariance - Return types and exception throw statements
  • Contravariance - Input parameters
  • Invariance - Input and Output parameters

Давайте рассмотрим некоторые примеры счетчиков, чтобы лучше понять приведенный выше список:

//Covariance of return types: OK
class Monkey { Monkey clone() }
class Human extends Monkey { Human clone() }
 
Monkey m = new Human();
Monkey m2 = m.clone(); //You get a Human instance, which is ok,
                       //since a Human is-a Monkey.
 
//Contravariance of return types: NOT OK
class Fruit
class Orange extends Fruit
 
class KitchenRobot { Orange make() }
class Mixer extends KitchenRobot { Fruit make() }
 
KitchenRobot kr = new Mixer();
Orange o = kr.make(); //Orange expected, but got a fruit (too general!)
 
//Contravariance of parameter types: OK
class Food
class FastFood extends Food
 
class Person { eat(FastFood food) }
class FatPerson extends Person { eat(Food food) }
 
Person p = new FatPerson();
p.eat(new FastFood()); //No problem: FastFood is-a Food, which FatPerson eats.
 
//Covariance of parameter types: NOT OK
class Person { eat(Food food) }
class FatPerson extends Person { eat(FastFood food) }
 
Person p = new FatPerson();
p.eat(new Food()); //Oops! FastFood expected, but got Food (too general).

Эта тема настолько сложна, что я могу продолжить ее очень долго. Советую вам самостоятельно проверить Cov. и Contrav. из Generics. Далее, вам нужно знать, как работает динамическая привязка, чтобы полностью понять примеры (какие методы получают точное название).

Термины возникли из принципа замещения Лискова, который определяет необходимые критерии для моделирования типа данных как подтипа другого. Возможно, вы также захотите исследовать его.

.
14
ответ дан 27 November 2019 в 06:02
поделиться
Другие вопросы по тегам:

Похожие вопросы: