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?

Patrick Desjardins
fonte

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.

Jon Skeet
fonte
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();
Amy B
fonte
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.

karcsi
fonte
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();
Chuck Rostance
fonte
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();
Ivan
fonte
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();
cahit beyaz
fonte
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());
David Fahlander
fonte
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 .

Contango
fonte
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

Timothy Khouri
fonte
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 ...)

Joel
fonte
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);
Andrzej Gis
fonte
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 .

Vladimir Nesterovsky
fonte
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.

Harry .Naeem
fonte
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.

mqp
fonte
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.

Canuck Cáspio
fonte
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();
Arindam
fonte
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();
Waldemar Gałęzinowski
fonte
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.

GWLlosa
fonte
-5

Por favor, tente com o código abaixo.

var Item = GetAll().GroupBy(x => x .Id).ToList();
Mohamed Hammam
fonte
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