5

在有关逆变的msdn 页面上,我发现了一个非常有趣的示例,它显示了“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. 鉴于很明显,当我们想要区分员工时,我们需要考虑 JobTitle 字段进行比较,并且很明显,逆变比较器(例如人员比较器)不适合该工作,因为它不知道任何新成员员工已定义。

当然,任何语言特性,即使是一个非常奇怪的特性,都可以有它的用途,即使它在某些情况下是不合逻辑的,但在这种情况下,我认为它不会太频繁地成为默认行为。事实上,在我看来,因为我们有点破坏类型安全,当一个方法需要一个 Employee 比较器时,我们实际上可以放入一个人甚至对象比较器,它会毫无问题地编译。虽然很难想象我们的默认方案是将 Employee 视为对象..或基本 Person。

那么这些接口的默认逆变器真的是一个很好的逆变器吗?

编辑:我明白什么是逆变和协方差。我在问为什么那些比较接口被更改为默认逆变。

4

3 回答 3

6

逆变器的定义如下。F从类型到类型映射到T的映射在if和is 类型中F<T>是逆变的,使得每个类型的对象都可以分配给一个类型的变量,每个类型的对象都可以分配给一个类型的变量(反转分配兼容性)。TUVUVF<V>F<U>F

特别是,如果T -> IComparer<T>then 请注意,类型的变量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);

逆变允许你跳过这些咒语,然后说

IComparer<V> vComparer = someUComparer;

当然,以上只是当V : U。有了逆变,你可以在U赋值兼容的时候做到这一点V

于 2011-06-22T02:34:42.533 回答
4

这个问题更像是咆哮,但让我们回过头来谈谈比较器。

IEqualityComparer<T>当您需要覆盖对象可用的任何默认相等比较器时,这很有用。它可以使用自己的相等逻辑(覆盖 Equals 和 GetHashCode),也可以使用默认的引用相等,等等。关键是你不想要它的默认值。 IEqualityComparer<T>允许您精确指定您希望用于相等的内容。它让您可以根据需要定义尽可能多的不同方法来解决您可能遇到的许多不同问题。

许多不同的问题之一可能恰好可以通过一个已经存在的较小派生类型的比较器来解决。这就是这里发生的一切,您有能力提供解决您需要解决的问题的比较器。您可以使用更通用的比较器,同时拥有更多派生的集合。

在这个问题中,您说“只比较基本属性是可以的,但我不能将较小的派生对象(或兄弟对象)放入集合中。”

于 2011-06-22T02:33:45.003 回答
3

员工就是一个人。由于比较器需要 Person 的超类,因此只要这些元素扩展 Person,您就可以确保这些元素可以进行比较。这个想法是 Person's 应该始终与 Person's 具有可比性。一个完美的例子说明为什么会这样:

如果我们有 Person.ssn,则应该在 .equals() 方法中进行比较。因为我们比较了 ssn,我们确保每个人的 ssn 都是独一无二的;那么员工是经理或油炸厨师并不重要,因为我们已确保他们是相同的。

现在,如果您可以拥有多个具有相同 SSN 和不同 Employee 属性的 Person;那么你应该考虑不要让 Person 成为逆变类型,并使 Employee 成为可接受的最广泛的类型。

逆变有助于对接口进行编程,而不需要了解接口的所有可能实现。这当然允许更好的可扩展性并创建接口的新实例以扩展程序的功能。

逆变数组类型还可以帮助我们创建扩展接口并将其传回的嵌套类。例如,如果我们创建了一个 Employee 数组,并且我们不一定希望将 Employee 暴露给世界各地;我们可以发回一个 Person 数组;并且由于逆变,我们实际上可以将 Employee 数组作为 Persons 数组返回。

于 2011-06-22T02:35:55.687 回答