Хеш-коды должны зависеть от используемого определения равенства, так что если 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;
}
}
В итоге:
- Определение равенства по умолчанию для ссылочного типа зависит только от идентичности.
- В большинстве случаев мы этого хотим.
- Когда человек, определяющий класс, решает, что это не то, что ему нужно, он может переопределить это поведение.
- Когда человек, использующий класс, снова хочет другое определение равенства, он может использовать
IEqualityComparer<T>
и IEqualityComparer
, чтобы его словари, хэш-карты, хэш-наборы и т. Д. Использовали свою концепцию равенства.
- Мутировать объект ужасно, пока он является ключом к хеш-структуре. Неизменяемость может быть использована для гарантии того, что этого не произойдет, но это не обязательно и не всегда желательно.
В целом, фреймворк дает нам хорошие настройки по умолчанию и детальные возможности переопределения.
* В случае десятичной дроби в структуре существует ошибка, поскольку в некоторых случаях используется сокращение, когда это безопасно, а не в других случаях, но в то время как структура, содержащая десятичную дробь, является одним из случаев, когда короткий путь небезопасен, он неправильно определен как случай, когда он безопасен.