6

On the msdn page on contravariance I find a quite interesting example that shows "benefits of contravariance in IComparer"

First they use a fairly odd base & derived classes:

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class Employee : Person { }

I can already say that its a bad example cause no class ever just inherits a base class without adding at least a little something of its own.

Then they create a simple IEqualityComparer class

class PersonComparer : IEqualityComparer<Person>
{
    public bool Equals(Person x, Person y)
    {            
        ..
    }
    public int GetHashCode(Person person)
    {
       ..
    }
}

Next the example in question goes.

List<Employee> employees = new List<Employee> {
               new Employee() {FirstName = "Michael", LastName = "Alexander"},
               new Employee() {FirstName = "Jeff", LastName = "Price"}
            };

IEnumerable<Employee> noduplicates = 
employees.Distinct<Employee>(new PersonComparer());

Now my question - first of all in this case Employee is an unneeded class, its true that it can use PersonComparer for this situation because it is in fact just a person class!

In real world however Employee will have at least one new field, lets say a JobTitle. Given that its pretty clear that when we want distint Employees we would need to take that JobTitle field in mind for comparison, and its pretty clear that Contravariant Comparer such as person comparer isn't suited for that job, cause it cannot know any new members Employee has defined.

Now of course any language feature even a very odd one could have its uses, even if its illogical for some situation, but in this case I think it won't be useful far too often to be a default behavior. In fact it appears to me as we are breaking type safety a little bit, when a method expects a Employee comparer we can in fact put in a person or even object comparer and it will compile with no problems. While its hard to imagine our default scenario would be to treat Employee like an object..or basic Person.

So is it really a good contravariance for default for those interfaces?

EDIT: I understand what contravariance and covariance is. I am asking why those comparing interfaces were changed to be contravariant on default.

Valentin Kuzub
  • 11,703
  • 7
  • 56
  • 93
  • 2
    Try to editorialize less when you ask questions. – jason Jun 22 '11 at 02:35
  • I keep getting misunderstood when I miss something in question. When its clear its hard not to see what I am asking about I think. For example you answer about covariance when I am clearly asking about contravariance. – Valentin Kuzub Jun 22 '11 at 02:38
  • 2
    I am a little affected by that comment , when you try upvote it says "the question shows research effort, it is useful and clear". I try to follow those guidelines. When I look at question I cannot see much "editorialization" , you can edit question Jason to remove that. I trust your judgement. – Valentin Kuzub Jun 22 '11 at 02:55

3 Answers3

6

The definition of contravariant is the following. A map F from types to types mapping T to F<T> is contravariant in T if whenever U and V are types such that every object of type U can be asssigned to a variable of type V, every object of type F<V> can be assigned to a variable of type F<U> (F reverses assignment compatibility).

In particular, if T -> IComparer<T> then note that a variable of type IComparer<Derived> can receive an object implementing IComparer<Base>. This is contravariance.

The reason that we say that IComparer<T> is contravariant in T is because you can say

class SomeAnimalComparer  : IComparer<Animal> { // details elided }

and then:

IComparer<Cat> catComparer = new SomeAnimalComparer();

Edit: You say:

I understand what contravariance and covariance is. I am asking why those comparing interfaces were changed to be contravariant on default.

Changed? I mean, IComparer<T> is "naturally" contravariant. The definition of IComparer<T> is:

 public interface IComparer<T> {
     int Compare(T x, T y);
 }

Note that T only appears in an "in" position in this interface. That is, there are no methods that return instances of T. Any such interface is "naturally" contravariant in T.

Given this, what reason do you have for not wanting to make it contravariant? If you have an object that knows how to compare instances of U, and V is assignment compatible to U, why shouldn't you also be able to think of this object as something that knows how to compare instances of V? This is what contravariance allows.

Before contravariance you would have to wrap:

 class ContravarianceWrapperForIComparer<U, V> : IComparer<V> where V : U {
      private readonly IComparer<U> comparer;
      public ContravarianceWrapperForIComparer(IComparer<U> comparer) {
          this.comparer = comparer;
      }
      public int Compare(V x, V y) { return this.comparer.Compare(x, y); }
 }

And then you could say

class SomeUComparer : IComparer<U> { // details elided }

IComparer<U> someUComparer = new SomeUComparer();
IComparer<V> vComparer = 
    new ContravarianceWrapperForIComparer<U, V>(someUComparer);

Contravariance allows you to skip these incantations and just say

IComparer<V> vComparer = someUComparer;

Of course, the above was only when V : U. With contravariance you can do it whenever U is assignment compatible from V.

jason
  • 236,483
  • 35
  • 423
  • 525
  • I understand covariance so that part is not regarding the question. When you say that it knows how to compare two Cats - it has no idea how to compare two Cats. It can only Compare animals. So when we want to compare cats it will be comparing them as animals.. – Valentin Kuzub Jun 22 '11 at 02:37
  • @Valentin, you seem to be under the impression that this contravariance example is making you use a less specific comparer than you would desire. It's doing no such thing. It's *allowing you to choose to use such a comparer.* You can continue to use whatever comparison logic you deem appropriate for the given problem. – Anthony Pegram Jun 22 '11 at 02:39
  • Hey Jason. See my Edit in question too. I clearly understand contravariance, and what it means for us here when interfaces in question were changed to support that. My question however is not about what it is , but about whether such change in those interfaces is really benefitial for developers , its pretty narrow question. I did try to be clear about this too.. I also thought there couldve been a chance I didn't understand result of this change clear but it appears I did. – Valentin Kuzub Jun 22 '11 at 03:34
  • The fact that a generic type parameter T only appears in "input positions" does not imply that the interface is contravariant with respect to T; it just means that the interface itself would be legal if T were declared contravariant, not that the interface promises contravariance. If it's likely that many implementations of IFoo that would crash if given an IFoo, the IFoo should be invariant with regard to , even if IFoo itself only uses T as an input parameter. – supercat Jul 15 '11 at 19:23
4

This question comes off more as a rant, but let's back up a moment and talk about the comparer.

The IEqualityComparer<T> is useful when you need to override whatever default equality comparer is available for the object. It could be using its own equality logic (overriding Equals and GetHashCode), it could be using a default referential equality, whatever. The point is you don't want whatever its default is. IEqualityComparer<T> allows you to specify precisely what you wish to use for equality. And it lets you define as many different ways as you need to solve your many different problems you might have.

One of those many different problems might just happen to be solvable by a comparer that already exists for a lesser derived type. That's all that's happening here, you have the ability to supply the comparer that solves the problem you need to be solved. You can use the more generic comparer while having a more derived collection.

In this problem, you're saying "it's OK to compare on just the base properties but it's not OK for me to put a lesser derived object (or sibling) into the collection."

Anthony Pegram
  • 123,721
  • 27
  • 225
  • 246
  • could you elaborate last sentence. I think your answer might be closest to actual thought process behind that decision, but I really dont understand what you mean when you say its not ok for me to put a lesser derived object into the collection.(regarding contravariance) – Valentin Kuzub Jun 22 '11 at 03:01
  • 2
    @Valentin, I simply was stating what the rules of the example conveys. I have a `List`. By definition, I cannot add a `Person`, a `Student`, or an `Owner` to the list. I can add only `Employee` objects. *I want this restriction.* However, for the specific problem at hand, *I do not care about the unique properties of `Employee` when I compare these items. So any old comparer of `Person` will do.* This is not true in all scenarios, certainly, but it's true in *this* one. – Anthony Pegram Jun 22 '11 at 03:53
3

An Employee IS a Person. Since the comparator requires a super class of Person then you ensure that these elements CAN be compared as long as they extend Person. The idea is that Person's should always be comparable to Person's. A perfect example of why this is is the case:

If we have Person.ssn, that should be compared in the .equals() method. Because we compare the ssn's and we are ensured that ssn's are unique per every person; then it doesn't matter that the Employee is a Manager, or a Fry Cook, since we have ensured that they are the same.

Now, if you have can have multiple Person's with the same SSN and different Employee attributes; then you should consider not making Person being the contravariant type and making Employee the widest type acceptable.

Contravariance helps to program to the interface and not have know every possible implementation of an interface. This of course allows for better extensibility and creating new instantiations of an interface to expand the functionality of your program.

Contravariant array types also help us to create nested classes that extend the interface and pass those back. For example, if we create an array of Employee's and we don't necessarily want to expose Employee to the res of the world; we can send back an array of Persons; and because of Contravariance, we can actually send back the array of Employee's as an array of Persons.

Suroot
  • 4,315
  • 1
  • 22
  • 28
  • I thought that if person class in example had SSN (or passport number or something really person specific). It could be a good example. But then again, its a rare case when we have some kind of ID field in base class. In general case we don't ( I hope you agree). As I say in question this is a default behavior for general case and I ask whether its good, or whether it could be a source of problems quite often? – Valentin Kuzub Jun 22 '11 at 02:47
  • This all depends on what you are using to compare the equality between two objects. If an ID is not available or is setup in a subclass (i.e. you have employee_id in the Employee class) you can do this one of two ways. Either A) you make Employee the base subclass (you cannot pass in a person to compare). B) You create an abstract method to get the ID value (let the subclass implement the ID). – Suroot Jun 22 '11 at 02:57