GetHashCode переопределение объекта, содержащего универсальный массив

StackOverflow https://stackoverflow.com/questions/638761

  •  10-07-2019
  •  | 
  •  

Вопрос

У меня есть класс, который содержит следующие два свойства:

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top