Как С# определяет хэш-код для объекта?

Этот вопрос вытекает из обсуждения кортежи.

Я начал думать о хеш-коде, который должен быть у кортежа. Что, если мы примем класс KeyValuePair как кортеж? Он не переопределяет метод GetHashCode(), поэтому, вероятно, он не будет знать о хэш-кодах своих "потомков"... Таким образом, во время выполнения будет вызываться Object.GetHashCode(), который не знает о структура реального объекта.

Затем мы можем создать два экземпляра некоторого ссылочного типа, которые на самом деле равны Equal из-за перегруженных функций GetHashCode() и Equals(). И используйте их как «детей» в кортежах, чтобы «обмануть» словарь.

Но это не работает! Время выполнения каким-то образом выясняет структуру нашего кортежа и вызывает перегруженный GetHashCode нашего класса!

Как это работает? Какой анализ делает Object.GetHashCode()?

Может ли это повлиять на производительность в каком-то плохом сценарии, когда мы используем какие-то сложные ключи? (наверное, невозможный сценарий... но все же)

Рассмотрим этот код в качестве примера:

namespace csharp_tricks
{
    class Program
    {
        class MyClass
        {
            int keyValue;
            int someInfo;

            public MyClass(int key, int info)
            {
                keyValue = key;
                someInfo = info;
            }

            public override bool Equals(object obj)
            {
                MyClass other = obj as MyClass;
                if (other == null) return false;

                return keyValue.Equals(other.keyValue);
            }

            public override int GetHashCode()
            {
                return keyValue.GetHashCode();
            }
        }

        static void Main(string[] args)
        {
            Dictionary<object, object> dict = new Dictionary<object, object>();

            dict.Add(new KeyValuePair<MyClass,object>(new MyClass(1, 1), 1), 1);

            //here we get the exception -- an item with the same key was already added
            //but how did it figure out the hash code?
            dict.Add(new KeyValuePair<MyClass,object>(new MyClass(1, 2), 1), 1); 

            return;
        }
    }
}

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

  • Будьте осторожны со своими ключами и их хэш-кодами :-)
  • Для сложных ключей словаря вы должны правильно переопределить Equals() и GetHashCode().

person Max Galkin    schedule 19.09.2008    source источник
comment
Это отличная статья о GetHashCode от Effective C#: awprofessional.com/content /images/0321245660/items/   -  person torial    schedule 19.09.2008


Ответы (6)


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

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

person Pop Catalin    schedule 19.09.2008
comment
Красиво, но это не ответ. - person Max Galkin; 19.09.2008
comment
Но как они могли идентифицировать объект, не генерируя хэш? Разве не в этом смысл GetHashCode? - person Cory R. King; 19.09.2008
comment
Кори, хэш-код ключа — это число, используемое для быстрого вычисления местоположения блока в хеш-таблице (блок — это пара ключ-значение или несколько пар kv) после вычисления местоположения, если блок содержит ключ ( проверка на равенство)... - person Pop Catalin; 19.09.2008
comment
... (если ключ равен ключу из этого сегмента), то значение возвращается, в противном случае позиция повторно хэшируется и проверяется другое местоположение. Алгоритм завершается, когда проверяемый сегмент пуст или все сегменты были проверены. - person Pop Catalin; 19.09.2008
comment
Таким образом, хеш-код указывает не значение, а первую позицию в хеш-таблице, в которой нужно найти ключ. - person Pop Catalin; 19.09.2008
comment
@PopCatalin Я не согласен с вашим постом. Вы можете и должны переопределить GetHashCode и Equals, но должны использовать только неизменяемые поля для GetHashCode. И хеш-коды могут не использоваться для уникальной идентификации объектов, но они используются для значительного сокращения набора возможных объектов, для которых хеш-таблица должна проверять равенство. - person Daniel A.A. Pelsmaeker; 04.10.2012

Вот правильные реализации Hash и равенства для кортежа Quad (содержит 4 компонента кортежа внутри). Этот код обеспечивает правильное использование этого конкретного кортежа в HashSets и словарях.

Подробнее об этом (включая исходный код) здесь.

Обратите внимание на использование ключевого слова unchecked (во избежание переполнения) и создание исключения NullReferenceException, если obj имеет значение null (как того требует базовый метод).

public override bool Equals(object obj)
{
    if (ReferenceEquals(null, obj))
        throw new NullReferenceException("obj is null");
    if (ReferenceEquals(this, obj)) return true;
    if (obj.GetType() != typeof (Quad<T1, T2, T3, T4>)) return false;
    return Equals((Quad<T1, T2, T3, T4>) obj);
}

public bool Equals(Quad<T1, T2, T3, T4> obj)
{
    if (ReferenceEquals(null, obj)) return false;
    if (ReferenceEquals(this, obj)) return true;
    return Equals(obj.Item1, Item1)
        && Equals(obj.Item2, Item2)
            && Equals(obj.Item3, Item3)
                && Equals(obj.Item4, Item4);
}

public override int GetHashCode()
{
    unchecked
    {
        int result = Item1.GetHashCode();
        result = (result*397) ^ Item2.GetHashCode();
        result = (result*397) ^ Item3.GetHashCode();
        result = (result*397) ^ Item4.GetHashCode();
        return result;
    }
}
public static bool operator ==(Quad<T1, T2, T3, T4> left, Quad<T1, T2, T3, T4> right)
{
    return Equals(left, right);
}


public static bool operator !=(Quad<T1, T2, T3, T4> left, Quad<T1, T2, T3, T4> right)
{
    return !Equals(left, right);
}
person Rinat Abdullin    schedule 30.09.2008
comment
+1 за правильную реализацию GetHashCode, однако я считаю, что вы не должны генерировать исключение из Equals(object) - эта реализация также непоследовательна: quad.Equals(null) выдает NullReferenceException, а quad.Equals((Quad)null) возвращает ложный :-). - person Milan Gardian; 01.05.2009

Ознакомьтесь с этим публикацией Брэда Абрамса, а также комментарий Брайана Грункемейера для получения дополнительной информации о том, как работает object.GetHashCode. Также взгляните на первый комментарий в блоге Аянде post. Я не знаю, следуют ли текущим выпускам Framework этим правилам или они действительно изменили их, как предполагал Брэд.

person Scott Dorman    schedule 19.09.2008
comment
Эти объяснения также противоречат приведенному коду. Каким-то образом среда выполнения проникает внутрь MyClass.GetHashCode() и использует его для получения хэш-кода KeyValuePair. Но что именно делает время выполнения? - person Max Galkin; 19.09.2008

Кажется, теперь у меня есть подсказка.

Я думал, что KeyValuePair — это ссылочный тип, но это не так, это структура. И поэтому он использует метод ValueType.GetHashCode(). MSDN для этого говорит: «Одно или несколько полей производного типа используются для вычисления возвращаемого значения».

Если вы возьмете реальный ссылочный тип в качестве «поставщика кортежей», вы обманете словарь (или себя...).

using System.Collections.Generic;

namespace csharp_tricks
{
    class Program
    {
        class MyClass
        {
            int keyValue;
            int someInfo;

            public MyClass(int key, int info)
            {
                keyValue = key;
                someInfo = info;
            }

            public override bool Equals(object obj)
            {
                MyClass other = obj as MyClass;
                if (other == null) return false;

                return keyValue.Equals(other.keyValue);
            }

            public override int GetHashCode()
            {
                return keyValue.GetHashCode();
            }
        }

        class Pair<T, R>
        {
            public T First { get; set; }
            public R Second { get; set; }
        }

        static void Main(string[] args)
        {
            var dict = new Dictionary<Pair<int, MyClass>, object>();

            dict.Add(new Pair<int, MyClass>() { First = 1, Second = new MyClass(1, 2) }, 1);

            //this is a pair of the same values as previous! but... no exception this time...
            dict.Add(new Pair<int, MyClass>() { First = 1, Second = new MyClass(1, 3) }, 1);

            return;
        }
    }
}
person Max Galkin    schedule 19.09.2008

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

Это полностью из воспоминаний о том, что я кратко прочитал, так что принимайте это как хотите.

Редактировать: Книга была Inside C# от MS Press. Тот, что с лезвием пилы на обложке. Автор потратил много времени на объяснение того, как вещи были реализованы в CLR, как язык переводится в MSIL и т.д. ЭСТ. Если найдете книгу, то не плохое чтение.

Редактировать. Сформируйте ссылку, если она выглядит как

Object.GetHashCode() использует внутреннее поле в классе System.Object для генерации хеш-значения. Каждому созданному объекту присваивается уникальный ключ объекта, сохраняемый как целое число при его создании. Эти ключи начинаются с 1 и увеличиваются каждый раз, когда создается новый объект любого типа.

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

person Dan Blair    schedule 19.09.2008
comment
Это объяснение противоречит примеру кода в вопросе. - person Max Galkin; 19.09.2008

поэтому, вероятно, он не будет знать хэш-коды своих «детей».

Ваш пример, кажется, доказывает обратное :-) Хэш-код для ключа MyClass и значения 1 одинаков для обоих KeyValuePair . Реализация KeyValuePair должна использовать как Key, так и Value для собственного хеш-кода.

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

Рассмотрим более сложный случай:

public class HappyClass
{

    enum TheUnit
    {
        Points,
        Picas,
        Inches
    }

    class MyDistanceClass
    {
        int distance;
        TheUnit units;

        public MyDistanceClass(int theDistance, TheUnit unit)
        {
            distance = theDistance;

            units = unit;
        }
        public static int ConvertDistance(int oldDistance, TheUnit oldUnit, TheUnit newUnit)
        {
            // insert real unit conversion code here :-)
            return oldDistance * 100;
        }

        /// <summary>
        /// Figure out if we are equal distance, converting into the same units of measurement if we have to
        /// </summary>
        /// <param name="obj">the other guy</param>
        /// <returns>true if we are the same distance</returns>
        public override bool Equals(object obj)
        {
            MyDistanceClass other = obj as MyDistanceClass;
            if (other == null) return false;

            if (other.units != this.units)
            {
                int newDistance = MyDistanceClass.ConvertDistance(other.distance, other.units, this.units);
                return distance.Equals(newDistance);
            }
            else
            {
                return distance.Equals(other.distance);
            }


        }

        public override int GetHashCode()
        {
            // even if the distance is equal in spite of the different units, the objects are not
            return distance.GetHashCode() * units.GetHashCode();
        }
    }
    static void Main(string[] args)
    {

        // these are the same distance... 72 points = 1 inch
        MyDistanceClass distPoint = new MyDistanceClass(72, TheUnit.Points);
        MyDistanceClass distInch = new MyDistanceClass(1, TheUnit.Inch);

        Debug.Assert(distPoint.Equals(distInch), "these should be true!");
        Debug.Assert(distPoint.GetHashCode() != distInch.GetHashCode(), "But yet they are fundimentally different values");

        Dictionary<object, object> dict = new Dictionary<object, object>();

        dict.Add(new KeyValuePair<MyDistanceClass, object>(distPoint, 1), 1);

        //this should not barf
        dict.Add(new KeyValuePair<MyDistanceClass, object>(distInch, 1), 1);

        return;
    }

}

По сути... в моем примере вы хотите, чтобы два объекта, находящиеся на одинаковом расстоянии, возвращали "true" для Equals, но при этом возвращали разные хэш-коды.

person Cory R. King    schedule 19.09.2008
comment
KeyValuePair не реализует GetHashCode. Ваш пример СОВЕРШЕННО неверен. Откройте MSDN: если два объекта сравниваются как равные, метод GetHashCode для каждого объекта должен возвращать одно и то же значение. - person Max Galkin; 19.09.2008