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

Ответы

Ответ 1

Из-за проблем, возникающих в этом потоке, я отправляю еще один ответ, показывающий, что произойдет, если вы ошибетесь... в основном, что вы не можете использовать массив 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);
    }
}

Ответ 2

FWIW, очень опасно использовать содержимое значений в вашем хеш-коде. Вы должны сделать это, только если вы можете гарантировать, что он никогда не изменится. Однако, поскольку он разоблачен, я не думаю, что гарантировать это возможно. Хэш-код объекта никогда не должен меняться. В противном случае он теряет значение в качестве ключа в Hashtable или Dictionary. Рассмотрим труднодоступную ошибку использования объекта в качестве ключа в Hashtable, его хэш-код изменяется из-за внешнего влияния, и вы больше не можете найти его в Hashtable!

Ответ 3

Поскольку hashCode является ключом для хранения объекта (lllike в хэш-таблице), я бы использовал только Id.GetHashCode()

Ответ 4

Как насчет чего-то типа:

    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, а не для сравнения ссылок в массиве.

Ответ 5

public override int GetHashCode() {
   return Id.GetHashCode() ^ Values.GetHashCode();  
}

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

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

Ответ 6

Я бы сделал это следующим образом:

long result = Id.GetHashCode();
foreach(T val in Values)
    result ^= val.GetHashCode();
return result;

Ответ 7

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

public override int GetHashCode()
{
  return Id ^ Values.GetHashCode();
}

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

Ответ 8

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

Я не могу поверить в это. Я получил концепцию из некоторых реализаций .net gethashcode. Я использую 419 (afterall, это мой любимый большой премьер), но вы можете выбрать практически любое разумное простое (не слишком маленькое... не слишком большое).

Итак, вот как я получаю мои хэш-коды:

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

Ответ 9

Мне просто пришлось добавить еще один ответ, потому что один из наиболее очевидных (и самых простых в реализации) решений не упоминался - не включая сборку в вашем расчете GetHashCode!

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

public override int GetHashCode() 
{ 
    return 42; 
} 

Конечно, это дало бы худшую возможную производительность в поиске хэш-таблицы O (n) вместо O (1), но она по-прежнему функционально корректна.

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

Игнорирование членов коллекции при вычислении хеш-кода также может привести к повышению производительности, несмотря на уменьшенное распределение значений хеш-кода. Помните, что использование хэш-кода должно повысить производительность в хеш-таблице, не требуя вызова Equals N раз, и вместо этого потребуется только один раз вызвать GetHashCode и быстрый поиск хэш-таблицы. Если каждый объект имеет внутренний массив с 10 000 предметов, которые все участвуют в вычислении хеш-кода, любые выгоды, получаемые от хорошего распределения, вероятно, будут потеряны. Было бы лучше иметь незначительно менее распространенный хеш-код, если его генерация значительно дешевле.