Object.GetHashCode ()의 기본 구현


162

기본 구현은 어떻게 GetHashCode()작동합니까? 그리고 구조, 클래스, 배열 등을 효율적이고 충분히 처리합니까?

나는 어떤 경우에 내 자신을 포장해야하며 어떤 경우에는 기본 구현에 안전하게 의존하여 잘 수행 할 수 있는지 결정하려고합니다. 가능하다면 바퀴를 재발 명하고 싶지 않습니다.


기사에 남은 의견을 살펴보십시오. stackoverflow.com/questions/763731/gethashcode-extension-method
Paul Westcott


34
제외 : 당신은 할 수 얻을 수 (경우에도 기본 해시 코드를 GetHashCode()사용하여 오버라이드 (override)되어있다)System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj)
마크 Gravell

@MarcGravell이 기여해 주셔서 감사합니다. 정확하게이 답변을 찾고있었습니다.
Andrew Savinykh

@MarcGravell 그러나 다른 방법으로 어떻게해야합니까?
Tomáš Zato-복원 모니카

답변:


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

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

InternalGetHashCode 는 CLR 의 ObjectNative :: GetHashCode 함수에 맵핑 되며 다음과 같습니다.

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 의 전체 구현 은 상당히 크기 때문에 C ++ 소스 코드에 연결하는 것이 더 쉽습니다 .


5
그 문서 인용문은 아주 초기 버전에서 나온 것이어야합니다. 현재 MSDN 기사에서 더 이상 이와 같이 작성되지 않았습니다. 아마도 잘못 되었기 때문일 것입니다.
Hans Passant

4
그들은 표현을 바꾸었지만 여전히 기본적으로 같은 것을 말합니다. "결과적으로이 방법의 기본 구현은 해싱 목적으로 고유 한 객체 식별자로 사용되어서는 안됩니다."
David Brown

7
설명서에서 구현이 해싱에 특히 유용하지 않다고 주장하는 이유는 무엇입니까? 객체가 자신과 같고 다른 것이 없다면, 주어진 객체 인스턴스에 대해 항상 동일한 값을 반환하고 일반적으로 다른 인스턴스에 대해 다른 값을 반환하는 해시 코드 방법은 무엇입니까?
supercat

3
@ ta.speot.is : 특정 인스턴스 가 사전에 이미 추가 되었는지 여부를 확인하려는 경우 참조 평등이 완벽합니다. 아시다시피 문자열을 사용하면 일반적으로 동일한 문자 시퀀스를 포함하는 문자열 이 이미 추가 되었는지 여부에 더 관심 이 있습니다. 그래서 string재정의 GetHashCode합니다. 반면에 다양한 컨트롤이 Paint이벤트를 처리하는 횟수를 세고 싶다고 가정 합니다. 당신은 사용할 수 있습니다 Dictionary<Object, int[]>( int[]저장 될 때마다 정확히 하나의 항목을 보유).
supercat

6
@ It'sNotALie입니다. 그런 다음 Archive.org 에 사본을
보내

88

클래스의 경우 기본값은 기본적으로 참조 평등이며 일반적으로 좋습니다. 구조체를 작성하는 경우 평등을 무시하는 것이 더 일반적이지만 (권투를 피하기 위해), 어쨌든 구조체를 작성하는 것은 매우 드 rare니다!

평등을 무시하면, 당신은 항상 일치해야 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 ( ^) 등을 사용하는 경우 일반적입니다 .


인수 합산 알고리즘의 이점에 대한 탁월한 지적; 내가 전에 몰랐던 것!
허점

위에서 언급 한대로 합산 합산으로 오버플로 예외가 발생하지 않습니까?
sinelaw

4
@sinelaw 예, 수행해야합니다 unchecked. 다행히도 uncheckedC #의 기본값이지만 명시 적으로 만드는 것이 좋습니다. 편집
Marc Gravell

7

ObjectGetHashCode메소드에 대한 문서 는 "이 메소드의 기본 구현이 해싱 목적으로 고유 한 오브젝트 식별자로 사용되어서는 안됩니다" 라고 말합니다 . 과에 대한 하나의 치형은 말한다 "당신이 파생 된 유형의를 GetHashCode 메서드를 호출 할 경우, 반환 값은 해시 테이블의 키로서 사용하기에 적합 할 가능성이 없습니다." .

기본 데이터 유형이 좋아하는 byte, short, int, long, charstring좋은의를 GetHashCode 메서드를 구현합니다. Point예를 들어 일부 다른 클래스와 구조 GetHashCode는 특정 요구에 적합하거나 적합하지 않은 방법을 구현 합니다. 당신은 그것이 충분히 좋은지 확인하기 위해 그것을 시도해야합니다.

각 클래스 또는 구조에 대한 설명서는 기본 구현을 무시하는지 여부를 알려줄 수 있습니다. 재정의하지 않으면 자신의 구현을 사용해야합니다. GetHashCode메소드 를 사용해야 할 위치에 자신을 작성하는 클래스 또는 구조체 의 경우 적절한 멤버를 사용하여 해시 코드를 계산하는 자체 구현을 작성해야합니다.


2
정기적으로 자신의 구현을 추가 해야한다는 데 동의하지 않습니다 . 간단히 말해, 대다수의 클래스 (특히)는 평등에 대해 테스트되지 않습니다. 구조체를 작성하는 (이미 드문 경우) 더 일반적이며 사실입니다.
Marc Gravell

@Marc Gravel : 물론 그것은 내가 말하려는 의도가 아닙니다. 마지막 단락을 조정하겠습니다. :)
Guffa

기본 데이터 유형은 적어도 제 경우에는 좋은 GetHashCode 메소드를 구현하지 않습니다. 예를 들어, int에 대한 GetHashCode는 숫자 자체를 반환합니다. (123) .GetHashCode ()는 123을 반환합니다.
fdermishin

5
@ user502144 그리고 무엇이 잘못 되었나요? 평등에 대한 오 탐지없이 쉽게 계산할 수있는 완벽한 고유 식별자입니다.
Richard Rast

@Richard Rast : Hashtable에서 사용될 때 키가 잘못 배포 될 수 있다는 점을 제외하고는 괜찮습니다. 이 답변을 살펴보십시오 : stackoverflow.com/a/1388329/502144
fdermishin

5

우리가 커스텀 구조체를 재정의 GetHashCode하고 기본 구현이 "해시 테이블의 키로 사용하기에 적합하지 않은지" 를 설명하는 답변을 찾을 수 Equals없으므로이 블로그에 대한 링크를 남겨 두겠습니다. post 는 문제가 발생한 실제 사례를 설명합니다.

전체 게시물을 읽는 것이 좋지만 여기에 요약이 있습니다 (강조 및 설명 추가).

구조체의 기본 해시가 느리고 좋지 않은 이유 :

CLR이 설계된 방식으로 정의 된 멤버 System.ValueType또는 System.Enum유형 에 대한 모든 호출은 [...] 권투 할당을 유발할 수 있습니다 .

해시 함수의 구현자는 딜레마에 직면합니다. 해시 함수를 적절하게 분배하거나 빠르게 만듭니다. 경우에 따라 두 가지를 모두 달성하는 것이 가능하지만 에서 일반적으로 수행 하기는 어렵 습니다 ValueType.GetHashCode.

구조체의 표준 해시 함수는 모든 필드의 해시 코드를 "결합"합니다. 그러나 ValueType메소드 에서 필드의 해시 코드를 얻는 유일한 방법은 리플렉션사용하는 것 입니다. 따라서 CLR 작성자는 배포판을 통해 속도를 교환하기로 결정했으며 기본 GetHashCode버전 은 null이 아닌 첫 번째 필드의 해시 코드를 반환 하고 유형 ID로 "조정"합니다. [...] 그렇지 않은 경우 합당한 동작입니다 . 예를 들어, 운이 좋지 않고 구조체의 첫 번째 필드가 대부분의 인스턴스에 대해 동일한 값을 갖는 경우 해시 함수는 항상 동일한 결과를 제공합니다 . 그리고 상상할 수 있듯이 이러한 인스턴스가 해시 세트 또는 해시 테이블에 저장된 경우 성능이 크게 저하됩니다.

[...] 리플렉션 기반 구현이 느립니다 . 아주 느린.

[...] 모두 ValueType.EqualsValueType.GetHashCode특별한 최적화를 가지고있다. 유형에 "포인터"가없고 적절하게 압축 된 [...] 인 경우보다 최적의 버전이 사용됩니다. GetHashCode인스턴스를 반복하고 4 바이트의 XOR 블록을 Equals사용 하고 메소드 는를 사용하여 두 인스턴스를 비교합니다 memcmp. [...] 그러나 최적화는 매우 까다 롭습니다. 첫째, 최적화가 언제 활성화되는지 알기가 어렵습니다 [...] 둘째, 메모리 비교가 반드시 올바른 결과를 제공하지는 않습니다 . 다음은 간단한 예입니다. [...] -0.0이며 +0.0같지만 이진 표현이 다릅니다.

게시물에 설명 된 실제 문제 :

private readonly HashSet<(ErrorLocation, int)> _locationsWithHitCount;
readonly struct ErrorLocation
{
    // Empty almost all the time
    public string OptionalDescription { get; }
    public string Path { get; }
    public int Position { get; }
}

우리는 기본 평등 구현을 가진 커스텀 구조체를 포함하는 튜플을 사용했습니다. 그리고 불행하게도, 구조체는 거의 언제나 [빈 문자열]에 동일 옵션 첫 번째 필드를했다 . 세트의 요소 수가 크게 증가하여 실제 성능 문제가 발생하여 수만 개의 항목으로 콜렉션을 초기화하는 데 몇 분이 소요될 때까지 성능은 정상이었습니다.

따라서 structs 의 경우에는 "어떤 경우에 내 자신의 패키지를 작성하고 어떤 경우에는 기본 구현에 안전하게 의존 할 수 있는지"라는 질문에 대답하려면 structs 를 재정의 Equals하고 GetHashCode사용자 정의 struct가 a로 사용될 때마다 해시 테이블의 키 또는 Dictionary.
또한 IEquatable<T>이 경우 권투를 피하기 위해 구현 하는 것이 좋습니다 .

다른 답변에서 언급했듯이 클래스를 작성하는 경우 참조 동등성을 사용하는 기본 해시가 일반적으로 좋으므로이 경우 재정의가 필요 하지 않은 한이 경우 귀찮게하지 않습니다 Equals( GetHashCode따라서 적절 하게 재정의해야 함 ).


1

일반적으로 Equals를 재정의하는 경우 GetHashCode를 재정의하려고합니다. 그 이유는 둘 다 클래스 / 구조의 동등성을 비교하는 데 사용되기 때문입니다.

Foo A, B를 확인할 때 Equals가 사용됩니다.

만약에 (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를 재정 의하여 개체 참조가 아닌 내부 값을 기준으로 동등성을보고 할 수 있습니다.


2
^ = 접근 방식은 특히 해시 생성을위한 좋은 접근 방식이 아닙니다. 예를 들어 Prop1 = Prop2 = 3 인 경우와 같이 일반적이고 예측 가능한 충돌이 많이 발생하는 경향이 있습니다.
Marc Gravell

값이 동일하면 객체가 같으므로 충돌에 문제가 발생하지 않습니다. 13 * Hash + NewHash는 흥미 롭습니다.
Bennett Dill

2
벤 : Obj1 {Prop1 = 12, Prop2 = 12} 및 Obj2 {Prop1 = 13, Prop2 = 13}에 대해 시도
Tomáš Kafka

0

POCO를 다루는 경우이 유틸리티를 사용하여 삶을 다소 단순화 할 수 있습니다.

var hash = HashCodeUtil.GetHashCode(
           poco.Field1,
           poco.Field2,
           ...,
           poco.FieldN);

...

public static class HashCodeUtil
{
    public static int GetHashCode(params object[] objects)
    {
        int hash = 13;

        foreach (var obj in objects)
        {
            hash = (hash * 7) + (!ReferenceEquals(null, obj) ? obj.GetHashCode() : 0);
        }

        return hash;
    }
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.