LINQ: valores distintos

136

Tengo el siguiente elemento establecido desde un XML:

id           category

5            1
5            3
5            4
5            3
5            3

Necesito una lista distinta de estos elementos:

5            1
5            3
5            4

¿Cómo puedo distinguir también la categoría AND Id en LINQ?

balint
fuente

Respuestas:

221

¿Estás tratando de distinguirte por más de un campo? Si es así, solo use un tipo anónimo y el operador Distinct y debería estar bien:

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

Si está tratando de obtener un conjunto distinto de valores de un tipo "más grande", pero solo está mirando algún subconjunto de propiedades para el aspecto de distinción, probablemente desee DistinctByimplementarlo en MoreLINQ en 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;
         }
     }
 }

(Si pasa nullcomo comparador, usará el comparador predeterminado para el tipo de clave).

Jon Skeet
fuente
Ah, entonces, por "tipo más grande" puede querer decir que todavía quiero todas las propiedades en el resultado, aunque solo quiero comparar algunas propiedades para determinar la distinción.
The Red Pea
@TheRedPea: Sí, exactamente.
Jon Skeet
27

Además de la respuesta de Jon Skeet, también puede usar el grupo por expresiones para obtener los grupos únicos junto con un conteo para cada iteración de grupos:

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
fuente
44
Escribiste "además de la respuesta de Jon Skeet" ... No sé si tal cosa es posible. ;)
Yehuda Makarov
13

Para cualquiera que siga mirando; Aquí hay otra forma de implementar un 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;
        }
    }

luego puede crear una extensión para linq Distinct que puede incluir 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
fuente
Al observar la fuente de referencia, Distinct utiliza un conjunto de hash para almacenar elementos que ya ha producido. Siempre devolver el mismo código hash significa que cada elemento devuelto previamente se examina cada vez. Un código hash más robusto aceleraría las cosas porque solo se compararía con elementos en el mismo depósito hash. Cero es un valor predeterminado razonable, pero podría valer la pena admitir una segunda lambda para el código hash.
Darryl
¡Buen punto! Intentaré editar cuando tenga tiempo, si estás trabajando en este dominio en este momento, siéntete libre de editar
Ricky G
8

Llego un poco tarde a la respuesta, pero es posible que desee hacer esto si desea todo el elemento, no solo los valores por los que desea agrupar:

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

Esto le dará el primer elemento completo que coincida con su grupo por selección, al igual que el segundo ejemplo de Jon Skeets usando DistinctBy, pero sin implementar el comparador IEqualityComparer. DistinctBy probablemente será más rápido, pero la solución anterior implicará menos código si el rendimiento no es un problema.

Olle Johansson
fuente
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
fuente
0

Como estamos hablando de tener cada elemento exactamente una vez, un "conjunto" tiene más sentido para mí.

Ejemplo con clases 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;
        }
    }

Ahora

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

setList tendrá elementos únicos

Pensé en esto mientras trataba con lo .Except()que devuelve una diferencia establecida

Aditya AVS
fuente