Distinto () con lambda?


746

Bene, quindi ho un enumerabile e desidero ottenere valori distinti da esso.

Utilizzando System.Linq, c'è ovviamente un metodo di estensione chiamato Distinct. Nel caso semplice, può essere utilizzato senza parametri, come:

var distinctValues = myStringList.Distinct();

Bene e buono, ma se ho un elenco numeroso di oggetti per i quali devo specificare l'uguaglianza, l'unico sovraccarico disponibile è:

var distinctValues = myCustomerList.Distinct(someEqualityComparer);

L'argomento del comparatore di uguaglianza deve essere un'istanza di IEqualityComparer<T>. Posso farlo, ovviamente, ma è un po 'prolisso e, beh, rozzo.

Quello che mi sarei aspettato è un sovraccarico che richiederebbe un lambda, diciamo Func <T, T, bool>:

var distinctValues
    = myCustomerList.Distinct((c1, c2) => c1.CustomerId == c2.CustomerId);

Qualcuno sa se esiste una tale estensione o una soluzione alternativa equivalente? Oppure mi sfugge qualcosa?

In alternativa, c'è un modo per specificare un IEqualityComparer inline (imbarazzami)?

Aggiornare

Ho trovato una risposta di Anders Hejlsberg a un post in un forum MSDN su questo argomento. Lui dice:

Il problema che incontrerai è che quando due oggetti si equivalgono devono avere lo stesso valore di ritorno GetHashCode (altrimenti la tabella hash utilizzata internamente da Distinct non funzionerà correttamente). Utilizziamo IEqualityComparer perché integra implementazioni compatibili di Equals e GetHashCode in un'unica interfaccia.

Suppongo abbia senso..


2
consultare stackoverflow.com/questions/1183403/… per una soluzione che utilizza GroupBy

17
Grazie per l'aggiornamento di Anders Hejlsberg!
Tor Haugen,

No, non ha senso: in che modo due oggetti che contengono valori identici possono restituire due diversi codici hash ??
GY

Potrebbe aiutare - soluzione per .Distinct(new KeyEqualityComparer<Customer,string>(c1 => c1.CustomerId)), e spiegare perché GetHashCode () è importante per il corretto funzionamento.
marbel82,

Correlato / possibile duplicato di: LINQ's Distinct () su una particolare proprietà
Marc.2377

Risposte:


1029
IEnumerable<Customer> filteredList = originalList
  .GroupBy(customer => customer.CustomerId)
  .Select(group => group.First());

12
Eccellente! Questo è davvero facile da incapsulare anche in un metodo di estensione, come DistinctBy(o addirittura Distinct, poiché la firma sarà unica).
Tomas Aschan,

1
Non funziona per me! <Il metodo 'First' può essere utilizzato solo come operazione di query finale. Prendi invece in considerazione il metodo "FirstOrDefault" in questa istanza.> Anche se ho provato "FirstOrDefault" non ha funzionato.
JatSing,

63
@TorHaugen: tieni presente che la creazione di tutti questi gruppi comporta un costo. Questo non può trasmettere l'input e finirà per bufferizzare tutti i dati prima di restituire qualsiasi cosa. Questo potrebbe non essere rilevante per la tua situazione, ovviamente, ma preferisco l'eleganza di DistinctBy :)
Jon Skeet,

2
@JonSkeet: è abbastanza buono per i programmatori VB.NET che non vogliono importare librerie aggiuntive per una sola funzione. Senza ASync CTP, VB.NET non supporta l' yieldistruzione, pertanto lo streaming non è tecnicamente possibile. Grazie per la tua risposta però. Lo userò durante la codifica in C #. ;-)
Alex Essilfie l'

2
@BenGripka: non è proprio la stessa cosa. Ti dà solo gli ID cliente. Voglio l'intero cliente :)
Ryanman,

496

Mi sembra che tu voglia DistinctByda MoreLINQ . È quindi possibile scrivere:

var distinctValues = myCustomerList.DistinctBy(c => c.CustomerId);

Ecco una versione ridotta di DistinctBy(nessun controllo di nullità e nessuna opzione per specificare il proprio comparatore di chiavi):

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

14
Sapevo che la migliore risposta sarebbe stata pubblicata da Jon Skeet semplicemente leggendo il titolo del post. Se ha qualcosa a che fare con LINQ, Skeet è il tuo uomo. Leggi "C # in profondità" per ottenere una conoscenza linq simile a Dio.
nocarrier

2
Bella risposta!!! inoltre, per tutti i VB_Complainers sulla yield+ extra lib, foreach può essere riscritto comereturn source.Where(element => knownKeys.Add(keySelector(element)));
denis morozov

5
@ sudhAnsu63 questa è una limitazione di LinqToSql (e di altri fornitori di linq). L'intento di LinqToX è tradurre la tua espressione lambda C # nel contesto nativo di X. Cioè, LinqToSql converte il tuo C # in SQL ed esegue quel comando in modo nativo ogni volta che è possibile. Ciò significa che qualsiasi metodo che risiede in C # non può essere "passato" a un linqProvider se non c'è modo di esprimerlo in SQL (o in qualunque provider linq in uso). Vedo questo nei metodi di estensione per convertire gli oggetti dati per visualizzare i modelli. Puoi aggirare il problema "materializzando" la query, chiamando ToList () prima di DistinctBy ().
Michael Blackburn,

1
E ogni volta che torno a questa domanda continuo a chiedermi perché non adottano almeno parte di MoreLinq nel BCL.
Shimmy Weitzhandler,

2
@Shimmy: mi farebbe piacere che ... Non sono sicuro di quale sia la fattibilità. Posso sollevarlo nella .NET Foundation però ...
Jon Skeet,

39

Per concludere . Penso che la maggior parte delle persone che sono venute qui come me desiderino la soluzione più semplice possibile senza utilizzare alcuna libreria e con le migliori prestazioni possibili .

(Il gruppo accettato dal metodo per me penso che sia eccessivo in termini di prestazioni.)

Ecco un semplice metodo di estensione che utilizza l' interfaccia IEqualityComparer che funziona anche con valori null.

Uso:

var filtered = taskList.DistinctBy(t => t.TaskExternalId).ToArray();

Codice del metodo di estensione

public static class LinqExtensions
{
    public static IEnumerable<T> DistinctBy<T, TKey>(this IEnumerable<T> items, Func<T, TKey> property)
    {
        GeneralPropertyComparer<T, TKey> comparer = new GeneralPropertyComparer<T,TKey>(property);
        return items.Distinct(comparer);
    }   
}
public class GeneralPropertyComparer<T,TKey> : IEqualityComparer<T>
{
    private Func<T, TKey> expr { get; set; }
    public GeneralPropertyComparer (Func<T, TKey> expr)
    {
        this.expr = expr;
    }
    public bool Equals(T left, T right)
    {
        var leftProp = expr.Invoke(left);
        var rightProp = expr.Invoke(right);
        if (leftProp == null && rightProp == null)
            return true;
        else if (leftProp == null ^ rightProp == null)
            return false;
        else
            return leftProp.Equals(rightProp);
    }
    public int GetHashCode(T obj)
    {
        var prop = expr.Invoke(obj);
        return (prop==null)? 0:prop.GetHashCode();
    }
}

19

No, non esiste un sovraccarico del metodo di estensione per questo. Ho trovato questo frustrante me stesso in passato e come tale di solito scrivo una classe di aiuto per affrontare questo problema. L'obiettivo è convertire un Func<T,T,bool>in IEqualityComparer<T,T>.

Esempio

public class EqualityFactory {
  private sealed class Impl<T> : IEqualityComparer<T,T> {
    private Func<T,T,bool> m_del;
    private IEqualityComparer<T> m_comp;
    public Impl(Func<T,T,bool> del) { 
      m_del = del;
      m_comp = EqualityComparer<T>.Default;
    }
    public bool Equals(T left, T right) {
      return m_del(left, right);
    } 
    public int GetHashCode(T value) {
      return m_comp.GetHashCode(value);
    }
  }
  public static IEqualityComparer<T,T> Create<T>(Func<T,T,bool> del) {
    return new Impl<T>(del);
  }
}

Ciò consente di scrivere quanto segue

var distinctValues = myCustomerList
  .Distinct(EqualityFactory.Create((c1, c2) => c1.CustomerId == c2.CustomerId));

8
Questo ha comunque una cattiva implementazione del codice hash. È più facile crearne uno IEqualityComparer<T>da una proiezione: stackoverflow.com/questions/188120/…
Jon Skeet,

7
(Giusto per spiegare il mio commento sul codice hash - è molto facile con questo codice finire con Equals (x, y) == true, ma GetHashCode (x)! = GetHashCode (y). Fondamentalmente si rompe qualcosa come un hashtable .)
Jon Skeet,

Sono d'accordo con l'obiezione del codice hash. Ancora, +1 per il modello.
Tor Haugen,

@Jon, sì, sono d'accordo che l'implementazione originale di GetHashcode non è ottimale (era pigro). L'ho cambiato essenzialmente per usare ora EqualityComparer <T> .Default.GetHashcode () che è leggermente più standard. Sinceramente, l'unica garanzia di funzionamento dell'implementazione GetHashcode in questo scenario è semplicemente restituire un valore costante. Uccide la ricerca hashtable ma è garantito che sia funzionalmente corretto.
JaredPar,

1
@JaredPar: esattamente. Il codice hash deve essere coerente con la funzione di uguaglianza che stai utilizzando, che presumibilmente non è quella predefinita altrimenti non ti preoccuperesti :) Ecco perché preferisco usare una proiezione: puoi ottenere sia l'uguaglianza che un hash sensibile codice in questo modo. Inoltre rende il codice chiamante meno duplicato. Certo, funziona solo nei casi in cui si desidera la stessa proiezione due volte, ma è ogni caso che ho visto in pratica :)
Jon Skeet,

18

Soluzione abbreviata

myCustomerList.GroupBy(c => c.CustomerId, (key, c) => c.FirstOrDefault());

1
Potresti aggiungere qualche spiegazione del perché questo è migliorato?
Keith Pinson,

Questo in realtà ha funzionato bene per me quando Konrad no.
neoscribe

13

Questo farà quello che vuoi ma non so delle prestazioni:

var distinctValues =
    from cust in myCustomerList
    group cust by cust.CustomerId
    into gcust
    select gcust.First();

Almeno non è prolisso.


12

Ecco un semplice metodo di estensione che fa quello che mi serve ...

public static class EnumerableExtensions
{
    public static IEnumerable<TKey> Distinct<T, TKey>(this IEnumerable<T> source, Func<T, TKey> selector)
    {
        return source.GroupBy(selector).Select(x => x.Key);
    }
}

È un peccato che non abbiano inserito un metodo distinto come questo nel framework, ma ehi.


questa è la soluzione migliore senza dover aggiungere quella libreria morelinq.
toddmo,

Ma, ho dovuto cambiare x.Keyper x.First()e cambiare il valore di ritorno aIEnumerable<T>
toddmo

@toddmo Grazie per il feedback :-) Sì, sembra logico ... Aggiornerò la risposta dopo aver indagato ulteriormente.
David Kirkland,

1
non è mai troppo tardi per dire grazie per la soluzione, semplice e pulita
Ali

4

Qualcosa che ho usato che ha funzionato bene per me.

/// <summary>
/// A class to wrap the IEqualityComparer interface into matching functions for simple implementation
/// </summary>
/// <typeparam name="T">The type of object to be compared</typeparam>
public class MyIEqualityComparer<T> : IEqualityComparer<T>
{
    /// <summary>
    /// Create a new comparer based on the given Equals and GetHashCode methods
    /// </summary>
    /// <param name="equals">The method to compute equals of two T instances</param>
    /// <param name="getHashCode">The method to compute a hashcode for a T instance</param>
    public MyIEqualityComparer(Func<T, T, bool> equals, Func<T, int> getHashCode)
    {
        if (equals == null)
            throw new ArgumentNullException("equals", "Equals parameter is required for all MyIEqualityComparer instances");
        EqualsMethod = equals;
        GetHashCodeMethod = getHashCode;
    }
    /// <summary>
    /// Gets the method used to compute equals
    /// </summary>
    public Func<T, T, bool> EqualsMethod { get; private set; }
    /// <summary>
    /// Gets the method used to compute a hash code
    /// </summary>
    public Func<T, int> GetHashCodeMethod { get; private set; }

    bool IEqualityComparer<T>.Equals(T x, T y)
    {
        return EqualsMethod(x, y);
    }

    int IEqualityComparer<T>.GetHashCode(T obj)
    {
        if (GetHashCodeMethod == null)
            return obj.GetHashCode();
        return GetHashCodeMethod(obj);
    }
}

@Mukus Non sono sicuro del motivo per cui stai chiedendo il nome della classe qui. Avevo bisogno di dare un nome alla classe per implementare IEqualityComparer, quindi ho appena aggiunto il prefisso My.
Kleinux,

4

Tutte le soluzioni che ho visto qui si basano sulla selezione di un campo già comparabile. Se uno ha bisogno di confrontare in modo diverso, tuttavia, questa soluzione qui sembra funzionare in generale, per qualcosa come:

somedoubles.Distinct(new LambdaComparer<double>((x, y) => Math.Abs(x - y) < double.Epsilon)).Count()

Cos'è LambdaComparer, da dove lo stai importando?
Patrick Graham,

@PatrickGraham collegato nella risposta: brendan.enrick.com/post/…
Dmitry Ledentsov

3

Prendi un altro modo:

var distinctValues = myCustomerList.
Select(x => x._myCaustomerProperty).Distinct();

La sequenza restituisce elementi distinti confrontandoli con la proprietà '_myCaustomerProperty'.


1
Sono venuto qui per dire questo. QUESTA dovrebbe essere la risposta accettata
Still.Tony

5
No, questa non dovrebbe essere la risposta accettata, a meno che tutto ciò che desideri siano valori distinti della proprietà personalizzata. La domanda OP generale era come restituire oggetti distinti in base a una proprietà specifica dell'oggetto.
tomo,

2

È possibile utilizzare InlineComparer

public class InlineComparer<T> : IEqualityComparer<T>
{
    //private readonly Func<T, T, bool> equalsMethod;
    //private readonly Func<T, int> getHashCodeMethod;
    public Func<T, T, bool> EqualsMethod { get; private set; }
    public Func<T, int> GetHashCodeMethod { get; private set; }

    public InlineComparer(Func<T, T, bool> equals, Func<T, int> hashCode)
    {
        if (equals == null) throw new ArgumentNullException("equals", "Equals parameter is required for all InlineComparer instances");
        EqualsMethod = equals;
        GetHashCodeMethod = hashCode;
    }

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

    public int GetHashCode(T obj)
    {
        if (GetHashCodeMethod == null) return obj.GetHashCode();
        return GetHashCodeMethod(obj);
    }
}

Esempio di utilizzo :

  var comparer = new InlineComparer<DetalleLog>((i1, i2) => i1.PeticionEV == i2.PeticionEV && i1.Etiqueta == i2.Etiqueta, i => i.PeticionEV.GetHashCode() + i.Etiqueta.GetHashCode());
  var peticionesEV = listaLogs.Distinct(comparer).ToList();
  Assert.IsNotNull(peticionesEV);
  Assert.AreNotEqual(0, peticionesEV.Count);

Fonte: https://stackoverflow.com/a/5969691/206730
Utilizzo di IEqualityComparer per Union
Posso specificare il mio comparatore di tipi espliciti in linea?


2

Puoi usare LambdaEqualityComparer:

var distinctValues
    = myCustomerList.Distinct(new LambdaEqualityComparer<OurType>((c1, c2) => c1.CustomerId == c2.CustomerId));


public class LambdaEqualityComparer<T> : IEqualityComparer<T>
    {
        public LambdaEqualityComparer(Func<T, T, bool> equalsFunction)
        {
            _equalsFunction = equalsFunction;
        }

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

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

        private readonly Func<T, T, bool> _equalsFunction;
    }

1

Un modo complicato per farlo è usare l' Aggregate()estensione, usando un dizionario come accumulatore con i valori delle proprietà-chiave come chiavi:

var customers = new List<Customer>();

var distincts = customers.Aggregate(new Dictionary<int, Customer>(), 
                                    (d, e) => { d[e.CustomerId] = e; return d; },
                                    d => d.Values);

E una soluzione in stile GroupBy utilizza ToLookup():

var distincts = customers.ToLookup(c => c.CustomerId).Select(g => g.First());

Bello, ma perché non crearne uno Dictionary<int, Customer>invece?
ruffin,

0

Suppongo che tu abbia un IEnumerable e nel tuo esempio delegato, vorresti che c1 e c2 si riferissero a due elementi in questo elenco?

Credo che tu possa ottenere questo risultato con un self join var distintoResults = from c1 in myList join c2 in myList on


0

Se Distinct()non produce risultati unici, prova questo:

var filteredWC = tblWorkCenter.GroupBy(cc => cc.WCID_I).Select(grp => grp.First()).Select(cc => new Model.WorkCenter { WCID = cc.WCID_I }).OrderBy(cc => cc.WCID); 

ObservableCollection<Model.WorkCenter> WorkCenter = new ObservableCollection<Model.WorkCenter>(filteredWC);


0

Ecco come puoi farlo:

public static class Extensions
{
    public static IEnumerable<T> MyDistinct<T, V>(this IEnumerable<T> query,
                                                    Func<T, V> f, 
                                                    Func<IGrouping<V,T>,T> h=null)
    {
        if (h==null) h=(x => x.First());
        return query.GroupBy(f).Select(h);
    }
}

Questo metodo consente di utilizzarlo specificando un parametro come .MyDistinct(d => d.Name), ma consente anche di specificare una condizione di avere come secondo parametro in questo modo:

var myQuery = (from x in _myObject select x).MyDistinct(d => d.Name,
        x => x.FirstOrDefault(y=>y.Name.Contains("1") || y.Name.Contains("2"))
        );

NB Ciò consentirebbe anche di specificare altre funzioni come ad esempio .LastOrDefault(...).


Se vuoi esporre solo la condizione, puoi renderla ancora più semplice implementandola come:

public static IEnumerable<T> MyDistinct2<T, V>(this IEnumerable<T> query,
                                                Func<T, V> f,
                                                Func<T,bool> h=null
                                                )
{
    if (h == null) h = (y => true);
    return query.GroupBy(f).Select(x=>x.FirstOrDefault(h));
}

In questo caso, la query sarebbe simile a:

var myQuery2 = (from x in _myObject select x).MyDistinct2(d => d.Name,
                    y => y.Name.Contains("1") || y.Name.Contains("2")
                    );

NB Qui, l'espressione è più semplice, ma la nota .MyDistinct2usa .FirstOrDefault(...)implicitamente.


Nota: gli esempi sopra riportati utilizzano la seguente classe demo

class MyObject
{
    public string Name;
    public string Code;
}

private MyObject[] _myObject = {
    new MyObject() { Name = "Test1", Code = "T"},
    new MyObject() { Name = "Test2", Code = "Q"},
    new MyObject() { Name = "Test2", Code = "T"},
    new MyObject() { Name = "Test5", Code = "Q"}
};

0

IEnumerable estensione lambda:

public static class ListExtensions
{        
    public static IEnumerable<T> Distinct<T>(this IEnumerable<T> list, Func<T, int> hashCode)
    {
        Dictionary<int, T> hashCodeDic = new Dictionary<int, T>();

        list.ToList().ForEach(t => 
            {   
                var key = hashCode(t);
                if (!hashCodeDic.ContainsKey(key))
                    hashCodeDic.Add(key, t);
            });

        return hashCodeDic.Select(kvp => kvp.Value);
    }
}

Uso:

class Employee
{
    public string Name { get; set; }
    public int EmployeeID { get; set; }
}

//Add 5 employees to List
List<Employee> lst = new List<Employee>();

Employee e = new Employee { Name = "Shantanu", EmployeeID = 123456 };
lst.Add(e);
lst.Add(e);

Employee e1 = new Employee { Name = "Adam Warren", EmployeeID = 823456 };
lst.Add(e1);
//Add a space in the Name
Employee e2 = new Employee { Name = "Adam  Warren", EmployeeID = 823456 };
lst.Add(e2);
//Name is different case
Employee e3 = new Employee { Name = "adam warren", EmployeeID = 823456 };
lst.Add(e3);            

//Distinct (without IEqalityComparer<T>) - Returns 4 employees
var lstDistinct1 = lst.Distinct();

//Lambda Extension - Return 2 employees
var lstDistinct = lst.Distinct(employee => employee.EmployeeID.GetHashCode() ^ employee.Name.ToUpper().Replace(" ", "").GetHashCode()); 
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.