Выполнение 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();
}
}