Як використовувати LINQ Distinct () з кількома полями


75

У мене є такий клас EF, похідний з бази даних (спрощений)

class Product
{ 
     public string ProductId;
     public string ProductName;
     public string CategoryId;
     public string CategoryName;
}

ProductIdє первинним ключем таблиці.

За неправильне дизайнерське рішення, прийняте дизайнером БД (я не можу його змінити), я маю CategoryIdта CategoryNameв цій таблиці.

Мені потрібен DropDownList з (різними) CategoryIdяк значення та CategoryNameяк текст . Тому я застосував такий код:

product.Select(m => new {m.CategoryId, m.CategoryName}).Distinct();

який логічно повинен створити анонімний об’єкт із властивостями CategoryIdта CategoryNameяк. У Distinct()гарантії , що немає дублює пари ( CategoryId, CategoryName).

Але насправді це не працює. Наскільки я зрозумів Distinct()роботи саме тоді, коли в колекції є лише одне поле, інакше вона їх просто ігнорує ... це правильно? Чи є якийсь обхідний шлях? Дякую!

ОНОВЛЕННЯ

Вибачте product:

List<Product> product = new List<Product>();

Я знайшов альтернативний спосіб отримати той самий результат, що Distinct():

product.GroupBy(d => new {d.CategoryId, d.CategoryName}) 
       .Select(m => new {m.Key.CategoryId, m.Key.CategoryName})

"є лише одне поле у ​​колекції" є безглуздим. Що ви маєте на увазі?
леппі

1
@leppie, я думаю, він має на увазі, коли проектує на одне значення, а не на анонімний тип (містить більше одного поля).
sehe

Msgstr "За неправильне дизайнерське рішення, прийняте дизайнером БД (я не можу його змінити)". Можливо, ви не можете змінити базу даних, але це не означає, що ви не можете виправити це у своїй моделі EF. Це чудово EF.
Стівен

Де це не працює? Де ви знаходитесь: класичний asp.net, mvc? Що таке "товар" у продукті? Виберіть?
Рафаель Алтхаус,

@leppie, вибачте, я мав на увазі "власність у колекції"
CiccioMiami

Відповіді:


70

Я припускаю, що ви використовуєте відмінний, як виклик методу у списку. Вам потрібно використовувати результат запиту як джерело даних для вашого DropDownList, наприклад, матеріалізуючи його через ToList.

var distinctCategories = product
                        .Select(m => new {m.CategoryId, m.CategoryName})
                        .Distinct()
                        .ToList();
DropDownList1.DataSource     = distinctCategories;
DropDownList1.DataTextField  = "CategoryName";
DropDownList1.DataValueField = "CategoryId";

Інший спосіб, якщо вам потрібні реальні об’єкти замість анонімного типу з лише кількома властивостями, це використання GroupByз анонімним типом:

List<Product> distinctProductList = product
    .GroupBy(m => new {m.CategoryId, m.CategoryName})
    .Select(group => group.First())  // instead of First you can also apply your logic here what you want to take, for example an OrderBy
    .ToList();

Третім варіантом є використання MoreLinqDistinctBy .


1
Прихильники @CiccioMiami, переконайтеся, що ви застосовуєте цю відповідь лише до анонімних типів. Для набраних класів може знадобитися такий делегат, як цей .
crokusek

Правильно, чіткий
код

1
@imdadhusen: не тільки для анонімних типів, тип повинен перевизначити Equals+, GetHashCodeабо ви повинні вказати власний IEQualityCpomparer<TypeName>при перевантаженні Distinct.
Тім Шмельтер

@Tim, чи можу я разом використовувати Де та групувати за умовами? У вашому другому запиті або коді я просто хочу застосувати де умова перед групою, так що це можливо?
Md Aslam

11

Distinct () гарантує відсутність пари дублікатів (CategoryId, CategoryName).

- саме це

Анонімні типи "магічно" реалізують EqualsіGetHashcode

Я припускаю ще десь помилку. Чутливість до регістру? Змінні класи? Непорівнянні поля?


Це те, що я хоч, але відповідь нижче говорить ні. / мене розгубило.
леппі


4

Окремий метод повертає різні елементи послідовності.

Якщо ви подивитесь на його реалізацію за допомогою Reflector, ви побачите, що він створюється DistinctIteratorдля вашого анонімного типу. Виразний ітератор додає елементи Setпри переліченні над колекцією. Цей перерахувач пропускає всі елементи, які вже є Set. Setвикористання GetHashCodeта Equalsметоди для визначення, чи елемент вже існує в Set.

Як GetHashCodeі Equalsреалізовано для анонімного типу? Як зазначено на msdn :

Методи Equals та GetHashCode для анонімних типів визначаються з точки зору методів Equals та GetHashcode властивостей, два екземпляри одного і того ж анонімного типу рівні, лише якщо всі їх властивості рівні.

Отже, ви обов’язково повинні мати різні анонімні об’єкти під час ітерації на окремій колекції. І результат не залежить від того, скільки полів ви використовуєте для свого анонімного типу.


4

Використовуйте Keyключове слово, вибране вами, буде працювати, як показано нижче.

product.Select(m => new {Key m.CategoryId, Key m.CategoryName}).Distinct();

Я усвідомлюю, що це стосується старої теми, але вважав, що це може допомогти деяким людям. Я зазвичай кодую у VB.NET під час роботи з .NET, тому Keyможу перекласти по-різному на C #.


3
Для розрізнення кількох полів у VB.Net потрібно використовувати ключові слова «Key» для кожної властивості анонімного типу. (В іншому випадку хеш-код, який використовується для порівняння, не отримуватиме належного обчислення) У C # немає ключового слова "Key" - натомість усі властивості в анонімних типах автоматично є полями Key.
вон

4

Відповідаючи на заголовок питання (що привернуло людей сюди), ігноруючи, що в прикладі використовуються анонімні типи ....

Це рішення також буде працювати для неанонімних типів. Це не потрібно для анонімних типів.

Клас помічників:

/// <summary>
/// Allow IEqualityComparer to be configured within a lambda expression.
/// From /programming/98033/wrap-a-delegate-in-an-iequalitycomparer
/// </summary>
/// <typeparam name="T"></typeparam>
public class LambdaEqualityComparer<T> : IEqualityComparer<T>
{
    readonly Func<T, T, bool> _comparer;
    readonly Func<T, int> _hash;

    /// <summary>
    /// Simplest constructor, provide a conversion to string for type T to use as a comparison key (GetHashCode() and Equals().
    /// /programming/98033/wrap-a-delegate-in-an-iequalitycomparer, user "orip"
    /// </summary>
    /// <param name="toString"></param>
    public LambdaEqualityComparer(Func<T, string> toString)
        : this((t1, t2) => toString(t1) == toString(t2), t => toString(t).GetHashCode())
    {
    }

    /// <summary>
    /// Constructor.  Assumes T.GetHashCode() is accurate.
    /// </summary>
    /// <param name="comparer"></param>
    public LambdaEqualityComparer(Func<T, T, bool> comparer)
        : this(comparer, t => t.GetHashCode())
    {
    }

    /// <summary>
    /// Constructor, provide a equality comparer and a hash.
    /// </summary>
    /// <param name="comparer"></param>
    /// <param name="hash"></param>
    public LambdaEqualityComparer(Func<T, T, bool> comparer, Func<T, int> hash)
    {
        _comparer = comparer;
        _hash = hash;
    }

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

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

Найпростіше використання:

List<Product> products = duplicatedProducts.Distinct(
    new LambdaEqualityComparer<Product>(p =>
        String.Format("{0}{1}{2}{3}",
            p.ProductId,
            p.ProductName,
            p.CategoryId,
            p.CategoryName))
        ).ToList();

Найпростішим (але не таким ефективним) використанням є зіставлення з рядковим поданням, щоб уникнути власного хешування. Рівні рядки вже мають рівні хеш-коди.

Посилання:
Оберніть делегата в IEqualityComparer


4

Це моє рішення, воно підтримує селектори ключів різних типів:

public static IEnumerable<TSource> DistinctBy<TSource>(this IEnumerable<TSource> source, params Func<TSource, object>[] keySelectors)
{
    // initialize the table
    var seenKeysTable = keySelectors.ToDictionary(x => x, x => new HashSet<object>());

    // loop through each element in source
    foreach (var element in source)
    {
        // initialize the flag to true
        var flag = true;

        // loop through each keySelector a
        foreach (var (keySelector, hashSet) in seenKeysTable)
        {                    
            // if all conditions are true
            flag = flag && hashSet.Add(keySelector(element));
        }

        // if no duplicate key was added to table, then yield the list element
        if (flag)
        {
            yield return element;
        }
    }
}

Щоб використовувати його:

list.DistinctBy(d => d.CategoryId, d => d.CategoryName)

1
Перевага цього підходу полягає в тому, що він працює як на анонімних, так і на визначених типах.
crokusek

Виникли проблеми з цим рішенням у .net 4.7.2. Можливо, це працює лише в .net Core
drzounds

1
public List<ItemCustom2> GetBrandListByCat(int id)
    {

        var OBJ = (from a in db.Items
                   join b in db.Brands on a.BrandId equals b.Id into abc1
                   where (a.ItemCategoryId == id)
                   from b in abc1.DefaultIfEmpty()
                   select new
                   {
                       ItemCategoryId = a.ItemCategoryId,
                       Brand_Name = b.Name,
                       Brand_Id = b.Id,
                       Brand_Pic = b.Pic,

                   }).Distinct();


        List<ItemCustom2> ob = new List<ItemCustom2>();
        foreach (var item in OBJ)
        {
            ItemCustom2 abc = new ItemCustom2();
            abc.CategoryId = item.ItemCategoryId;
            abc.BrandId = item.Brand_Id;
            abc.BrandName = item.Brand_Name;
            abc.BrandPic = item.Brand_Pic;
            ob.Add(abc);
        }
        return ob;

    }

0

рішення вашої проблеми виглядає так:

public class Category {
  public long CategoryId { get; set; }
  public string CategoryName { get; set; }
} 

...

public class CategoryEqualityComparer : IEqualityComparer<Category>
{
   public bool Equals(Category x, Category y)
     => x.CategoryId.Equals(y.CategoryId)
          && x.CategoryName .Equals(y.CategoryName, 
 StringComparison.OrdinalIgnoreCase);

   public int GetHashCode(Mapping obj)
     => obj == null 
         ? 0
         : obj.CategoryId.GetHashCode()
           ^ obj.CategoryName.GetHashCode();
}

...

 var distinctCategories = product
     .Select(_ => 
        new Category {
           CategoryId = _.CategoryId, 
           CategoryName = _.CategoryName
        })
     .Distinct(new CategoryEqualityComparer())
     .ToList();

-3
Employee emp1 = new Employee() { ID = 1, Name = "Narendra1", Salary = 11111, Experience = 3, Age = 30 };Employee emp2 = new Employee() { ID = 2, Name = "Narendra2", Salary = 21111, Experience = 10, Age = 38 };
Employee emp3 = new Employee() { ID = 3, Name = "Narendra3", Salary = 31111, Experience = 4, Age = 33 };
Employee emp4 = new Employee() { ID = 3, Name = "Narendra4", Salary = 41111, Experience = 7, Age = 33 };

List<Employee> lstEmployee = new List<Employee>();

lstEmployee.Add(emp1);
lstEmployee.Add(emp2);
lstEmployee.Add(emp3);
lstEmployee.Add(emp4);

var eemmppss=lstEmployee.Select(cc=>new {cc.ID,cc.Age}).Distinct();

Що робити, якщо я хочу зараз зробити замовлення за віком?
Si8,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.