IComparer和IEqualityComparer接口中逆变的好处

Val*_*zub 5 c# contravariance c#-4.0

在逆向变换的msdn页面上,我找到了一个非常有趣的例子,显示了"IComparer逆变的好处"

首先,他们使用相当奇怪的基础和派生类:

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

public class Employee : Person { }
Run Code Online (Sandbox Code Playgroud)

我已经可以说它是一个糟糕的例子,因为没有类只是继承一个基类而不至少添加一些自己的东西.

然后他们创建一个简单的IEqualityComparer类

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

接下来是有问题的例子.

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());
Run Code Online (Sandbox Code Playgroud)

现在我的问题 - 首先在这种情况下,Employee是一个不需要的类,它确实可以使用PersonComparer来实现这种情况,因为它实际上只是一个人类!

但是在现实世界中Employee,至少会有一个新的领域,比如说JobTitle.鉴于我们非常清楚,当我们需要distint Employees时,我们需要考虑JobTitle字段进行比较,并且很明显Contravariant Comparer(例如人员比较器)不适合该工作,因为它不能知道任何新成员员工已定义.

当然,任何语言功能,即使是非常奇怪的功能,也可能有其用途,即使它在某种情况下不合逻辑,但在这种情况下,我认为它常常不会成为默认行为.事实上,在我看来,因为我们正在打破类型安全性,当一个方法需要一个Employee比较器时,我们实际上可以放入一个人甚至是对象比较器,它将编译没有任何问题.虽然我们很难想象我们的默认场景是将Employee视为一个对象......或者基本的人.

那么这些接口的默认是否真的是一个很好的逆转?

编辑:我理解逆变和协方差是什么.我问为什么那些比较接口在默认情况下被改为逆变.

jas*_*son 6

逆变的定义如下.的地图F从类型类型的映射TF<T>是逆变的在T如果每当UV使得类型的每个对象类型U可以asssigned到类型的变量V,类型的每个对象F<V>可以被分配给类型的变量F<U>(F反转分配兼容性).

特别是,如果T -> IComparer<T>那么注意类型的变量IComparer<Derived>可以接收实现的对象IComparer<Base>.这是相反的.

我们说这IComparer<T>是逆变的原因T是因为你可以说

class SomeAnimalComparer  : IComparer<Animal> { // details elided }
Run Code Online (Sandbox Code Playgroud)

然后:

IComparer<Cat> catComparer = new SomeAnimalComparer();
Run Code Online (Sandbox Code Playgroud)

编辑:你说:

我理解逆变和协方差是什么.我问为什么那些比较接口在默认情况下被改为逆变.

改变了吗?我的意思是,IComparer<T>"自然"逆变.定义IComparer<T>是:

 public interface IComparer<T> {
     int Compare(T x, T y);
 }
Run Code Online (Sandbox Code Playgroud)

请注意,T此界面中仅出现在"in"位置.也就是说,没有返回实例的方法T.任何这样的界面都是"自然的"逆变的T.

鉴于此,你有什么理由不想让它逆变?如果你有一个知道如何比较实例UV赋值兼容的对象,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); }
 }
Run Code Online (Sandbox Code Playgroud)

然后你可以说

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

IComparer<U> someUComparer = new SomeUComparer();
IComparer<V> vComparer = 
    new ContravarianceWrapperForIComparer<U, V>(someUComparer);
Run Code Online (Sandbox Code Playgroud)

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

IComparer<V> vComparer = someUComparer;
Run Code Online (Sandbox Code Playgroud)

当然,以上只是当时V : U.有了逆转,只要U赋值兼容,你就可以做到V.


Ant*_*ram 4

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

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

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

在这个问题中,您是说“仅比较基本属性是可以的,但将次要派生对象(或同级对象)放入集合中是不行的。”

  • @Valentin,我只是在说明示例的规则所传达的内容。我有一个“列表&lt;员工&gt;”。根据定义,我无法将“Person”、“Student”或“Owner”添加到列表中。我只能添加“Employee”对象。*我想要这个限制。*但是,对于当前的具体问题,*当我比较这些项目时,我不关心“Employee”的独特属性。因此,任何旧的“Person”比较器都可以。*当然,这并非在所有情况下都是如此,但在 *this* 场景中却是这样。 (2认同)