GetHashCode переопределение объекта, содержащего универсальный массив
У меня есть класс, который содержит следующие два свойства:
public int Id { get; private set; }
public T[] Values { get; private set; }
Я сделал это IEquatable<T>
и переопределить object.Equals
как это:
public override bool Equals(object obj)
{
return Equals(obj as SimpleTableRow<T>);
}
public bool Equals(SimpleTableRow<T> other)
{
// Check for null
if(ReferenceEquals(other, null))
return false;
// Check for same reference
if(ReferenceEquals(this, other))
return true;
// Check for same Id and same Values
return Id == other.Id && Values.SequenceEqual(other.Values);
}
При переопределении object.Equals
Я также должен переопределить GetHashCode
конечно. Но какой код я должен реализовать? Как мне создать хеш-код из общего массива? И как мне совместить это с Id
целое число?
public override int GetHashCode()
{
return // What?
}
9 ответов
Из-за проблем, поднятых в этой теме, я публикую другой ответ, показывающий, что произойдет, если вы ошибетесь... в основном, что вы не можете использовать массив GetHashCode()
; правильное поведение заключается в том, что при его запуске предупреждения не выводятся... переключите комментарии, чтобы исправить это:
using System;
using System.Collections.Generic;
using System.Linq;
static class Program
{
static void Main()
{
// first and second are logically equivalent
SimpleTableRow<int> first = new SimpleTableRow<int>(1, 2, 3, 4, 5, 6),
second = new SimpleTableRow<int>(1, 2, 3, 4, 5, 6);
if (first.Equals(second) && first.GetHashCode() != second.GetHashCode())
{ // proven Equals, but GetHashCode() disagrees
Console.WriteLine("We have a problem");
}
HashSet<SimpleTableRow<int>> set = new HashSet<SimpleTableRow<int>>();
set.Add(first);
set.Add(second);
// which confuses anything that uses hash algorithms
if (set.Count != 1) Console.WriteLine("Yup, very bad indeed");
}
}
class SimpleTableRow<T> : IEquatable<SimpleTableRow<T>>
{
public SimpleTableRow(int id, params T[] values) {
this.Id = id;
this.Values = values;
}
public int Id { get; private set; }
public T[] Values { get; private set; }
public override int GetHashCode() // wrong
{
return Id.GetHashCode() ^ Values.GetHashCode();
}
/*
public override int GetHashCode() // right
{
int hash = Id;
if (Values != null)
{
hash = (hash * 17) + Values.Length;
foreach (T t in Values)
{
hash *= 17;
if (t != null) hash = hash + t.GetHashCode();
}
}
return hash;
}
*/
public override bool Equals(object obj)
{
return Equals(obj as SimpleTableRow<T>);
}
public bool Equals(SimpleTableRow<T> other)
{
// Check for null
if (ReferenceEquals(other, null))
return false;
// Check for same reference
if (ReferenceEquals(this, other))
return true;
// Check for same Id and same Values
return Id == other.Id && Values.SequenceEqual(other.Values);
}
}
FWIW, очень опасно использовать содержимое значений в вашем хэш-коде. Это следует делать только в том случае, если вы можете гарантировать, что оно никогда не изменится. Тем не менее, поскольку он разоблачен, я не думаю, что это возможно. Хеш-код объекта никогда не должен изменяться. В противном случае он теряет свое значение как ключ в Hashtable или Dictionary. Рассмотрим трудно обнаруживаемую ошибку использования объекта в качестве ключа в Hashtable, его хэш-код изменяется из-за внешнего влияния, и вы больше не можете найти его в Hashtable!
Поскольку hashCode является своего рода ключом для хранения объекта (как в хеш-таблице), я бы использовал только Id.GetHashCode()
Как насчет чего-то вроде:
public override int GetHashCode()
{
int hash = Id;
if (Values != null)
{
hash = (hash * 17) + Values.Length;
foreach (T t in Values)
{
hash *= 17;
if (t != null) hash = hash + t.GetHashCode();
}
}
return hash;
}
Это должно быть совместимо с SequenceEqual
вместо того, чтобы делать сравнение ссылок на массив.
Мне просто нужно было добавить другой ответ, потому что не было упомянуто одно из наиболее очевидных (и самых простых в реализации) решений - не считая коллекцию в вашем GetHashCode
расчет!
Главное, что, казалось, здесь забыли, это то, что уникальность результата GetHashCode
не требуется (или во многих случаях даже возможно). Неравные объекты не должны возвращать неравные хеш-коды, единственное требование - чтобы равные объекты возвращали равные хеш-коды. Таким образом, согласно этому определению, следующая реализация GetHashCode
является правильным для всех объектов (при условии, что есть правильный Equals
реализация):
public override int GetHashCode()
{
return 42;
}
Конечно, это дало бы наихудшую возможную производительность при поиске в хеш-таблице, O(n) вместо O(1), но это все еще функционально правильно.
Имея это в виду, моя общая рекомендация при реализации GetHashCode
для объекта, который имеет какой-либо вид коллекции, поскольку один или несколько его членов просто игнорируют их и вычисляют GetHashCode
исключительно на основе других скалярных членов. Это будет работать очень хорошо, за исключением случаев, когда вы помещаете в хеш-таблицу огромное количество объектов, в которых все их скалярные члены имеют одинаковые значения, что приводит к идентичным хеш-кодам.
Игнорирование членов коллекции при вычислении хеш-кода также может привести к повышению производительности, несмотря на уменьшенное распределение значений хеш-кода. Помните, что использование хеш-кода должно улучшить производительность в хеш-таблице, не требуя вызова Equals
N раз, и вместо этого потребуется только один раз вызвать GetHashCode и быстрый поиск в хеш-таблице. Если у каждого объекта есть внутренний массив с 10000 элементов, которые все участвуют в вычислении хеш-кода, любые выгоды от хорошего распределения, вероятно, будут потеряны. Было бы лучше иметь немного менее распределенный хеш-код, если его генерация значительно дешевле.
public override int GetHashCode() {
return Id.GetHashCode() ^ Values.GetHashCode();
}
В комментариях и других ответах есть несколько хороших моментов. ОП должен рассмотреть, будут ли Значения использоваться как часть "ключа", если объект использовался в качестве ключа в словаре. Если это так, то они должны быть частью хеш-кода, иначе нет.
С другой стороны, я не уверен, почему метод GetHashCode должен отражать SequenceEqual. Он предназначен для вычисления индекса в хеш-таблице, а не для полного определения равенства. Если существует много коллизий хеш-таблиц, использующих алгоритм выше, и если они отличаются по последовательности значений, то должен быть выбран алгоритм, который учитывает последовательность. Если последовательность не имеет значения, сэкономьте время и не принимайте его во внимание.
Я знаю, что этот поток довольно старый, но я написал этот метод, чтобы позволить мне вычислять хэш-коды нескольких объектов. Это было очень полезно для этого самого случая. Он не идеален, но он отвечает моим потребностям и, скорее всего, вашим.
Я не могу взять кредит на это. Я получил концепцию от некоторых реализаций.net gethashcode. Я использую 419 (в конце концов, это мое любимое большое простое число), но вы можете выбрать практически любое разумное простое число (не слишком маленькое... не слишком большое).
Итак, вот как я получаю свои хэш-коды:
using System.Collections.Generic;
using System.Linq;
public static class HashCodeCalculator
{
public static int CalculateHashCode(params object[] args)
{
return args.CalculateHashCode();
}
public static int CalculateHashCode(this IEnumerable<object> args)
{
if (args == null)
return new object().GetHashCode();
unchecked
{
return args.Aggregate(0, (current, next) => (current*419) ^ (next ?? new object()).GetHashCode());
}
}
}
При условии, что Id и Значения никогда не изменятся, и Значения не равны нулю...
public override int GetHashCode()
{
return Id ^ Values.GetHashCode();
}
Обратите внимание, что ваш класс не является неизменным, так как любой может изменить содержимое значений, потому что это массив. Учитывая это, я бы не стал создавать хеш-код, используя его содержимое.
Я бы сделал это так:
long result = Id.GetHashCode();
foreach(T val in Values)
result ^= val.GetHashCode();
return result;