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() {FirstNa开发者_运维技巧me = "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.
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
.
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."
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.
精彩评论