Envelopper un délégué dans un IEqualityComparer

127

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

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

Marcelo Cantos
la source

Réponses:

44

Habituellement, je résoudre ce problème en commentant @Sam sur la réponse (j'ai fait quelques modifications sur le message d'origine pour le nettoyer un peu sans modifier le comportement.)

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

class FuncEqualityComparer<T> : IEqualityComparer<T>
{
    readonly Func<T, T, bool> _comparer;
    readonly Func<T, int> _hash;

    public FuncEqualityComparer( Func<T, T, bool> comparer )
        : this( comparer, t => 0 ) // NB Cannot assume anything about how e.g., t.GetHashCode() interacts with the comparer's behavior
    {
    }

    public FuncEqualityComparer( Func<T, T, bool> comparer, Func<T, int> hash )
    {
        _comparer = comparer;
        _hash = hash;
    }

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

    public int GetHashCode( T obj )
    {
        return _hash( obj );
    }
}
Ruben Bartelink
la source
5
En ce qui me concerne, c'est la bonne réponse. Tout ce IEqualityComparer<T>qui laisse de GetHashCodecôté est tout simplement cassé.
Dan Tao
1
@Joshua Frank: Il n'est pas valide d'utiliser l'égalité de hachage pour impliquer l'égalité - seul l'inverse est vrai. En bref, @Dan Tao a tout à fait raison dans ce qu'il dit, et cette réponse est simplement l'application de ce fait à une réponse auparavant incomplète
Ruben Bartelink
2
@Ruben Bartelink: Merci d'avoir clarifié. Mais je ne comprends toujours pas votre politique de hachage de t => 0. Si tous les objets hachent toujours la même chose (zéro), alors n'est-ce pas encore plus cassé que d'utiliser obj.GetHashCode, selon le point de @Dan Tao? Pourquoi ne pas toujours forcer l'appelant à fournir une bonne fonction de hachage?
Joshua Frank
1
Ainsi, il n'est pas raisonnable de supposer qu'un algorithme arbitraire dans un Func qui lui a été fourni ne peut pas retourner vrai malgré les codes de hachage différents. Votre argument selon lequel renvoyer zéro tout le temps n'est tout simplement pas du hachage est vrai. C'est pourquoi il y a une surcharge qui prend le hachage Func lorsque le profileur nous dit que les recherches ne sont pas suffisamment efficaces. Le seul point dans tout cela est que si vous allez avoir un algorithme de hachage par défaut, il devrait être celui qui fonctionne 100% du temps et qui n'a pas de comportement superficiellement correct dangereux. Et puis on peut travailler sur la performance!
Ruben Bartelink
4
En d'autres termes, puisque vous utilisez un comparateur personnalisé, il n'a rien à voir avec le code de hachage par défaut de l'objet lié au comparateur par défaut , vous ne pouvez donc pas l'utiliser.
Peet Brits
170

Sur l'importance de GetHashCode

D'autres ont déjà commenté le fait que toute IEqualityComparer<T>implémentation personnalisée devrait vraiment inclure une GetHashCodeméthode ; mais personne n'a pris la peine d'expliquer pourquoi en détail.

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

Prenez Distinct, par exemple. Considérez les implications de cette méthode d'extension si tout ce qu'elle utilisait était une Equalsméthode. Comment déterminer si un élément a déjà été numérisé dans une séquence si vous ne l'avez déjà fait Equals? Vous énumérez l'ensemble de la collection de valeurs que vous avez déjà consultées et recherchez une correspondance. Cela entraînerait l' Distinctutilisation d'un algorithme O (N 2 ) du pire des cas au lieu d'un algorithme O (N)!

Heureusement, ce n'est pas le cas. Distinctne pas simplement utiliser Equals; il utilise GetHashCodeaussi. En fait, il ne fonctionne absolument pas correctement sans un IEqualityComparer<T>qui fournit un bonGetHashCode . Voici un exemple artificiel illustrant cela.

Disons que j'ai le type suivant:

class Value
{
    public string Name { get; private set; }
    public int Number { get; private set; }

    public Value(string name, int number)
    {
        Name = name;
        Number = number;
    }

    public override string ToString()
    {
        return string.Format("{0}: {1}", Name, Number);
    }
}

Maintenant, disons que j'ai un List<Value>et je veux trouver tous les éléments avec un nom distinct. C'est un cas d'utilisation parfait pour Distinctutiliser un comparateur d'égalité personnalisé. Alors utilisons la Comparer<T>classe de la réponse d' Aku :

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

Maintenant, si nous avons un tas d' Valueéléments avec la même Namepropriété, ils devraient tous se réduire en une seule valeur renvoyée par Distinct, non? Voyons voir...

var values = new List<Value>();

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

Production:

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?

Et quoi GroupBy? Essayons ça:

var grouped = values.GroupBy(x => x, comparer);

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

Production:

[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 Distinctd'utiliser a HashSet<T>(ou équivalent) en interne, et GroupByd'utiliser quelque chose comme un en Dictionary<TKey, List<T>>interne. Cela pourrait-il expliquer pourquoi ces méthodes ne fonctionnent pas? Essayons ça:

var uniqueValues = new HashSet<Value>(values, comparer);

foreach (Value x in uniqueValues)
{
    Console.WriteLine(x);
}

Production:

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

Ouais ... commence à avoir un sens?

Espérons qu'à partir de ces exemples, il est clair pourquoi l'inclusion d'un approprié GetHashCodedans toute IEqualityComparer<T>implémentation est si importante.


Réponse originale

Élargissement de la réponse d'orip :

Il y a quelques améliorations qui peuvent être apportées ici.

  1. Tout d'abord, je prendrais un Func<T, TKey>au lieu de Func<T, object>; cela empêchera l'encadrement des clés de type valeur dans le réel keyExtractorlui-même.
  2. Deuxièmement, j'ajouterais en fait une where TKey : IEquatable<TKey>contrainte; cela évitera le boxing dans l' Equalsappel ( object.Equalsprend un objectparamètre; vous avez besoin d'une IEquatable<TKey>implémentation pour prendre un TKeyparamètre sans le boxing). Clairement, cela peut poser une restriction trop sévère, vous pouvez donc créer une classe de base sans la contrainte et une classe dérivée avec elle.

Voici à quoi pourrait ressembler le code résultant:

public class KeyEqualityComparer<T, TKey> : IEqualityComparer<T>
{
    protected readonly Func<T, TKey> keyExtractor;

    public KeyEqualityComparer(Func<T, TKey> 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 StrictKeyEqualityComparer<T, TKey> : KeyEqualityComparer<T, TKey>
    where TKey : IEquatable<TKey>
{
    public StrictKeyEqualityComparer(Func<T, TKey> 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));
    }
}
Dan Tao
la source
1
Votre StrictKeyEqualityComparer.Equalsméthode semble être la même que KeyEqualityComparer.Equals. La TKey : IEquatable<TKey>contrainte fait-elle TKey.Equalsfonctionner différemment?
Justin Morgan
2
@JustinMorgan: Oui - dans le premier cas, étant donné qu'il TKeypeut s'agir de n'importe quel type arbitraire, le compilateur utilisera la méthode virtuelle Object.Equalsqui nécessitera la mise en boîte des paramètres de type valeur, par exemple int. Dans ce dernier cas, cependant, TKeyétant contraint à mettre en œuvre IEquatable<TKey>, la TKey.Equalsméthode sera utilisée qui ne nécessitera aucune boxe.
Dan Tao
2
Très intéressant, merci pour l'info. Je n'avais aucune idée que GetHashCode avait ces implications LINQ avant de voir ces réponses. Excellent à savoir pour une utilisation future.
Justin Morgan
1
@JohannesH: Probablement! Aurait éliminé le besoin de StringKeyEqualityComparer<T, TKey>trop.
Dan Tao
1
+1 @DanTao: Merci tardivement pour une grande exposition sur les raisons pour lesquelles il ne faut jamais ignorer les codes de hachage lors de la définition de l'égalité dans .Net.
Marcelo Cantos
118

Lorsque vous souhaitez personnaliser la vérification d'égalité, 99% du temps, vous êtes intéressé par la définition des clés à comparer, pas par la comparaison elle-même.

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

Usage:

var foo = new List<string> { "abc", "de", "DE" };

// case-insensitive distinct
var distinct = foo.Distinct(new KeyEqualityComparer<string>( x => x.ToLower() ) );

La KeyEqualityComparerclasse:

public class KeyEqualityComparer<T> : IEqualityComparer<T>
{
    private readonly Func<T, object> keyExtractor;

    public KeyEqualityComparer(Func<T,object> 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();
    }
}
orip
la source
3
C'est bien mieux que la réponse d'aku.
SLaks
Certainement la bonne approche. Il y a quelques améliorations qui peuvent être apportées, à mon avis, que j'ai mentionnées dans ma propre réponse.
Dan Tao
1
C'est un code très élégant, mais il ne répond pas à la question, c'est pourquoi j'ai accepté la réponse de @ aku à la place. Je voulais un wrapper pour Func <T, T, bool> et je n'ai pas besoin d'extraire une clé, car la clé est déjà séparée dans mon dictionnaire.
Marcelo Cantos
6
@Marcelo: C'est bien, vous pouvez le faire; mais sachez que si vous allez adopter l'approche de @ aku, vous devriez vraiment ajouter a Func<T, int>pour fournir le code de hachage d'une Tvaleur (comme cela a été suggéré par exemple dans la réponse de Ruben ). Sinon, l' IEqualityComparer<T>implémentation qui vous reste est assez cassée, en particulier en ce qui concerne son utilité dans les méthodes d'extension LINQ. Voir ma réponse pour une discussion sur pourquoi c'est.
Dan Tao
C'est bien, mais si la clé sélectionnée était un type de valeur, il n'y aurait pas de boxe inutile. Il serait peut-être préférable d'avoir une clé TKey pour définir la clé.
Graham Ambrose
48

J'ai bien peur qu'il n'y ait pas de telle enveloppe prête à l'emploi. Cependant, il n'est pas difficile d'en créer un:

class Comparer<T>: IEqualityComparer<T>
{
    private readonly Func<T, T, bool> _comparer;

    public Comparer(Func<T, T, bool> 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.ToString().ToLower().GetHashCode();
    }
}

...

Func<int, int, bool> f = (x, y) => x == y;
var comparer = new Comparer<int>(f);
Console.WriteLine(comparer.Equals(1, 1));
Console.WriteLine(comparer.Equals(1, 2));
aku
la source
1
Cependant, soyez prudent avec cette implémentation de GetHashCode. Si vous comptez l'utiliser dans une sorte de table de hachage, vous voudrez quelque chose d'un peu plus robuste.
thecoop
46
ce code a un sérieux problème! il est facile de trouver une classe qui a deux objets qui sont égaux en termes de ce comparateur mais qui ont des codes de hachage différents.
empi
10
Pour remédier à cela, la classe a besoin d'un autre membre private readonly Func<T, int> _hashCodeResolverqui doit également être passé dans le constructeur et utilisé dans la GetHashCode(...)méthode.
herzmeister
6
Je suis curieux: pourquoi utilisez-vous obj.ToString().ToLower().GetHashCode()au lieu de obj.GetHashCode()?
Justin Morgan
3
Les endroits dans le cadre qui prennent un IEqualityComparer<T>hachage invariablement utilisent le hachage dans les coulisses (par exemple, GroupBy, Distinct, Except, Join de LINQ, etc.) et le contrat MS concernant le hachage est rompu dans cette implémentation. Voici l'extrait de la documentation de MS: "Des implémentations sont nécessaires pour garantir que si la méthode Equals renvoie true pour deux objets x et y, la valeur renvoyée par la méthode GetHashCode pour x doit être égale à la valeur renvoyée pour y." Voir: msdn.microsoft.com/en-us/library/ms132155
devgeezer
22

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

  1. S'appuie sur EqualityComparer<>.Defaultpour faire la comparaison réelle afin d'éviter la boxe pour les types de valeur structqui ont été implémentés IEquatable<>.

  2. Depuis EqualityComparer<>.Defaultutilisé, il n'explose pas null.Equals(something).

  3. Fournit un wrapper statique autour IEqualityComparer<>duquel aura une méthode statique pour créer l'instance de comparateur - facilite l'appel. Comparer

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

    avec

    new EqualityComparer<Person, int>(p => p.ID);
  4. Ajout d'une surcharge à spécifier IEqualityComparer<>pour la clé.

La classe:

public static class Equality<T>
{
    public static IEqualityComparer<T> CreateComparer<V>(Func<T, V> keySelector)
    {
        return CreateComparer(keySelector, null);
    }

    public static IEqualityComparer<T> CreateComparer<V>(Func<T, V> keySelector, 
                                                         IEqualityComparer<V> comparer)
    {
        return new KeyEqualityComparer<V>(keySelector, comparer);
    }

    class KeyEqualityComparer<V> : IEqualityComparer<T>
    {
        readonly Func<T, V> keySelector;
        readonly IEqualityComparer<V> comparer;

        public KeyEqualityComparer(Func<T, V> keySelector, 
                                   IEqualityComparer<V> comparer)
        {
            if (keySelector == null)
                throw new ArgumentNullException("keySelector");

            this.keySelector = keySelector;
            this.comparer = comparer ?? EqualityComparer<V>.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<Person>.CreateComparer(p => p.ID);
var comparer2 = Equality<Person>.CreateComparer(p => p.Name);
var comparer3 = Equality<Person>.CreateComparer(p => p.Birthday.Year);
var comparer4 = Equality<Person>.CreateComparer(p => p.Name, StringComparer.CurrentCultureIgnoreCase);

La personne est une classe simple:

class Person
{
    public int ID { get; set; }
    public string Name { get; set; }
    public DateTime Birthday { get; set; }
}
ldp615
la source
3
+1 pour fournir une implémentation qui vous permet de fournir un comparateur pour la clé. En plus de donner plus de flexibilité, cela évite également les types de valeur de boxe pour les comparaisons et le hachage.
devgeezer
2
C'est la réponse la plus étoffée ici. J'ai également ajouté un chèque nul. Achevée.
nawfal
11
public class FuncEqualityComparer<T> : IEqualityComparer<T>
{
    readonly Func<T, T, bool> _comparer;
    readonly Func<T, int> _hash;

    public FuncEqualityComparer( Func<T, T, bool> comparer )
        : this( comparer, t => t.GetHashCode())
    {
    }

    public FuncEqualityComparer( Func<T, T, bool> comparer, Func<T, int> 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 extensions: -

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

    public static bool SequenceEqual<T>( this IEnumerable<T> first, IEnumerable<T> second, Func<T, T, bool> comparer, Func<T, int> hash )
    {
        return first.SequenceEqual( second, new FuncEqualityComparer<T>( comparer, hash ) );
    }
}
Ruben Bartelink
la source
@Sam (qui n'existe plus à partir de ce commentaire): code nettoyé sans ajustement de comportement (et +1). Riff ajouté sur stackoverflow.com/questions/98033/…
Ruben Bartelink
6

La réponse d'orip est excellente.

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

public static IEnumerable<T> Distinct<T>(this IEnumerable<T> list, Func<T, object>    keyExtractor)
{
    return list.Distinct(new KeyEqualityComparer<T>(keyExtractor));
}
var distinct = foo.Distinct(x => x.ToLower())
Bruno
la source
2

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

Marcelo Cantos
la source
2

L'implémentation à (texte allemand) Implémentation de IEqualityCompare avec l'expression lambda se soucie des valeurs nulles et utilise des méthodes d'extension pour générer IEqualityComparer.

Pour créer un IEqualityComparer dans une union Linq, il vous suffit d'écrire

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

Le comparateur:

public class LambdaEqualityComparer<TSource, TComparable> : IEqualityComparer<TSource>
{
  Func<TSource, TComparable> _keyGetter;

  public LambdaEqualityComparer(Func<TSource, TComparable> 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 ajouter 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<TSource> Union<TSource, TComparable>(
           this IEnumerable<TSource> source1, 
           IEnumerable<TSource> source2, 
            Func<TSource, TComparable> keySelector)
        {
            return source1.Union(source2, 
               new LambdaEqualityComparer<TSource, TComparable>(keySelector));
       }
   }
Frit
la source
1

Juste une optimisation: nous pouvons utiliser EqualityComparer prêt à l'emploi pour les comparaisons de valeurs, plutôt que de les déléguer.

Cela rendrait également l'implémentation plus propre car la logique de comparaison réelle reste maintenant dans GetHashCode () et Equals () que vous avez peut-être déjà surchargés.

Voici le code:

public class MyComparer<T> : IEqualityComparer<T> 
{ 
  public bool Equals(T x, T y) 
  { 
    return EqualityComparer<T>.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 objet.

Cet article m'a aidé: c # comparer deux valeurs génériques

Sushil

Sushil
la source
1
NB même problème que celui identifié dans le commentaire sur stackoverflow.com/questions/98033/… - CANT suppose que obj.GetHashCode () a du sens
Ruben Bartelink
4
Je ne comprends pas le but de celui-ci. Vous avez créé un comparateur d'égalité qui est équivalent au comparateur d'égalité par défaut. Alors pourquoi ne pas l'utiliser directement?
CodesInChaos
1

La réponse d'orip est excellente. Élargissement de la réponse d'orip:

Je pense que la clé de la solution est d'utiliser la "méthode d'extension" pour transférer le "type anonyme".

    public static class Comparer 
    {
      public static IEqualityComparer<T> CreateComparerForElements<T>(this IEnumerable<T> enumerable, Func<T, object> keyExtractor)
      {
        return new KeyEqualityComparer<T>(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();
matrice
la source
0
public static Dictionary<TKey, TValue> Distinct<TKey, TValue>(this IEnumerable<TValue> items, Func<TValue, TKey> selector)
  {
     Dictionary<TKey, TValue> result = null;
     ICollection collection = items as ICollection;
     if (collection != null)
        result = new Dictionary<TKey, TValue>(collection.Count);
     else
        result = new Dictionary<TKey, TValue>();
     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);

Max
la source
-2

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

public class MyComparer<T> : IEqualityComparer<T>
{
  private Func<T, T, bool> _compare;
  MyComparer(Func<T, T, bool> compare)
  {
    _compare = compare;
  }

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

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

Remarque: je n'ai pas encore compilé et exécuté ceci, il peut donc y avoir une faute de frappe ou un autre bogue.

Gregg
la source
1
NB même problème que celui identifié dans le commentaire sur stackoverflow.com/questions/98033/… - CANT suppose que obj.GetHashCode () a du sens
Ruben Bartelink