with tutorial expresiones c# linq distinct

tutorial - linq lambda c#



¿Puedes crear un simple ''EqualityComparer<T>'' usando una expresión lambda (8)

IMPORTANTE: ESTA NO ES UNA PREGUNTA LINQ-TO-SQL . Esto es LINQ a los objetos.

Breve pregunta:

¿Hay una forma simple en LINQ para objetos para obtener una lista distinta de objetos de una lista basada en una propiedad clave en los objetos.

Larga pregunta

Estoy intentando hacer una operación Distinct() en una lista de objetos que tienen una clave como una de sus propiedades.

class GalleryImage { public int Key { get;set; } public string Caption { get;set; } public string Filename { get; set; } public string[] Tags {g et; set; } }

Tengo una lista de objetos de la Gallery que contienen GalleryImage[] .

Debido a la forma en que funciona el servicio web [sic], tengo duplicados del objeto GalleryImage . pensé que sería una simple cuestión usar Distinct() para obtener una lista distinta.

Esta es la consulta LINQ que quiero usar:

var allImages = Galleries.SelectMany(x => x.Images); var distinctImages = allImages.Distinct<GalleryImage>(new EqualityComparer<GalleryImage>((a, b) => a.id == b.id));

El problema es que EqualityComparer es una clase abstracta.

No quiero:

  • implementar IEquatable en GalleryImage porque se genera
  • tiene que escribir una clase separada para implementar IEqualityComparer como se Distinct()

¿Hay alguna implementación concreta de EqualityComparer alguna parte que me falta?

Hubiera pensado que habría una manera fácil de obtener objetos ''distintos'' de un conjunto basado en una clave.


implementar IEquatable en GalleryImage porque se genera

Un enfoque diferente sería generar GalleryImage como una clase parcial, y luego tener otro archivo con la herencia y la implementación de Equitable, Equals, GetHash.


¿Qué tal una clase genérica de desechar el IEqualityComparer ?

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(); } }

Entonces ahora puedes usar Distinct .

var distinctImages = allImages.Distinct( new ThrowAwayEqualityComparer<GalleryImage>((a, b) => a.Key == b.Key));

Es posible que pueda salirse con la <GalleryImage> , pero no estoy seguro de si el compilador podría inferir el tipo (no tiene acceso ahora).

Y en un método de extensión adicional:

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... }


(Aquí hay dos soluciones; vea el final para la segunda):

Mi biblioteca MiscUtil tiene una clase ProjectionEqualityComparer (y dos clases de apoyo para hacer uso de la inferencia de tipo).

Aquí hay un ejemplo de usarlo:

EqualityComparer<GalleryImage> comparer = ProjectionEqualityComparer<GalleryImage>.Create(x => x.id);

Aquí está el código (comentarios eliminados)

// 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)); } }

Segunda solución

Para hacer esto solo por Distinct, puede usar la extensión DistinctBy en 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; } } }

En ambos casos, ThrowIfNull ve así:

public static void ThrowIfNull<T>(this T data, string name) where T : class { if (data == null) { throw new ArgumentNullException(name); } }



Esta idea se debate here , y aunque espero que el equipo .NET Core adopte un método para generar IEqualityComparer<T> s de lambda, le sugiero que vote y haga un comentario sobre esa idea, y use lo siguiente:

Uso:

IEqualityComparer<Contact> comp1 = EqualityComparerImpl<Contact>.Create(c => c.Name); var comp2 = EqualityComparerImpl<Contact>.Create(c => c.Name, c => c.Age); class Contact { public Name { get; set; } public Age { get; set; } }

Código:

public class EqualityComparerImpl<T> : IEqualityComparer<T> { public static EqualityComparerImpl<T> Create( params Expression<Func<T, object>>[] properties) => new EqualityComparerImpl<T>(properties); PropertyInfo[] _properties; EqualityComparerImpl(Expression<Func<T, object>>[] properties) { if (properties == null) throw new ArgumentNullException(nameof(properties)); if (properties.Length == 0) throw new ArgumentOutOfRangeException(nameof(properties)); var length = properties.Length; var extractions = new PropertyInfo[length]; for (int i = 0; i < length; i++) { var property = properties[i]; extractions[i] = ExtractProperty(property); } _properties = extractions; } public bool Equals(T x, T y) { if (ReferenceEquals(x, y)) //covers both are null return true; if (x == null || y == null) return false; var len = _properties.Length; for (int i = 0; i < _properties.Length; i++) { var property = _properties[i]; if (!Equals(property.GetValue(x), property.GetValue(y))) return false; } return true; } public int GetHashCode(T obj) { if (obj == null) return 0; var hashes = _properties .Select(pi => pi.GetValue(obj)?.GetHashCode() ?? 0).ToArray(); return Combine(hashes); } static int Combine(int[] hashes) { int result = 0; foreach (var hash in hashes) { uint rol5 = ((uint)result << 5) | ((uint)result >> 27); result = ((int)rol5 + result) ^ hash; } return result; } static PropertyInfo ExtractProperty(Expression<Func<T, object>> property) { if (property.NodeType != ExpressionType.Lambda) throwEx(); var body = property.Body; if (body.NodeType == ExpressionType.Convert) if (body is UnaryExpression unary) body = unary.Operand; else throwEx(); if (!(body is MemberExpression member)) throwEx(); if (!(member.Member is PropertyInfo pi)) throwEx(); return pi; void throwEx() => throw new NotSupportedException($"The expression ''{property}'' isn''t supported."); } }


Esto es lo mejor que se me ocurre por el problema en la mano. Todavía tengo curiosidad por saber si hay una buena manera de crear un EqualityComparer sobre la marcha.

Galleries.SelectMany(x => x.Images).ToLookup(x => x.id).Select(x => x.First());

Crear tabla de búsqueda y tomar ''top'' de cada uno

Nota: esto es lo mismo que sugirió @charlie pero usando ILookup, que creo que es lo que un grupo debe ser de todos modos.


Puede agrupar por el valor clave y luego seleccionar el elemento superior de cada grupo. ¿Eso funcionaría para ti?


Sobre la base de la respuesta de Charlie Flowers, puede crear su propio método de extensión para hacer lo que desee que internamente utiliza la agrupación:

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(); }

También podría crear una clase genérica derivada de EqualityComparer, pero parece que desea evitar esto:

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(); } }