LINQ: różne wartości

136

Mam następujący zestaw elementów z XML:

id           category

5            1
5            3
5            4
5            3
5            3

Potrzebuję osobnej listy tych elementów:

5            1
5            3
5            4

Jak mogę wyróżnić kategorię ORAZ identyfikator również w LINQ?

balint
źródło

Odpowiedzi:

221

Czy starasz się odróżnić więcej niż jedno pole? Jeśli tak, po prostu użyj typu anonimowego i operatora Distinct i powinno być dobrze:

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

Jeśli próbujesz uzyskać odrębny zestaw wartości „większego” typu, ale patrząc tylko na pewien podzbiór właściwości dla aspektu odrębności, prawdopodobnie chcesz, aby DistinctByzaimplementowano je w MoreLINQ w 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;
         }
     }
 }

(Jeśli przekażesz nulljako element porównujący, użyje domyślnej funkcji porównującej dla typu klucza).

Jon Skeet
źródło
Och, więc przez „większy typ” możesz mieć na myśli, że nadal chcę wszystkich właściwości w wyniku, mimo że chcę porównać tylko kilka właściwości, aby określić odrębność?
The Red Pea
@TheRedPea: Tak, dokładnie.
Jon Skeet,
27

Oprócz odpowiedzi Jona Skeeta możesz również użyć grupy według wyrażeń, aby uzyskać unikalne grupy wraz z liczbą dla każdej iteracji grup:

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
źródło
4
Napisałeś „oprócz odpowiedzi Jona Skeeta”… Nie wiem, czy coś takiego jest możliwe. ;)
Yehuda Makarov
13

Dla każdego, kto wciąż szuka; oto inny sposób implementacji niestandardowej funkcji porównującej lambda.

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

możesz następnie utworzyć rozszerzenie linq Distinct, które może przyjmować lambdy

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

Stosowanie:

var availableItems = list.Distinct((p, p1) => p.Id== p1.Id);
Ricky G.
źródło
Patrząc na źródło odniesienia, Distinct używa zestawu skrótów do przechowywania elementów, które już uzyskał. Zawsze zwracanie tego samego kodu skrótu oznacza, że ​​każdy poprzednio zwrócony element jest sprawdzany za każdym razem. Bardziej niezawodny kod skrótu przyspieszyłby działanie, ponieważ porównywałby tylko elementy z tego samego zasobnika mieszania. Zero jest rozsądną wartością domyślną, ale warto wspierać drugą lambdę dla kodu skrótu.
Darryl
Słuszna uwaga! Spróbuję edytować, gdy będę miał czas, jeśli obecnie pracujesz w tej domenie, nie krępuj się edytować
Ricky G
8

Trochę spóźniłem się z odpowiedzią, ale możesz to zrobić, jeśli chcesz mieć cały element, a nie tylko wartości, według których chcesz pogrupować:

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

To da ci pierwszy cały element pasujący do twojej grupy przez zaznaczenie, podobnie jak drugi przykład Jona Skeetsa używającego DistinctBy, ale bez implementowania funkcji porównującej IEqualityComparer. DistinctBy najprawdopodobniej będzie szybsze, ale powyższe rozwiązanie będzie wymagało mniej kodu, jeśli wydajność nie jest problemem.

Olle Johansson
źródło
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
źródło
0

Ponieważ mówimy o posiadaniu każdego elementu dokładnie raz, „zestaw” ma dla mnie większy sens.

Przykład z zaimplementowanymi klasami i IEqualityComparer:

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

Teraz

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

setList będą miały unikalne elementy

Myślałem o tym, mając do czynienia z tym, .Except()który zwraca różnicę zestawu

Aditya AVS
źródło