Distinto () com lambda?

746

Certo, então eu tenho um enumerável e desejo obter valores distintos dele.

Usando System.Linq, é claro que existe um método de extensão chamado Distinct. No caso simples, pode ser usado sem parâmetros, como:

var distinctValues = myStringList.Distinct();

Muito bem, mas se eu tiver um grande número de objetos para os quais preciso especificar igualdade, a única sobrecarga disponível é:

var distinctValues = myCustomerList.Distinct(someEqualityComparer);

O argumento comparador de igualdade deve ser uma instância de IEqualityComparer<T>. Eu posso fazer isso, é claro, mas é um pouco detalhado e, bem, desajeitado.

O que eu esperava era uma sobrecarga que levaria um lambda, digamos um Func <T, T, bool>:

var distinctValues
    = myCustomerList.Distinct((c1, c2) => c1.CustomerId == c2.CustomerId);

Alguém sabe se existe alguma extensão ou alguma solução alternativa? Ou eu estou esquecendo de alguma coisa?

Como alternativa, existe uma maneira de especificar um inline IEqualityComparer (me envergonhar)?

Atualizar

Encontrei uma resposta de Anders Hejlsberg em uma postagem em um fórum do MSDN sobre esse assunto. Ele diz:

O problema que você encontrará é que, quando dois objetos se comparam da mesma forma, eles devem ter o mesmo valor de retorno GetHashCode (ou a tabela de hash usada internamente pelo Distinct não funcionará corretamente). Usamos IEqualityComparer porque empacota implementações compatíveis de Equals e GetHashCode em uma única interface.

Suponho que faz sentido ..

Tor Haugen
fonte
2
consulte stackoverflow.com/questions/1183403/… para obter uma solução usando GroupBy
17
Obrigado pela atualização de Anders Hejlsberg!
Tor Haugen
Não, não faz sentido - como dois objetos que contêm valores idênticos podem retornar dois códigos hash diferentes?
GY
Poderia ajudar - solução para .Distinct(new KeyEqualityComparer<Customer,string>(c1 => c1.CustomerId)), e explicar por que GetHashCode () é importante para o trabalho corretamente.
Marcel82 # 12/16
Relacionado / possível duplicado de: LINQ's Distinct () em uma propriedade particular
Marc.2377

Respostas:

1029
IEnumerable<Customer> filteredList = originalList
  .GroupBy(customer => customer.CustomerId)
  .Select(group => group.First());
Carlo Bos
fonte
12
Excelente! Isso também é muito fácil de encapsular em um método de extensão, como DistinctBy(ou até mesmo Distinct, uma vez que a assinatura será única).
Tomas Aschan
1
Não funciona para mim! <O método 'Primeiro' pode ser usado apenas como uma operação de consulta final. Considere usar o método 'FirstOrDefault' nesta instância.> Até eu tentei 'FirstOrDefault' que não funcionou.
JatSing
63
@ TorHaugen: Esteja ciente de que há um custo envolvido na criação de todos esses grupos. Isso não pode transmitir a entrada e acabará armazenando em buffer todos os dados antes de retornar qualquer coisa. Isso pode não ser relevante para a sua situação, é claro, mas eu prefiro a elegância de DistinctBy :)
Jon Skeet
2
@ JonSkeet: Isso é bom o suficiente para codificadores VB.NET que não desejam importar bibliotecas adicionais para apenas um recurso. Sem o ASync CTP, o VB.NET não suporta a yieldinstrução, portanto, tecnicamente, o streaming não é possível. Obrigado pela sua resposta embora. Vou usá-lo ao codificar em C #. ;-)
Alex Essilfie
2
@ BenGripka: Isso não é o mesmo. Apenas fornece os IDs dos clientes. Eu quero todo o cliente :)
ryanman
496

Parece-me que você deseja DistinctBydo MoreLINQ . Você pode então escrever:

var distinctValues = myCustomerList.DistinctBy(c => c.CustomerId);

Aqui está uma versão detalhada de DistinctBy(sem verificação de nulidade e sem opção para especificar seu próprio comparador de chaves):

public static IEnumerable<TSource> DistinctBy<TSource, TKey>
     (this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
    HashSet<TKey> knownKeys = new HashSet<TKey>();
    foreach (TSource element in source)
    {
        if (knownKeys.Add(keySelector(element)))
        {
            yield return element;
        }
    }
}
Jon Skeet
fonte
14
Eu sabia que a melhor resposta seria postada por Jon Skeet simplesmente lendo o título da postagem. Se tem algo a ver com o LINQ, Skeet é o seu homem. Leia 'C # In Depth' para obter conhecimento linq semelhante a Deus.
Nocarrier
2
Ótima resposta!!! também, para todos os VB_Complainers sobre o yield+ lib adicional, foreach pode ser re-escrita comoreturn source.Where(element => knownKeys.Add(keySelector(element)));
Denis Morozov
5
@ sudhAnsu63 essa é uma limitação do LinqToSql (e de outros provedores linq). A intenção do LinqToX é converter sua expressão lambda C # no contexto nativo do X. Ou seja, o LinqToSql converte seu c # em SQL e executa esse comando nativamente sempre que possível. Isso significa que qualquer método que reside em C # não pode ser "transmitido" a um linqProvider se não houver maneira de expressá-lo em SQL (ou qualquer provedor linq que você esteja usando). Vejo isso nos métodos de extensão para converter objetos de dados para visualizar modelos. Você pode contornar isso "materializando" a consulta, chamando ToList () antes de DistinctBy ().
Michael Blackburn
1
E sempre que volto a essa pergunta, fico me perguntando por que eles não adotam pelo menos parte do MoreLinq no BCL.
Shimmy Weitzhandler
2
@Shimmy: Eu certamente apreciaria isso ... Não tenho certeza qual é a viabilidade. I pode aumentá-lo na Fundação .NET embora ...
Jon Skeet
39

Para embrulhar as coisas . Eu acho que a maioria das pessoas que vieram aqui como eu deseja a solução mais simples possível sem usar nenhuma biblioteca e com o melhor desempenho possível .

(Para mim, o grupo aceito pelo método é um exagero em termos de desempenho.)

Aqui está um método de extensão simples usando a interface IEqualityComparer que também funciona para valores nulos.

Uso:

var filtered = taskList.DistinctBy(t => t.TaskExternalId).ToArray();

Código do método de extensão

public static class LinqExtensions
{
    public static IEnumerable<T> DistinctBy<T, TKey>(this IEnumerable<T> items, Func<T, TKey> property)
    {
        GeneralPropertyComparer<T, TKey> comparer = new GeneralPropertyComparer<T,TKey>(property);
        return items.Distinct(comparer);
    }   
}
public class GeneralPropertyComparer<T,TKey> : IEqualityComparer<T>
{
    private Func<T, TKey> expr { get; set; }
    public GeneralPropertyComparer (Func<T, TKey> expr)
    {
        this.expr = expr;
    }
    public bool Equals(T left, T right)
    {
        var leftProp = expr.Invoke(left);
        var rightProp = expr.Invoke(right);
        if (leftProp == null && rightProp == null)
            return true;
        else if (leftProp == null ^ rightProp == null)
            return false;
        else
            return leftProp.Equals(rightProp);
    }
    public int GetHashCode(T obj)
    {
        var prop = expr.Invoke(obj);
        return (prop==null)? 0:prop.GetHashCode();
    }
}
Anestis Kivranoglou
fonte
19

Não existe tal sobrecarga de método de extensão para isso. Eu achei isso frustrante no passado e, como tal, costumo escrever uma classe auxiliar para lidar com esse problema. O objetivo é converter um Func<T,T,bool>para IEqualityComparer<T,T>.

Exemplo

public class EqualityFactory {
  private sealed class Impl<T> : IEqualityComparer<T,T> {
    private Func<T,T,bool> m_del;
    private IEqualityComparer<T> m_comp;
    public Impl(Func<T,T,bool> del) { 
      m_del = del;
      m_comp = EqualityComparer<T>.Default;
    }
    public bool Equals(T left, T right) {
      return m_del(left, right);
    } 
    public int GetHashCode(T value) {
      return m_comp.GetHashCode(value);
    }
  }
  public static IEqualityComparer<T,T> Create<T>(Func<T,T,bool> del) {
    return new Impl<T>(del);
  }
}

Isso permite que você escreva o seguinte

var distinctValues = myCustomerList
  .Distinct(EqualityFactory.Create((c1, c2) => c1.CustomerId == c2.CustomerId));
JaredPar
fonte
8
Isso tem uma implementação desagradável de código hash. É mais fácil criar uma IEqualityComparer<T>a partir de uma projeção: stackoverflow.com/questions/188120/...
Jon Skeet
7
(Apenas para explicar meu comentário sobre o código hash - é muito fácil com esse código acabar com Equals (x, y) == true, mas GetHashCode (x)! = GetHashCode (y). Isso basicamente quebra algo como uma hashtable .)
Jon Skeet
Eu concordo com a objeção do código hash. Ainda assim, +1 para o padrão.
21119 Tor Haugen
@ Jon, sim, eu concordo que a implementação original do GetHashcode é menor que a ideal (estava sendo preguiçosa). Eu mudei para essencialmente usar agora EqualityComparer <T> .Default.GetHashcode (), que é um pouco mais padrão. Sinceramente, o único garantido para funcionar na implementação do GetHashcode nesse cenário é simplesmente retornar um valor constante. Mata a pesquisa de hashtable, mas é garantido que esteja funcionalmente correto.
JaredPar
1
@JaredPar: Exatamente. O código de hash deve ser consistente com a função de igualdade que você está usando, que provavelmente não é a padrão, caso contrário você não se incomodaria :) É por isso que eu prefiro usar uma projeção - você pode obter igualdade e um hash sensato codifique dessa maneira. Também faz com que o código de chamada tenha menos duplicação. É certo que ele só funciona em casos onde você deseja que a mesma projeção duas vezes, mas isso é todos os casos que eu vi na prática :)
Jon Skeet
18

Solução taquigráfica

myCustomerList.GroupBy(c => c.CustomerId, (key, c) => c.FirstOrDefault());
Arasu RRK
fonte
1
Você poderia adicionar uma explicação de por que isso foi aprimorado?
Keith Pinson
Isso realmente funcionou muito bem para mim quando o Konrad não.
neoscribe
13

Isso fará o que você deseja, mas eu não sei sobre desempenho:

var distinctValues =
    from cust in myCustomerList
    group cust by cust.CustomerId
    into gcust
    select gcust.First();

Pelo menos não é detalhado.

Gordon Freeman
fonte
12

Aqui está um método de extensão simples que faz o que eu preciso ...

public static class EnumerableExtensions
{
    public static IEnumerable<TKey> Distinct<T, TKey>(this IEnumerable<T> source, Func<T, TKey> selector)
    {
        return source.GroupBy(selector).Select(x => x.Key);
    }
}

É uma pena que eles não tenham adotado um método distinto como esse no framework, mas ei.

David Kirkland
fonte
esta é a melhor solução sem precisar adicionar a biblioteca morelinq.
toddmo
Mas, eu tive que mudar x.Keypara x.First()e altere o valor de retorno paraIEnumerable<T>
toddmo
@toddmo Obrigado pelo feedback :-) Sim, parece lógico ... Vou atualizar a resposta depois de investigar mais.
David Kirkland
1
nunca é tarde demais para dizer obrigado para a solução, simples e limpo
Ali
4

Algo que usei que funcionou bem para mim.

/// <summary>
/// A class to wrap the IEqualityComparer interface into matching functions for simple implementation
/// </summary>
/// <typeparam name="T">The type of object to be compared</typeparam>
public class MyIEqualityComparer<T> : IEqualityComparer<T>
{
    /// <summary>
    /// Create a new comparer based on the given Equals and GetHashCode methods
    /// </summary>
    /// <param name="equals">The method to compute equals of two T instances</param>
    /// <param name="getHashCode">The method to compute a hashcode for a T instance</param>
    public MyIEqualityComparer(Func<T, T, bool> equals, Func<T, int> getHashCode)
    {
        if (equals == null)
            throw new ArgumentNullException("equals", "Equals parameter is required for all MyIEqualityComparer instances");
        EqualsMethod = equals;
        GetHashCodeMethod = getHashCode;
    }
    /// <summary>
    /// Gets the method used to compute equals
    /// </summary>
    public Func<T, T, bool> EqualsMethod { get; private set; }
    /// <summary>
    /// Gets the method used to compute a hash code
    /// </summary>
    public Func<T, int> GetHashCodeMethod { get; private set; }

    bool IEqualityComparer<T>.Equals(T x, T y)
    {
        return EqualsMethod(x, y);
    }

    int IEqualityComparer<T>.GetHashCode(T obj)
    {
        if (GetHashCodeMethod == null)
            return obj.GetHashCode();
        return GetHashCodeMethod(obj);
    }
}
Kleinux
fonte
@Mukus Não sei por que você está perguntando sobre o nome da turma aqui. Eu precisava nomear a classe como algo para implementar o IEqualityComparer, então prefixei o My.
Kleinux
4

Todas as soluções que vi aqui dependem da seleção de um campo já comparável. Se for necessário comparar de uma maneira diferente, porém, esta solução aqui parece funcionar geralmente, para algo como:

somedoubles.Distinct(new LambdaComparer<double>((x, y) => Math.Abs(x - y) < double.Epsilon)).Count()
Dmitry Ledentsov
fonte
O que é o LambdaComparer, de onde você está importando isso?
Patrick Graham
@PatrickGraham vinculado na resposta: brendan.enrick.com/post/…
Dmitry Ledentsov
3

Pegue de outra maneira:

var distinctValues = myCustomerList.
Select(x => x._myCaustomerProperty).Distinct();

A sequência retorna elementos distintos comparando-os pela propriedade '_myCaustomerProperty'.

Prumo
fonte
1
Vim aqui para dizer isso. ESTE deve ser a resposta aceita
Still.Tony
5
Não, essa não deve ser a resposta aceita, a menos que tudo que você deseja seja valores distintos da propriedade customizada. A questão geral do OP era como retornar objetos distintos com base em uma propriedade específica do objeto.
tomo 27/05
2

Você pode usar o InlineComparer

public class InlineComparer<T> : IEqualityComparer<T>
{
    //private readonly Func<T, T, bool> equalsMethod;
    //private readonly Func<T, int> getHashCodeMethod;
    public Func<T, T, bool> EqualsMethod { get; private set; }
    public Func<T, int> GetHashCodeMethod { get; private set; }

    public InlineComparer(Func<T, T, bool> equals, Func<T, int> hashCode)
    {
        if (equals == null) throw new ArgumentNullException("equals", "Equals parameter is required for all InlineComparer instances");
        EqualsMethod = equals;
        GetHashCodeMethod = hashCode;
    }

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

    public int GetHashCode(T obj)
    {
        if (GetHashCodeMethod == null) return obj.GetHashCode();
        return GetHashCodeMethod(obj);
    }
}

Amostra de uso :

  var comparer = new InlineComparer<DetalleLog>((i1, i2) => i1.PeticionEV == i2.PeticionEV && i1.Etiqueta == i2.Etiqueta, i => i.PeticionEV.GetHashCode() + i.Etiqueta.GetHashCode());
  var peticionesEV = listaLogs.Distinct(comparer).ToList();
  Assert.IsNotNull(peticionesEV);
  Assert.AreNotEqual(0, peticionesEV.Count);

Fonte: https://stackoverflow.com/a/5969691/206730
Usando o IEqualityComparer for Union
Posso especificar meu comparador explícito de tipo explícito?

Kiquenet
fonte
2

Você pode usar o LambdaEqualityComparer:

var distinctValues
    = myCustomerList.Distinct(new LambdaEqualityComparer<OurType>((c1, c2) => c1.CustomerId == c2.CustomerId));


public class LambdaEqualityComparer<T> : IEqualityComparer<T>
    {
        public LambdaEqualityComparer(Func<T, T, bool> equalsFunction)
        {
            _equalsFunction = equalsFunction;
        }

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

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

        private readonly Func<T, T, bool> _equalsFunction;
    }
Валентин Миронов
fonte
1

Uma maneira complicada de fazer isso é usar Aggregate()extension, usando um dicionário como acumulador com os valores da propriedade-chave como chaves:

var customers = new List<Customer>();

var distincts = customers.Aggregate(new Dictionary<int, Customer>(), 
                                    (d, e) => { d[e.CustomerId] = e; return d; },
                                    d => d.Values);

E uma solução no estilo GroupBy está usando ToLookup():

var distincts = customers.ToLookup(c => c.CustomerId).Select(g => g.First());
Arturo Menchaca
fonte
Bom, mas por que não criar um Dictionary<int, Customer>?
Ruffin
0

Suponho que você tenha um IEnumerable e, no seu exemplo de delegado, gostaria que c1 e c2 se referissem a dois elementos nesta lista?

Eu acredito que você poderia conseguir isso com uma associação automática var distinctResults = de c1 em myList junção c2 em myList em

MattH
fonte
0

Se Distinct()não produzir resultados exclusivos, tente este:

var filteredWC = tblWorkCenter.GroupBy(cc => cc.WCID_I).Select(grp => grp.First()).Select(cc => new Model.WorkCenter { WCID = cc.WCID_I }).OrderBy(cc => cc.WCID); 

ObservableCollection<Model.WorkCenter> WorkCenter = new ObservableCollection<Model.WorkCenter>(filteredWC);
Andy Singh
fonte
0

O pacote Microsoft System.Interactive possui uma versão do Distinct que usa um seletor de chave lambda. É efetivamente o mesmo que a solução de Jon Skeet, mas pode ser útil que as pessoas saibam e consultem o restante da biblioteca.

Niall Connaughton
fonte
0

Veja como você pode fazer isso:

public static class Extensions
{
    public static IEnumerable<T> MyDistinct<T, V>(this IEnumerable<T> query,
                                                    Func<T, V> f, 
                                                    Func<IGrouping<V,T>,T> h=null)
    {
        if (h==null) h=(x => x.First());
        return query.GroupBy(f).Select(h);
    }
}

Este método permite que você o use especificando um parâmetro como .MyDistinct(d => d.Name), mas também permite especificar uma condição de ter como um segundo parâmetro como:

var myQuery = (from x in _myObject select x).MyDistinct(d => d.Name,
        x => x.FirstOrDefault(y=>y.Name.Contains("1") || y.Name.Contains("2"))
        );

Nota: Isso também permitirá que você especifique outras funções, como por exemplo .LastOrDefault(...).


Se você deseja expor apenas a condição, pode ser ainda mais simples implementando-a como:

public static IEnumerable<T> MyDistinct2<T, V>(this IEnumerable<T> query,
                                                Func<T, V> f,
                                                Func<T,bool> h=null
                                                )
{
    if (h == null) h = (y => true);
    return query.GroupBy(f).Select(x=>x.FirstOrDefault(h));
}

Nesse caso, a consulta seria parecida com:

var myQuery2 = (from x in _myObject select x).MyDistinct2(d => d.Name,
                    y => y.Name.Contains("1") || y.Name.Contains("2")
                    );

NB Aqui, a expressão é mais simples, mas a nota é .MyDistinct2usada .FirstOrDefault(...)implicitamente.


Nota: Os exemplos acima estão usando a seguinte classe de demonstração

class MyObject
{
    public string Name;
    public string Code;
}

private MyObject[] _myObject = {
    new MyObject() { Name = "Test1", Code = "T"},
    new MyObject() { Name = "Test2", Code = "Q"},
    new MyObject() { Name = "Test2", Code = "T"},
    new MyObject() { Name = "Test5", Code = "Q"}
};
Matt
fonte
0

IEnumerable extensão lambda:

public static class ListExtensions
{        
    public static IEnumerable<T> Distinct<T>(this IEnumerable<T> list, Func<T, int> hashCode)
    {
        Dictionary<int, T> hashCodeDic = new Dictionary<int, T>();

        list.ToList().ForEach(t => 
            {   
                var key = hashCode(t);
                if (!hashCodeDic.ContainsKey(key))
                    hashCodeDic.Add(key, t);
            });

        return hashCodeDic.Select(kvp => kvp.Value);
    }
}

Uso:

class Employee
{
    public string Name { get; set; }
    public int EmployeeID { get; set; }
}

//Add 5 employees to List
List<Employee> lst = new List<Employee>();

Employee e = new Employee { Name = "Shantanu", EmployeeID = 123456 };
lst.Add(e);
lst.Add(e);

Employee e1 = new Employee { Name = "Adam Warren", EmployeeID = 823456 };
lst.Add(e1);
//Add a space in the Name
Employee e2 = new Employee { Name = "Adam  Warren", EmployeeID = 823456 };
lst.Add(e2);
//Name is different case
Employee e3 = new Employee { Name = "adam warren", EmployeeID = 823456 };
lst.Add(e3);            

//Distinct (without IEqalityComparer<T>) - Returns 4 employees
var lstDistinct1 = lst.Distinct();

//Lambda Extension - Return 2 employees
var lstDistinct = lst.Distinct(employee => employee.EmployeeID.GetHashCode() ^ employee.Name.ToUpper().Replace(" ", "").GetHashCode()); 
Shantanu
fonte