Контравариантность объяснила - PullRequest
36 голосов
/ 26 декабря 2009

Прежде всего, я прочитал много объяснений на SO и в блогах о ковариации и контравариантности, и большое спасибо Эрику Липперту за создание такой замечательной серии о Ковариация и Контравариантность .

Однако у меня есть более конкретный вопрос, который я пытаюсь немного обдумать.

Насколько я понимаю, объяснение Эрика состоит в том, что ковариантность и контравариантность - это прилагательные, которые описывают преобразование. Ковариантное преобразование - это то, что сохраняет порядок типов, а Контравариантное преобразование - это то, что обращает его вспять.

Я понимаю ковариацию таким образом, что я думаю, что большинство разработчиков понимают интуитивно.

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

Операция возврата здесь ковариантна, так как мы сохраняем размер, при котором оба Животных по-прежнему больше, чем Млекопитающие или Жираф. На этом примечании большинство операций возврата являются ковариантными, контрвариантные операции не имеют смысла.

  //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(); 

Этот фрагмент кода, конечно, не имеет смысла для большинства разработчиков.

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

bool Compare(Mammal mammal1, Mammal mammal2);

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

Однако в чем разница между следующим кодом

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?

Точно так же, что вы не можете сделать что-то подобное, вы не можете сделать

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

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

Полагаю, я спрашиваю, что делает аргумент метода передачей контравариантного преобразования.

Извините за длинный пост, возможно, я неправильно это понимаю.

EDIT:

В рамках некоторого разговора, приведенного ниже, я понимаю, что, например, использование уровня делегата может ясно показать противоречивость. Рассмотрим следующий пример

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

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

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

void ProcessMammal(Mammal someMammal);

Action<Mammal> processMethod = ProcessMammal;
Action<Animal> someAction = processMethod;

Конечно, это незаконно, потому что кто-то может передать любое Животное какому-либо действию, когда ProcessMammal ожидает что-нибудь Млекопитающее или более конкретное (меньше, чем Млекопитающее). Вот почему какое-то действие должно быть только действием или чем-то более конкретным (действием)

Однако это представляет слой делегатов посередине, необходимо ли, чтобы для выполнения противоположного проецирования был посланник посередине? И если бы мы определяли Process как интерфейс, мы объявили бы параметр аргумента как контравариантный тип только потому, что не хотели бы, чтобы кто-то мог делать то, что я показал выше, с делегатами?

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

Ответы [ 5 ]

27 голосов
/ 30 декабря 2009

Обновление: Упс. Как оказалось, я перепутал дисперсию и «совместимость присваивания» в своем первоначальном ответе. Отредактировал ответ соответственно. Также я написал пост в блоге, который, я надеюсь, должен ответить на такие вопросы лучше: Covariance and Contravariance FAQ

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

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];
    // You can pass PersonComparer, 
    // which implements IEqualityComparer<Person>,
    // although the method expects IEqualityComparer<Employee>.
    IEnumerable<Employee> noduplicates =
        employees.Distinct<Employee>(new PersonComparer());
    
  2. Использование дисперсии в делегатах

    // Event hander that accepts a parameter of the EventArgs type.
    private void MultiHandler(object sender, System.EventArgs e)
    {
       label1.Text = System.DateTime.Now.ToString();
    }
    public Form1()
    {
        InitializeComponent();
        // You can use a method that has an EventArgs parameter,
        // although the event expects the KeyEventArgs parameter.
        this.button1.KeyDown += this.MultiHandler;
        // You can use the same method 
        // for an event that expects the MouseEventArgs parameter.
        this.button1.MouseClick += this.MultiHandler;
     }
    
  3. Использование дисперсии для делегатов общего назначения и функций

     static void AddToContacts(Person person)
     {
       // This method adds a Person object
       // to a contact list.
     }
    
     // The Action delegate expects 
     // a method that has an Employee parameter,
     // but you can assign it a method that has a Person parameter
     // because Employee derives from Person.
     Action<Employee> addEmployeeToContacts = AddToContacts;
    

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

13 голосов
/ 30 декабря 2009

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

Эти термины не классифицируют операции. Термины Ковариантность, Контравариантность и Инвариантность описывают отношения между некоторыми аспектами классов и их подклассами.

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

Мы обычно рассматриваем следующие аспекты, когда говорим о Cov., Contrav. и инв .::1028*

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

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

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. и контрав. в объектно-ориентированных языках .:
  • Covariance - типы возвращаемых значений и операторы выброса исключений
  • Contravariance - входные параметры
  • Инвариантность - входные и выходные параметры

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

//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. и контрав. обобщений самостоятельно. Кроме того, вам необходимо знать, как работает динамическое связывание, чтобы полностью понять примеры (какие именно методы вызываются).

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

10 голосов
/ 26 декабря 2009

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

Animal someAnimal = new Giraffe();

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

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

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

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

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

Это также недопустимо в C # 3, но так должно быть, поскольку любое действие, предпринимаемое животным, может справиться с передачей жирафа. Однако, начиная с Giraffe < Animal и Action<Animal> < Action<Giraffe>, проекция перевернула отношения типов. Это законно в 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());
1 голос
/ 01 июня 2017

Посмотрите на это так: Если у меня есть функция func, которая имеет дело с подтипом Mammal вида Mammal m = Func (g (Mammal)) , я могу заменить Mammal чем-то, что охватывает Млекопитающее , которое здесь является базой Животное .

С точки зрения спортивной аналогии, чтобы понять изображение ниже, вы можете поймать мяч голыми руками, как в крикете, но также возможно (и проще) поймать мяч, используя бейсбольные перчатки.

Слева вы видите ковариацию, внутри части параметров вы видите контравариацию.

enter image description here

Вы можете задаться вопросом: «Почему левая зеленая кривая больше красной? Не является ли подтип, который обычно делает больше, чем базовый, как предполагается, больше?» Ответ: Нет. Размер скобки обозначает разнообразие разрешенных объектов, например, диаграмму Венна. Набор Млекопитающих меньше, чем Набор Животных. Точно так же, f (Млекопитающее) меньше, чем f (Животное), поскольку оно поддерживает только меньший набор объектов. (т.е. функция, которая обрабатывает млекопитающих, не будет обрабатывать всех животных, но функция, которая обрабатывает животных, всегда может обрабатывать млекопитающих). Следовательно, отношение инвертируется, так как вместо f (млекопитающее) можно передать f (животное), что делает его контравариантным.

1 голос
/ 26 декабря 2009

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

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

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

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

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

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

...