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);
}
}
Aquí hay un artículo interesante que extiende LINQ para este propósito ... http://www.singingeels.com/Articles/Extending_LINQ__Specifying_a_Property_in_the_Distinct_Function.aspx
El Distinct predeterminado compara objetos en función de su código hash: para hacer que sus objetos funcionen fácilmente con Distinct, puede anular el método GetHashcode ... pero mencionó que está recuperando sus objetos de un servicio web, por lo que es posible que no pueda hacerlo en este caso.
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();
}
}