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

person Svish    schedule 12.03.2009    source источник


Ответы (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);
    }
}
person Marc Gravell    schedule 12.03.2009
comment
Не могли бы вы объяснить причину правильной версии GetHashCode()? - person Vinko Vrsalovic; 01.06.2009
comment
@Vinko: Можешь уточнить? Вы имеете в виду, почему хэш-код имеет значение? - или почему такой подход? Учитывая количество ваших повторений и ответов, я предполагаю последнее; это просто способ получить хэш, учитывающий все значения. Умножение на простое число и добавление следующего хэша — это очень распространенный подход к хешированию, который позволяет избежать коллизий (в отличие от xor; в этом случае набор всех 8 может легко дать предсказуемый хэш-код 0). Я что-то пропустил? - person Marc Gravell; 01.06.2009
comment
См. также: stackoverflow.com/questions/263400#263416... другое простое число, но тот же эффект. - person Marc Gravell; 01.06.2009
comment
Да, это был вопрос. Спасибо. - person Vinko Vrsalovic; 01.06.2009
comment
Начал заново, извините. stackoverflow.com/questions/2626839/, в любом случае, моя цель - равенство, я хотел бы пропустить часть реализации GetHashCode. И да, начальное значение равно 0. В любом случае, я использую EF, поэтому все объекты инициализируются с идентификатором как 0, а затем свойства устанавливаются индивидуально один за другим, а не инициализатором, по этой причине, если он хэшируется, когда идентификатор не еще не загружен, он сходит с ума, возможно, вы знаете, как решить эту проблему и наслаждаться как правильным хэшированием, так и равенством для этого изменяемого объекта. - person Shimmy Weitzhandler; 13.04.2010
comment
Умножение int на простое число (которое также должно быть близко к степени 2, например, 17 или 31 хорошо) распределяет биты, если можно ожидать, что два входных целых числа будут близки по значению (например, они являются членами перечисления или идентификаторы в системе с гораздо менее чем 2 миллиардами строк) - person Eric J.; 12.03.2012
comment
Можете ли вы объяснить с ошибкой/проблемой, что именно может быть проблемой для класса, если GetHashCode() не переопределяется в случае переопределения equals? - person Nilish; 22.05.2012
comment
Обратите внимание, что в вашей реализации IEquatable‹TableRow‹T›› отсутствует проверка того же типа. объект производного класса TableRow будет считаться равным. Это нарушило бы правило, что если A равно B, то B должно быть равно A: tableRow.Equals(derivedTableRow) может возвращать true, но производныйTableRow.Equals(tableRow) возвращает false; Простой пример: класс Person с именем и днем ​​рождения и производный класс Child со свойством AttendingSchool. Child может иметь те же значения, что и Person, поэтому Person.Equals(Child), но Child.Equals(Person) возвращает false, потому что Person as Child возвращает null - person Harald Coppoolse; 20.08.2015

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

person Dustin Campbell    schedule 12.03.2009
comment
Это требует большего количества голосов. Я всегда делал неправильное предположение между концепцией GetHashCode и хешем MD5 загруженного файла. GetHashCode предназначен для сравнения не содержимого, а контейнера. Чтобы убедиться, что он указывает на одно и то же место в памяти. Я использовал GetHashCode, чтобы проверить, изменился ли объект с момента последнего сохранения в базе данных. Я сохранил клонированный список только для сравнения объектов, но после переопределения GetHashCode все, основанное на хеш-таблице, начало вести себя странно. Теперь я просто переместил свое переопределение в собственный метод и сохранил словарь с хэшем содержимого. - person Pluc; 29.10.2014
comment
@Pluc: GetHashCode предназначен для того, чтобы убедиться, что контейнер указывает на одно и то же место в памяти, ну, не совсем. Он предназначен для сравнения контента, просто он может иметь ложные срабатывания из-за коллизий. Как MD5, но с большей вероятностью коллизий. - person Groo; 29.06.2017
comment
its hashcode changes because of an outside influence and you can no longer find it in the Hashtable! - для меня это имеет смысл, если объект был изменен, это уже не тот же объект, поэтому его не должно быть в хеш-таблице, словаре, хэш-наборе или чем-то еще. - person Mykhailo Seniutovych; 24.03.2019

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

person Jhonny D. Cano -Leftware-    schedule 12.03.2009
comment
На самом деле это лучше, чем использование Values.GetHashCode(), поскольку сохраняет совместимость с Equals. - person Thomas Dufour; 20.08.2011

Как насчет чего-то вроде:

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

person Marc Gravell    schedule 12.03.2009
comment
Опасно сравнивать содержимое Values, потому что не гарантируется, что они будут одинаковыми на протяжении всего времени существования объекта. Поскольку массив открыт, любой внешний класс может изменить его, что повлияет на хэш-код! - person Dustin Campbell; 12.03.2009
comment
Однако дело в том, что он совместим с опубликованным методом Equals. - person Marc Gravell; 12.03.2009
comment
Это также влияет на равноправие. И вы не можете использовать ссылку на arary для вычисления хэш-кода, потому что в итоге вы получите два одинаковых объекта с разными хеш-кодами. - person Grzenio; 12.03.2009
comment
@Grzenio - это направлено на меня или на Дастина? Я не использую ссылку именно по этой причине... - person Marc Gravell; 12.03.2009
comment
Извините за путаницу, это был ответ на комментарий Дастина здесь и его код одновременно. - person Grzenio; 12.03.2009

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

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

public override int GetHashCode() 
{ 
    return 42; 
} 

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

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

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

person Allon Guralnek    schedule 21.08.2012
comment
Назначение хеш-кода — не просто выбрать хэш-багет, но и в более общем плане быстро отсеять вещи, которые могут быть признаны неравными. Класс должен основывать свою концепцию равенства на концепции инкапсулированной последовательности только в том случае, если последовательность является неизменной. Предполагая, что последовательность неизменяема, класс, вероятно, должен включать элементы последовательности в свой вычисляемый хэш-код (который, в свою очередь, должен кэшироваться). В противном случае, если добавить в словарь десять объектов с массивами из 5000 элементов, отличающихся последним элементом, попытка найти элемент приведет к... - person supercat; 26.09.2012
comment
...все 5000 элементов нового элемента сравниваются со всеми 5000 элементами каждого из десяти объектов. Напротив, если бы каждый элемент вычислял и кэшировал хеш-значение для содержимого массива, даже если все десять хэш-значений были сопоставлены с одним и тем же хэш-сегментом, самое большее, что могло бы произойти, если бы все хеш-значения были разными, это то, что хеш-значение новый объект будет сравниваться с кэшированными хэш-значениями других десяти. Если пара значений хеш-функции сталкивается, это все равно не будет реальной проблемой — всего лишь одна дополнительная группа сравнений из 5000 элементов (а не десять). - person supercat; 26.09.2012
comment
@supercat: Здесь вы делаете много предположений: что последовательность неизменяема, что объект кэширует свой собственный хэш-код (я никогда этого не видел), но, что наиболее важно, единственные данные объекта, на которых основывается хэш-код, последовательность (обратите внимание, что в исходном вопросе объект имеет свойство Id, которого почти во всех случаях достаточно для создания уникального хэш-кода). В любом случае, вы говорите об очень конкретном сценарии, который я не вижу, как он связан ни с общим случаем, ни с исходным вопросом. - person Allon Guralnek; 26.09.2012
comment
Если последовательность не является неизменной, она не должна участвовать в equals. Мое предположение о том, что тип был неизменным, было основано на том, что OP хотел проверить последовательности на равенство. Если кто-то, вероятно, имеет и сравнивает друг с другом множество экземпляров объекта, которые будут идентичными (согласно определению, используемому equals), за исключением некоторого признака, этот признак обычно должен быть частью хэш-кода. Java считает целесообразным кэшировать хеш-код для его наиболее распространенного неизменяемого типа, похожего на последовательность (строка). - person supercat; 26.09.2012
comment
Не могу поверить, что читаю это. Последний GetHashCode(), который я написал, специально должен был перечислять коллекцию в объекте для работы, как и Equals(). - person Joshua; 19.12.2016
comment
@Джошуа: Это не имеет никакого смысла. GetHashCode() никогда должен ничего не делать для работы. Любая работа, которую вы делаете, направлена ​​только на то, чтобы сделать ее более равномерно распределенной. Equals(), с другой стороны, должен выполнять всю работу, чтобы функционировать правильно. - person Allon Guralnek; 19.12.2016
comment
@AllonGuralnek: Вы видели, что происходит, когда вы помещаете объекты в коллекции хэшей с помощью нефункционального GetHashCode()? GetHashCode() должен был работать в моем случае, потому что алгоритмы должны были быть быстрее, чем N^2. - person Joshua; 19.12.2016

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

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

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

person John Saunders    schedule 12.03.2009
comment
Я также не думаю, что в массивах реализован GetHashCode с учетом всех элементов - person Grzenio; 12.03.2009
comment
Это будет выполнять эталонное сравнение значений и не будет совместимо с SequenceEqual (т.е. для разных массивов с одинаковым содержимым). - person Marc Gravell; 12.03.2009
comment
Ребята, я уже говорил это раньше, но будьте осторожны, используя все элементы открытого массива. Результат GetHashCode() должен быть одинаковым на протяжении всего времени существования объекта, иначе он не будет работать как ключ хеш-таблицы. Нет никакой гарантии, что этот массив не изменится, поэтому не используйте его в GetHashCode! - person Dustin Campbell; 12.03.2009
comment
@Dustin: Хорошее разъяснение. Именно это я имел в виду, когда говорил, следует ли использовать объект в качестве ключа. Такие объекты не могут измениться таким образом, чтобы изменить их хеш-код или равенство, пока они действуют как ключ. - person John Saunders; 12.03.2009
comment
@John - такие моменты очень важны и хорошо подняты: однако публикация реализации GetHashCode(), которая несовместима с опубликованным Equals(), очень неправильно и может привести к множеству проблем - потерянные данные и т.д. - person Marc Gravell; 12.03.2009
comment
@Marc: можете ли вы опубликовать URL-адрес, в котором говорится, что две реализации должны быть эквивалентны (и это определяет эквивалентность)? Хотя цели похожи, они не идентичны. Sure Equals сравнивает неключевые поля. Пока два одинаковых объекта имеют одинаковый хеш-код? В чем проблема? - person John Saunders; 12.03.2009
comment
msdn.microsoft.com/en-us/library/system. object.getashcode.aspx Если два объекта сравниваются как равные, метод GetHashCode для каждого объекта должен возвращать одно и то же значение. - где сравнить как равные означает Equals() - person Marc Gravell; 12.03.2009
comment
stackoverflow.com/questions/371328/ - person Marc Gravell; 12.03.2009
comment
Обратите внимание, что SequenceEqual (в опубликованном Equals) будет рассматривать два разных массива с одинаковым содержимым как равные; но у них будут разные хэш-коды, поэтому ваш код не будет генерировать действительные хэш-коды. - person Marc Gravell; 12.03.2009
comment
Или для демонстрации: stackoverflow.com/questions/638761/ - person Marc Gravell; 12.03.2009
comment
Если неизменяемый класс содержит массивы, которые будут записаны только во время построения и после построения никогда не будут подвергаться воздействию чего-либо, что могло бы их записать, может быть полезно, чтобы два экземпляра класса называли себя равными, только если они содержат массивы, равные по последовательности. . В этом сценарии хэш-код класса должен учитывать содержимое массива, поскольку именно содержимое массивов определяет равенство. - person supercat; 26.09.2012

Я бы сделал это так:

long result = Id.GetHashCode();
foreach(T val in Values)
    result ^= val.GetHashCode();
return result;
person Grzenio    schedule 12.03.2009
comment
довольно разумно - обратите внимание, что xor может привести к большому количеству коллизий; обычно предпочтительнее умножение/сложение - person Marc Gravell; 12.03.2009
comment
интересно, многие люди советовали мне вместо этого использовать xor. Я должен прочитать больше об этом тогда. - person Grzenio; 12.03.2009
comment
В ответ на это; каким будет хэш {3,3,3,3}? и {4,4,4,4}? или {4,0,0,4}? или {1,0,1,0}? Вы видите проблему... - person Marc Gravell; 12.03.2009
comment
@MarcGravell: умножение плохое. Жаль, что в С# нет левого или правого бита. - person Joshua; 19.12.2016
comment
@ Джошуа, если под вращением вы подразумеваете круговой сдвиг, то его легко смоделировать с помощью сдвига влево и вправо. Если вы не это имеете в виду, то, пожалуйста, дайте мне знать - мне действительно любопытно. - person Marc Gravell; 19.12.2016
comment
@MarcGravell: Да, это так, и сгенерированный код раздражающе медленный по сравнению с правильной инструкцией ЦП. - person Joshua; 19.12.2016
comment
если под круговым сдвигом вы подразумеваете сдвиг битов влево или вправо, для этого вы просто умножаете или делите на 2? - person David Klempfner; 28.03.2018
comment
@Backwards_Dave Это будет обычная смена. При вращении или круговом сдвиге биты, смещенные в одну сторону, одновременно смещаются обратно в другую сторону. Если вы разделите 0xF9 на 2 четыре раза подряд, у вас останется 0x0F. Но если вы повернете 0xF9 вправо на 4 позиции (при 8-битных регистрах), у вас останется 0x9F. - person Carvo Loco; 12.05.2018

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

Я не могу взять на себя ответственность за это. Я получил концепцию из некоторых реализаций .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());
        }
    }
}
person D. Patrick    schedule 18.12.2010

При условии, что Id и Values ​​никогда не изменятся, а Values ​​не равно null...

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

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

person Dustin Campbell    schedule 12.03.2009
comment
Это будет выполнять эталонное сравнение значений и не будет совместимо с SequenceEqual (т.е. для разных массивов с одинаковым содержимым). - person Marc Gravell; 12.03.2009
comment
Верно, но поскольку массив открыт и любой внешний код может его изменить, сравнивать содержимое откровенно опасно. - person Dustin Campbell; 12.03.2009
comment
Так что я действительно должен просто использовать HashCode идентификатора? - person Svish; 12.03.2009
comment
Это означает, что... ЕСЛИ результат Equals изменится, результат GetHashCode не обязательно должен измениться, но если GetHashCode изменится, то Equals тоже изменится? - person Svish; 12.03.2009
comment
Не обязательно. Ссылка на Values ​​не должна меняться (если вы не измените ее в своем коде) - поэтому ее можно использовать. У Джона Сондерса есть лучший ответ здесь. - person Dustin Campbell; 12.03.2009
comment
@Dustin: У Джона Сондерса есть лучший ответ здесь - нет, публиковать ответ, в котором GetHashCode() несовместим с Equals(), нехорошо. Это очень плохо и может привести к множеству проблем. - person Marc Gravell; 12.03.2009