Distinct () de LINQ em uma propriedade específica


1095

Estou brincando com o LINQ para aprender sobre isso, mas não consigo descobrir como usá-lo Distinctquando não tenho uma lista simples (é fácil fazer uma lista simples de números inteiros, essa não é a questão). O que eu quero usar Distinct em uma lista de um Objeto em uma ou mais propriedades do objeto?

Exemplo: se um objeto estiver Person, com Propriedade Id. Como posso obter toda pessoa e usá Distinct-la com a propriedade Iddo objeto?

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

Como posso obter apenas Person1e Person3? Isso é possível?

Se não for possível com o LINQ, qual seria a melhor maneira de ter uma lista Persondependendo de algumas de suas propriedades no .NET 3.5?

Respostas:


1248

EDIT : Isso agora faz parte do MoreLINQ .

O que você precisa é um "distinto por" efetivamente. Não acredito que faça parte do LINQ como está, embora seja bastante fácil escrever:

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

Portanto, para encontrar os valores distintos usando apenas a Idpropriedade, você pode usar:

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

E para usar várias propriedades, você pode usar tipos anônimos, que implementam a igualdade adequadamente:

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

Não testado, mas deve funcionar (e agora compila pelo menos).

No entanto, ele assume o comparador padrão para as chaves - se você deseja passar um comparador de igualdade, basta transmiti-lo ao HashSetconstrutor.



1
@ ashes999: Não sei o que você quer dizer. O código está presente na resposta e na biblioteca - dependendo se você está feliz em assumir uma dependência.
Jon tiro ao prato

10
@ ashes999: Se você está fazendo isso apenas em um único local, é sempre GroupBymais fácil usar . Se você precisar em mais de um lugar, é muito mais limpo (IMO) encapsular a intenção.
21813 Jon Skeet

5
@ MatthewWhited: Dado que não há menção IQueryable<T>aqui, não vejo como isso é relevante. Concordo que isso não seria adequado para EF etc, mas dentro do LINQ to Objects acho que é mais adequado do que GroupBy. O contexto da pergunta é sempre importante.
precisa saber é o seguinte

7
O projeto mudou no github, aqui está o código de DistinctBy: github.com/morelinq/MoreLINQ/blob/master/MoreLinq/DistinctBy.cs
Phate01

1858

E se eu quiser obter uma lista distinta com base em uma ou mais propriedades?

Simples! Você deseja agrupá-los e escolher um vencedor do grupo.

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

Se você deseja definir grupos em várias propriedades, veja como:

List<Person> distinctPeople = allPeople
  .GroupBy(p => new {p.PersonId, p.FavoriteColor} )
  .Select(g => g.First())
  .ToList();

1
@ErenErsonmez sure. Com o meu código publicado, se a execução adiada for desejada, interrompa a chamada ToList.
Amy B

5
Resposta muito boa! Realmente me ajudou no Linq-to-Entities direcionado a partir de uma exibição sql onde eu não conseguia modificar a exibição. Eu precisava usar FirstOrDefault () em vez de First () - tudo é bom.
Alex KeySmith #

8
Eu tentei e ele deve mudar para Select (g => g.FirstOrDefault ())

26
@ChocapicSz Nope. Ambos Single()e SingleOrDefault()cada um são lançados quando a fonte possui mais de um item. Nesta operação, esperamos a possibilidade de cada grupo ter mais de um item. Nesse caso, First()é preferível FirstOrDefault()que cada grupo deva ter pelo menos um membro ... a menos que você esteja usando EntityFramework, que não pode descobrir que cada grupo tem pelo menos um membro e exige FirstOrDefault().
Amy B

2
Atualmente, parece não haver suporte no EF Core, mesmo usando o FirstOrDefault() github.com/dotnet/efcore/issues/12088 . Estou no 3.1 e recebo erros "incapazes de traduzir".
Collin M. Barrett

78

Usar:

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

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

Isso whereajuda a filtrar as entradas (poderia ser mais complexa) groupbye selectexecutar a função distinta.


1
Perfeito e funciona sem estender o Linq ou usar outra dependência.
DavidScherer

77

Você também pode usar a sintaxe da consulta se quiser que ela seja semelhante a 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();

4
Hmm, meus pensamentos são tanto a sintaxe da consulta quanto a sintaxe fluente da API, que são tão LINQ quanto as outras e são justas as preferências sobre quais as pessoas usam. Eu prefiro a API fluente então eu consideraria que mais LINK-Como mas então eu acho que é subjetiva
Max Carroll

O LINQ-Like não tem nada a ver com preferência, sendo "LINQ-like" tem a ver com parecer com uma linguagem de consulta diferente sendo incorporada ao C #, prefiro a interface fluente, proveniente de fluxos java, mas NÃO é do tipo LINQ.
Ryan The Leach

Excelente!! Você é meu herói!
Farzin Kanzi

63

Eu acho que é o suficiente:

list.Select(s => s.MyField).Distinct();

43
E se ele precisar de volta seu objeto completo, não apenas esse campo em particular?
Festim Cahani 12/08/2015

1
Qual exatamente o objeto dos vários objetos que possuem o mesmo valor de propriedade?
DonRumatta

40

Solução primeiro agrupe por seus campos e selecione o primeiro item de falha padrão.

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

26

Você pode fazer isso com o padrão Linq.ToLookup(). Isso criará uma coleção de valores para cada chave exclusiva. Basta selecionar o primeiro item da coleção

Persons.ToLookup(p => p.Id).Select(coll => coll.First());

17

O código a seguir é funcionalmente equivalente à resposta de Jon Skeet .

Testado no .NET 4.5, deve funcionar em qualquer versão anterior do 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)));
}

Incidencialmente, confira a versão mais recente do Jon Skeet do DistinctBy.cs no Google Code .


3
Isso me deu um "erro de sequência sem valores", mas a resposta de Skeet produziu o resultado correto.
O que seria legal

10

Escrevi um artigo que explica como estender a função Distinct para que você possa fazer o seguinte:

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.

Aqui está o artigo: Estendendo LINQ - Especificando uma Propriedade na Função Distinta


3
Seu artigo tem um erro, deve haver um <T> após Distinct: public static IEnumerable <T> Distinct (this ... Também não parece que funcionará (muito bem) em mais de uma propriedade, ou seja, uma combinação dos primeiros e sobrenomes.
row1 17/03

2
+1, um pequeno erro não é motivo suficiente para o voto negativo, que tão bobo, costumava chamar um erro de digitação. E ainda estou para ver uma função genérica que funcionará para qualquer número de propriedades! Espero que o voto negativo tenha votado contra todas as outras respostas neste tópico também. Mas ei, o que é esse segundo tipo de objeto? Oponho-me !
Nawfal

4
O link está quebrado
Tom Lint

7

Pessoalmente, uso a seguinte classe:

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

Em seguida, um método de extensão:

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

Finalmente, o uso pretendido:

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

A vantagem que encontrei usando essa abordagem é a reutilização da LambdaEqualityComparerclasse para outros métodos que aceitam um IEqualityComparer. (Ah, e deixo o yieldmaterial para a implementação original do LINQ ...)


5

Caso você precise de um método Distinct em várias propriedades, confira minhas PowerfulExtensions biblioteca . Atualmente, ele está em uma fase muito jovem, mas já é possível usar métodos como Distinct, Union, Intersect, Exceto em qualquer número de propriedades;

É assim que você o usa:

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

5

Quando enfrentamos essa tarefa em nosso projeto, definimos uma pequena API para compor comparadores.

Portanto, o caso de uso era assim:

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

E a própria API se parece com isso:

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

Mais detalhes estão no nosso site: IEqualityComparer no LINQ .


5

Você pode usar DistinctBy () para obter registros Distinct por uma propriedade de objeto. Basta adicionar a seguinte declaração antes de usá-la:

using Microsoft.Ajax.Utilities;

e use-o da seguinte maneira:

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

onde 'Index' é a propriedade na qual eu quero que os dados sejam distintos.


4

Você pode fazer isso (embora não seja rápido demais) assim:

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

Ou seja, "selecione todas as pessoas onde não há outra pessoa diferente na lista com o mesmo ID".

Lembre-se, no seu exemplo, que selecionaria a pessoa 3. Não tenho certeza de como saber o que você deseja, dentre as duas anteriores.


4

Se você não quiser adicionar a biblioteca MoreLinq ao seu projeto apenas para obter a DistinctByfuncionalidade, poderá obter o mesmo resultado final usando a sobrecarga do Distinctmétodo Linq que leva umIEqualityComparer argumento.

Você começa criando uma classe comparadora de igualdade customizada genérica que usa a sintaxe lambda para executar a comparação customizada de duas instâncias de uma classe 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);
    }
}

Então, no seu código principal, você o usa da seguinte maneira:

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! :)

O acima pressupõe o seguinte:

  • Propriedade Person.Id é do tipoint
  • A peoplecoleção não contém nenhum elemento nulo

Se a coleção puder conter nulos, basta reescrever as lambdas para verificar se há nulos, por exemplo:

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

EDITAR

Essa abordagem é semelhante à da resposta de Vladimir Nesterovsky, mas mais simples.

Também é semelhante ao da resposta de Joel, mas permite uma lógica de comparação complexa envolvendo várias propriedades.

No entanto, se seus objetos só puderem diferir, Identão outro usuário deu a resposta correta de que tudo que você precisa fazer é substituir as implementações padrão de GetHashCode()e Equals()na sua Personclasse e, em seguida, basta usar o Distinct()método pronto para uso do Linq para filtrar quaisquer duplicatas.


Eu quero obter apenas itens exclusivos no dictonary, você pode ajudar, estou usando este código Se TempDT não é nada, então 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();

Você quis dizer em Select() new Personvez de new Player? O fato de você estar solicitando de IDalguma forma não informa Distinct()sobre o uso dessa propriedade para determinar a exclusividade; portanto, isso não funcionará.
BACON

1

Substituir os métodos Equals (object obj) e 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();
    }
}

e depois chame:

List<Person> distinctList = new[] { person1, person2, person3 }.Distinct().ToList();

No entanto, GetHashCode () deve ser mais avançado (para contar também o nome), esta resposta é provavelmente melhor pela minha opinião. Na verdade, para arquivar a lógica de destino, não há necessidade de substituir GetHashCode (), Equals () é suficiente, mas se precisarmos de desempenho, precisamos substituí-lo. Todos os algs de comparação, primeiro verifique o hash e, se forem iguais, chame Equals ().
Oleg Skripnyak

Além disso, em Equals () a primeira linha deve ser "se (! (Obj é Person)) retornar false". Mas a melhor prática é usar objetos separados convertidos para um tipo, como "var o = obj como Person; if (o == null) retorna false;" em seguida, verificar a igualdade com o sem lançar
Oleg Skripnyak

1
Substituir iguais como esse não é uma boa ideia, pois pode ter consequências indesejadas para outros programadores que esperam que a Igualdade da Pessoa seja determinada em mais de uma propriedade.
B2K

0

Você deve substituir Equals on person para realmente fazer Equals on Person.id. Isso deve resultar no comportamento que você procura.


-5

Por favor, tente com o código abaixo.

var Item = GetAll().GroupBy(x => x .Id).ToList();

3
Uma resposta curta é bem-vinda, no entanto, não fornecerá muito valor para os usuários que estão tentando entender o que está acontecendo por trás do problema. Reserve um tempo para explicar qual é o problema real para causar o problema e como resolvê-lo. Obrigado ~
Ouvido
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.