Distinct () avec lambda?


746

Bon, j'ai donc un énumérable et je souhaite en obtenir des valeurs distinctes.

En utilisant System.Linq, il y a bien sûr une méthode d'extension appelée Distinct. Dans le cas simple, il peut être utilisé sans paramètres, comme:

var distinctValues = myStringList.Distinct();

C'est bien, mais si j'ai une liste d'objets pour lesquels je dois spécifier l'égalité, la seule surcharge disponible est:

var distinctValues = myCustomerList.Distinct(someEqualityComparer);

L'argument du comparateur d'égalité doit être une instance de IEqualityComparer<T>. Je peux le faire, bien sûr, mais c'est un peu bavard et, bien, grossier.

Ce à quoi je m'attendais, c'est une surcharge qui prendrait un lambda, disons un Func <T, T, bool>:

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

Quelqu'un sait-il si une telle extension existe, ou une solution de contournement équivalente? Ou est-ce que je manque quelque chose?

Sinon, existe-t-il un moyen de spécifier une ligne IEqualityComparer (embarrassez-moi)?

Mise à jour

J'ai trouvé une réponse d'Anders Hejlsberg à un message dans un forum MSDN sur ce sujet. Il dit:

Le problème que vous allez rencontrer est que lorsque deux objets sont comparables, ils doivent avoir la même valeur de retour GetHashCode (sinon la table de hachage utilisée en interne par Distinct ne fonctionnera pas correctement). Nous utilisons IEqualityComparer car il regroupe les implémentations compatibles de Equals et GetHashCode dans une seule interface.

Je suppose que cela a du sens ..


2
voir stackoverflow.com/questions/1183403/… pour une solution utilisant GroupBy

17
Merci pour la mise à jour d'Anders Hejlsberg!
Tor Haugen

Non, cela n'a pas de sens - comment deux objets contenant des valeurs identiques peuvent-ils renvoyer deux codes de hachage différents ??
GY

Il pourrait aider - solution pour .Distinct(new KeyEqualityComparer<Customer,string>(c1 => c1.CustomerId)), et expliquer pourquoi GetHashCode () est important de travailler correctement.
marbel82

Réponses:


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

12
Excellent! C'est aussi très facile à encapsuler dans une méthode d'extension, comme DistinctBy(ou même Distinct, car la signature sera unique).
Tomas Aschan

1
Ça ne marche pas pour moi! <La méthode 'First' ne peut être utilisée que comme opération de requête finale. Envisagez plutôt d'utiliser la méthode «FirstOrDefault» dans cet exemple.> Même j'ai essayé «FirstOrDefault», cela n'a pas fonctionné.
JatSing

63
@TorHaugen: Sachez simplement que la création de tous ces groupes a un coût. Cela ne peut pas diffuser l'entrée et finira par mettre en mémoire tampon toutes les données avant de retourner quoi que ce soit. Cela peut ne pas être pertinent pour votre situation bien sûr, mais je préfère l'élégance de DistinctBy :)
Jon Skeet

2
@JonSkeet: C'est assez bon pour les codeurs VB.NET qui ne veulent pas importer de bibliothèques supplémentaires pour une seule fonctionnalité. Sans ASync CTP, VB.NET ne prend pas en charge l' yieldinstruction, donc la diffusion n'est techniquement pas possible. Merci pour votre réponse. Je vais l'utiliser lors du codage en C #. ;-)
Alex Essilfie

2
@BenGripka: Ce n'est pas tout à fait la même chose. Il ne vous donne que les identifiants client. Je veux tout le client :)
ryanman

496

Il me semble que vous voulez DistinctByde MoreLINQ . Vous pouvez alors écrire:

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

Voici une version réduite de DistinctBy(pas de vérification de la nullité et pas d'option pour spécifier votre propre comparateur de clés):

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
Je savais que la meilleure réponse serait postée par Jon Skeet simplement en lisant le titre du post. Si ça a quelque chose à voir avec LINQ, Skeet est votre homme. Lisez «C # en profondeur» pour atteindre une connaissance linq semblable à Dieu.
nocarrier

2
très bonne réponse!!! aussi, pour tous les VB_Complainers sur le yield+ extra lib, foreach peut être réécrit commereturn source.Where(element => knownKeys.Add(keySelector(element)));
denis morozov

5
@ sudhAnsu63 c'est une limitation de LinqToSql (et d'autres fournisseurs linq). Le but de LinqToX est de traduire votre expression lambda C # dans le contexte natif de X. Autrement dit, LinqToSql convertit votre C # en SQL et exécute cette commande en mode natif dans la mesure du possible. Cela signifie que toute méthode qui réside en C # ne peut pas être "transmise" à un fournisseur linq s'il n'y a aucun moyen de l'exprimer en SQL (ou quel que soit le fournisseur linq que vous utilisez). Je vois cela dans les méthodes d'extension pour convertir des objets de données en modèles de vue. Vous pouvez contourner cela en «matérialisant» la requête, en appelant ToList () avant DistinctBy ().
Michael Blackburn

1
Et chaque fois que je reviens à cette question, je continue à me demander pourquoi ils n'adoptent pas au moins une partie de MoreLinq dans la BCL.
Shimmy Weitzhandler

2
@Shimmy: J'en serais certainement ravi ... Je ne suis pas sûr de la faisabilité. Je peux le soulever dans la Fondation .NET cependant ...
Jon Skeet

39

Pour conclure . Je pense que la plupart des gens qui sont venus ici comme moi veulent la solution la plus simple possible sans utiliser de bibliothèques et avec les meilleures performances possibles .

(Le groupe accepté par méthode pour moi, je pense, est une surpuissance en termes de performances.)

Voici une méthode d'extension simple utilisant l' interface IEqualityComparer qui fonctionne également pour les valeurs nulles.

Usage:

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

Code de méthode d'extension

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

Non, il n'y a pas une telle surcharge de méthode d'extension pour cela. J'ai trouvé cela frustrant moi-même dans le passé et en tant que tel, j'écris habituellement un cours d'aide pour faire face à ce problème. Le but est de convertir un Func<T,T,bool>en IEqualityComparer<T,T>.

Exemple

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

Cela vous permet d'écrire ce qui suit

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

8
Cela a cependant une implémentation de code de hachage désagréable. Il est plus facile de créer un à IEqualityComparer<T>partir d'une projection: stackoverflow.com/questions/188120/…
Jon Skeet

7
(Juste pour expliquer mon commentaire sur le code de hachage - il est très facile avec ce code de se retrouver avec Equals (x, y) == true, mais GetHashCode (x)! = GetHashCode (y). Cela casse fondamentalement quelque chose comme une table de hachage .)
Jon Skeet

Je suis d'accord avec l'objection du code de hachage. Pourtant, +1 pour le motif.
Tor Haugen

@ Jon, oui, je suis d'accord que l'implémentation originale de GetHashcode est loin d'être optimale (était paresseuse). Je l'ai changé pour utiliser essentiellement EqualityComparer <T> .Default.GetHashcode () qui est légèrement plus standard. En vérité, la seule garantie de fonctionnement de l'implémentation GetHashcode dans ce scénario est de renvoyer simplement une valeur constante. Supprime la recherche de la table de hachage, mais sa fonctionnalité est garantie.
JaredPar

1
@JaredPar: Exactement. Le code de hachage doit être cohérent avec la fonction d'égalité que vous utilisez, qui n'est probablement pas celle par défaut, sinon vous ne vous en soucieriez pas :) C'est pourquoi je préfère utiliser une projection - vous pouvez obtenir à la fois l'égalité et un hachage sensible coder de cette façon. Cela rend également le code appelant moins de duplication. Certes, cela ne fonctionne que dans les cas où vous voulez deux fois la même projection, mais c'est tous les cas que j'ai vus dans la pratique :)
Jon Skeet

18

Solution sténographique

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

1
Pourriez-vous ajouter une explication de la raison pour laquelle cela est amélioré?
Keith Pinson

Cela a vraiment bien fonctionné pour moi quand Konrad ne l'a pas fait.
neoscribe le

13

Cela fera ce que vous voulez mais je ne connais pas les performances:

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

Au moins, ce n'est pas verbeux.


12

Voici une méthode d'extension simple qui fait ce dont j'ai besoin ...

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

C'est dommage qu'ils n'aient pas cuit une méthode distincte comme celle-ci dans le cadre, mais bon ho.


c'est la meilleure solution sans avoir à ajouter cette bibliothèque morelinq.
toddmo

Mais, je devais changer x.Keypour x.First()changer la valeur de retour àIEnumerable<T>
toddmo

@toddmo Merci pour la rétroaction :-) Ouais, ça semble logique ... Je mettrai à jour la réponse après une enquête plus approfondie.
David Kirkland,

1
il n'est jamais trop tard pour dire merci pour la solution, simple et propre
Ali

4

Quelque chose que j'ai utilisé et qui a bien fonctionné pour moi.

/// <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 Je ne sais pas pourquoi vous demandez le nom de la classe ici. J'avais besoin de nommer la classe quelque chose afin d'implémenter IEqualityComparer donc j'ai juste préfixé le My.
Kleinux

4

Toutes les solutions que j'ai vues ici reposent sur la sélection d'un domaine déjà comparable. Si l'on doit comparer d'une manière différente, cependant, cette solution semble fonctionner généralement, pour quelque chose comme:

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

Qu'est-ce que LambdaComparer, d'où importez-vous cela?
Patrick Graham

@PatrickGraham lié dans la réponse: brendan.enrick.com/post/…
Dmitry Ledentsov

3

Prenez une autre voie:

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

La séquence renvoie des éléments distincts les compare par la propriété '_myCaustomerProperty'.


1
Je suis venu ici pour dire cela. Cela devrait être la réponse acceptée
Still.Tony

5
Non, cela ne devrait pas être la réponse acceptée, sauf si vous ne souhaitez que des valeurs distinctes de la propriété personnalisée. La question générale du PO était de savoir comment renvoyer des objets distincts en fonction d'une propriété spécifique de l'objet.
tomo

2

Vous pouvez utiliser 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);
    }
}

Exemple d'utilisation :

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

Source: https://stackoverflow.com/a/5969691/206730
Utilisation d'IEqualityComparer pour Union
Puis-je spécifier mon comparateur de type explicite en ligne?


2

Vous pouvez utiliser 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

Une façon délicate de le faire est d'utiliser l' Aggregate()extension, en utilisant un dictionnaire comme accumulateur avec les valeurs de propriété de clé comme clés:

var customers = new List<Customer>();

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

Et une solution de style GroupBy utilise ToLookup():

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

Bien, mais pourquoi ne pas simplement en créer un à la Dictionary<int, Customer>place?
ruffin

0

Je suppose que vous avez un IEnumerable, et dans votre exemple de délégué, vous aimeriez que c1 et c2 se réfèrent à deux éléments dans cette liste?

Je crois que vous pourriez y parvenir avec une auto-jointure var distinctResults = de c1 dans ma liste, rejoignez c2 dans ma liste sur


0

Si Distinct()ne produit pas de résultats uniques, essayez celui-ci:

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

Voici comment procéder:

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

Cette méthode vous permet de l'utiliser en spécifiant un paramètre comme .MyDistinct(d => d.Name), mais elle vous permet également de spécifier une condition ayant comme deuxième paramètre comme ceci:

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 Cela vous permettrait également de spécifier d'autres fonctions comme par exemple .LastOrDefault(...).


Si vous souhaitez exposer uniquement la condition, vous pouvez la rendre encore plus simple en l'implémentant comme:

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

Dans ce cas, la requête ressemblerait à ceci:

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

NB Ici, l'expression est plus simple, mais note .MyDistinct2utilise .FirstOrDefault(...)implicitement.


Remarque: Les exemples ci-dessus utilisent la classe de démonstration suivante

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

Usage:

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()); 
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.