Контракт Equals () для .NET Dictionary / IDictionary vs контракт equals () для Java Map

Ностальгируя по Collections.unmodifiableMap(), я реализовал оболочку IDictionary только для чтения на основе это обсуждение, и мой модульный тест быстро натолкнулся на проблему:

Assert.AreEqual (backingDictionary, readOnlyDictionary);

не выполняется, даже если пары ключ-значение совпадают. Я поигрался еще немного, и, по крайней мере, похоже (спасибо Simonyi)

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

действительно проходит.

Я быстро просмотрел _5 _ и документации IDictionary, а также к моему удивлению, я не смог найти эквивалент Java Map договариваются, что два Maps с равным entrySet()s должны быть равны. (В документации сказано, что Dictionary - не IDictionary - переопределяет Equals(), но не говорится, что делает это переопределение.)

Таким образом, похоже, что равенство "ключ-значение" в C # является свойством конкретного класса Dictionary, а не интерфейса IDictionary. Это правильно? Это вообще верно в отношении всего System.Collections фреймворка?

Если это так, мне было бы интересно прочитать обсуждение того, почему MS выбрала этот подход, а также того, какой предпочтительный способ - это проверить равенство содержимого коллекции в C #.

И, наконец, я бы не отказался указать на хорошо протестированную ReadOnlyDictionary реализацию. :)


ETA: Чтобы было ясно, я не ищу предложения по тестированию моей реализации - это относительно тривиально. Я ищу руководство по условиям контракта, обязательного для выполнения этими тестами. И почему.


ETA: Ребята, я знаю IDictionary, что это интерфейс, и я знаю, что интерфейсы не могут реализовывать методы. То же самое и на Java. Тем не менее, интерфейс Java Map документирует ожидание определенное поведение из equals() метода. Несомненно, должны быть интерфейсы .NET, которые делают такие вещи, даже если интерфейсов сбора среди них нет.


person David Moles    schedule 12.10.2010    source источник
comment
В пространстве имен MS.Internal.Utility в сборке WindowsBase есть ReadOnlyDictionary ‹K, V›. Он не отменяет Equals.   -  person dtb    schedule 13.10.2010
comment
Я пишу приложение Mono для iOS, но это интересный момент. Два MS.Internal.Utility.ReadOnlyDictionaries с одинаковым содержимым равны друг другу?   -  person David Moles    schedule 13.10.2010
comment
Для более поздних читателей: (1) класс Algorithms в PowerCollections предоставляет ReadOnly методы для упаковки коллекций (включая словари) только для чтения. (2) SequenceEqual () LINQ работает с упорядоченными коллекциями (включая словари). ).   -  person David Moles    schedule 24.06.2011


Ответы (6)


Переопределение равенства обычно выполняется только с классами, которые имеют определенную семантику значений (например, string). Справочное равенство - это то, что людей чаще всего беспокоит с большинством ссылочных типов и хорошим значением по умолчанию, особенно в случаях, которые могут быть менее чем ясными (это два словаря с точно такими же парами ключ-значение, но разными компараторами равенства [и, следовательно, добавление одна и та же дополнительная пара «ключ-значение» может сделать их теперь разными] равными или нет?) или где равенство значений не будет часто искать.

В конце концов, вы ищете случай, когда два разных типа считаются равными. Переопределение равенства, вероятно, все равно подведет вас.

Тем более, что вы всегда можете достаточно быстро создать свой собственный компаратор равенства:

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

Это не годится для всех возможных случаев, когда нужно сравнивать словари, но в этом я и хотел.

Заполнение BCL методами равенства, «вероятно, то, что они имеют в виду», было бы неприятностью, а не помощью.

person Jon Hanna    schedule 12.10.2010
comment
Я признаю, что средство сравнения на равенство добавляет лишнюю морщину, которой нет в Java. - person David Moles; 13.10.2010
comment
@David, у Java есть способ определить внешнее определение эквивалентности между объектами, правда? - person Jon Hanna; 13.10.2010
comment
И да и нет. Там Comparator, и если ваша реализация Comparator.compareTo(a, b) возвращает 0, a и b должны быть равны. Однако обычно никто не обнаруживает этого, пока они не построят TreeSet или TreeMap (сортированные реализации по умолчанию) и члены не начнут выпадать из него из-за плохой реализации Comparator. В стандартной библиотеке нет ничего похожего на IEqualityComparer - коллекции просто используют equals(). (За исключением, как уже отмечалось, TreeMap/TreeSet, в котором используется ярлык.) - person David Moles; 13.10.2010
comment
@Jon: Маленькая придирка: ваш GetHashCode метод зависит от порядка dict.Keys, и этот порядок никоим образом не гарантируется. Два IDictionary<K,V> экземпляра, содержащие один и тот же набор пар ключ-значение, теоретически могут создавать разные хэш-коды, даже если они сравниваются как равные. - person LukeH; 13.10.2010
comment
Ах, тогда я так понимаю, что Comparator не поддерживает вид слабого упорядочивания, при котором вещи могут быть отсортированы в одном и том же положении, но не равны. Я думаю, что предпочитаю разделение _1 _ / _ 2_ (и сопоставимое _3 _ / _ 4_ для определения значения по умолчанию внутри класса), но то, что вы описываете, в большинстве случаев все равно помогает. Вы привели меня в панику, так как я не особо смотрю на Java, но я был шокирован мыслью, что у него может не быть такого провайдера эквивалентности, и что я, возможно, не заметил! - person Jon Hanna; 13.10.2010
comment
@LukeH. Вы совершенно правы. Редактирование для замены методом, не зависящим от порядка. - person Jon Hanna; 13.10.2010
comment
Предложения: зачем перечислять только ключи и выполнять x[key] поиск? Почему бы не зациклить сам словарь? См. Ответ Люка. Также вам следует избегать боксов для типов значений. - person nawfal; 09.11.2013

Я бы предложил использовать CollectionAssert.AreEquivalent () из NUnit. Assert.AreEqual () действительно не предназначен для коллекций. http://www.nunit.org/index.php?p=collectionAssert&r=2.4

person Alex Lo    schedule 12.10.2010
comment
Это интересно. Похоже, CollectionAssert.AreEqual() действительно проходит. - Нет, я беру это обратно, это не так. Но должно быть. Может, у меня действительно ошибка. - person David Moles; 13.10.2010
comment
Конечно, CollectionAssert - это то, что вам нужно. - person Alex Lo; 13.10.2010
comment
Вы можете прокомментировать, почему CollectionAssert не подходит? - person Alex Lo; 26.10.2010

Для более поздних читателей, вот что мне сказали / что я смог выяснить:

  1. Контракт для коллекций .NET, в отличие от коллекций Java, не включает какого-либо конкретного поведения для Equals() или GetHashCode().
  2. Метод расширения LINQ Enumerable.SequenceEqual() будет работать для упорядоченных коллекций, включая словари, которые представлены как IEnumerable<KeyValuePair>; KeyValuePair - это структура, а ее метод Equals использует отражение, чтобы сравнить содержимое.
  3. Enumerable предоставляет другие методы расширения, которые можно использовать для объединения проверки на равенство содержимого, например Union() и Intersect().

Я прихожу к мысли, что, несмотря на удобство методов Java, они могут быть не лучшей идеей, если мы говорим об изменяемых коллекциях и о типичной неявной equals() семантике - о том, что два equal объекта взаимозаменяемы. .NET не обеспечивает хорошей поддержки неизменяемых коллекций, в отличие от библиотеки PowerCollections с открытым исходным кодом.

person David Moles    schedule 24.06.2011
comment
В Java, в отличие от .NET, нет понятия компаратора равенства. Когда изменяемые типы используются как сущности, ссылочное равенство имеет смысл; при использовании в качестве значений, которые не изменятся, пока они находятся в словаре, равенство значений имеет смысл. В большинстве случаев, когда что-то, что ничего не знает о классе, будет сравнивать экземпляры, лучше ошибиться, сообщая о неравенстве, что делает эталонное равенство .NET по умолчанию. В случаях, когда код, хранящий вещи в словаре, знает, что равенство значений имеет больше смысла, он может указать компаратор равенства, который это проверяет. - person supercat; 14.11.2013
comment
Поскольку в Java нет типа компаратора равенства, единственный способ сделать свои значения пригодными для использования в качестве ключей словаря - это переопределить свои методы, связанные с равенством, для использования равенства значений и надеяться, что экземпляры, которые используются как сущности, не получат хранится в словаре. - person supercat; 14.11.2013

Таким образом, похоже, что равенство "ключ-значение" в C # является свойством конкретного класса Dictionary, а не интерфейса IDictionary. Это правильно? Верно ли это в целом для всей структуры System.Collections?

Если это так, мне было бы интересно прочитать обсуждение того, почему MS выбрала этот подход.

Я думаю, что это довольно просто - IDictionary - это интерфейс, а интерфейсы не могут иметь никаких реализаций, а в мире .NET равенство двух объектов определяется с помощью метода Equals. Таким образом, просто невозможно переопределить Equals для интерфейса IDictionary, чтобы он имел «равенство ключ-значение».

person Andrew Bezzub    schedule 25.10.2010

вы сделали большую ошибку в своем исходном посте. Вы говорили о методе Equals() в интерфейсе IDictionary. В этом-то и дело!

Equals () - это виртуальный метод System.Object, который классы могут переопределять. Интерфейсы не реализуют методы вообще. Вместо этого экземпляры интерфейсов являются ссылочными типами, таким образом наследуя от System.Object и потенциально объявляя переопределение Equals().

Теперь суть ... System.Collections.Generic.Dictionary<K,V> не переопределяет Equals. Вы сказали, что реализовали свой IDictionary по-своему и разумно переопределили Equals, но посмотрите на свой собственный код.

Assert.AreEqual (backingDictionary, readOnlyDictionary); 

Этот метод в основном реализован как return backingDictionary.Equals(readOnlyDictionary), и снова в этом суть.

Базовый метод Equals () возвращает false, если два объекта являются экземплярами разных классов, вы не можете это контролировать. В противном случае, если два объекта относятся к одному и тому же типу, каждый член сравнивается посредством отражения (только члены, а не свойства) с использованием подхода Equals() вместо == (который в руководстве называется «сравнение значений» вместо «сравнение ссылок»).

Итак, во-первых, я не удивлюсь, если Assert.AreEqual (readOnlyDictionary,backingDictionary); удастся, потому что это вызовет определенный пользователем метод Equals.

Я не сомневаюсь, что подходы других пользователей в этой ветке работают, но я просто хотел объяснить вам, в чем заключалась ошибка в вашем первоначальном подходе. Конечно, Microsoft лучше бы реализовала метод Equals, который сравнивает текущий экземпляр с любым другим экземпляром IDictionary, но, опять же, это вышло бы за рамки класса Dictionary, который является общедоступным автономным классом и не предназначен для использования. единственная общедоступная реализация IDictionary. Например, когда вы определяете интерфейс, фабрику и защищенный класс, который реализует его в библиотеке, вы можете захотеть сравнить класс с другими экземплярами базового интерфейса, а не с самим классом, который не является общедоступным.

Я надеюсь, что был вам полезен. Ваше здоровье.

person usr-local-ΕΨΗΕΛΩΝ    schedule 25.10.2010
comment
Извините, я неточно сказал. То же самое и в Java - equals() - это метод на Object. Однако интерфейс Java Map переопределяет equals() (не совсем; он просто повторно объявляет его) на документ ожидание, что он вернет истину для любых двух реализаций Map с одним и тем же ключом - ›сопоставление значений. Я хочу знать, каковы эти ожидания в коллекциях .NET и почему. - person David Moles; 26.10.2010
comment
Понятно сейчас ... вам, возможно, придется вникнуть в умы разработчиков .NET :( - person usr-local-ΕΨΗΕΛΩΝ; 27.10.2010
comment
@djechelon: если для инкапсуляции идентичности используются две ссылки, они должны сравниваться как равные тогда и только тогда, когда они идентифицируют один и тот же объект. Две неразделенные ссылки, которые инкапсулируют состояние и не инкапсулируют идентичность, должны сравниваться как равные, если они инкапсулируют одно и то же состояние. К сожалению, ни .NET, ни Java не делают различий между ссылками, которые должны инкапсулировать идентичность, и ссылками, которые не должны инкапсулировать - ограничение, которое, помимо прочего, не усложняет тестирование на равенство и клонирование, но лежит в основе многих ошибок, связанных с изменчивостью. - person supercat; 21.11.2013

person    schedule
comment
Незначительное предложение: используйте EqualityComparer<TValue>.Default.Equals(kvp.Value, yValue), чтобы избежать нулевого исключения в методе Equals. Точно так же в методе GetHashCode вам нужно будет проверить kvp.Value == null. - person nawfal; 09.11.2013