Enveloppez un délégué dans un IEqualityComparer

Plusieurs fonctions Linq.Enumerable utilisent un IEqualityComparer . Existe-t-il une classe wrapper pratique qui adapte un delegate(T,T)=>bool pour implémenter IEqualityComparer ? Il est assez facile d’en écrire un (si vous ignorez les problèmes de définition d’un code de hachage correct), mais j’aimerais savoir s’il existe une solution prête à l’emploi.

Plus précisément, je souhaite effectuer des opérations sur Dictionary s, en utilisant uniquement les clés pour définir l’appartenance (tout en conservant les valeurs selon des règles différentes).

D’habitude, j’obtiens ce problème en commentant @Sam sur la réponse (j’ai fait quelques modifications sur le post d’origine pour le nettoyer un peu sans modifier le comportement).

Ce qui suit est mon riff de réponse de @ Sam , avec un correctif critique [IMNSHO] à la politique de hachage par défaut: –

 class FuncEqualityComparer : IEqualityComparer { readonly Func _comparer; readonly Func _hash; public FuncEqualityComparer( Func comparer ) : this( comparer, t => 0 ) // NB Cannot assume anything about how eg, t.GetHashCode() interacts with the comparer's behavior { } public FuncEqualityComparer( Func comparer, Func hash ) { _comparer = comparer; _hash = hash; } public bool Equals( T x, T y ) { return _comparer( x, y ); } public int GetHashCode( T obj ) { return _hash( obj ); } } 

Sur l’importance de GetHashCode

D’autres ont déjà commenté le fait que toute implémentation personnalisée de IEqualityComparer devrait vraiment inclure une méthode GetHashCode ; mais personne n’a pris la peine d’expliquer pourquoi dans le moindre détail.

Voici pourquoi. Votre question mentionne spécifiquement les méthodes d’extension LINQ; Presque tous utilisent des codes de hachage pour fonctionner correctement, car ils utilisent des tables de hachage en interne pour des raisons d’efficacité.

Prenez Distinct , par exemple. Considérez les implications de cette méthode d’extension si tout ce qu’elle utilisait était une méthode Equals . Comment déterminez-vous si un élément a déjà été scanné dans une séquence si vous avez uniquement des Equals ? Vous énumérez l’ensemble des valeurs que vous avez déjà consultées et recherchez une correspondance. Cela aboutirait à Distinct utilisant un algorithme de pire cas (O 2 ) au lieu d’un algorithme O (N)!

Heureusement, ce n’est pas le cas. Distinct n’utilise pas seulement Equals ; Il utilise également GetHashCode . En fait, il ne fonctionne absolument pas correctement sans IEqualityComparer qui fournit un GetHashCode correct . Vous trouverez ci-dessous un exemple artificiel illustrant cela.

Disons que j’ai le type suivant:

 class Value { public ssortingng Name { get; private set; } public int Number { get; private set; } public Value(ssortingng name, int number) { Name = name; Number = number; } public override ssortingng ToSsortingng() { return ssortingng.Format("{0}: {1}", Name, Number); } } 

Maintenant, disons que j’ai une List et que je veux trouver tous les éléments avec un nom distinct. Ceci est un cas d’utilisation parfait pour Distinct utilisant un comparateur d’égalité personnalisée. Alors, utilisons la Comparer de la réponse d’ Aku :

 var comparer = new Comparer((x, y) => x.Name == y.Name); 

Maintenant, si nous avons un groupe d’éléments Value avec la même propriété Name , ils devraient tous se réduire en une valeur renvoyée par Distinct , non? Voyons voir…

 var values = new List(); var random = new Random(); for (int i = 0; i < 10; ++i) { values.Add("x", random.Next()); } var distinct = values.Distinct(comparer); foreach (Value x in distinct) { Console.WriteLine(x); } 

Sortie:

 x: 1346013431
 x: 1388845717
 x: 1576754134
 x: 1104067189
 x: 1144789201
 x: 1862076501
 x: 1573781440
 x: 646797592
 x: 655632802
 x: 1206819377

Hmm, ça n'a pas marché, n'est-ce pas?

Qu'en est-il de GroupBy ? Essayons ça:

 var grouped = values.GroupBy(x => x, comparer); foreach (IGrouping g in grouped) { Console.WriteLine("[KEY: '{0}']", g); foreach (Value x in g) { Console.WriteLine(x); } } 

Sortie:

 [KEY = 'x: 1346013431']
 x: 1346013431
 [KEY = 'x: 1388845717']
 x: 1388845717
 [KEY = 'x: 1576754134']
 x: 1576754134
 [KEY = 'x: 1104067189']
 x: 1104067189
 [KEY = 'x: 1144789201']
 x: 1144789201
 [KEY = 'x: 1862076501']
 x: 1862076501
 [KEY = 'x: 1573781440']
 x: 1573781440
 [KEY = 'x: 646797592']
 x: 646797592
 [KEY = 'x: 655632802']
 x: 655632802
 [KEY = 'x: 1206819377']
 X: 1206819377

Encore une fois: n'a pas fonctionné.

Si vous y réfléchissez, il serait logique que Distinct utilise un HashSet (ou équivalent) en interne, et que GroupBy utilise quelque chose comme Dictionary> interne. Est-ce que cela pourrait expliquer pourquoi ces méthodes ne fonctionnent pas? Essayons ça:

 var uniqueValues = new HashSet(values, comparer); foreach (Value x in uniqueValues) { Console.WriteLine(x); } 

Sortie:

 x: 1346013431
 x: 1388845717
 x: 1576754134
 x: 1104067189
 x: 1144789201
 x: 1862076501
 x: 1573781440
 x: 646797592
 x: 655632802
 x: 1206819377

Ouais ... commence à avoir du sens?

Espérons que ces exemples expliquent pourquoi il est si important d’inclure un GetHashCode approprié dans toute implémentation IEqualityComparer .


Réponse originale

En expansion sur la réponse d'Orip :

Il y a quelques améliorations à apporter ici.

  1. Tout d'abord, je prendrais un Func au lieu de Func ; Cela empêchera la boxe des clés de type valeur dans le keyExtractor proprement dit.
  2. Deuxièmement, j'appendais en fait une contrainte where TKey : IEquatable ; cela empêchera la boxe dans l'appel Equals ( object.Equals prend un paramètre object ; vous avez besoin d'une IEquatable pour prendre un paramètre TKey sans la mettre en boîte). Clairement, cela peut poser une ressortingction trop sévère, vous pouvez donc créer une classe de base sans la contrainte et une classe dérivée.

Voici à quoi pourrait ressembler le code résultant:

 public class KeyEqualityComparer : IEqualityComparer { protected readonly Func keyExtractor; public KeyEqualityComparer(Func keyExtractor) { this.keyExtractor = keyExtractor; } public virtual bool Equals(T x, T y) { return this.keyExtractor(x).Equals(this.keyExtractor(y)); } public int GetHashCode(T obj) { return this.keyExtractor(obj).GetHashCode(); } } public class SsortingctKeyEqualityComparer : KeyEqualityComparer where TKey : IEquatable { public SsortingctKeyEqualityComparer(Func keyExtractor) : base(keyExtractor) { } public override bool Equals(T x, T y) { // This will use the overload that accepts a TKey parameter // instead of an object parameter. return this.keyExtractor(x).Equals(this.keyExtractor(y)); } } 

Lorsque vous souhaitez personnaliser le contrôle d’égalité, vous souhaitez, dans 99% des cas, définir les clés à comparer et non la comparaison elle-même.

Cela pourrait être une solution élégante (concept de la méthode de sorting par liste de Python).

Usage:

 var foo = new List { "abc", "de", "DE" }; // case-insensitive distinct var distinct = foo.Distinct(new KeyEqualityComparer( x => x.ToLower() ) ); 

La classe KeyEqualityComparer :

 public class KeyEqualityComparer : IEqualityComparer { private readonly Func keyExtractor; public KeyEqualityComparer(Func keyExtractor) { this.keyExtractor = keyExtractor; } public bool Equals(T x, T y) { return this.keyExtractor(x).Equals(this.keyExtractor(y)); } public int GetHashCode(T obj) { return this.keyExtractor(obj).GetHashCode(); } } 

J’ai bien peur qu’il n’y ait pas un tel emballage. Cependant, il n’est pas difficile d’en créer un:

 class Comparer: IEqualityComparer { private readonly Func _comparer; public Comparer(Func comparer) { if (comparer == null) throw new ArgumentNullException("comparer"); _comparer = comparer; } public bool Equals(T x, T y) { return _comparer(x, y); } public int GetHashCode(T obj) { return obj.ToSsortingng().ToLower().GetHashCode(); } } ... Func f = (x, y) => x == y; var comparer = new Comparer(f); Console.WriteLine(comparer.Equals(1, 1)); Console.WriteLine(comparer.Equals(1, 2)); 

Identique à la réponse de Dan Tao, mais avec quelques améliorations:

  1. S’appuie sur EqualityComparer<>.Default pour effectuer la comparaison réelle, elle évite la boxe pour les types de valeur ( struct s) qui ont implémenté IEquatable<> .

  2. Depuis EqualityComparer<>.Default il n’explose pas sur null.Equals(something) .

  3. Un wrapper statique fourni autour de IEqualityComparer<> qui aura une méthode statique pour créer l’instance de comparateur – eases. Comparer

     Equality.CreateComparer(p => p.ID); 

    avec

     new EqualityComparer(p => p.ID); 
  4. Ajout d’une surcharge pour spécifier IEqualityComparer<> pour la clé.

La classe:

 public static class Equality { public static IEqualityComparer CreateComparer(Func keySelector) { return CreateComparer(keySelector, null); } public static IEqualityComparer CreateComparer(Func keySelector, IEqualityComparer comparer) { return new KeyEqualityComparer(keySelector, comparer); } class KeyEqualityComparer : IEqualityComparer { readonly Func keySelector; readonly IEqualityComparer comparer; public KeyEqualityComparer(Func keySelector, IEqualityComparer comparer) { if (keySelector == null) throw new ArgumentNullException("keySelector"); this.keySelector = keySelector; this.comparer = comparer ?? EqualityComparer.Default; } public bool Equals(T x, T y) { return comparer.Equals(keySelector(x), keySelector(y)); } public int GetHashCode(T obj) { return comparer.GetHashCode(keySelector(obj)); } } } 

vous pouvez l’utiliser comme ceci:

 var comparer1 = Equality.CreateComparer(p => p.ID); var comparer2 = Equality.CreateComparer(p => p.Name); var comparer3 = Equality.CreateComparer(p => p.Birthday.Year); var comparer4 = Equality.CreateComparer(p => p.Name, SsortingngComparer.CurrentCultureIgnoreCase); 

Personne est une classe simple:

 class Person { public int ID { get; set; } public ssortingng Name { get; set; } public DateTime Birthday { get; set; } } 
 public class FuncEqualityComparer : IEqualityComparer { readonly Func _comparer; readonly Func _hash; public FuncEqualityComparer( Func comparer ) : this( comparer, t => t.GetHashCode()) { } public FuncEqualityComparer( Func comparer, Func hash ) { _comparer = comparer; _hash = hash; } public bool Equals( T x, T y ) { return _comparer( x, y ); } public int GetHashCode( T obj ) { return _hash( obj ); } } 

Avec des extensions: –

 public static class SequenceExtensions { public static bool SequenceEqual( this IEnumerable first, IEnumerable second, Func comparer ) { return first.SequenceEqual( second, new FuncEqualityComparer( comparer ) ); } public static bool SequenceEqual( this IEnumerable first, IEnumerable second, Func comparer, Func hash ) { return first.SequenceEqual( second, new FuncEqualityComparer( comparer, hash ) ); } } 

La réponse d’Orip est géniale.

Voici une petite méthode d’extension pour le rendre encore plus facile:

 public static IEnumerable Distinct(this IEnumerable list, Func keyExtractor) { return list.Distinct(new KeyEqualityComparer(keyExtractor)); } var distinct = foo.Distinct(x => x.ToLower()) 

Je vais répondre à ma propre question. Pour traiter les dictionnaires comme des ensembles, la méthode la plus simple semble être d’appliquer les opérations set à dict.Keys, puis de reconvertir en dictionnaires avec Enumerable.ToDictionary (…).

L’implémentation de (texte allemand) L’ implémentation d’IEqualityCompare avec l’expression lambda concerne les valeurs nulles et utilise des méthodes d’extension pour générer IEqualityComparer.

Pour créer un IEqualityComparer dans une union Linq, vous devez simplement écrire

 persons1.Union(persons2, person => person.LastName) 

Le comparateur:

 public class LambdaEqualityComparer : IEqualityComparer { Func _keyGetter; public LambdaEqualityComparer(Func keyGetter) { _keyGetter = keyGetter; } public bool Equals(TSource x, TSource y) { if (x == null || y == null) return (x == null && y == null); return object.Equals(_keyGetter(x), _keyGetter(y)); } public int GetHashCode(TSource obj) { if (obj == null) return int.MinValue; var k = _keyGetter(obj); if (k == null) return int.MaxValue; return k.GetHashCode(); } } 

Vous devez également append une méthode d’extension pour prendre en charge l’inférence de type

 public static class LambdaEqualityComparer { // source1.Union(source2, lambda) public static IEnumerable Union( this IEnumerable source1, IEnumerable source2, Func keySelector) { return source1.Union(source2, new LambdaEqualityComparer(keySelector)); } } 

Une seule optimisation: nous pouvons utiliser le produit EqualityComparer prêt à l’emploi pour les comparaisons de valeur, plutôt que de le déléguer.

Cela permettrait également à l’implémentation d’être plus propre car la logique de comparaison actuelle rest désormais dans GetHashCode () et Equals () que vous avez peut-être déjà surchargée.

Voici le code:

 public class MyComparer : IEqualityComparer { public bool Equals(T x, T y) { return EqualityComparer.Default.Equals(x, y); } public int GetHashCode(T obj) { return obj.GetHashCode(); } } 

N’oubliez pas de surcharger les méthodes GetHashCode () et Equals () sur votre object.

Cet article m’a aidé: c # compare deux valeurs génériques

Sushil

La réponse d’Orip est géniale. En expansion sur la réponse d’Orip:

Je pense que la clé de la solution est d’utiliser “Méthode d’extension” pour transférer le “type anonyme”.

  public static class Comparer { public static IEqualityComparer CreateComparerForElements(this IEnumerable enumerable, Func keyExtractor) { return new KeyEqualityComparer(keyExtractor); } } 

Usage:

 var n = ItemList.Select(s => new { s.Vchr, s.Id, s.Ctr, s.Vendor, s.Description, s.Invoice }).ToList(); n.AddRange(OtherList.Select(s => new { s.Vchr, s.Id, s.Ctr, s.Vendor, s.Description, s.Invoice }).ToList();); n = n.Distinct(x=>new{Vchr=x.Vchr,Id=x.Id}).ToList(); 
 public static Dictionary Distinct(this IEnumerable items, Func selector) { Dictionary result = null; ICollection collection = items as ICollection; if (collection != null) result = new Dictionary(collection.Count); else result = new Dictionary(); foreach (TValue item in items) result[selector(item)] = item; return result; } 

Cela permet de sélectionner une propriété avec lambda comme ceci: .Select(y => y.Article).Distinct(x => x.ArticleID);

Je ne connais pas une classe existante mais quelque chose comme:

 public class MyComparer : IEqualityComparer { private Func _compare; MyComparer(Func compare) { _compare = compare; } public bool Equals(T x, Ty) { return _compare(x, y); } public int GetHashCode(T obj) { return obj.GetHashCode(); } } 

Note: Je n’ai pas encore compilé et exécuté ceci, donc il pourrait y avoir une faute de frappe ou un autre bogue.