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);
}
При наличии переопределения GetHashCode
я также должен переопределить Id
, конечно. Но какой код я должен реализовать? Как мне создать хеш-код из общего массива? И как мне объединить его с <=> целым числом?
public override int GetHashCode()
{
return // What?
}
Решение
Из-за проблем, поднятых в этой теме, я публикую еще один ответ, показывающий, что произойдет, если вы ошибетесь ... в основном, что вы не можете использовать массив 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
расчет!
Главное, что, похоже, здесь забыли, - это то, что уникальность результата Equals
не требуется (а во многих случаях даже невозможна). Неравные объекты не должны возвращать неравные хеш-коды, единственное требование - чтобы равные объекты возвращали равные хеш-коды. Таким образом, согласно этому определению следующая реализация <=> является правильной для всех объектов (при условии, что есть правильная реализация <=>):
public override int GetHashCode()
{
return 42;
}
Конечно, это приведет к наихудшей производительности при поиске в хэш-таблице, O (n) вместо O (1), но это все еще функционально правильно.
Имея это в виду, моя общая рекомендация при реализации <=> для объекта, который имеет коллекцию любого типа в качестве одного или нескольких его членов, - просто игнорировать их и вычислять <=> исключительно на основе другого скаляра. члены. Это будет работать очень хорошо, за исключением случаев, когда вы помещаете в хеш-таблицу огромное количество объектов, в которых все их скалярные члены имеют одинаковые значения, что приводит к идентичным хеш-кодам.
Игнорирование членов коллекции при вычислении хеш-кода также может привести к повышению производительности, несмотря на уменьшенное распределение значений хеш-кода. Помните, что использование хеш-кода должно повысить производительность в хеш-таблице, не требуя вызова <=> N раз, а вместо этого потребует только один раз вызова GetHashCode и быстрого поиска в хеш-таблице. Если у каждого объекта есть внутренний массив с 10 000 элементов, которые все участвуют в вычислении хеш-кода, любые выгоды от хорошего распределения, вероятно, будут потеряны. Было бы лучше иметь немного менее распределенный хэш-код, если его создание значительно дешевле.
public override int GetHashCode() {
return Id.GetHashCode() ^ Values.GetHashCode();
}
<Ч>
В комментариях и других ответах есть несколько хороших моментов. ОП должен рассмотреть, будут ли Значения использоваться как часть & Quot; key & Quot; если объект был использован в качестве ключа в словаре. Если это так, то они должны быть частью хеш-кода, иначе - нет.
С другой стороны, я не уверен, почему метод GetHashCode должен отражать SequenceEqual. Он предназначен для вычисления индекса в хэш-таблице, а не для полного определения равенства. Если существует много коллизий хеш-таблиц, использующих алгоритм выше, и если они отличаются по последовательности значений, то должен быть выбран алгоритм, который учитывает последовательность. Если последовательность не имеет значения, сэкономьте время и не принимайте его во внимание.
Я бы сделал это так:
long result = Id.GetHashCode();
foreach(T val in Values)
result ^= val.GetHashCode();
return result;
Я знаю, что этот поток довольно старый, но я написал этот метод, чтобы позволить мне вычислять хэш-коды нескольких объектов. Это было очень полезно для этого самого случая. Он не идеален, но он отвечает моим потребностям и, скорее всего, вашим.
Я не могу взять кредит на это. Я получил концепцию из некоторых реализаций .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();
}
Обратите внимание, что ваш класс не является неизменным, так как любой может изменить содержимое значений, потому что это массив. Учитывая это, я бы не стал создавать хеш-код, используя его содержимое.