Преимущества контравариантности в интерфейсах IComparer и IEqualityComparer - PullRequest
5 голосов
/ 22 июня 2011

На странице 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 isненужный класс, это правда, что он может использовать PersonComparer для этой ситуации, потому что на самом деле это просто класс person!

Однако в реальном мире Employee будет иметь по крайней мере одно новое поле, скажем, JobTitle.Учитывая, что совершенно очевидно, что, когда мы хотим получить удаленных сотрудников, нам нужно учитывать это поле JobTitle для сравнения, и совершенно ясно, что Contravariant Comparer, например, Person Comparer, не подходит для этой работы, потому что он не может знать новых членов.Сотрудник определил.

Теперь, конечно, любая языковая функция, даже очень странная, может быть использована, даже если она нелогична для некоторой ситуации, но в этом случае я думаю, что она будет слишком полезна, чтобы быть поведением по умолчанию,На самом деле, мне кажется, что мы немного нарушаем безопасность типов, когда метод ожидает компаратора Employee, который мы можем фактически вставить в компаратор для человека или даже объекта, и он будет компилироваться без проблем.Хотя трудно представить, что наш сценарий по умолчанию будет относиться к Employee как к объекту ... или к базовой персоне.

Так действительно ли это хорошая противоположность по умолчанию для этих интерфейсов?

РЕДАКТИРОВАТЬ: Я понимаю, что такое контравариантность и ковариантность.Я спрашиваю, почему эти сравниваемые интерфейсы были изменены по умолчанию на контравариантные.

Ответы [ 3 ]

6 голосов
/ 22 июня 2011

Определение контравариантности следующее.Карта 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);

Контравариантность позволяет пропустить эти заклинанияи просто скажите

IComparer<V> vComparer = someUComparer;

Конечно, выше было только тогда, когда V : U.С контравариантностью вы можете делать это, когда U совместимо с назначением от V.

3 голосов
/ 22 июня 2011

Этот вопрос звучит скорее как пустяк, но давайте вернемся немного назад и поговорим о компараторе.

IEqualityComparer<T> полезен, когда вам нужно переопределить любой доступный для сравнения объект сравнения по умолчанию. Он может использовать собственную логику равенства (переопределяя Equals и GetHashCode), он может использовать ссылочное равенство по умолчанию, что угодно. Дело в том, что вам не нужно, какое бы значение по умолчанию было. IEqualityComparer<T> позволяет вам точно указать, что вы хотите использовать для равенства. И это позволяет вам определять столько разных способов, сколько вам нужно, чтобы решить множество разных проблем, которые могут у вас возникнуть.

Одна из многих проблем может быть решена компаратором, который уже существует для менее производного типа. Это все, что здесь происходит, у вас есть возможность предоставить компаратор, который решает проблему, которую нужно решить. Вы можете использовать более общий компаратор, имея более производную коллекцию.

В этой задаче вы говорите: « все в порядке, сравнивать только базовые свойства, но я не могу помещать в коллекцию меньший производный объект (или родственный элемент). *

2 голосов
/ 22 июня 2011

Сотрудник - это человек. Поскольку для компаратора требуется суперкласс Person, вы гарантируете, что эти элементы МОГУТ сравниваться до тех пор, пока они расширяют Person. Идея заключается в том, что личности всегда должны быть сопоставимы с личностью. Прекрасный пример того, почему это так:

Если у нас есть Person.ssn, это следует сравнить в методе .equals (). Потому что мы сравниваем ssn, и мы уверены, что ssn уникальны для каждого человека; тогда не имеет значения, что Сотрудник - Менеджер или Фрай Кук, поскольку мы убедились, что они одинаковы.

Теперь, если у вас есть несколько человек с одним SSN и разными атрибутами сотрудников; тогда вам следует подумать о том, чтобы не делать Лицо контравариантным типом, а сотрудника - самым приемлемым типом.

Contravariance помогает программировать интерфейс и не знает всех возможных реализаций интерфейса. Это, конечно, обеспечивает лучшую расширяемость и создание новых экземпляров интерфейса для расширения функциональности вашей программы.

Контравариантные типы массивов также помогают нам создавать вложенные классы, которые расширяют интерфейс и передают их обратно. Например, если мы создаем массив Employee's, и мы не обязательно хотим показывать Employee остальному миру; мы можем отправить обратно массив Persons; и из-за Contravariance мы можем на самом деле отправить обратно массив Employee в виде массива Persons.

...