Реализация по умолчанию для Object.GetHashCode()

Как работает стандартная реализация для GetHashCode()? И эффективно ли он обрабатывает структуры, классы, массивы и т.д.?

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

Ответ 1

namespace System {
    public class Object {
        [MethodImpl(MethodImplOptions.InternalCall)]
        internal static extern int InternalGetHashCode(object obj);

        public virtual int GetHashCode() {
            return InternalGetHashCode(this);
        }
    }
}

InternalGetHashCode сопоставляется с функцией ObjectNative:: GetHashCode в CLR, которая выглядит следующим образом:

FCIMPL1(INT32, ObjectNative::GetHashCode, Object* obj) {  
    CONTRACTL  
    {  
        THROWS;  
        DISABLED(GC_NOTRIGGER);  
        INJECT_FAULT(FCThrow(kOutOfMemoryException););  
        MODE_COOPERATIVE;  
        SO_TOLERANT;  
    }  
    CONTRACTL_END;  

    VALIDATEOBJECTREF(obj);  

    DWORD idx = 0;  

    if (obj == 0)  
        return 0;  

    OBJECTREF objRef(obj);  

    HELPER_METHOD_FRAME_BEGIN_RET_1(objRef);        // Set up a frame  

    idx = GetHashCodeEx(OBJECTREFToObject(objRef));  

    HELPER_METHOD_FRAME_END();  

    return idx;  
}  
FCIMPLEND

Полная реализация GetHashCodeEx довольно велика, поэтому проще просто ссылаться на исходный код на С++.

Ответ 2

Для класса значения по умолчанию являются по существу ссылочным равенством, и это обычно нормально. Если вы пишете структуру, чаще всего отменяется равенство (не в последнюю очередь избегать бокса), но очень редко вы пишете структуру в любом случае!

При переопределении равенства вы всегда должны иметь соответствующие Equals() и GetHashCode() (т.е. для двух значений, если Equals() возвращает true, они должны возвращать один и тот же хеш-код, но обратное не требуется) - и обычно также предоставляются операторы ==/!=, и часто для реализации IEquatable<T> тоже.

Для генерации хеш-кода обычно используется суммируемая сумма, так как это позволяет избежать столкновений парных значений - например, для базового хэша поля 2:

unchecked // disable overflow, for the unlikely possibility that you
{         // are compiling with overflow-checking enabled
    int hash = 27;
    hash = (13 * hash) + field1.GetHashCode();
    hash = (13 * hash) + field2.GetHashCode();
    return hash;
}

Это имеет то преимущество, что:

  • хэш {1,2} не совпадает с хешем {2,1}
  • хэш {1,1} не совпадает с хешем {2,2}

и т.д., что может быть общим, если использовать невзвешенную сумму или xor (^) и т.д.

Ответ 3

Документация для метода GetHashCode для Object гласит: "реализация по умолчанию этого метода не должна использоваться как уникальный объект идентификатор для целей хэширования." и для ValueType говорится: "Если вы вызываете метод GetHashCode производного типа, возвращаемое значение вряд ли подходит для использования в качестве ключ в хэш-таблице.".

Основные типы данных, такие как byte, short, int, long, char и string реализуют хороший метод GetHashCode. Некоторые другие классы и структуры, например Point, реализуют метод GetHashCode, который может или не подходит для ваших конкретных потребностей. Вам просто нужно попробовать, чтобы убедиться, достаточно ли это.

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

Ответ 4

Вообще говоря, если вы переопределяете Equals, вы хотите переопределить GetHashCode. Причина этого заключается в том, что оба используются для сравнения равенства вашего класса/структуры.

Равные значения используются при проверке Foo A, B;

если (A == B)

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

Equals(obj o)
{
    if (o == null) return false;
    MyType Foo = o as MyType;
    if (Foo == null) return false;
    if (Foo.Prop1 != this.Prop1) return false;

    return Foo.Prop2 == this.Prop2;
}

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

Я обычно делаю,

GetHashCode()
{
    int HashCode = this.GetType().ToString().GetHashCode();
    HashCode ^= this.Prop1.GetHashCode();
    etc.

    return HashCode;
}

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

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