开发者

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<Mammal> mammalList = Enumerable.Empty<Mamm开发者_StackOverflowal>();
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;

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<out T>
{
    void Process(T val);
}


Update: Ooops. As it turned out, I mixed up variance and "assignment compatibility" in my initial answer. Edited the answer accordingly. Also I wrote a blog post that I hope should answer such questions better: Covariance and Contravariance FAQ

Answer: I guess the answer to your first question is that you don't have contravariance in this example:

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

Furthermore, you don't even have covariance here. What you have is called "assignment compatibility", which means that you can always assign an instance of a more derived type to an instance of a less derived type.

In C#, variance is supported for arrays, delegates, and generic interfaces. As Eric Lippert said in his blog post What's the difference between covariance and assignment compatibility? is that it's better to think about variance as "projection" of types.

Covariance is easier to understand, because it follows the assignment compatibility rules (array of a more derived type can be assigned to an array of a less derived type, "object[] objs = new string[10];"). Contravariance reverses these rules. For example, imagine that you could do something like "string[] strings = new object[10];". Of course, you can't do this because of obvious reasons. But that would be contravariance (but again, arrays are not contravariant, they support covariance only).

Here are the examples from MSDN that I hope will show you what contravariance really means (I own these documents now, so if you think something is unclear in the docs, feel free to give me feedback):

  1. Using Variance in Interfaces for Generic Collections

    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. Using Variance in Delegates

    // 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. Using Variance for Func and Action Generic Delegates

     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;
    

Hope this helps.


Covariance and Contravariance are not things you can observe when instancing classes. Thus it is wrong to speak about one of them when looking at a simple class instantiation, like in your example: Animal someAnimal = new Giraffe(); //covariant operation

These terms do not classify operations. The terms Covariance, Contravariance and Invariance describe the relationship between certain aspects of classes and their subclasses.

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).

We generally regard the following aspects, when talking about Cov., Contrav. and Inv.:

  • Methods
    • Parameter types
    • Return types
    • Other signature related aspects like thrown exceptions.
  • Generics

Let us have a look at a few examples to get a better understanding of the terms.

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) }
In both cases, "method" gets overridden! Further, the above examples are the only legal occurrences of Cov. and Contrav. in object oriented languages.:

  • Covariance - Return types and exception throw statements
  • Contravariance - Input parameters
  • Invariance - Input and Output parameters

Let us have a look at some counter examples to better understand the above list:

//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).

This topic is so sophisticated, that I could go on for a very long time. I advise you to check Cov. and Contrav. of Generics by yourself. Further, you need to know how dynamic binding works to fully understand the examples (which methods get exactly called).

The terms arose from the Liskov substitution principle, which defines necessary criteria for modelling a data type as a sub type of another one. You might also want to investigate it.


My understanding is that it is not subtype relationships which are co/contra-variant but rather operations (or projections) between those types (such as delegates and generics). Therefore:

Animal someAnimal = new Giraffe();

is not co-variant, but rather this is just assignment compatibility since the type Giraffe is 'smaller than' the type Animal. Co/contra-variance becomes an issue when you have some projection between these types, such as:

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

This is not valid in C#3, however it should be possible since a sequence of giraffes is a sequence of animals. The projection T -> IEnumerable<T> preserves the 'direction' of the type relationship since Giraffe < Animal and IEnumerable<Giraffe> < IEnumerable<Animal> (note that assignment requires that the type of the left-hand side is at least as wide as the right).

Contra-variance reverses the type relationship:

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

This is also not legal in C#3, but it should be since any action taking an animal can cope with being passed a Giraffe. However, since Giraffe < Animal and Action<Animal> < Action<Giraffe> the projection has reversed the type relationships. This is legal in C#4.

So to answer the questions in your example:

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


Look at it this way: If I have a function func that deals with Subtype Mammal, of the form Mammal m = Func(g(Mammal)), I can swap out Mammal with something that encompasses Mammal, which here is the Base Animal.

In terms of a sporting analogy to understand the below image, you can catch a ball with your bare hands like in Cricket, but it's also possible (and easier) to catch a ball using Baseball gloves.

What you see on the left is covariance, what you see inside the parameter part is contravariance.

Contravariance explained

You may wonder "Why is the left green curve bigger than the red curve? Isn't the Subtype which usually does more than the basetype supposed to be bigger?" Answer: No. The size of the bracket denotes the variety of objects allowed, like a Venn diagram. A Set of Mammal is smaller than Set Animal. Similarly, f(Mammal) is smaller than f(Animal) as it only supports a smaller set of objects. (i.e a function that handles Mammals won't handle all Animals, but a function that handles Animals can always handle a Mammal). Hence, the relationship is inverted as f(animal) can be passed in instead of f(mammal) thereby making it contravariant.


(Edited in response to comments)

This MSDN article on the topic described covariance and contravariance as it applies to matching a function to a delegate. A variable of the delegate type:

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

could (because of contravariance) be populated with the function:

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

From my reading, it doesn't have to do with calling the function directly, but matching functions with delegates. I'm not sure that it can be boiled down to the level you demonstrate, with individual variables or object assignments being contravariant or covariant. But the assignment of a delegate uses contravariance or covariance in a way that makes sense to me according to the linked article. Because the delegate's signature contains more derived types than the actual instance, this is referred to as "contravariance", something separate from "covariance" in which a delegate's return type is less derived than the actual instance.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜