Відмінність () LINQ від певної властивості


1094

Я граю з LINQ, щоб дізнатися про це, але не можу зрозуміти, як користуватися, Distinctколи у мене немає простого списку (простий список цілих чисел зробити досить просто, це не питання). Що я хочу, якщо я хочу використовувати Distinct у списку Об'єкта на одній або кількох властивостях об'єкта?

Приклад: Якщо об’єкт є Person, з властивістю Id. Як я можу отримати всю Особу та використовувати Distinctна них майно Idоб'єкта?

Person1: Id=1, Name="Test1"
Person2: Id=1, Name="Test1"
Person3: Id=2, Name="Test2"

Як я можу отримати справедливий Person1і Person3? Це можливо?

Якщо це неможливо з LINQ, який би кращий спосіб мати список Personзалежно від деяких його властивостей у .NET 3.5?

Відповіді:


1243

EDIT : Зараз це частина MoreLINQ .

Те, що вам потрібно, це ефективно "розрізнення". Я не вірю, що вона є частиною LINQ як є, хоча написати досить просто:

public static IEnumerable<TSource> DistinctBy<TSource, TKey>
    (this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
    HashSet<TKey> seenKeys = new HashSet<TKey>();
    foreach (TSource element in source)
    {
        if (seenKeys.Add(keySelector(element)))
        {
            yield return element;
        }
    }
}

Отже, щоб знайти різні значення, використовуючи лише Idвластивість, ви можете використовувати:

var query = people.DistinctBy(p => p.Id);

А для використання декількох властивостей можна використовувати анонімні типи, які належним чином реалізують рівність:

var query = people.DistinctBy(p => new { p.Id, p.Name });

Неперевірений, але він повинен працювати (і він зараз принаймні компілює).

Він передбачає порівняння за замовчуванням для ключів - якщо ви хочете передати порівняння рівності, просто передайте його HashSetконструктору.



1
@ ashes999: Я не впевнений, що ти маєш на увазі. Код присутній у відповіді та в бібліотеці - залежно від того, чи раді ви приймати залежність.
Джон Скіт

10
@ ashes999: Якщо ви робите це лише в одному місці, будь-коли, тоді, звичайно, використовувати GroupByпростіше. Якщо вам це потрібно в більш ніж одному місці, набагато чистіше (ІМО) інкапсулювати наміри.
Джон Скіт

5
@MatthewWhited: З огляду на те, що тут не згадується IQueryable<T>, я не бачу, наскільки це актуально. Я погоджуюсь, що це не підходить для EF тощо, але в межах LINQ to Objects я вважаю, що це більше підходить, ніж GroupBy. Контекст питання завжди важливий.
Джон Скіт

7
Проект перемістився на github, ось код DistinctBy: github.com/morelinq/MoreLINQ/blob/master/MoreLinq/DistinctBy.cs
Phate01

1858

Що робити, якщо я хочу отримати чіткий список на основі однієї або декількох властивостей?

Просто! Ви хочете згрупувати їх і вибрати переможця з групи.

List<Person> distinctPeople = allPeople
  .GroupBy(p => p.PersonId)
  .Select(g => g.First())
  .ToList();

Якщо ви хочете визначити групи на кількох властивостях, ось як:

List<Person> distinctPeople = allPeople
  .GroupBy(p => new {p.PersonId, p.FavoriteColor} )
  .Select(g => g.First())
  .ToList();

1
@ErenErsonmez впевнений. З моїм розміщеним кодом, якщо потрібно відкладене виконання, залиште виклик ToList.
Емі Б

5
Дуже приємна відповідь! Realllllly допоміг мені в Linq-to-Entities, керованому з виду sql, де я не міг змінити подання. Мені потрібно було використовувати FirstOrDefault (), а не First () - все добре.
Алекс Кейсміт

8
Я спробував це, і він повинен змінитись на Select (g => g.FirstOrDefault ())

26
@ChocapicSz Nope. І кожен, Single()і SingleOrDefault()кожен кидок, коли у джерела більше одного елемента. У цій операції ми очікуємо можливості, що кожна група може мати більше одного елемента. З цього приводу First()перевага віддається FirstOrDefault()тому, що в кожній групі повинен бути принаймні один член .... якщо ви не використовуєте EntityFramework, який не може зрозуміти, що кожна група має хоча б одного члена та вимоги FirstOrDefault().
Емі Б

2
Начебто зараз не підтримується в EF Core, навіть використовуючи FirstOrDefault() github.com/dotnet/efcore/isissue/12088 Я на 3.1, і я не можу "перекласти" помилки.
Коллін М. Барретт

78

Використання:

List<Person> pList = new List<Person>();
/* Fill list */

var result = pList.Where(p => p.Name != null).GroupBy(p => p.Id).Select(grp => grp.FirstOrDefault());

whereДозволяє фільтрувати записи (може бути більш складним) , а groupbyй selectвиконують певну функцію.


1
Ідеально, і працює без розширення Linq або використання іншої залежності.
DavidScherer

77

Ви також можете використовувати синтаксис запитів, якщо ви хочете, щоб він виглядав у всіх LINQ-подібних формах:

var uniquePeople = from p in people
                   group p by new {p.ID} //or group by new {p.ID, p.Name, p.Whatever}
                   into mygroup
                   select mygroup.FirstOrDefault();

4
Хм, мої думки, і синтаксис запитів, і синтаксис вільного API є такими ж LINQ, як один одного, і його просто перевагу над тим, якими користуються люди. Я сам віддаю перевагу вільному API, тому я вважаю, що це більше LINK-Like, але тоді я думаю, що це суб'єктивно
Макс Керролл

LINQ-Like не має нічого спільного з уподобанням, оскільки "схожий на LINQ" пов'язаний з тим, щоб виглядати, як інша мова запитів вбудовується в C #, я віддаю перевагу вільному інтерфейсу, що надходить від потоків Java, але це НЕ LINQ-Like.
Райан Ліч

Відмінно !! Ти мій герой!
Farzin Kanzi

63

Я думаю, що цього достатньо:

list.Select(s => s.MyField).Distinct();

43
Що робити, якщо він потребує назад свого повного об'єкта, а не тільки цього конкретного поля?
Фестим Кахані

1
Що саме об'єкт із кількох об'єктів, які мають однакове значення властивості?
donRumatta

40

Розв’яжіть спочатку групу за своїми полями, а потім виберіть перший або за замовчуванням елемент.

    List<Person> distinctPeople = allPeople
   .GroupBy(p => p.PersonId)
   .Select(g => g.FirstOrDefault())
   .ToList();

26

Це можна зробити зі стандартом Linq.ToLookup(). Це створить набір значень для кожного унікального ключа. Просто виберіть перший елемент колекції

Persons.ToLookup(p => p.Id).Select(coll => coll.First());

17

Наступний код функціонально еквівалентний відповіді Джона Скіта .

Тестований на .NET 4.5, повинен працювати на будь-якій попередній версії LINQ.

public static IEnumerable<TSource> DistinctBy<TSource, TKey>(
  this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
  HashSet<TKey> seenKeys = new HashSet<TKey>();
  return source.Where(element => seenKeys.Add(keySelector(element)));
}

Випадково перегляньте останню версію DistinctBy.cs від Jon Skeet на Google Code .


3
Це дало мені "послідовність не має помилок значень", але відповідь Скіта дала правильний результат.
Що було б круто

10

Я написав статтю, в якій пояснюється, як розширити функцію Distinct, щоб ви могли зробити наступне:

var people = new List<Person>();

people.Add(new Person(1, "a", "b"));
people.Add(new Person(2, "c", "d"));
people.Add(new Person(1, "a", "b"));

foreach (var person in people.Distinct(p => p.ID))
    // Do stuff with unique list here.

Ось стаття: Розширення LINQ - Вказівка ​​властивості у відмінній функції


3
У вашій статті є помилка, після розрізнення повинно бути <T>: public static IEnumerable <T> Distinct (це ... Також не схоже, що воно буде працювати (добре) на більш ніж одній властивості, тобто комбінації першого та прізвища.
рядок1,

2
+1, незначна помилка не є достатньою причиною для прихильності, що просто так нерозумно часто закликає друкарські помилки. І я ще не бачу загальної функції, яка буде працювати для будь-якої кількості властивостей! Я сподіваюсь, що молодший працівник також відмовився від будь-якої іншої відповіді в цій темі. Але ей, що це за об'єкт другого типу ?? Я заперечую!
nawfal

4
Ваше посилання порушено
Том Лінт,

7

Особисто я використовую наступний клас:

public class LambdaEqualityComparer<TSource, TDest> : 
    IEqualityComparer<TSource>
{
    private Func<TSource, TDest> _selector;

    public LambdaEqualityComparer(Func<TSource, TDest> selector)
    {
        _selector = selector;
    }

    public bool Equals(TSource obj, TSource other)
    {
        return _selector(obj).Equals(_selector(other));
    }

    public int GetHashCode(TSource obj)
    {
        return _selector(obj).GetHashCode();
    }
}

Потім, спосіб розширення:

public static IEnumerable<TSource> Distinct<TSource, TCompare>(
    this IEnumerable<TSource> source, Func<TSource, TCompare> selector)
{
    return source.Distinct(new LambdaEqualityComparer<TSource, TCompare>(selector));
}

Нарешті, передбачуване використання:

var dates = new List<DateTime>() { /* ... */ }
var distinctYears = dates.Distinct(date => date.Year);

Перевага, яку я виявив, використовуючи цей підхід, - це повторне використання LambdaEqualityComparerкласу для інших методів, які приймають IEqualityComparer. (О, і я залишаю yieldматеріали для оригінальної реалізації LINQ ...)


5

Якщо вам потрібен метод розрізнення для кількох властивостей, ви можете перевірити мою бібліотеку PowerfulExtensions . В даний час він знаходиться в дуже молодій стадії, але вже можна використовувати такі методи, як Distinct, Union, Intersect, за винятком будь-якої кількості властивостей;

Ось як ви його використовуєте:

using PowerfulExtensions.Linq;
...
var distinct = myArray.Distinct(x => x.A, x => x.B);

5

Коли ми стикалися з таким завданням в нашому проекті, ми визначили невеликий API для складання компараторів.

Отже, випадок використання виглядав так:

var wordComparer = KeyEqualityComparer.Null<Word>().
    ThenBy(item => item.Text).
    ThenBy(item => item.LangID);
...
source.Select(...).Distinct(wordComparer);

І сам API виглядає так:

using System;
using System.Collections;
using System.Collections.Generic;

public static class KeyEqualityComparer
{
    public static IEqualityComparer<T> Null<T>()
    {
        return null;
    }

    public static IEqualityComparer<T> EqualityComparerBy<T, K>(
        this IEnumerable<T> source,
        Func<T, K> keyFunc)
    {
        return new KeyEqualityComparer<T, K>(keyFunc);
    }

    public static KeyEqualityComparer<T, K> ThenBy<T, K>(
        this IEqualityComparer<T> equalityComparer,
        Func<T, K> keyFunc)
    {
        return new KeyEqualityComparer<T, K>(keyFunc, equalityComparer);
    }
}

public struct KeyEqualityComparer<T, K>: IEqualityComparer<T>
{
    public KeyEqualityComparer(
        Func<T, K> keyFunc,
        IEqualityComparer<T> equalityComparer = null)
    {
        KeyFunc = keyFunc;
        EqualityComparer = equalityComparer;
    }

    public bool Equals(T x, T y)
    {
        return ((EqualityComparer == null) || EqualityComparer.Equals(x, y)) &&
                EqualityComparer<K>.Default.Equals(KeyFunc(x), KeyFunc(y));
    }

    public int GetHashCode(T obj)
    {
        var hash = EqualityComparer<K>.Default.GetHashCode(KeyFunc(obj));

        if (EqualityComparer != null)
        {
            var hash2 = EqualityComparer.GetHashCode(obj);

            hash ^= (hash2 << 5) + hash2;
        }

        return hash;
    }

    public readonly Func<T, K> KeyFunc;
    public readonly IEqualityComparer<T> EqualityComparer;
}

Детальніше на нашому сайті: IEqualityComparer в LINQ .


5

Ви можете використовувати DistinctBy () для отримання розрізнених записів за властивістю об'єкта. Просто додайте наступне твердження перед його використанням:

використання Microsoft.Ajax.Utilities;

а потім використовувати його так:

var listToReturn = responseList.DistinctBy(x => x.Index).ToList();

де 'Index' є властивістю, на якій я хочу, щоб дані відрізнялися.


4

Ви можете це зробити (хоч і не блискавично) так:

people.Where(p => !people.Any(q => (p != q && p.Id == q.Id)));

Тобто, "виберіть усіх людей, де в списку немає іншої людини з тим самим ідентифікатором".

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


4

Якщо ви не хочете додати бібліотеку MoreLinq до свого проекту лише для того, щоб отримати DistinctByфункціональність, ви можете отримати той самий кінцевий результат, використовуючи перевантаження методу Linq, Distinctякий бере IEqualityComparerаргумент.

Ви починаєте зі створення загального класу порівняння рівнів користувача, який використовує синтаксис лямбда для виконання спеціального порівняння двох екземплярів загального класу:

public class CustomEqualityComparer<T> : IEqualityComparer<T>
{
    Func<T, T, bool> _comparison;
    Func<T, int> _hashCodeFactory;

    public CustomEqualityComparer(Func<T, T, bool> comparison, Func<T, int> hashCodeFactory)
    {
        _comparison = comparison;
        _hashCodeFactory = hashCodeFactory;
    }

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

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

Потім у своєму головному коді ви використовуєте його так:

Func<Person, Person, bool> areEqual = (p1, p2) => int.Equals(p1.Id, p2.Id);

Func<Person, int> getHashCode = (p) => p.Id.GetHashCode();

var query = people.Distinct(new CustomEqualityComparer<Person>(areEqual, getHashCode));

Вуаля! :)

Сказане передбачає наступне:

  • Властивість Person.Idтипуint
  • peopleКолекція не містить будь - яких елементів невизначені

Якщо колекція може містити нулі, просто перепишіть лямбда, щоб перевірити наявність нуля, наприклад:

Func<Person, Person, bool> areEqual = (p1, p2) => 
{
    return (p1 != null && p2 != null) ? int.Equals(p1.Id, p2.Id) : false;
};

EDIT

Цей підхід схожий на той, що відповідає у відповіді Володимира Нестеровського, але простіший.

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

Однак якщо ваші об’єкти можуть колись відрізнятися, Idто інший користувач дав правильну відповідь, що все, що вам потрібно зробити, це переосмислити реалізацію за замовчуванням GetHashCode()та Equals()у вашому Personкласі, а потім просто застосувати Distinct()метод " out of the box" від Linq для фільтрації з будь-яких дублікатів.


Я хочу отримати лише унікальні елементи в довіднику. Чи можете ви допомогти, я використовую цей код, якщо TempDT IsNot Nothing тоді m_ConcurrentScriptDictionary = TempDT.AsEnumerable.ToDictionary (Функція (x) x.SafeField (fldClusterId, NULL_ID_VALUE), Функція (y) y.SafeField (fldParamValue11, NULL_ID_VALUE))
RSB

2

Найкращий спосіб зробити це сумісним з іншими версіями .NET - це перекрити значення Equals та GetHash для вирішення цього питання (див. Питання щодо переповнення стека. Цей код повертає різні значення. Однак, я хочу повернути колекцію з сильним типом, на відміну від анонімний тип ), але якщо вам потрібно щось загальне в усьому коді, рішення в цій статті чудові.


1
List<Person>lst=new List<Person>
        var result1 = lst.OrderByDescending(a => a.ID).Select(a =>new Player {ID=a.ID,Name=a.Name} ).Distinct();

Ви мали намір Select() new Personзамість цього new Player? Те, що ви замовляєте, IDякось не сповіщає Distinct()використовувати цю властивість для визначення унікальності, однак це не спрацює.
БАКОН

1

Перевизначити рівні (об’єкт obj) та GetHashCode () методи:

class Person
{
    public int Id { get; set; }
    public int Name { get; set; }

    public override bool Equals(object obj)
    {
        return ((Person)obj).Id == Id;
        // or: 
        // var o = (Person)obj;
        // return o.Id == Id && o.Name == Name;
    }
    public override int GetHashCode()
    {
        return Id.GetHashCode();
    }
}

а потім просто зателефонуйте:

List<Person> distinctList = new[] { person1, person2, person3 }.Distinct().ToList();

Однак GetHashCode () повинен бути більш досконалим (рахувати також Ім'я), на мою думку, ця відповідь, мабуть, найкраща. Власне, для архівації цільової логіки не потрібно переосмислювати GetHashCode (), достатньо рівнянь (), але якщо нам потрібна продуктивність, ми повинні її перекрити. Усі алгоритми порівняння, спочатку перевірте хеш, а якщо вони рівні, то виклик рівнянь ().
Олег Скрипняк

Крім того, там, у рівності (), перший рядок повинен бути "if (! (Obj is Person)) return false". Але найкраща практика полягає в тому, щоб використовувати окремий об'єкт, призначений для типу, наприклад "var o = obj як Person; if (o == null) return false;" потім перевірити рівність з o без кастингу
Олег Скрипняк

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

0

Ви повинні мати змогу перевизначити рівних на людину, щоб насправді зробити рівними на Person.id. Це повинно призвести до поведінки, яку ви хочете.


-5

Спробуйте спробувати код нижче.

var Item = GetAll().GroupBy(x => x .Id).ToList();

3
Коротка відповідь вітається, проте вона не надасть великої цінності останнім користувачам, які намагаються зрозуміти, що відбувається за проблемою. Будь ласка, приділіть трохи часу, щоб пояснити, що є справжньою проблемою, щоб викликати проблему та як її вирішити. Дякую ~
Херен
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.