Distinct de LINQ () en una propiedad particular

1095

Estoy jugando con LINQ para aprenderlo, pero no puedo entender cómo usarlo Distinctcuando no tengo una lista simple (una lista simple de enteros es bastante fácil de hacer, esta no es la pregunta). ¿Qué debo hacer si quiero usar Distinct en una lista de un Objeto en una o más propiedades del objeto?

Ejemplo: si un objeto es Person, con Propiedad Id. ¿Cómo puedo obtener toda la Persona y usarla Distinctcon la propiedad Iddel objeto?

Person1: Id=1, Name="Test1"
Person2: Id=1, Name="Test1"
Person3: Id=2, Name="Test2"

¿Cómo puedo ser justo Person1y Person3? ¿Es eso posible?

Si no es posible con LINQ, ¿cuál sería la mejor manera de tener una lista Persondependiendo de algunas de sus propiedades en .NET 3.5?

Patrick Desjardins
fuente

Respuestas:

1249

EDITAR : Esto ahora es parte de MoreLINQ .

Lo que necesita es un "distintivo" de manera efectiva. No creo que sea parte de LINQ tal como está, aunque es bastante fácil de escribir:

public static IEnumerable<TSource> DistinctBy<TSource, TKey>
    (this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
    HashSet<TKey> seenKeys = new HashSet<TKey>();
    foreach (TSource element in source)
    {
        if (seenKeys.Add(keySelector(element)))
        {
            yield return element;
        }
    }
}

Entonces, para encontrar los valores distintos usando solo la Idpropiedad, puede usar:

var query = people.DistinctBy(p => p.Id);

Y para usar varias propiedades, puede usar tipos anónimos, que implementan la igualdad adecuadamente:

var query = people.DistinctBy(p => new { p.Id, p.Name });

No probado, pero debería funcionar (y ahora al menos se compila).

Sin embargo, asume el comparador predeterminado para las claves: si desea pasar un comparador de igualdad, simplemente páselo al HashSetconstructor.

Jon Skeet
fuente
1
@ ashes999: No estoy seguro de lo que quieres decir. El código está presente en la respuesta y en la biblioteca, dependiendo de si está contento de asumir una dependencia.
Jon Skeet
10
@ ashes999: Si solo haces esto en un solo lugar, seguro que usarlo GroupByes más sencillo. Si lo necesita en más de un lugar, es mucho más limpio (IMO) encapsular la intención.
Jon Skeet
55
@MatthewWhited: Dado que no se menciona IQueryable<T>aquí, no veo cómo es relevante. Estoy de acuerdo en que esto no sería adecuado para EF, etc., pero dentro de LINQ to Objects creo que es más adecuado que GroupBy. El contexto de la pregunta siempre es importante.
Jon Skeet
77
El proyecto se movió en github, aquí está el código de DistinctBy: github.com/morelinq/MoreLINQ/blob/master/MoreLinq/DistinctBy.cs
Phate01
1859

¿Qué sucede si quiero obtener una lista distinta basada en una o más propiedades?

¡Simple! Desea agruparlos y elegir un ganador del grupo.

List<Person> distinctPeople = allPeople
  .GroupBy(p => p.PersonId)
  .Select(g => g.First())
  .ToList();

Si desea definir grupos en varias propiedades, aquí le mostramos cómo:

List<Person> distinctPeople = allPeople
  .GroupBy(p => new {p.PersonId, p.FavoriteColor} )
  .Select(g => g.First())
  .ToList();
Amy B
fuente
1
@ErenErsonmez seguro. Con mi código publicado, si se desea la ejecución diferida, salga de la llamada a la Lista.
Amy B
55
Muy buena respuesta! Reallyllly me ayudó en Linq-to-Entities desde una vista sql donde no pude modificar la vista. Necesitaba usar FirstOrDefault () en lugar de First (), todo está bien.
Alex KeySmith
8
Lo probé y debería cambiar a Select (g => g.FirstOrDefault ())
26
@ChocapicSz Nope. Ambos Single()y SingleOrDefault()cada lanzamiento cuando la fuente tiene más de un elemento. En esta operación, esperamos la posibilidad de que cada grupo tenga más de un elemento. Para el caso, First()es preferible en lugar de FirstOrDefault()porque cada grupo debe tener al menos un miembro ... a menos que esté utilizando EntityFramework, que no puede darse cuenta de que cada grupo tiene al menos un miembro y las demandas FirstOrDefault().
Amy B
2
Parece que actualmente no es compatible con EF Core, incluso usando FirstOrDefault() github.com/dotnet/efcore/issues/12088 Estoy en 3.1, y recibo errores "incapaces de traducir".
Collin M. Barrett
78

Utilizar:

List<Person> pList = new List<Person>();
/* Fill list */

var result = pList.Where(p => p.Name != null).GroupBy(p => p.Id).Select(grp => grp.FirstOrDefault());

Lo whereayuda a filtrar las entradas (podría ser más complejo) groupbyy a selectrealizar la función distinta.

karcsi
fuente
1
Perfecto, y funciona sin extender Linq o usar otra dependencia.
DavidScherer
77

También puede usar la sintaxis de consulta si desea que se vea como LINQ:

var uniquePeople = from p in people
                   group p by new {p.ID} //or group by new {p.ID, p.Name, p.Whatever}
                   into mygroup
                   select mygroup.FirstOrDefault();
Chuck Rostance
fuente
44
Hmm, mis pensamientos son tanto la sintaxis de consulta como la sintaxis de API fluida son tan LINQ como las demás y su preferencia sobre las que usan las personas. Yo mismo prefiero la API fluida, así que lo consideraría más como LINK, pero supongo que es subjetivo
Max Carroll
LINQ-Like no tiene nada que ver con la preferencia, ser "LINQ-like" tiene que ver con parecerse a un lenguaje de consulta diferente incrustado en C #, prefiero la interfaz fluida, proveniente de flujos de Java, pero NO es LINQ-Like.
Ryan The Leach
¡¡Excelente!! ¡Eres mi héroe!
Farzin Kanzi
63

Creo que es suficiente:

list.Select(s => s.MyField).Distinct();
Ivan
fuente
43
¿Qué pasa si necesita recuperar su objeto completo, no solo ese campo en particular?
Festim Cahani
1
¿Qué objeto exactamente de los varios objetos que tienen el mismo valor de propiedad?
donRumatta
40

Solución, primero agrupe por sus campos, luego seleccione el primer elemento predeterminado.

    List<Person> distinctPeople = allPeople
   .GroupBy(p => p.PersonId)
   .Select(g => g.FirstOrDefault())
   .ToList();
cahit beyaz
fuente
26

Puedes hacer esto con el estándar Linq.ToLookup(). Esto creará una colección de valores para cada clave única. Solo selecciona el primer elemento de la colección

Persons.ToLookup(p => p.Id).Select(coll => coll.First());
David Fahlander
fuente
17

El siguiente código es funcionalmente equivalente a la respuesta de Jon Skeet .

Probado en .NET 4.5, debería funcionar en cualquier versión anterior de LINQ.

public static IEnumerable<TSource> DistinctBy<TSource, TKey>(
  this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
  HashSet<TKey> seenKeys = new HashSet<TKey>();
  return source.Where(element => seenKeys.Add(keySelector(element)));
}

Incidentalmente, vea la última versión de Jon Skeet de DistinctBy.cs en Google Code .

Aplazamiento de pago
fuente
3
Esto me dio un "error de secuencia no tiene valores", pero la respuesta de Skeet produjo el resultado correcto.
Qué sería genial
10

Escribí un artículo que explica cómo extender la función Distinct para que pueda hacer lo siguiente:

var people = new List<Person>();

people.Add(new Person(1, "a", "b"));
people.Add(new Person(2, "c", "d"));
people.Add(new Person(1, "a", "b"));

foreach (var person in people.Distinct(p => p.ID))
    // Do stuff with unique list here.

Aquí está el artículo: Ampliación de LINQ: especificación de una propiedad en la función distinta

Timothy Khouri
fuente
3
Su artículo tiene un error, debe haber un <T> después de Distinct: public static IEnumerable <T> Distinct (esto ... Además, no parece que funcione (bien) en más de una propiedad, es decir, una combinación de primero y apellidos
fila1
2
+1, un error menor no es una razón suficiente para hacer un voto negativo, que es tan tonto que a menudo es un error tipográfico. ¡Y todavía no he visto una función genérica que funcione para cualquier cantidad de propiedad! Espero que el votante negativo haya votado negativamente a cualquier otra respuesta en este hilo también. Pero, ¿qué es este segundo tipo siendo objeto? Me opongo!
nawfal
44
Su enlace está roto
Tom Lint
7

Personalmente uso la siguiente clase:

public class LambdaEqualityComparer<TSource, TDest> : 
    IEqualityComparer<TSource>
{
    private Func<TSource, TDest> _selector;

    public LambdaEqualityComparer(Func<TSource, TDest> selector)
    {
        _selector = selector;
    }

    public bool Equals(TSource obj, TSource other)
    {
        return _selector(obj).Equals(_selector(other));
    }

    public int GetHashCode(TSource obj)
    {
        return _selector(obj).GetHashCode();
    }
}

Entonces, un método de extensión:

public static IEnumerable<TSource> Distinct<TSource, TCompare>(
    this IEnumerable<TSource> source, Func<TSource, TCompare> selector)
{
    return source.Distinct(new LambdaEqualityComparer<TSource, TCompare>(selector));
}

Finalmente, el uso previsto:

var dates = new List<DateTime>() { /* ... */ }
var distinctYears = dates.Distinct(date => date.Year);

La ventaja que encontré al usar este enfoque es la reutilización de la LambdaEqualityComparerclase para otros métodos que aceptan un IEqualityComparer. (Ah, y dejo las yieldcosas a la implementación original de LINQ ...)

Joel
fuente
5

En caso de que necesite un método distinto en varias propiedades, puede consultar mi biblioteca de PowerfulExtensions . Actualmente se encuentra en una etapa muy joven, pero ya puede utilizar métodos como Distinct, Union, Intersect, Except en cualquier cantidad de propiedades;

Así es como lo usas:

using PowerfulExtensions.Linq;
...
var distinct = myArray.Distinct(x => x.A, x => x.B);
Andrzej Gis
fuente
5

Cuando enfrentamos tal tarea en nuestro proyecto, definimos una pequeña API para componer comparadores.

Entonces, el caso de uso fue así:

var wordComparer = KeyEqualityComparer.Null<Word>().
    ThenBy(item => item.Text).
    ThenBy(item => item.LangID);
...
source.Select(...).Distinct(wordComparer);

Y la API en sí se ve así:

using System;
using System.Collections;
using System.Collections.Generic;

public static class KeyEqualityComparer
{
    public static IEqualityComparer<T> Null<T>()
    {
        return null;
    }

    public static IEqualityComparer<T> EqualityComparerBy<T, K>(
        this IEnumerable<T> source,
        Func<T, K> keyFunc)
    {
        return new KeyEqualityComparer<T, K>(keyFunc);
    }

    public static KeyEqualityComparer<T, K> ThenBy<T, K>(
        this IEqualityComparer<T> equalityComparer,
        Func<T, K> keyFunc)
    {
        return new KeyEqualityComparer<T, K>(keyFunc, equalityComparer);
    }
}

public struct KeyEqualityComparer<T, K>: IEqualityComparer<T>
{
    public KeyEqualityComparer(
        Func<T, K> keyFunc,
        IEqualityComparer<T> equalityComparer = null)
    {
        KeyFunc = keyFunc;
        EqualityComparer = equalityComparer;
    }

    public bool Equals(T x, T y)
    {
        return ((EqualityComparer == null) || EqualityComparer.Equals(x, y)) &&
                EqualityComparer<K>.Default.Equals(KeyFunc(x), KeyFunc(y));
    }

    public int GetHashCode(T obj)
    {
        var hash = EqualityComparer<K>.Default.GetHashCode(KeyFunc(obj));

        if (EqualityComparer != null)
        {
            var hash2 = EqualityComparer.GetHashCode(obj);

            hash ^= (hash2 << 5) + hash2;
        }

        return hash;
    }

    public readonly Func<T, K> KeyFunc;
    public readonly IEqualityComparer<T> EqualityComparer;
}

Más detalles se encuentran en nuestro sitio: IEqualityComparer en LINQ .

Vladimir Nesterovsky
fuente
5

Puede usar DistinctBy () para obtener registros distintos por una propiedad de objeto. Simplemente agregue la siguiente declaración antes de usarla:

usando Microsoft.Ajax.Utilities;

y luego úsalo como sigue:

var listToReturn = responseList.DistinctBy(x => x.Index).ToList();

donde 'Índice' es la propiedad en la que quiero que los datos sean distintos.

Harry. Naeem
fuente
4

Puede hacerlo (aunque no a la velocidad del rayo) así:

people.Where(p => !people.Any(q => (p != q && p.Id == q.Id)));

Es decir, "seleccione a todas las personas donde no haya otra persona diferente en la lista con la misma ID".

Tenga en cuenta, en su ejemplo, que solo seleccionaría a la persona 3. No estoy seguro de cómo decir cuál quiere, de los dos anteriores.

mqp
fuente
4

Si no desea agregar la biblioteca MoreLinq a su proyecto solo para obtener la DistinctByfuncionalidad, puede obtener el mismo resultado final utilizando la sobrecarga del Distinctmétodo de Linq que toma un IEqualityComparerargumento.

Comienza creando una clase de comparación de igualdad personalizada genérica que utiliza la sintaxis lambda para realizar una comparación personalizada de dos instancias de una clase genérica:

public class CustomEqualityComparer<T> : IEqualityComparer<T>
{
    Func<T, T, bool> _comparison;
    Func<T, int> _hashCodeFactory;

    public CustomEqualityComparer(Func<T, T, bool> comparison, Func<T, int> hashCodeFactory)
    {
        _comparison = comparison;
        _hashCodeFactory = hashCodeFactory;
    }

    public bool Equals(T x, T y)
    {
        return _comparison(x, y);
    }

    public int GetHashCode(T obj)
    {
        return _hashCodeFactory(obj);
    }
}

Luego, en su código principal, lo usa así:

Func<Person, Person, bool> areEqual = (p1, p2) => int.Equals(p1.Id, p2.Id);

Func<Person, int> getHashCode = (p) => p.Id.GetHashCode();

var query = people.Distinct(new CustomEqualityComparer<Person>(areEqual, getHashCode));

Voila! :)

Lo anterior supone lo siguiente:

  • Propiedad Person.Id es de tipoint
  • La peoplecolección no contiene ningún elemento nulo.

Si la colección podría contener nulos, simplemente reescriba las lambdas para verificar si hay nulos, por ejemplo:

Func<Person, Person, bool> areEqual = (p1, p2) => 
{
    return (p1 != null && p2 != null) ? int.Equals(p1.Id, p2.Id) : false;
};

EDITAR

Este enfoque es similar al de la respuesta de Vladimir Nesterovsky pero más simple.

También es similar al de la respuesta de Joel, pero permite una lógica de comparación compleja que involucra múltiples propiedades.

Sin embargo, si sus objetos solo pueden diferir para Identonces, otro usuario dio la respuesta correcta de que todo lo que necesita hacer es anular las implementaciones predeterminadas de GetHashCode()y Equals()en su Personclase y luego simplemente usar el Distinct()método listo para usar de Linq para filtrar fuera cualquier duplicado.

Canuck Caspio
fuente
Quiero obtener solo elementos únicos en dictonary, ¿pueden ayudarme? Estoy usando este código. Si TempDT no es nada, entonces m_ConcurrentScriptDictionary = TempDT.AsEnumerable.ToDictionary (Function (x) x.SafeField (fldClusterId, NULL_ID_VALUE), Function (y) y.SafeField (fldParamValue11, NULL_ID_VALUE))
RSB
1
List<Person>lst=new List<Person>
        var result1 = lst.OrderByDescending(a => a.ID).Select(a =>new Player {ID=a.ID,Name=a.Name} ).Distinct();
Arindam
fuente
¿Querías decir en Select() new Personlugar de new Player? Sin embargo, el hecho de que esté ordenando IDno informa de alguna manera el Distinct()uso de esa propiedad para determinar la unicidad, por lo que esto no funcionará.
BACON
1

Métodos Override Equals (object obj) y GetHashCode () :

class Person
{
    public int Id { get; set; }
    public int Name { get; set; }

    public override bool Equals(object obj)
    {
        return ((Person)obj).Id == Id;
        // or: 
        // var o = (Person)obj;
        // return o.Id == Id && o.Name == Name;
    }
    public override int GetHashCode()
    {
        return Id.GetHashCode();
    }
}

y luego solo llama:

List<Person> distinctList = new[] { person1, person2, person3 }.Distinct().ToList();
Waldemar Gałęzinowski
fuente
Sin embargo, GetHashCode () debería ser más avanzado (para contar también el Nombre), esta respuesta es probablemente la mejor según mi opinión. En realidad, para archivar la lógica de destino, no hay necesidad de anular GetHashCode (), Equals () es suficiente, pero si necesitamos rendimiento, tenemos que anularlo. Todos los algs de comparación, primero verifique el hash y, si son iguales, llame a Equals ().
Oleg Skripnyak
Además, en Equals () la primera línea debe ser "if (! (Obj is Person)) return false". Pero la mejor práctica es usar objetos separados fundidos en un tipo, como "var o = obj como Persona; if (o == null) return false;" luego verifique la igualdad con o sin lanzar
Oleg Skripnyak
1
Reemplazar Iguales como este no es una buena idea, ya que podría tener consecuencias no deseadas para otros programadores que esperan que la Igualdad de la persona se determine en más de una sola propiedad.
B2K
0

Debería poder anular Equals en persona para hacer Equals en Person.id. Esto debería resultar en el comportamiento que buscas.

GWLlosa
fuente
-5

Por favor intente con el siguiente código.

var Item = GetAll().GroupBy(x => x .Id).ToList();
Mohamed Hammam
fuente
3
Una respuesta corta es bienvenida, sin embargo, no proporcionará mucho valor a los últimos usuarios que están tratando de entender qué está sucediendo detrás del problema. Dedique algo de tiempo para explicar cuál es el problema real que causa el problema y cómo resolverlo. Gracias ~
Hearen