Можете ли вы создать простой "EqualityComparer <T>", используя выражение лямбда
ВАЖНО: ВОПРОС: НЕ ЛИЦО-SQL. Это LINQ для объектов.
Короткий вопрос:
Есть ли простой способ в LINQ для объектов получить отдельный список объектов из списка на основе свойства ключа для объектов.
Длинный вопрос:
Я пытаюсь сделать операцию Distinct()
в списке объектов, у которых есть ключ как один из их свойств.
class GalleryImage {
public int Key { get;set; }
public string Caption { get;set; }
public string Filename { get; set; }
public string[] Tags {g et; set; }
}
У меня есть список объектов Gallery
, содержащих GalleryImage[]
.
Из-за того, как работает веб-сервис [sic], у меня есть дубликаты
GalleryImage
объект. я подумал, что было бы просто использовать Distinct()
для получения отдельного списка.
Это запрос LINQ, который я хочу использовать:
var allImages = Galleries.SelectMany(x => x.Images);
var distinctImages = allImages.Distinct<GalleryImage>(new
EqualityComparer<GalleryImage>((a, b) => a.id == b.id));
Проблема заключается в том, что EqualityComparer
является абстрактным классом.
Я не хочу:
- реализовать IEquatable на
GalleryImage
, потому что он сгенерирован
- нужно написать отдельный класс для реализации
IEqualityComparer
как показанный здесь
Есть ли конкретная реализация EqualityComparer
где-то, что мне не хватает?
Я бы подумал, что будет простой способ получить "разные" объекты из набора, основанного на ключе.
Ответы
Ответ 1
(Здесь есть два решения - см. конец для второго):
My MiscUtil библиотека имеет класс ProjectionEqualityComparer
(и два поддерживающих класса, чтобы использовать вывод типа).
Вот пример его использования:
EqualityComparer<GalleryImage> comparer =
ProjectionEqualityComparer<GalleryImage>.Create(x => x.id);
Здесь код (комментарии удалены)
// Helper class for construction
public static class ProjectionEqualityComparer
{
public static ProjectionEqualityComparer<TSource, TKey>
Create<TSource, TKey>(Func<TSource, TKey> projection)
{
return new ProjectionEqualityComparer<TSource, TKey>(projection);
}
public static ProjectionEqualityComparer<TSource, TKey>
Create<TSource, TKey> (TSource ignored,
Func<TSource, TKey> projection)
{
return new ProjectionEqualityComparer<TSource, TKey>(projection);
}
}
public static class ProjectionEqualityComparer<TSource>
{
public static ProjectionEqualityComparer<TSource, TKey>
Create<TKey>(Func<TSource, TKey> projection)
{
return new ProjectionEqualityComparer<TSource, TKey>(projection);
}
}
public class ProjectionEqualityComparer<TSource, TKey>
: IEqualityComparer<TSource>
{
readonly Func<TSource, TKey> projection;
readonly IEqualityComparer<TKey> comparer;
public ProjectionEqualityComparer(Func<TSource, TKey> projection)
: this(projection, null)
{
}
public ProjectionEqualityComparer(
Func<TSource, TKey> projection,
IEqualityComparer<TKey> comparer)
{
projection.ThrowIfNull("projection");
this.comparer = comparer ?? EqualityComparer<TKey>.Default;
this.projection = projection;
}
public bool Equals(TSource x, TSource y)
{
if (x == null && y == null)
{
return true;
}
if (x == null || y == null)
{
return false;
}
return comparer.Equals(projection(x), projection(y));
}
public int GetHashCode(TSource obj)
{
if (obj == null)
{
throw new ArgumentNullException("obj");
}
return comparer.GetHashCode(projection(obj));
}
}
Второе решение
Чтобы сделать это только для Distinct, вы можете использовать расширение DistinctBy
в MoreLINQ:
public static IEnumerable<TSource> DistinctBy<TSource, TKey>
(this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector)
{
return source.DistinctBy(keySelector, null);
}
public static IEnumerable<TSource> DistinctBy<TSource, TKey>
(this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
IEqualityComparer<TKey> comparer)
{
source.ThrowIfNull("source");
keySelector.ThrowIfNull("keySelector");
return DistinctByImpl(source, keySelector, comparer);
}
private static IEnumerable<TSource> DistinctByImpl<TSource, TKey>
(IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
IEqualityComparer<TKey> comparer)
{
HashSet<TKey> knownKeys = new HashSet<TKey>(comparer);
foreach (TSource element in source)
{
if (knownKeys.Add(keySelector(element)))
{
yield return element;
}
}
}
В обоих случаях ThrowIfNull
выглядит следующим образом:
public static void ThrowIfNull<T>(this T data, string name) where T : class
{
if (data == null)
{
throw new ArgumentNullException(name);
}
}
Ответ 2
Основываясь на ответе Чарли Флауэр, вы можете создать свой собственный метод расширения, чтобы делать то, что хотите, которое внутри себя использует группировку:
public static IEnumerable<T> Distinct<T, U>(
this IEnumerable<T> seq, Func<T, U> getKey)
{
return
from item in seq
group item by getKey(item) into gp
select gp.First();
}
Вы также можете создать общий класс, полученный из EqualityComparer, но похоже, что вы хотели бы избежать этого:
public class KeyEqualityComparer<T,U> : IEqualityComparer<T>
{
private Func<T,U> GetKey { get; set; }
public KeyEqualityComparer(Func<T,U> getKey) {
GetKey = getKey;
}
public bool Equals(T x, T y)
{
return GetKey(x).Equals(GetKey(y));
}
public int GetHashCode(T obj)
{
return GetKey(obj).GetHashCode();
}
}
Ответ 3
Это лучшее, что я могу придумать для проблемы.
Все еще любопытно, есть ли хороший способ создать EqualityComparer
на лету, хотя.
Galleries.SelectMany(x => x.Images).ToLookup(x => x.id).Select(x => x.First());
Создайте таблицу поиска и возьмите "верх" от каждого
Примечание: это то же самое, что и @charlie, но с использованием ILookup, который, я думаю, является тем, что группа должна быть в любом случае.
Ответ 4
Вы можете группировать по значению ключа, а затем выбирать верхний элемент из каждой группы. Будет ли это работать для вас?
Ответ 5
Как насчет выброса IEqualityComparer
generic class?
public class ThrowAwayEqualityComparer<T> : IEqualityComparer<T>
{
Func<T, T, bool> comparer;
public ThrowAwayEqualityComparer<T>(Func<T, T, bool> comparer)
{
this.comparer = comparer;
}
public bool Equals(T a, T b)
{
return comparer(a, b);
}
public int GetHashCode(T a)
{
return a.GetHashCode();
}
}
Итак, теперь вы можете использовать Distinct
.
var distinctImages = allImages.Distinct(
new ThrowAwayEqualityComparer<GalleryImage>((a, b) => a.Key == b.Key));
Вы можете уйти с <GalleryImage>
, но я не уверен, может ли компилятор указать тип (сейчас у него нет доступа.)
И в дополнительном методе расширения:
public static class IEnumerableExtensions
{
public static IEnumerable<TValue> Distinct<TValue>(this IEnumerable<TValue> @this, Func<TValue, TValue, bool> comparer)
{
return @this.Distinct(new ThrowAwayEqualityComparer<TValue>(comparer);
}
private class ThrowAwayEqualityComparer...
}
Ответ 6
Вот интересная статья, которая расширяет LINQ для этой цели...
http://www.singingeels.com/Articles/Extending_LINQ__Specifying_a_Property_in_the_Distinct_Function.aspx
По умолчанию Distinct сравнивает объекты на основе их hashcode - чтобы ваши объекты работали с Distinct, вы могли бы переопределить метод GetHashcode.. но вы упомянули, что вы извлекаете свои объекты из веб-службы, поэтому вы, возможно, не сможете для этого в этом случае.
Ответ 7
реализовать IEquatable в GalleryImage, поскольку он создан
Другой подход состоял бы в том, чтобы генерировать GalleryImage как частичный класс, а затем иметь другой файл с реализацией наследования и IEquatable, Equals, GetHash.