2010-10-12 5 views
7

Nostalgisch für Collections.unmodifiableMap(), habe ich implementiert einen schreibgeschützten IDictionary Wrapper basierend auf this discussion, und mein Komponententest lief schnell auf ein Problem:Equals() Vertrag für .NET-Wörterbuch/IDictionary vs equals() Vertrag für Java-Karte

Assert.AreEqual (backingDictionary, readOnlyDictionary); 

schlägt fehl, obwohl die Schlüssel/Wert-Paare übereinstimmen. Ich spielte ein wenig mehr herum, und es sieht zumindest so aus (danke Simonyi)

Assert.AreEquals (backingDictionary, new Dictionary<..> { /* same contents */ }); 

geht vorbei.

Ich nahm einen kurzen Blick durch die Dictionary und IDictionary Dokumentation, und zu meiner Überraschung konnte ich keine Äquivalent des Java Map Vertrages finden, die zwei Maps mit gleich entrySet()s gleich sein müssen. (Die docs sagen, dass Dictionary - nichtIDictionary - überschreibt Equals(), aber nicht sagen, was das Überschreiben der Fall ist.)

So sieht es aus wie Schlüssel-Wert-Gleichheit in C# ist eine Eigenschaft der Dictionary konkrete Klasse nicht von der IDictionary Schnittstelle. Ist das richtig? Gilt es generell für das gesamte System.Collections Framework?

Wenn ja, würde mich interessieren, eine Diskussion darüber zu lesen, warum MS diesen Ansatz wählte - und auch, wie der bevorzugte Weg wäre, die Gleichheit der Sammlungsinhalte in C# zu überprüfen.

Und schließlich hätte ich nichts gegen einen Zeiger auf eine gut getestete ReadOnlyDictionary Implementierung. :)


ETA: Um klar zu sein, ich bin nicht die Suche nach Anregungen, wie meine Implementierung zu testen - das relativ trivial ist. Ich suche nach Anleitung auf welchen Vertrag diese Tests erzwingen sollten. Und warum.


ETA: Leute, Ich weißIDictionary eine Schnittstelle ist, und ich Schnittstellen können nicht wissen, Methoden implementieren. In Java ist es dasselbe. Dennoch dokumentiert die Java Map Schnittstelle eine Erwartung von certain behavior von der equals() Methode. Sicherlich muss es .NET-Schnittstellen geben, die solche Dinge tun, selbst wenn die Sammelschnittstellen nicht darunter sind.

+0

Es gibt ein ReadOnlyDictionary im Namespace MS.Internal.Utility in der WindowsBase-Assembly. Es überschreibt nicht Equals. – dtb

+1

Ich schreibe eine Mono-App für iOS, aber das ist ein interessanter Datenpunkt. Sind zwei "MS.Internal.Utility.ReadOnlyDictionaries" mit den gleichen Inhalten gleich? –

+1

Hinweis für spätere Leser: (1) Die 'Algorithms'-Klasse in [PowerCollections] (http://powercollections.codeplex.com/) bietet 'ReadOnly'-Methoden zum Umbrechen von Sammlungen (einschließlich Wörterbüchern) als schreibgeschützt. (2) LINQs [SequenceEqual()] (http://msdn.microsoft.com/en-us/library/bb348567.aspx) arbeitet mit geordneten Sammlungen (einschließlich Wörterbüchern). –

Antwort

2

Für spätere Leser, hier ist was ich habe, um herauszufinden, erzählt/konnte:

  1. Der Vertrag für .NET Sammlungen, im Gegensatz zu Java Sammlungen, nicht enthält kein spezifisches Verhalten für Equals() oder GetHashCode().
  2. LINQ Enumerable.SequenceEqual() Erweiterungsmethode wird geordnete Sammlungen für die Arbeit, einschließlich Wörterbücher - die Gegenwart als IEnumerable<KeyValuePair>; KeyValuePair ist eine Struktur, und ihre Equals Methode uses reflection , um den Inhalt zu vergleichen.
  3. Enumerable bietet andere Erweiterung Methoden, die verwendet werden können, zusammen einen Gehalt Gleichheitsprüfung, wie wie Union() und Intersect() schustern.

ich komme um auf die Idee, dass, praktisch, da die Java-Methoden sind, werden sie vielleicht nicht die beste Idee, wenn wir über wandelbar Sammlungen sprechen sind, und über die typische implizite equals() Semantik -, dass zwei equal Objekte sind austauschbar. .NET bietet keine sehr gute Unterstützung für unveränderbare Sammlungen, aber die Open-Source-Bibliothek PowerCollections tut dies.

+0

Java hat im Gegensatz zu .NET kein Konzept eines" Gleichheitsvergleichs ". Wenn veränderbare Typen als Entitäten verwendet werden, ist Referenzgleichheit sinnvoll; Als Werte, die sich nicht ändern, während sie in einem Wörterbuch sind, macht Wertgleichheit Sinn.In den meisten Fällen etwas, das nichts weiß Eine Klasse wird Instanzen vergleichen, es ist besser, auf der Seite des Meldens von Dingen ungleich zu sein, was die Standard-Referenzgleichheit von .NET tut. In Fällen, in denen Code, der Dinge in einem Wörterbuch speichert, weiß, dass Wertgleichheit sinnvoller ist, kann er einen Gleichheitsvergleich angeben, der das testet. – supercat

+0

Da Java keinen Gleichheitsvergleichstypen hat, können die Typen ihre Werte nur als Dictionary-Schlüssel verwenden, indem sie ihre gleichheitsbezogenen Methoden überschreiben, um Wertgleichheit zu verwenden, und hoffen, dass Instanzen, die als Entitäten verwendet werden, dies nicht tun in ein Wörterbuch gespeichert werden. – supercat

3

Overriding equals wird normalerweise nur mit Klassen durchgeführt, die eine Grad-Wert-Semantik haben (z. B. string). Die Referenzgleichheit ist bei den meisten Referenztypen die häufigste Ursache für die Referenzgleichheit und insbesondere bei weniger als eindeutigen Fällen (zwei Wörterbücher mit genau denselben Schlüssel-Wert-Paaren, aber unterschiedlichen Gleichheitsvergleichern [und damit auch Hinzufügungen]) das gleiche zusätzliche Schlüssel-Wert-Paar könnte sie jetzt anders machen] gleich oder nicht?) oder wo Wertgleichheit nicht häufig gesucht wird.

Immerhin suchen Sie nach einem Fall, in dem zwei verschiedene Typen als gleich angesehen werden. Ein Gleichheits-Override würde wahrscheinlich immer noch fehlschlagen.

Dies umso mehr, als Sie Ihren eigenen Gleichheitsvergleich immer schaffen können schnell genug:

public class SimpleDictEqualityComparer<TKey, TValue> : IEqualityComparer<IDictionary<TKey, TValue>> 
{ 
    // We can do a better job if we use a more precise type than IDictionary and use 
    // the comparer of the dictionary too. 
    public bool Equals(IDictionary<TKey, TValue> x, IDictionary<TKey, TValue> y) 
    { 
     if(ReferenceEquals(x, y)) 
      return true; 
     if(ReferenceEquals(x, null) || ReferenceEquals(y, null)) 
      return false; 
     if(x.Count != y.Count) 
      return false; 
     TValue testVal = default(TValue); 
     foreach(TKey key in x.Keys) 
      if(!y.TryGetValue(key, out testVal) || !Equals(testVal, x[key])) 
       return false; 
     return true; 
    } 
    public int GetHashCode(IDictionary<TKey, TValue> dict) 
    { 
     unchecked 
     { 
      int hash = 0x15051505; 
      foreach(TKey key in dict.Keys) 
      { 
       var value = dict[key]; 
       var valueHash = value == null ? 0 : value.GetHashCode(); 
       hash ^= ((key.GetHashCode() << 16 | key.GetHashCode() >> 16)^valueHash); 
      } 
      return hash; 
     } 
    } 
} 

, dass nicht alle möglichen Fälle dienen würde, wo man will, Wörterbücher vergleichen, aber dann, das war mein Punkt.

Das Auffüllen der BCL mit "wahrscheinlich, was sie meinen" Gleichheitsmethoden wäre eine Belästigung, keine Hilfe.

+0

Der Gleichheitsvergleich fügt eine zusätzliche Falte hinzu, die Java nicht hat, gebe ich zu. –

+0

@David, Java hat eine Möglichkeit, eine externe Definition der Äquivalenz zwischen Objekten zu definieren, sicher? –

+0

Ja und Nein. Es gibt 'Comparator', und wenn Ihre Implementierung von' Comparator.compareTo (a, b) '0 zurückgibt, sollen' a' und 'b' gleich sein. Normalerweise entdeckt dies niemand, bis sie ein 'TreeSet' oder' TreeMap' konstruieren (die sortierten Standardimplementierungen) und die Mitglieder damit beginnen, aufgrund einer schlechten 'Comparator'-Implementierung auszufallen. Es gibt nicht wirklich etwas wie IEqualityComparer in der Standardbibliothek - Sammlungen verwenden einfach 'equals()'. (Außer, wie bereits erwähnt, TreeMap/TreeSet, die eine Abkürzung nehmen.) –

2

Ich würde vorschlagen, mit CollectionAssue.AreEquivalent() von NUnit. Assert.AreEqual() ist wirklich nicht für Sammlungen gedacht. http://www.nunit.org/index.php?p=collectionAssert&r=2.4

+0

Das ist interessant. Es sieht so aus, als ob 'CollectionAssert.AreEqual()' tatsächlich übergeben wird. - Nein, ich nehme das zurück, es tut es nicht. Aber es sollte. Vielleicht habe ich tatsächlich einen Fehler. –

+0

Sicher ist CollectionAssert, was Sie wollen. –

+0

Können Sie etwas dazu sagen, warum CollectionAssert nicht geeignet ist? –

1
public sealed class DictionaryComparer<TKey, TValue> 
    : EqualityComparer<IDictionary<TKey, TValue>> 
{ 
    public override bool Equals(
     IDictionary<TKey, TValue> x, IDictionary<TKey, TValue> y) 
    { 
     if (object.ReferenceEquals(x, y)) return true; 
     if ((x == null) || (y == null)) return false; 
     if (x.Count != y.Count) return false; 

     foreach (KeyValuePair<TKey, TValue> kvp in x) 
     { 
      TValue yValue; 
      if (!y.TryGetValue(kvp.Key, out yValue)) return false; 
      if (!kvp.Value.Equals(yValue)) return false; 
     } 
     return true; 
    } 

    public override int GetHashCode(IDictionary<TKey, TValue> obj) 
    { 
     unchecked 
     { 
      int hash = 1299763; 
      foreach (KeyValuePair<TKey, TValue> kvp in obj) 
      { 
       int keyHash = kvp.Key.GetHashCode(); 
       if (keyHash == 0) keyHash = 937; 

       int valueHash = kvp.Value.GetHashCode(); 
       if (valueHash == 0) valueHash = 318907; 

       hash += (keyHash * valueHash); 
      } 
      return hash; 
     } 
    } 
} 
+0

Kleiner Vorschlag: Verwenden Sie 'EqualityComparer .Default.Equals (kvp.Value, yValue)', um eine Null-Ausnahme in der Equals-Methode zu vermeiden. Ähnlich müssen Sie in der 'GetHashCode'-Methode nach' kvp.Value == null' suchen. – nawfal

1

So sieht es aus wie Schlüssel-Wert-Gleichheit in C# ist eine Eigenschaft der Wörterbuch konkreten Klasse, nicht der IDictionary Schnittstelle. Ist das richtig? Ist es allgemein für das gesamte System System.Collections?

Wenn ja, ich würde mich interessieren einige Diskussion zu lesen, warum MS entschied, dass Ansatz

Ich denke, es ist ganz einfach - IDictionary eine Schnittstelle und Schnittstellen können keine Implementierungen haben und In .NET World wird die Gleichheit zweier Objekte durch Equals Methode definiert. Es ist also unmöglich, Equals für die IDictionary-Schnittstelle zu überschreiben, um "Schlüssel-Wert-Gleichheit" zu besitzen.

0

Sie haben einen großen Fehler in Ihrem ursprünglichen Beitrag gemacht. Sie sprachen über die Equals() Methode in der IDictionary Schnittstelle. Das ist der Punkt!

Equals() ist eine virtuelle Methode von System.Object, die Klassen überschreiben können. Schnittstellen implementieren Methoden überhaupt nicht. Stattdessen sind Instanzen von Interfaces Referenztypen und erben somit von System.Object und potenziell, die eine Override von Equals() deklarieren.

Jetzt ist der Punkt ... System.Collections.Generic.Dictionary<K,V> tut nicht überschreiben entspricht. Sie sagten, Sie Ihre IDictionary Ihre eigene Art und Weise umgesetzt, und einigermaßen außer Kraft gesetzt Equals, aber

Assert.AreEqual (backingDictionary, readOnlyDictionary); 

Diese Methode als return backingDictionary.Equals(readOnlyDictionary) implementiert ist grundsätzlich auf eigenen Code aussehen und wieder hier ist der Punkt.

Die Methode Basic Equals() gibt false zurück, wenn zwei Objekte Instanzen verschiedener Klassen sind, die Sie nicht steuern können. Andernfalls, wenn die beiden Objekte vom selben Typ sind, wird jedes Mitglied über die Reflektion (nur Mitglieder, nicht Eigenschaften) unter Verwendung des Equals() Ansatzes anstelle von == verglichen (was das Handbuch "Wertvergleich" anstelle von "Referenzvergleich" nennt)

Also zunächst wäre ich nicht überrascht, wenn Assert.AreEqual (readOnlyDictionary,backingDictionary); erfolgreich ist, weil es eine benutzerdefinierte Equals-Methode auslösen würde.

Ich habe keine Zweifel, dass Ansätze von anderen Benutzern in diesem Thread arbeiten, aber ich wollte Ihnen nur erklären, was der Fehler in Ihrem ursprünglichen Ansatz war. Sicherlich hätte Microsoft eine Equals-Methode implementiert, die die aktuelle Instanz mit einer anderen IDictionary-Instanz vergleicht, aber auch dies wäre außerhalb des Bereichs der Dictionary-Klasse, die eine öffentliche eigenständige Klasse ist und nicht dazu gedacht ist die einzige öffentlich verfügbare Implementierung von IDictionary. Wenn Sie beispielsweise eine Schnittstelle, eine Factory und eine geschützte Klasse definieren, die diese in einer Bibliothek implementiert, möchten Sie die Klasse möglicherweise mit anderen Instanzen der Basisschnittstelle und nicht mit der Klasse selbst vergleichen, die nicht öffentlich ist.

Ich hoffe, Ihnen geholfen zu haben. Prost.

+1

Sorry, ich sprach ungenau. Es ist die gleiche Situation in Java - 'equals()' ist eine Methode auf 'Object'. Die Java-Map-Schnittstelle "überschreibt" jedoch "equals()" (nicht wirklich; sie deklariert sie einfach neu) in _document_ die_expectation_, dass sie für zwei "Map" -Implementierungen mit denselben key-> value-Zuordnungen den Wert true zurückgibt . Es ist, was diese Erwartungen in den .NET-Sammlungen sind und warum ich das wissen möchte. –

+0

Bekam es ... Sie müssen vielleicht in .NET Entwickler Gedanken lesen :( –

+0

@djechelon: Wenn zwei Referenzen verwendet werden, um die Identität zu kapseln, sollten sie gleich vergleichen, wenn und nur wenn sie das gleiche Objekt identifizieren. Zwei nicht freigegebene Referenzen die den Zustand * kapseln und die Identität nicht kapseln, sollten gleich sein, wenn sie den gleichen Zustand kapseln .. Leider unterscheidet weder .NET noch Java zwischen Verweisen, die die Identität kapseln sollten, und solchen, die dies nicht tun sollten - eine Einschränkung, die Gleichheitsprüfung nicht erschwert und Klonen unter anderem, ist aber die zugrunde liegende Ursache vieler Bugs, die mit Mutationen zusammenhängen. " – supercat

Verwandte Themen