Выполнение Distinct () с использованием базового класса IEqualityComparer и по-прежнему возвращает дочерний тип класса?

у меня есть несколько классов, производных от класса BaseClass здесь BaseClass просто имеет свойство Id.

теперь мне нужно сделать разные коллекции некоторых из этих объектов. У меня есть следующий код снова и снова для каждого из классов детей:

public class PositionComparer : IEqualityComparer<Position>
{
    public bool Equals(Position x, Position y)
    {
        return (x.Id == y.Id);
    }

    public int GetHashCode(Position obj)
    {
        return obj.Id.GetHashCode();
    }
}

учитывая, что логика основана только на Id, Я хотел создать один компаратор для уменьшения дублирования:

public class BaseClassComparer : IEqualityComparer<BaseClass>
{
    public bool Equals(BaseClass x, BaseClass y)
    {
        return (x.Id == y.Id);
    }

    public int GetHashCode(BaseClass obj)
    {
        return obj.Id.GetHashCode();
    }
}

но это, кажется, не compile:

  IEnumerable<Position> positions = GetAllPositions();
  positions = allPositions.Distinct(new BaseClassComparer())

...как говорится, он не может конвертировать из BaseClass to Position. Почему компаратор заставляет возвращаемое значение this Distinct() звонок?

5 ответов


Если вы посмотрите на определение Distinct задействован только один параметр универсального типа (и не один TCollection, используемый для входных и выходных коллекций, и один TComparison для компаратора). Это означает, что ваш BaseClassComparer ограничивает тип результата базовым классом, и преобразование при назначении невозможно.

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

public class GenericComparer<T> : IEqualityComparer<T> where T : BaseClass
{
    public bool Equals(T x, T y)
    {
        return x.Id == y.Id;
    }

    public int GetHashCode(T obj)
    {
        return obj.Id.GetHashCode();
    }
}

поскольку вам нужен экземпляр, а не только вызов метода, вы не можете позволить компилятору вывести общий тип (просмотреть это обсуждение), но должны сделать это при создании экземпляра:

IEnumerable<Position> positions;
positions = allPositions.Distinct(new GenericComparer<Position>());

Эрика объясняет основную причину всей проблемы (с точки зрения ковариации и контравариации).


UPDATE: этот вопрос был тема моего блога в июле 2013 года. Спасибо за отличный вопрос!


вы обнаружили неудачный случай ребра в алгоритме вывода универсального типа метода. У нас есть:

Distinct<X>(IEnumerable<X>, IEqualityComparer<X>)

где интерфейсы:

IEnumerable<out T> -- covariant

и

IEqualityComparer<in T> -- contravariant

когда мы делаем вывод из allPositions to IEnumerable<X> мы говорим:"IEnumerable<T> ковариантно в T, поэтому мы можем принять Position или любой более крупный тип. (Базовый тип "больше" производного типа; в мире больше животных, чем жирафов.)

когда мы делаем вывод из компаратора, мы говорим:"IEqualityComparer<T> является контравариантным в T, поэтому мы можем принять BaseClass или меньше типа."

Итак, что происходит, когда приходит время фактически вывести аргумент типа? У нас есть два кандидата: Position и BaseClass. оба удовлетворяют заявленным границам. Position удовлетворяет первой границе, потому что она идентична первой границе, и удовлетворяет второй границе, потому что она меньше второй границы. BaseClass удовлетворяет первой границе, потому что она больше первой границы и идентична второй границе.

у нас есть два победителя. Нам нужен тай-брейк. Что нам делать в этой ситуации?

это был вопрос некоторых дебатов, и есть аргументы с трех сторон: выберите более конкретный из типов, выберите более общие из типов или имеют тип вывода fail. Я не буду пересказывать весь аргумент, но достаточно сказать, что "более общие" сторона одержала победу.

(что еще хуже, в спецификации есть опечатка, которая говорит, что" выбрать более конкретное " - это правильная вещь! Это было результатом ошибки редактирования в процессе проектирования, что никогда не была исправлена. Компилятор реализует "выбрать более общее". Я напомнил Мэдс об ошибке и надеюсь, это будет исправлено в спецификации C# 5.)

Итак, поехали. В этом случае вывод типа выбирает более общий тип и делает вывод, что вызов означает Distinct<BaseClass>. Вывод типа никогда не принимает тип возвращаемого во внимание, и это, конечно, не брать чему присваивается выражение во внимание, поэтому тот факт, что он выбирает тип, несовместимый с переменной assigned-to, не является бизнесом.

мой совет в этом случае необходимо явно указать аргумент type.


представьте, если бы у вас было:

var positions = allPositions.Distinct(new BaseClassComparer());

что-то типа positions быть? Как компилятор выводит из аргумента, данного Distinct, который реализует IEqualityComparer<BaseClass> тип выражения IEnumerable<BaseClass>.

этот тип не может быть автоматически преобразован в IEnumerable<Position> поэтому компилятор выдает ошибку.


С IEqualityComparer<T> является контравариантным в тип T, вы можете использовать компаратор базового класса с distinct, если вы укажете общий параметр Distinct:

IEnumerable<Position> distinct = positions.Distinct<Position>(new BaseClassComparer());

если вы не укажете это, компилятор выведет тип T на BaseClass С BaseClassComparer осуществляет IEqualityComparer<BaseClass>.


вам нужны небольшие изменения в коде. Ниже рабочий пример:

public class BaseClass
{
    public int Id{get;set;}
}

public class Position : BaseClass
{
    public string Name {get;set;}
}
public class Comaprer<T> : IEqualityComparer<T>
    where T:BaseClass
{

    public bool Equals(T x, T y)
    {
        return (x.Id == y.Id);
    }

    public int GetHashCode(T obj)
    {
        return obj.Id.GetHashCode();
    }
}
class Program
{
    static void Main(string[] args)
    {
        List<Position> all = new List<Position> { new Position { Id = 1, Name = "name 1" }, new Position { Id = 2, Name = "name 2" }, new Position { Id = 1, Name = "also 1" } };
        var distinct = all.Distinct(new Comaprer<Position>());

        foreach(var d in distinct)
        {
            Console.WriteLine(d.Name);
        }
        Console.ReadKey();
    }
}