Объяснение контравариантности

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

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

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

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

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

Операция возврата здесь ковариантна, поскольку мы сохраняем размер, при котором оба Animal все еще больше, чем Mammal или Giraffe. В этой связи большинство операций возврата ковариантны, контравариантные операции не имеют смысла.

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

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

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

РЕДАКТИРОВАТЬ:

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

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

Конечно, это незаконно, потому что кто-то может передать любое Animal в someAction, тогда как ProcessMammal ожидает чего-нибудь, что относится к Mammal или более конкретному (меньше, чем Mammal). Вот почему someAction должно быть только Action или что-то более конкретное (Action)

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

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

person Stan R.    schedule 26.12.2009    source источник


Ответы (5)


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

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

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. Использование дисперсии для общих делегатов Func и Action

     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;
    

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

person Alexandra Rusina    schedule 30.12.2009
comment
@ Александра. Спасибо. Ваш последний пример наиболее показателен. Если бы это было наоборот для метода, который использовал Employee, вы не смогли бы назначить его Action ‹Person›. Это нарушило бы правила контравариантных параметров. Я думаю, это приводит меня к тому, что это контравариантное поведение, специфичное для типов Generic и Interface. - person Stan R.; 30.12.2009

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

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

Covariance
means that an aspect changes similar to the direction of inheritance.
Contravariance
means that an aspect changes opposite to the direction of inheritance.
Invariance
means that an aspect does not change from a class to its sub class(es).

Говоря о Cov., Contrav. и Инв .:

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

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

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

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

person Dave O.    schedule 30.12.2009

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

Animal someAnimal = new Giraffe();

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

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());
person Lee    schedule 26.12.2009
comment
Ли, я, конечно, понимаю разницу между совместимостью присваивания и ко / контрковариантностью. Введение промежуточного слоя, такого как делегат, конечно же, очень четко объясняет проблему. Можно было бы предположить, что действие ‹Mammal› можно было назначить Action ‹Animal›, но, конечно, это неверно, и только Action ‹Mammal› или меньше можно назначить Action ‹Mammal›. Да, этот вид слоя в середине ясно показывает контравариантность, но только потому, что есть слой ... мы говорим, что контравариантность возникает только тогда, когда вводится такой слой, как этот. - person Stan R.; 26.12.2009
comment
я хотел сказать со / против-дисперсию ... :), в любом случае я отредактировал свой вопрос. Также я верю, что совместимость присваивания ковариантна по своей природе, потому что ковариация сохраняет совместимость присваивания. Однако с точки зрения Action ‹Animal› = Action ‹Mammal›, эта совместимость присваивания не сохраняется, а фактически отменяется… что является контравариантностью. Даже действие ‹Animal› = Action ‹Mammal› по-прежнему совместимо с присваиванием (но только потому, что объявление его контравариантным не было бы). - person Stan R.; 26.12.2009

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

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

То, что вы видите слева, - это ковариация, а внутри части параметров - контравариантность.

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

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

person arviman    schedule 01.06.2017
comment
+1, этот вопрос был почти 7 лет назад, и с тех пор я понял его, но это может быть полезно для кого-то другого чтения. - person Stan R.; 21.06.2017

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

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

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

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

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

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

person BlueMonkMN    schedule 26.12.2009
comment
Это определенно не единственная область, к которой это применимо. Это все, что обсуждается в вашей статье, потому что она называется Ковариация и Контравариантность в делегатах. В нем ничего не упоминается точно так же, как в статье под названием «Спаривание у приматов» не говорится о том, как нерестятся лосося; это не значит, что они этого не делают. - person Adam Robinson; 26.12.2009
comment
Это по-прежнему полезная статья и полезный комментарий, независимо от того, правильно ли он понимает тему. - person Jim Schubert; 26.12.2009
comment
@Jim: Это полезный комментарий, но ковариация и контравариантность применимы только тогда, когда сопоставление функции с делегатом просто ложно и служит только для добавления путаницы (через дезинформацию) в уже ОЧЕНЬ широко неправильно понятую тему. - person Adam Robinson; 26.12.2009
comment
Статья, на которую вы ссылаетесь, взята из документации .NET 2.0. В то время было только расхождение в составе делегатов. Если вы посмотрите на версию этой статьи для .NET 4.0 (msdn.microsoft.com/en-us/library/ms173174 (VS.100) .aspx), вы увидите, что теперь он является частью более крупного набора документов, связанных с ковариацией и контравариантностью. - person Alexandra Rusina; 30.12.2009