Почему C # не реализует GetHashCode для коллекций? - PullRequest
17 голосов
/ 25 мая 2010

Я портирую что-то с Java на C #. В Java hashcode из ArrayList зависит от элементов в нем. В C # я всегда получаю один и тот же хэш-код от List ...

Почему это?

Для некоторых моих объектов хеш-код должен отличаться, потому что объекты в их свойстве list делают объекты не равными Я ожидаю, что хеш-код всегда уникален для состояния объекта и равен другому хеш-коду, когда объект равен. Я не прав?

Ответы [ 7 ]

15 голосов
/ 26 мая 2010

Для правильной работы хеш-коды должны быть неизменными - хеш-код объекта должен никогда не меняться.

Если хеш-код объекта действительно изменится, все словари, содержащие объект, перестанут работать.

Поскольку коллекции не являются неизменяемыми, они не могут реализовать GetHashCode.
Вместо этого они наследуют значение по умолчанию GetHashCode, которое возвращает (мы надеемся) уникальное значение для каждого экземпляра объекта. (Обычно на основе адреса памяти)

8 голосов
/ 14 ноября 2011

Хеш-коды должны зависеть от используемого определения равенства, так что если A == B, то A.GetHashCode() == B.GetHashCode() (но не обязательно обратное; A.GetHashCode() == B.GetHashCode() не влечет за собой A == B).

По умолчанию определение равенства типа значения основано на его значении, а ссылочного типа - на его идентичности (то есть по умолчанию экземпляр ссылочного типа равен только самому себе), следовательно, хеш-код по умолчанию для типа значения таков, что он зависит от значений полей, которые он содержит *, а для ссылочных типов - от идентификатора. Действительно, поскольку в идеале мы хотим, чтобы хэш-коды для неравных объектов отличались друг от друга, особенно в битах младших разрядов (скорее всего, это повлияет на значение повторного хеширования), мы обычно хотим два эквивалентных, но не -эквивалентные объекты с разными хешами.

Поскольку объект останется равным самому себе, также должно быть ясно, что реализация по умолчанию GetHashCode() будет продолжать иметь то же значение, даже когда объект мутирован (идентичность не мутирует даже для изменяемого объекта) .

Теперь в некоторых случаях ссылочные типы (или типы значений) переопределяют равенство. Примером этого является строка, где, например, "ABC" == "AB" + "C". Хотя есть два разных сравниваемых экземпляра строки, они считаются равными. В этом случае GetHashCode() необходимо переопределить, чтобы значение относилось к состоянию, в котором определено равенство (в данном случае последовательность символов содержится).

Хотя более распространено делать это с типами, которые также являются неизменяемыми, по ряду причин GetHashCode() не зависит от неизменности . Скорее, GetHashCode() должен оставаться непротиворечивым перед лицом изменчивости - измените значение, которое мы используем при определении хэша, и хэш должен измениться соответственно. Обратите внимание, что это проблема, если мы используем этот изменяемый объект в качестве ключа в структуре, использующей хеш, поскольку изменение объекта изменяет позицию, в которой он должен быть сохранен, не перемещая его в эту позицию (это также верно для любой другой случай, когда положение объекта в коллекции зависит от его значения - например, если мы сортируем список, а затем изменяем один из элементов в списке, список больше не сортируется). Однако это не означает, что мы должны использовать неизменяемые объекты только в словарях и хэш-наборах. Скорее это означает, что мы не должны мутировать объект, который находится в такой структуре, и сделать его неизменным - четкий способ гарантировать это.

Действительно, довольно много случаев, когда желательно хранить изменяемые объекты в таких структурах, и пока мы не изменяем их в течение этого времени, это нормально. Поскольку у нас нет гарантии, которую дает неизменность, мы хотим предоставить ее другим способом (например, провести в коллекции короткое время и быть доступным только из одного потока).

Следовательно, неизменность значений ключей - это один из тех случаев, когда что-то возможно, но обычно это идея. Тем, кто определяет алгоритм хэш-кода, не следует полагать, что любой такой случай всегда будет плохой идеей (они даже не знают, произошла ли мутация, пока объект хранился в такой структуре); они должны реализовать хеш-код, определенный для текущего состояния объекта, независимо от того, хорош он или нет в вызове данного объекта. Следовательно, например, хеш-код не должен запоминаться для изменяемого объекта, если только запоминание не очищается при каждом изменении. (В общем случае, тратить время на хеширование хеширования, как правило, бесполезно, поскольку структуры, которые неоднократно обращаются к одному и тому же хэш-коду объектов, будут иметь его собственные запоминания).

Теперь в рассматриваемом случае ArrayList использует стандартный случай равенства, основанный на идентичности, например ::

.
ArrayList a = new ArrayList();
ArrayList b = new ArrayList();
for(int i = 0; i != 10; ++i)
{
  a.Add(i);
  b.Add(i);
}
return a == b;//returns false

Теперь это действительно хорошая вещь. Зачем? Ну, а как вы узнали из вышеизложенного, что мы хотим считать a равным b? Мы могли бы, но есть много веских причин не делать этого и в других случаях.

Более того, гораздо проще переопределить равенство от основанного на идентичности до основанного на значениях, чем от основанного на значениях к основанному на идентичности. Наконец, существует несколько основанных на значениях определений равенства для многих объектов (классический случай - это разные взгляды на то, что делает строку равной), поэтому не существует даже единственного определения, которое работает. Например:

ArrayList c = new ArrayList();
for(short i = 0; i != 10; ++i)
{
  c.Add(i);
}

Если мы рассмотрели a == b выше, следует ли нам рассмотреть a == c также? Ответ зависит только от того, что нас волнует в определении равенства, которое мы используем, поэтому структура не может знать, каков правильный ответ для всех случаев, поскольку все случаи не совпадают.

Теперь, если мы заботимся о равенстве на основе значений в данном случае, у нас есть два очень простых варианта. Первый заключается в создании подкласса и равенства перебега:

public class ValueEqualList : ArrayList, IEquatable<ValueEqualList>
{
  /*.. most methods left out ..*/
  public Equals(ValueEqualList other)//optional but a good idea almost always when we redefine equality
  {
    if(other == null)
      return false;
    if(ReferenceEquals(this, other))//identity still entails equality, so this is a good shortcut
      return true;
    if(Count != other.Count)
      return false;
    for(int i = 0; i != Count; ++i)
      if(this[i] != other[i])
        return false;
    return true;
  }
  public override bool Equals(object other)
  {
    return Equals(other as ValueEqualList);
  }
  public override int GetHashCode()
  {
    int res = 0x2D2816FE;
    foreach(var item in this)
    {
        res = res * 31 + (item == null ? 0 : item.GetHashCode());
    }
    return res;
  }
}

Это предполагает, что мы всегда будем обращаться с такими списками таким образом. Мы также можем реализовать IEqualityComparer для данного случая:

public class ArrayListEqComp : IEqualityComparer<ArrayList>
{//we might also implement the non-generic IEqualityComparer, omitted for brevity
  public bool Equals(ArrayList x, ArrayList y)
  {
    if(ReferenceEquals(x, y))
      return true;
    if(x == null || y == null || x.Count != y.Count)
      return false;
    for(int i = 0; i != x.Count; ++i)
      if(x[i] != y[i])
        return false;
    return true;
  }
  public int GetHashCode(ArrayList obj)
  {
    int res = 0x2D2816FE;
    foreach(var item in obj)
    {
        res = res * 31 + (item == null ? 0 : item.GetHashCode());
    }
    return res;
  }
}

В итоге:

  1. Определение равенства по умолчанию для ссылочного типа зависит только от идентичности.
  2. В большинстве случаев мы этого хотим.
  3. Когда человек, определяющий класс, решает, что это не то, что ему нужно, он может переопределить это поведение.
  4. Когда человек, использующий класс, снова хочет другое определение равенства, он может использовать IEqualityComparer<T> и IEqualityComparer, чтобы его словари, хэш-карты, хэш-наборы и т. Д. Использовали свою концепцию равенства.
  5. Мутировать объект ужасно, пока он является ключом к хеш-структуре. Неизменяемость может быть использована для гарантии того, что этого не произойдет, но это не обязательно и не всегда желательно.

В целом, фреймворк дает нам хорошие настройки по умолчанию и детальные возможности переопределения.

* В случае десятичной дроби в структуре существует ошибка, поскольку в некоторых случаях используется сокращение, когда это безопасно, а не в других случаях, но в то время как структура, содержащая десятичную дробь, является одним из случаев, когда короткий путь небезопасен, он неправильно определен как случай, когда он безопасен.

8 голосов
/ 25 мая 2010

Да, вы не правы. Как в Java, так и в C # равенство подразумевает наличие одинакового хеш-кода, но обратное (не обязательно) верно.

См. GetHashCode для получения дополнительной информации.

3 голосов
/ 31 июля 2010

Основными причинами являются производительность и человеческая природа - люди склонны думать о хешах как о чем-то быстром, но обычно требуется обходить все элементы объекта хотя бы один раз.

Пример: если вы используете строку в качестве ключа в хеш-таблице, каждый запрос имеет сложность O (| s |) - используйте в 2 раза более длинные строки, и это будет стоить вам как минимум вдвое больше. Представьте, что это было полноценное дерево (просто список списков) - упс: -)

Если бы полный, глубокий расчет хеша был стандартной операцией для коллекции, огромный процент программистов просто использовал бы ее невольно, а затем обвинял бы фреймворк и виртуальную машину в медленной работе. Для чего-то столь же дорогого, как полное прохождение, очень важно, чтобы программист осознавал всю сложность. Единственное, чего нужно было достичь, это убедиться, что вы должны написать свое. Это также хороший сдерживающий фактор: -)

Другая причина - обновление тактики . Вычисление и обновление хэша на лету, а выполнение полного вычисления каждый раз требует суждения в зависимости от конкретного случая.

Immutabilty - просто академический полицейский - люди делают хэши для быстрого обнаружения изменений (например, хэши файлов), а также используют хеши для сложных структур, которые постоянно меняются. Хэш имеет много других применений, помимо 101 основы. Ключ снова в том, что то, что использовать для хеширования сложного объекта, должно быть вызовом суждения в каждом конкретном случае.

Использование адреса объекта (фактически, дескриптора, чтобы он не изменялся после GC) в качестве хэша - это на самом деле тот случай, когда значение хэш-функции остается неизменным для произвольного изменяемого объекта :-) Причина, по которой это делает C #, заключается в том, что он дешев снова подталкивает людей к подсчету своих.

3 голосов
/ 25 мая 2010

Хеш-код не может быть уникальным во всех вариациях большинства нетривиальных классов. В C # концепция равенства списков не такая, как в Java (см. здесь ), поэтому реализация хеш-кода также не такая - она ​​отражает равенство списков C #.

2 голосов
/ 25 мая 2010

Вы только отчасти ошибаетесь. Вы определенно ошибаетесь, когда думаете, что равные хеш-коды означают равные объекты, но равные объекты должны иметь одинаковые хеш-коды, что означает, что если хеш-коды различаются, то объекты тоже.

0 голосов
/ 25 мая 2010

Почему слишком философски. Создайте вспомогательный метод (может быть методом расширения) и рассчитайте хеш-код, как вам нравится. Может быть хэш-кодами элементов XOR

...