LINQ: valores distintos

136

Eu tenho o seguinte conjunto de itens de um XML:

id           category

5            1
5            3
5            4
5            3
5            3

Preciso de uma lista distinta desses itens:

5            1
5            3
5            4

Como posso diferenciar também a Categoria E ID no LINQ?

balint
fonte

Respostas:

221

Você está tentando se diferenciar em mais de um campo? Nesse caso, basta usar um tipo anônimo e o operador Distinct e tudo ficará bem:

var query = doc.Elements("whatever")
               .Select(element => new {
                             id = (int) element.Attribute("id"),
                             category = (int) element.Attribute("cat") })
               .Distinct();

Se você está tentando obter um conjunto distinto de valores de um tipo "maior", mas apenas olhando para um subconjunto de propriedades para o aspecto distintivo, provavelmente deseja DistinctByimplementá-lo no MoreLINQ em DistinctBy.cs:

 public static IEnumerable<TSource> DistinctBy<TSource, TKey>(
     this 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;
         }
     }
 }

(Se você passar nullcomo comparador, ele usará o comparador padrão para o tipo de chave.)

Jon Skeet
fonte
Ah, então, por "tipo maior" você pode querer dizer que ainda quero todas as propriedades no resultado, mesmo que eu queira apenas comparar algumas propriedades para determinar a distinção?
The Red Pea
@TheRedPea: Sim, exatamente.
Jon tiro ao prato
27

Além da resposta de Jon Skeet, você também pode usar o grupo por expressões para obter os grupos únicos com uma contagem para as iterações de cada grupo:

var query = from e in doc.Elements("whatever")
            group e by new { id = e.Key, val = e.Value } into g
            select new { id = g.Key.id, val = g.Key.val, count = g.Count() };
James Alexander
fonte
4
Você escreveu "além da resposta de Jon Skeet" ... Não sei se isso é possível. ;)
Yehuda Makarov
13

Para quem ainda procura; aqui está outra maneira de implementar um comparador lambda personalizado.

public class LambdaComparer<T> : IEqualityComparer<T>
    {
        private readonly Func<T, T, bool> _expression;

        public LambdaComparer(Func<T, T, bool> lambda)
        {
            _expression = lambda;
        }

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

        public int GetHashCode(T obj)
        {
            /*
             If you just return 0 for the hash the Equals comparer will kick in. 
             The underlying evaluation checks the hash and then short circuits the evaluation if it is false.
             Otherwise, it checks the Equals. If you force the hash to be true (by assuming 0 for both objects), 
             you will always fall through to the Equals check which is what we are always going for.
            */
            return 0;
        }
    }

você pode criar uma extensão para o linq Distinct que pode receber os lambda

   public static IEnumerable<T> Distinct<T>(this IEnumerable<T> list,  Func<T, T, bool> lambda)
        {
            return list.Distinct(new LambdaComparer<T>(lambda));
        }  

Uso:

var availableItems = list.Distinct((p, p1) => p.Id== p1.Id);
Ricky G
fonte
Observando a fonte de referência, o Distinct usa um conjunto de hash para armazenar elementos que já produziu. Sempre retornando o mesmo código hash significa que todos os elementos retornados anteriormente são examinados toda vez. Um código hash mais robusto agilizaria as coisas, porque seria comparado apenas com elementos no mesmo bucket de hash. Zero é um padrão razoável, mas pode valer a pena oferecer suporte a um segundo lambda para o código de hash.
Darryl
Bom ponto! Vou tentar editar quando eu chegar em tempo, se você estiver trabalhando neste domínio, no momento, fique à vontade para editar
Ricky G
8

Estou um pouco atrasado para a resposta, mas você pode fazer isso se quiser o elemento inteiro, não apenas os valores pelos quais deseja agrupar:

var query = doc.Elements("whatever")
               .GroupBy(element => new {
                             id = (int) element.Attribute("id"),
                             category = (int) element.Attribute("cat") })
               .Select(e => e.First());

Isso fornecerá o primeiro elemento inteiro correspondente ao seu grupo por seleção, como o segundo exemplo de Jon Skeets usando DistinctBy, mas sem implementar o comparador IEqualityComparer. O DistinctBy provavelmente será mais rápido, mas a solução acima envolverá menos código se o desempenho não for um problema.

Olle Johansson
fonte
4
// First Get DataTable as dt
// DataRowComparer Compare columns numbers in each row & data in each row

IEnumerable<DataRow> Distinct = dt.AsEnumerable().Distinct(DataRowComparer.Default);

foreach (DataRow row in Distinct)
{
    Console.WriteLine("{0,-15} {1,-15}",
        row.Field<int>(0),
        row.Field<string>(1)); 
}
Mohamed Elsayed
fonte
0

Como estamos falando de ter todos os elementos exatamente uma vez, um "conjunto" faz mais sentido para mim.

Exemplo com classes e IEqualityComparer implementados:

 public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }

        public Product(int x, string y)
        {
            Id = x;
            Name = y;
        }
    }

    public class ProductCompare : IEqualityComparer<Product>
    {
        public bool Equals(Product x, Product y)
        {  //Check whether the compared objects reference the same data.
            if (Object.ReferenceEquals(x, y)) return true;

            //Check whether any of the compared objects is null.
            if (Object.ReferenceEquals(x, null) || Object.ReferenceEquals(y, null))
                return false;

            //Check whether the products' properties are equal.
            return x.Id == y.Id && x.Name == y.Name;
        }
        public int GetHashCode(Product product)
        {
            //Check whether the object is null
            if (Object.ReferenceEquals(product, null)) return 0;

            //Get hash code for the Name field if it is not null.
            int hashProductName = product.Name == null ? 0 : product.Name.GetHashCode();

            //Get hash code for the Code field.
            int hashProductCode = product.Id.GetHashCode();

            //Calculate the hash code for the product.
            return hashProductName ^ hashProductCode;
        }
    }

Agora

List<Product> originalList = new List<Product> {new Product(1, "ad"), new Product(1, "ad")};
var setList = new HashSet<Product>(originalList, new ProductCompare()).ToList();

setList terá elementos únicos

Pensei nisso ao lidar com o .Except()que retorna uma diferença de set

Aditya AVS
fonte