关于逆转的msdn page我发现了一个非常有趣的例子,显示了“IComparer逆变的好处”
首先他们使用一个相当奇怪的基础&派生类:
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class Employee : Person { }
我已经可以说它是一个糟糕的例子,因为没有类只是继承一个基类而不至少添加一些自己的东西。
然后他们创建一个简单的IEqualityComparer类
class PersonComparer : IEqualityComparer<Person>
{
public bool Equals(Person x, Person y)
{
..
}
public int GetHashCode(Person person)
{
..
}
}
接下来讨论的例子就是。
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());
现在我的问题 - 首先在这种情况下,Employee是一个不需要的类,它确实可以使用PersonComparer来处理这种情况,因为它实际上只是一个人类!
在现实世界中,Employee
至少会有一个新字段,比方说JobTitle
。鉴于我们非常清楚,当我们需要distint Employees时,我们需要考虑JobTitle字段进行比较,并且很明显Contravariant Comparer(例如人员比较器)不适合该工作,因为它不能知道任何新成员员工已定义。
当然,任何语言功能,即使是非常奇怪的语言功能也可能有其用途,即使它在某种情况下不合逻辑,但在这种情况下,我认为它常常不会成为默认行为。事实上,在我看来,因为我们正在打破类型安全性,当一个方法需要一个Employee比较器时,我们实际上可以放入一个人甚至是对象比较器,它将编译没有问题。虽然我们很难想象我们的默认场景是将Employee视为一个对象......或者基本的人。
那么这些接口的默认是否真的是一个很好的逆转?
编辑:我理解逆变和协方差是什么。我问为什么那些比较接口在默认情况下被改为逆变。
答案 0 :(得分:6)
逆变的定义如下。如果F
和T
类型为每个对象,那么从F<T>
到T
的不同类型的地图U
在V
中是逆变的类型U
的类型可以分配给V
类型的变量,类型F<V>
的每个对象都可以分配给F<U>
类型的变量(F
反转赋值兼容性)。
特别是,如果T -> IComparer<T>
,请注意IComparer<Derived>
类型的变量可以接收实现IComparer<Base>
的对象。这是相反的。
我们在IComparer<T>
中说T
是逆变的原因是因为你可以说
class SomeAnimalComparer : IComparer<Animal> { // details elided }
然后:
IComparer<Cat> catComparer = new SomeAnimalComparer();
编辑:你说:
我理解逆变和协方差是什么。我问为什么那些比较接口在默认情况下被改为逆变。
改变?我的意思是,IComparer<T>
是“自然的”逆变。 IComparer<T>
的定义是:
public interface IComparer<T> {
int Compare(T x, T y);
}
请注意,T
仅出现在此界面的“in”位置。也就是说,没有返回T
实例的方法。 任何此类界面在T
中都是“自然”逆变。
鉴于此,你有什么理由不想让它逆变?如果您有一个知道如何比较U
实例的对象,并且V
赋值与U
兼容,为什么您也不能将此对象视为知道如何比较V
的实例?这就是逆变所允许的。
在逆转之前你必须包装:
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); }
}
然后你可以说
class SomeUComparer : IComparer<U> { // details elided }
IComparer<U> someUComparer = new SomeUComparer();
IComparer<V> vComparer =
new ContravarianceWrapperForIComparer<U, V>(someUComparer);
Contravariance允许你跳过这些咒语,然后说
IComparer<V> vComparer = someUComparer;
当然,上述内容仅适用于V : U
。如果U
与[{1}}分配兼容,则可以使用相反的方法。
答案 1 :(得分:3)
这个问题更像是一个咆哮,但让我们回顾一下并讨论比较器。
当您需要覆盖对象可用的任何默认相等比较器时,IEqualityComparer<T>
非常有用。它可以使用自己的相等逻辑(重写Equals和GetHashCode),它可以使用默认的引用相等,无论如何。关键是你不想要它的默认值。 IEqualityComparer<T>
允许你准确地指定你想要用于平等的东西。它可以让您定义尽可能多的不同方式来解决您可能遇到的许多不同问题。
对于较小的派生类型,已经存在的比较器可能恰好解决了许多不同问题中的一个。这就是这里发生的一切,你有能力提供解决你需要解决的问题的比较器。您可以使用更通用的比较器,同时拥有更多派生的集合。
在这个问题中,你说“可以只比较基本属性,但我不能将较小的派生对象(或兄弟)放入集合。” / p>
答案 2 :(得分:2)
员工是个人。由于比较器需要超类Person,因此只要它们扩展Person,就可以确保可以比较这些元素。这个想法是人的应该总是与人的相当。这就是原因的一个完美例子:
如果我们有Person.ssn,那应该在.equals()方法中进行比较。因为我们比较了ssn,我们确保ssn对每个人都是独一无二的;然后,员工是经理或弗莱库克并不重要,因为我们确保他们是相同的。
现在,如果你有多个具有相同SSN和不同Employee属性的Person;那么你应该考虑不要使Person成为逆变型,并使Employee成为最广泛的类型。
Contravariance有助于对接口进行编程,并且不了解接口的每个可能实现。这当然允许更好的可扩展性并创建接口的新实例化以扩展程序的功能。
Contravariant数组类型还有助于我们创建扩展接口并将其传递回去的嵌套类。例如,如果我们创建一个Employee数组,我们不一定要将Employee暴露给世界的res;我们可以寄回一组人员;而且由于反演,我们实际上可以将Employee的数组作为一组人员发回。