Existe-t-il une méthode intégrée pour comparer les collections?

179

Je voudrais comparer le contenu de quelques collections dans ma méthode Equals. J'ai un dictionnaire et un IList. Existe-t-il une méthode intégrée pour ce faire?

Édité: Je veux comparer deux dictionnaires et deux ILists, donc je pense que ce que signifie l'égalité est clair - si les deux dictionnaires contiennent les mêmes clés mappées aux mêmes valeurs, alors elles sont égales.

TimK
la source
Indépendamment de l'ordre ou pas pour IList? La question est ambiguë.
nawfal
Enumerable.SequenceEqualet ISet.SetEqualsfournissez des versions de cette fonctionnalité. Si vous voulez être indépendant de la commande et travailler avec des collections contenant des doublons, vous devrez créer les vôtres. Découvrez l'implémentation suggérée dans cet article
ChaseMedallion

Réponses:

188

Enumerable.SequenceEqual

Détermine si deux séquences sont égales en comparant leurs éléments à l'aide d'un IEqualityComparer (T) spécifié.

Vous ne pouvez pas comparer directement la liste et le dictionnaire, mais vous pouvez comparer la liste des valeurs du dictionnaire avec la liste

Glenn Slaven
la source
56
Le problème est que SequenceEqual s'attend à ce que les éléments soient dans le même ordre. La classe Dictionary ne garantit pas l'ordre des clés ou des valeurs lors de l'énumération, donc si vous allez utiliser SequenceEqual, vous devez d'abord trier les .Keys et .Values!
Orion Edwards
3
@Orion: ... sauf si vous voulez détecter des différences de commande, bien sûr :-)
schoetbi
31
@schoetbi: Pourquoi voudriez-vous détecter des différences de commande dans un conteneur qui ne garantit pas la commande ?
Matti Virkkunen
4
@schoetbi: C'est pour retirer un certain élément d'un IEnumerable. Cependant, un dictionnaire ne garantit pas l'ordre, .Keyset .Valuespeut donc renvoyer les clés et les valeurs dans l'ordre de leur choix, et cet ordre est susceptible de changer à mesure que le dictionnaire est également modifié. Je vous suggère de lire ce qu'est un dictionnaire et ce qu'il n'est pas.
Matti Virkkunen
5
MS 'TestTools et NUnit fournissent CollectionAssert.AreEquivalent
tymtam
44

Comme d'autres l'ont suggéré et noté, SequenceEqualest sensible à l'ordre. Pour résoudre cela, vous pouvez trier le dictionnaire par clé (qui est unique, et donc le tri est toujours stable), puis utiliser SequenceEqual. L'expression suivante vérifie si deux dictionnaires sont égaux quel que soit leur ordre interne:

dictionary1.OrderBy(kvp => kvp.Key).SequenceEqual(dictionary2.OrderBy(kvp => kvp.Key))

EDIT: Comme l'a souligné Jeppe Stig Nielsen, certains objets ont un IComparer<T>qui est incompatible avec le leur IEqualityComparer<T>, ce qui donne des résultats incorrects. Lorsque vous utilisez des clés avec un tel objet, vous devez spécifier une IComparer<T>clé correcte pour ces clés. Par exemple, avec des clés de chaîne (qui présentent ce problème), vous devez effectuer les opérations suivantes pour obtenir des résultats corrects:

dictionary1.OrderBy(kvp => kvp.Key, StringComparer.Ordinal).SequenceEqual(dictionary2.OrderBy(kvp => kvp.Key, StringComparer.Ordinal))
Allon Guralnek
la source
Et si le type de clé ne fonctionne pas CompareTo? Votre solution explosera alors. Et si le type de clé a un comparateur par défaut incompatible avec son comparateur d'égalité par défaut? C'est le cas string, vous savez. À titre d'exemple, ces dictionnaires (avec des comparateurs d'égalité implicites par défaut) échoueront à votre test (sous toutes les informations de culture dont je suis au courant):var dictionary1 = new Dictionary<string, int> { { "Strasse", 10 }, { "Straße", 20 }, }; var dictionary2 = new Dictionary<string, int> { { "Straße", 20 }, { "Strasse", 10 }, };
Jeppe Stig Nielsen
@JeppeStigNielsen: Quant à l'incompatibilité entre ICompareret IEqualityComparer- je n'étais pas au courant de ce problème, très intéressant! J'ai mis à jour la réponse avec une solution possible. À propos de l'absence de CompareTo, je pense que le développeur devrait s'assurer que le délégué fourni à la OrderBy()méthode renvoie quelque chose de comparable. Je pense que cela est vrai pour toute utilisation ou OrderBy(), même en dehors des comparaisons de dictionnaires.
Allon Guralnek le
15

En plus du SequenceEqual mentionné , qui

est vrai si deux listes sont de longueur égale et que leurs éléments correspondants se comparent égaux selon un comparateur

(qui peut être le comparateur par défaut, c'est-à-dire un remplacement Equals())

il est à noter que dans .Net4 il y a SetEquals sur les ISetobjets, qui

ignore l'ordre des éléments et tous les éléments en double.

Donc, si vous voulez avoir une liste d'objets, mais qu'ils n'ont pas besoin d'être dans un ordre spécifique, considérez qu'un ISet(comme a HashSet) peut être le bon choix.

Desty
la source
7

Jetez un œil à la méthode Enumerable.SequenceEqual

var dictionary = new Dictionary<int, string>() {{1, "a"}, {2, "b"}};
var intList = new List<int> {1, 2};
var stringList = new List<string> {"a", "b"};
var test1 = dictionary.Keys.SequenceEqual(intList);
var test2 = dictionary.Values.SequenceEqual(stringList);
aku
la source
13
Ce n'est pas fiable car SequenceEqual s'attend à ce que les valeurs sortent du dictionnaire dans un ordre fiable - Le dictionnaire ne donne aucune garantie de ce type sur l'ordre, et le dictionnaire.Keys pourrait bien sortir comme [2, 1] au lieu de [1, 2] et votre test échouerait
Orion Edwards
5

.NET Manque d'outils puissants pour comparer les collections. J'ai développé une solution simple que vous pouvez trouver sur le lien ci-dessous:

http://robertbouillon.com/2010/04/29/comparing-collections-in-net/

Cela effectuera une comparaison d'égalité quel que soit l'ordre:

var list1 = new[] { "Bill", "Bob", "Sally" };
var list2 = new[] { "Bob", "Bill", "Sally" };
bool isequal = list1.Compare(list2).IsSame;

Cela vérifiera si des éléments ont été ajoutés / supprimés:

var list1 = new[] { "Billy", "Bob" };
var list2 = new[] { "Bob", "Sally" };
var diff = list1.Compare(list2);
var onlyinlist1 = diff.Removed; //Billy
var onlyinlist2 = diff.Added;   //Sally
var inbothlists = diff.Equal;   //Bob

Cela verra quels éléments du dictionnaire ont changé:

var original = new Dictionary<int, string>() { { 1, "a" }, { 2, "b" } };
var changed = new Dictionary<int, string>() { { 1, "aaa" }, { 2, "b" } };
var diff = original.Compare(changed, (x, y) => x.Value == y.Value, (x, y) => x.Value == y.Value);
foreach (var item in diff.Different)
  Console.Write("{0} changed to {1}", item.Key.Value, item.Value.Value);
//Will output: a changed to aaa
user329244
la source
10
Bien sûr .NET dispose d'outils puissants pour comparer les collections (ce sont des opérations basées sur des ensembles). .Removedest le même que list1.Except(list2), .Addedest list2.Except(list1), .Equalest list1.Intersect(list2)et .Differentest original.Join(changed, left => left.Key, right => right.Key, (left, right) => left.Value == right.Value). Vous pouvez faire presque toutes les comparaisons avec LINQ.
Allon Guralnek
3
Correction: .Differentest original.Join(changed, left => left.Key, right => right.Key, (left, right) => new { Key = left.Key, NewValue = right.Value, Different = left.Value == right.Value).Where(d => d.Different). Et vous pouvez même ajouter OldValue = left.Valuesi vous avez également besoin de l'ancienne valeur.
Allon Guralnek
3
@AllonGuralnek vos suggestions sont bonnes, mais elles ne gèrent pas le cas où la liste n'est pas un véritable ensemble - où la liste contient le même objet plusieurs fois. Si vous comparez {1, 2} et {1, 2, 2}, rien n'a été ajouté / supprimé.
Niall Connaughton
4

Je ne connaissais pas la méthode Enumerable.SequenceEqual (vous apprenez quelque chose tous les jours ....), mais j'allais suggérer d'utiliser une méthode d'extension; quelque chose comme ça:

    public static bool IsEqual(this List<int> InternalList, List<int> ExternalList)
    {
        if (InternalList.Count != ExternalList.Count)
        {
            return false;
        }
        else
        {
            for (int i = 0; i < InternalList.Count; i++)
            {
                if (InternalList[i] != ExternalList[i])
                    return false;
            }
        }

        return true;

    }

Fait intéressant, après avoir pris 2 secondes pour lire sur SequenceEqual, il semble que Microsoft a créé la fonction que je vous ai décrite.

Giovanni Galbo
la source
4

Cela ne répond pas directement à vos questions, mais les TestTools et NUnit de MS fournissent

 CollectionAssert.AreEquivalent

qui fait à peu près ce que vous voulez.

tymtam
la source
Je cherchais ceci pour mon test NUnit
Blem
1

Pour comparer des collections, vous pouvez également utiliser LINQ. Enumerable.Intersectrenvoie toutes les paires égales. Vous pouvez comparer deux dictionnaires comme celui-ci:

(dict1.Count == dict2.Count) && dict1.Intersect(dict2).Count() == dict1.Count

La première comparaison est nécessaire car dict2peut contenir toutes les clés de dict1et plus.

Vous pouvez également utiliser think of variations en utilisant Enumerable.Exceptet Enumerable.Unionqui conduisent à des résultats similaires. Mais peut être utilisé pour déterminer les différences exactes entre les ensembles.

Chrono
la source
1

Que diriez-vous de cet exemple:

 static void Main()
{
    // Create a dictionary and add several elements to it.
    var dict = new Dictionary<string, int>();
    dict.Add("cat", 2);
    dict.Add("dog", 3);
    dict.Add("x", 4);

    // Create another dictionary.
    var dict2 = new Dictionary<string, int>();
    dict2.Add("cat", 2);
    dict2.Add("dog", 3);
    dict2.Add("x", 4);

    // Test for equality.
    bool equal = false;
    if (dict.Count == dict2.Count) // Require equal count.
    {
        equal = true;
        foreach (var pair in dict)
        {
            int value;
            if (dict2.TryGetValue(pair.Key, out value))
            {
                // Require value be equal.
                if (value != pair.Value)
                {
                    equal = false;
                    break;
                }
            }
            else
            {
                // Require key be present.
                equal = false;
                break;
            }
        }
    }
    Console.WriteLine(equal);
}

Gracieuseté: https://www.dotnetperls.com/dictionary-equals

ispostback
la source
value! = pair.Value fait une comparaison de références, utilisez plutôt Equals
kofifus
1

Pour les collections ordonnées (List, Array), utilisez SequenceEqual

pour l'utilisation de HashSet SetEquals

pour le dictionnaire, vous pouvez faire:

namespace System.Collections.Generic {
  public static class ExtensionMethods {
    public static bool DictionaryEquals<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> d1, IReadOnlyDictionary<TKey, TValue> d2) {
      if (object.ReferenceEquals(d1, d2)) return true; 
      if (d2 is null || d1.Count != d2.Count) return false;
      foreach (var (d1key, d1value) in d1) {
        if (!d2.TryGetValue(d1key, out TValue d2value)) return false;
        if (!d1value.Equals(d2value)) return false;
      }
      return true;
    }
  }
}

(Une solution plus optimisée utilisera le tri mais cela nécessitera IComparable<TValue>)

kofifus
la source
0

Non. Le cadre de collecte n'a aucun concept d'égalité. Si vous y réfléchissez, il n'y a aucun moyen de comparer des collections qui ne soit pas subjective. Par exemple, en comparant votre IList à votre Dictionary, seraient-ils égaux si toutes les clés étaient dans IList, toutes les valeurs étaient dans IList ou si les deux étaient dans IList? Il n'y a pas de moyen évident de comparer ces deux collections sans savoir à quoi elles doivent servir, donc une méthode d'égalité à usage général n'a aucun sens.

Mal Andy
la source
0

Non, car le framework ne sait pas comparer le contenu de vos listes.

Jetez un œil à ceci:

http://blogs.msdn.com/abhinaba/archive/2005/10/11/479537.aspx

Mark Ingram
la source
3
N'y a-t-il pas déjà plusieurs options pour indiquer au cadre comment comparer les éléments? IComparer<T>, Remplaçant object.Equals, IEquatable<T>, IComparable<T>...
Stefan Steinegger
0
public bool CompareStringLists(List<string> list1, List<string> list2)
{
    if (list1.Count != list2.Count) return false;

    foreach(string item in list1)
    {
        if (!list2.Contains(item)) return false;
    }

    return true;
}
mbadeveloper
la source
0

Il n'y en avait pas, il n'y en a pas et peut-être pas, du moins je le croirais. La raison derrière est que l'égalité des collections est probablement un comportement défini par l'utilisateur.

Les éléments des collections ne sont pas censés être dans un ordre particulier bien qu'ils aient un ordre naturellement, ce n'est pas sur quoi les algorithmes de comparaison devraient s'appuyer. Supposons que vous ayez deux collections de:

{1, 2, 3, 4}
{4, 3, 2, 1}

Sont-ils égaux ou non? Vous devez savoir mais je ne sais pas quel est votre point de vue.

Les collections sont conceptuellement non ordonnées par défaut, jusqu'à ce que les algorithmes fournissent les règles de tri. La même chose que le serveur SQL portera à votre attention est que lorsque vous essayez de faire la pagination, il vous oblige à fournir des règles de tri:

https://docs.microsoft.com/en-US/sql/t-sql/queries/select-order-by-clause-transact-sql?view=sql-server-2017

Encore deux autres collections:

{1, 2, 3, 4}
{1, 1, 1, 2, 2, 3, 4}

Encore une fois, sont-ils égaux ou non? À vous de me dire ..

La répétabilité des éléments d'une collection joue son rôle dans différents scénarios et certaines collections comme Dictionary<TKey, TValue> ne permettent même pas les éléments répétés.

Je pense que ces types d'égalité sont définis par l'application et que le cadre ne fournit donc pas toutes les implémentations possibles.

Eh bien, dans les cas généraux, Enumerable.SequenceEqualc'est assez bon mais il renvoie faux dans le cas suivant:

var a = new Dictionary<String, int> { { "2", 2 }, { "1", 1 }, };
var b = new Dictionary<String, int> { { "1", 1 }, { "2", 2 }, };
Debug.Print("{0}", a.SequenceEqual(b)); // false

J'ai lu quelques réponses à des questions comme celle-ci (vous pouvez google pour eux) et ce que j'utiliserais, en général:

public static class CollectionExtensions {
    public static bool Represents<T>(this IEnumerable<T> first, IEnumerable<T> second) {
        if(object.ReferenceEquals(first, second)) {
            return true;
        }

        if(first is IOrderedEnumerable<T> && second is IOrderedEnumerable<T>) {
            return Enumerable.SequenceEqual(first, second);
        }

        if(first is ICollection<T> && second is ICollection<T>) {
            if(first.Count()!=second.Count()) {
                return false;
            }
        }

        first=first.OrderBy(x => x.GetHashCode());
        second=second.OrderBy(x => x.GetHashCode());
        return CollectionExtensions.Represents(first, second);
    }
}

Cela signifie qu'une collection représente l'autre dans ses éléments, y compris des temps répétés sans tenir compte de la commande d'origine. Quelques notes de la mise en œuvre:

  • GetHashCode()est juste pour l'ordre et non pour l'égalité; Je pense que c'est suffisant dans ce cas

  • Count() n'énumère pas vraiment la collection et tombe directement dans l'implémentation de propriété de ICollection<T>.Count

  • Si les références sont égales, c'est juste Boris

Ken Kin
la source
0

J'ai fait ma propre méthode de comparaison. Il renvoie des valeurs communes, manquantes et supplémentaires.

private static void Compare<T>(IEnumerable<T> actual, IEnumerable<T> expected, out IList<T> common, out IList<T> missing, out IList<T> extra) {
    common = new List<T>();
    missing = new List<T>();
    extra = new List<T>();

    var expected_ = new LinkedList<T>( expected );
    foreach (var item in actual) {
        if (expected_.Remove( item )) {
            common.Add( item );
        } else {
            extra.Add( item );
        }
    }
    foreach (var item in expected_) {
        missing.Add( item );
    }
}
Denis535
la source