Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Benefits of contravariance in IComparer & IEqualityComparer interfaces

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.

like image 900
Valentin Kuzub Avatar asked Jun 22 '11 02:06

Valentin Kuzub


2 Answers

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.

like image 102
jason Avatar answered Sep 22 '22 12:09

jason


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

like image 34
Anthony Pegram Avatar answered Sep 21 '22 12:09

Anthony Pegram