Контравариантность объяснила

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

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

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

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

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

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

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

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

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

//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 ответов

Решение

Обновление: Упс. Как оказалось, я перепутал дисперсию и "совместимость присваивания" в своем первоначальном ответе. Отредактировал ответ соответственно. Также я написал пост в блоге, который, я надеюсь, должен лучше ответить на такие вопросы: 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. Использование дисперсии для делегатов Func и Generic

     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;
    

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

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

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

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

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

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

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

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. и контрав. в объектно-ориентированных языках.:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Другие вопросы по тегам