Distinct () di LINQ su una proprietà particolare


1095

Sto giocando con LINQ per conoscerlo, ma non riesco a capire come usare Distinctquando non ho un semplice elenco (un semplice elenco di numeri interi è abbastanza facile da fare, questa non è la domanda). Cosa devo fare se voglio usare Distinct in un elenco di un oggetto su una o più proprietà dell'oggetto?

Esempio: se un oggetto è Person, con Proprietà Id. Come posso ottenere tutte le persone e Distinctusarle con la proprietà Iddell'oggetto?

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

Come posso ottenere solo Person1e Person3? È possibile?

Se non fosse possibile con LINQ, quale sarebbe il modo migliore per avere un elenco di Persondipendendo da alcune delle sue proprietà in .NET 3.5?

Risposte:


1249

EDIT : ora fa parte di MoreLINQ .

Ciò di cui hai bisogno è un "distinto" in modo efficace. Non credo che faccia parte di LINQ così com'è, sebbene sia abbastanza facile scrivere:

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

Quindi, per trovare i valori distinti utilizzando solo la Idproprietà, è possibile utilizzare:

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

E per utilizzare più proprietà, è possibile utilizzare tipi anonimi, che implementano l'uguaglianza in modo appropriato:

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

Non testato, ma dovrebbe funzionare (e ora almeno si compila).

Presuppone tuttavia il comparatore predefinito per le chiavi: se si desidera passare a un comparatore di uguaglianza, è sufficiente passarlo al HashSetcostruttore.



1
@ ashes999: non sono sicuro di cosa intendi. Il codice è presente nella risposta e nella libreria, a seconda che tu sia felice di assumere una dipendenza.
Jon Skeet,

10
@ ashes999: se lo fai solo in un unico posto, mai, quindi, l'utilizzo GroupByè più semplice. Se ne hai bisogno in più di un posto, è molto più pulito (IMO) incapsulare l'intenzione.
Jon Skeet,

5
@MatthewWhited: Dato che qui non si fa menzione IQueryable<T>, non vedo quanto sia rilevante. Sono d'accordo che questo non sarebbe adatto per EF ecc., Ma in LINQ to Objects penso che sia più adatto di GroupBy. Il contesto della domanda è sempre importante.
Jon Skeet il

7
Il progetto si è spostato su github, ecco il codice di DistinctBy: github.com/morelinq/MoreLINQ/blob/master/MoreLinq/DistinctBy.cs
Phate01

1858

Cosa succede se desidero ottenere un elenco distinto basato su una o più proprietà?

Semplice! Vuoi raggrupparli e scegliere un vincitore fuori dal gruppo.

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

Se vuoi definire gruppi su più proprietà, ecco come:

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

1
@ErenErsonmez sicuro. Con il mio codice pubblicato, se si desidera l'esecuzione differita, lasciare la chiamata ToList.
Amy B,

5
Risposta molto bella! Mi ha davvero aiutato in Linq-to-Entities guidato da una vista sql dove non potevo modificare la vista. Avevo bisogno di usare FirstOrDefault () anziché First () - tutto va bene.
Alex KeySmith,

8
L'ho provato e dovrebbe cambiare in Select (g => g.FirstOrDefault ())

26
@ChocapicSz Nope. Entrambi Single()e SingleOrDefault()ogni lancio quando la fonte ha più di un oggetto. In questa operazione, prevediamo la possibilità che ogni gruppo possa avere più di un elemento. Del resto , First()è preferibile FirstOrDefault()perché ogni gruppo deve avere almeno un membro .... a meno che tu non stia usando EntityFramework, il che non riesce a capire che ogni gruppo ha almeno un membro e richiede FirstOrDefault().
Amy B,

2
Sembra non essere attualmente supportato in EF Core, anche usando FirstOrDefault() github.com/dotnet/efcore/issues/12088 Sono su 3.1 e ottengo errori di "incapacità di tradurre".
Collin M. Barrett,

78

Uso:

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

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

L' whereaiuta a filtrare le voci (potrebbe essere più complessa) e la groupbye selectsvolgono la funzione distinta.


1
Perfetto e funziona senza estendere Linq o usare un'altra dipendenza.
DavidScherer,

77

Puoi anche usare la sintassi della query se vuoi che appaia tutto simile a 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
Hmm i miei pensieri sono sia la sintassi della query che la fluente sintassi dell'API sono altrettanto LINQ come l'una dell'altra e la sua giusta preferenza su quali le persone usano. Io stesso preferisco l'API fluente, quindi considererei più LINK-Like ma poi credo sia soggettivo
Max Carroll,

LINQ-Like non ha nulla a che fare con le preferenze, essere "LINQ-like" ha a che fare con l'aspetto di un linguaggio di query diverso incorporato in C #, preferisco l'interfaccia fluente, proveniente da flussi Java, ma NON è LINQ-Like.
Ryan The Leach,

Eccellente!! Tu sei il mio eroe!
Farzin Kanzi,

63

Penso che sia abbastanza:

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

43
E se avesse bisogno del suo intero oggetto, non solo di quel particolare campo?
Festim Cahani,

1
Quale oggetto esattamente dei vari oggetti che hanno lo stesso valore di proprietà?
donRumatta,

40

Risolvi prima il gruppo dai campi, quindi seleziona l'elemento firstordefault.

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

26

Puoi farlo con lo standard Linq.ToLookup(). Ciò creerà una raccolta di valori per ogni chiave univoca. Basta selezionare il primo elemento nella raccolta

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

17

Il seguente codice è funzionalmente equivalente alla risposta di Jon Skeet .

Testato su .NET 4.5, dovrebbe funzionare su qualsiasi versione precedente di 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)));
}

Per inciso, controlla l'ultima versione di DistinctBy.cs di Jon Skeet su Google Code .


3
Questo mi ha dato una "sequenza senza errori di valori", ma la risposta di Skeet ha prodotto il risultato corretto.
Cosa sarebbe bello il

10

Ho scritto un articolo che spiega come estendere la funzione Distinct in modo da poter fare come segue:

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.

Ecco l'articolo: Estensione di LINQ - Specifica di una proprietà nella funzione distinta


3
Il tuo articolo ha un errore, dovrebbe esserci un <T> dopo Distinct: public static IEnumerable <T> Distinct (questo ... Inoltre non sembra che funzionerà (bene) su più di una proprietà, cioè una combinazione di prima e cognomi.
row1

2
+1, un errore minore non è una ragione sufficiente per il downvote, che solo così sciocco, ha chiamato un refuso spesso. E devo ancora vedere una funzione generica che funzionerà per qualsiasi numero di proprietà! Spero che il downvoter abbia votato a fondo su ogni altra risposta in questo thread. Ma hey, qual è questo secondo tipo di oggetto ?? Mi oppongo !
nawfal,

4
Il tuo collegamento è interrotto
Tom Lint,

7

Personalmente uso la seguente classe:

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

Quindi, un metodo di estensione:

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

Infine, l'uso previsto:

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

Il vantaggio che ho riscontrato usando questo approccio è il riutilizzo della LambdaEqualityComparerclasse per altri metodi che accettano un IEqualityComparer. (Oh, e lascio il yieldmateriale all'implementazione LINQ originale ...)


5

Nel caso in cui sia necessario un metodo Distinct su più proprietà, è possibile consultare la mia libreria PowerfulExtensions . Attualmente è in una fase molto giovane, ma già puoi usare metodi come Distinct, Union, Intersect, Tranne qualsiasi numero di proprietà;

Ecco come lo usi:

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

5

Quando abbiamo affrontato tale compito nel nostro progetto abbiamo definito una piccola API per comporre i comparatori.

Quindi, il caso d'uso era così:

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

E l'API stessa si presenta così:

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

Maggiori dettagli sono sul nostro sito: IEqualityComparer in LINQ .


5

È possibile utilizzare DistinctBy () per ottenere i record Distinct da una proprietà dell'oggetto. Basta aggiungere la seguente dichiarazione prima di usarla:

utilizzando Microsoft.Ajax.Utilities;

e poi usalo come segue:

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

dove 'Indice' è la proprietà su cui voglio che i dati siano distinti.


4

Puoi farlo (anche se non in un lampo) in questo modo:

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

Cioè, "seleziona tutte le persone in cui non c'è un'altra persona diversa nella lista con lo stesso ID".

Intendiamoci, nel tuo esempio, che sarebbe sufficiente selezionare la persona 3. Non sono sicuro di come dire quale vuoi, tra i due precedenti.


4

Se non si desidera aggiungere la libreria MoreLinq al proprio progetto solo per ottenere la DistinctByfunzionalità, è possibile ottenere lo stesso risultato finale utilizzando il sovraccarico del Distinctmetodo di Linq che accetta un IEqualityComparerargomento.

Si inizia creando una classe di confronto di uguaglianza personalizzata generica che utilizza la sintassi lambda per eseguire il confronto personalizzato di due istanze di una classe generica:

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

Quindi nel tuo codice principale lo usi in questo modo:

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

Ecco! :)

Quanto sopra presuppone quanto segue:

  • La proprietà Person.Idè di tipoint
  • La peopleraccolta non contiene alcun elemento null

Se la raccolta potrebbe contenere valori null, riscrivi semplicemente i lambdas per verificare la presenza di null, ad esempio:

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

MODIFICARE

Questo approccio è simile a quello nella risposta di Vladimir Nesterovsky, ma più semplice.

È anche simile a quello nella risposta di Joel, ma consente una complessa logica di confronto che coinvolge più proprietà.

Tuttavia, se i tuoi oggetti possono differire solo da Idallora un altro utente ha dato la risposta corretta che tutto ciò che devi fare è sovrascrivere le implementazioni predefinite di GetHashCode()e Equals()nella tua Personclasse e quindi utilizzare semplicemente il Distinct()metodo predefinito di Linq per filtrare eventuali duplicati.


Voglio ottenere solo elementi unici in dictonary, puoi aiutarmi, per favore, sto usando questo codice Se TempDT non è niente allora m_ConcurrentScriptDictionary = TempDT.AsEnumerable.ToDictionary (Function (x) x.SafeField (fldClusterId, NULL_ID_VALUE), Function (y) y.SafeField (fldParamValue11, NULL_ID_VALUE))
RSB


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();

Intendevi Select() new Personinvece di new Player? Il fatto che stai ordinando IDnon informa in qualche modo Distinct()di utilizzare quella proprietà nel determinare l'unicità, quindi non funzionerà.
BACON

1

Metodi Override Equals (object obj) e 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();
    }
}

e poi chiama:

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

Tuttavia GetHashCode () dovrebbe essere più avanzato (per contare anche il Nome), questa risposta è probabilmente la migliore per me. In realtà, per archiviare la logica di destinazione, non è necessario sovrascrivere GetHashCode (), Equals () è sufficiente, ma se abbiamo bisogno di prestazioni, dobbiamo sovrascriverlo. Tutti gli algoritmi di confronto, controlla prima l'hash e, se sono uguali, chiama Equals ().
Oleg Skripnyak,

Inoltre, in Equals () la prima riga dovrebbe essere "if (! (Obj is Person)) return false". Ma la migliore pratica è usare un oggetto separato proiettato su un tipo, come "var o = obj come Person; if (o == null) restituisce false;" quindi controlla l'uguaglianza con o senza il casting
Oleg Skripnyak,

1
Sostituire uguali come questo non è una buona idea in quanto potrebbe avere conseguenze indesiderate per altri programmatori che si aspettano che l'uguaglianza della persona sia determinata su più di una singola proprietà.
B2K,

0

Dovresti essere in grado di sovrascrivere Equals su persona per fare effettivamente Equals su Person.id. Ciò dovrebbe comportare il comportamento che stai cercando.


-5

Prova con il codice qui sotto.

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

3
Una risposta breve è benvenuta, tuttavia non fornirà molto valore a questi ultimi utenti che stanno cercando di capire cosa sta succedendo dietro il problema. Si prega di risparmiare un po 'di tempo per spiegare qual è il vero problema per causare il problema e come risolverlo. Grazie ~
Sentito il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.