C #의 GetHashCode 지침


136

Essential C # 3.0 및 .NET 3.5 책에서 다음을 읽었습니다.

개체의 데이터가 변경 되더라도 특정 개체의 수명 동안 GetHashCode ()의 반환 값은 일정해야합니다 (동일한 값). 대부분의 경우이를 적용하기 위해 메소드 리턴을 캐시해야합니다.

이것이 유효한 지침입니까?

.NET에서 몇 가지 기본 제공 유형을 시도했지만 이러한 방식으로 작동하지 않았습니다.


가능한 경우 수락 된 답변을 변경하는 것이 좋습니다.
Giffyguy

답변:


93

대답은 대부분 유효한 지침이지만 유효한 규칙은 아닙니다. 그것은 또한 전체 이야기를 말하지 않습니다.

요점은 가변 유형의 경우 두 개의 동일한 객체가 동일한 해시 코드를 반환해야하며 해시 코드가 객체의 수명 기간 동안 유효해야하기 때문에 가변 데이터의 해시 코드를 기반으로 할 수 없다는 것입니다. 해시 코드가 변경되면 올바른 해시 저장소에 더 이상 존재하지 않기 때문에 해시 컬렉션에서 손실되는 개체가 생깁니다.

예를 들어 객체 A는 해시 1을 반환하므로 해시 테이블의 빈 1에 들어갑니다. 그런 다음 해시 테이블 2를 반환하도록 객체 A를 변경합니다. 해시 테이블을 찾으면 빈 2를 찾고 찾을 수 없습니다. 객체가 빈 1에서 분리되어 있습니다. 객체의 수명 기간 동안 변경되지 않으며 GetHashCode 구현을 작성하는 것이 한 가지 이유입니다.


에릭 리퍼 업데이트에 대한 훌륭한 정보를 제공 하는 블로그게시했습니다GetHashCode .

추가 업데이트
위의 몇 가지 사항을 변경했습니다.

  1. 나는 지침과 규칙을 구분했다.
  2. 나는 "개체의 수명 동안"을 쳤다.

가이드 라인은 규칙이 아니라 가이드 일뿐입니다. 실제로, GetHashCode객체가 해시 테이블에 저장 될 때와 같이 객체가 지침을 따를 것으로 기대할 때만이 지침을 따라야합니다. 해시 테이블 (또는의 규칙에 의존하는 것)에서 객체를 사용하지 않으려는 경우 GetHashCode구현시 지침을 따를 필요가 없습니다.

"개체의 수명 동안"이 표시되면 "개체가 해시 테이블과 협력해야하는 시간"또는 이와 유사한 내용을 읽어야합니다. 대부분의 경우와 마찬가지로 GetHashCode규칙을 어길 때를 아는 것 입니다.


1
변경 가능한 유형 간의 동등성을 어떻게 결정합니까?
Jon B

9
평등을 확인하기 위해 GetHashCode를 사용해서는 안됩니다.
JSB ձոգչ

4
@JS Bangs-MSDN에서 : GetHashCode를 재정의하는 파생 클래스도 Equals를 재정 의하여 동일하다고 간주되는 두 개체가 동일한 해시 코드를 갖도록 보장해야합니다. 그렇지 않으면 해시 테이블 유형이 올바르게 작동하지 않을 수 있습니다.
Jon B

3
@Joan Venge : 두 가지. 첫째, Microsoft조차도 모든 구현에 대해 올바른 GetHashCode를 얻지 못했습니다. 둘째, 값 유형은 일반적으로 모든 값이 기존 인스턴스를 수정하는 것이 아니라 새 인스턴스이므로 변경할 수 없습니다.
Jeff Yates

17
a.Equals (b)는 a.GetHashCode () == b.GetHashCode ()를 의미해야하므로, 동등 비교에 사용되는 데이터가 변경되면 해시 코드가 가장 자주 변경되어야합니다. 문제는 가변 데이터를 기반으로하는 GetHashCode가 아니라고 말합니다. 문제는 변경 가능한 객체를 해시 테이블 키로 사용하고 실제로 변경하는 것입니다. 내가 잘못?
Niklas

120

오랜 시간이 지났지 만 그럼에도 불구하고 이유와 방법에 대한 설명을 포함 하여이 질문에 대한 올바른 대답을 제시해야한다고 생각합니다. 지금까지 가장 좋은 대답은 MSDN을 철저하게 인용하는 것입니다. 자신의 규칙을 만들려고하지 마십시오. MS 직원은 자신이하는 일을 알고있었습니다.

그러나 가장 먼저해야 할 일 : 질문에 인용 된 지침이 잘못되었습니다.

이제 그 이유는 두 가지입니다

첫 번째 이유 : 해시 코드가 계산 된 방식으로, 객체 자체가 변경 되더라도 등식 계약을 위반하는 것보다 객체 수명 기간 동안 변경되지 않습니다.

기억하십시오 : "두 개체가 동일하게 비교되면 각 개체에 대한 GetHashCode 메소드는 동일한 값을 리턴해야합니다. 그러나 두 개체가 동일하게 비교되지 않으면 두 오브젝트에 대한 GetHashCode 메소드는 다른 값을 리턴하지 않아도됩니다."

두 번째 문장은 종종 "객체 생성시 동일한 객체의 해시 코드가 동일해야한다는 유일한 규칙"으로 잘못 해석됩니다. 왜 그런지 모르겠지만, 여기 대부분의 답변의 본질에 관한 것입니다.

equals 메소드에서 이름이 사용되는 이름을 포함하는 두 개의 오브젝트를 생각해보십시오. 같은 이름-> 같은 것. 인스턴스 A 만들기 : 이름 = Joe 인스턴스 B 만들기 : 이름 = Peter

해시 코드 A와 해시 코드 B는 동일하지 않을 가능성이 높습니다. 인스턴스 B의 이름이 Joe로 변경되면 이제 어떻게됩니까?

질문의 지침에 따르면 B의 해시 코드는 변경되지 않습니다. 결과는 다음과 같습니다. A.Equals (B) ==> true 그러나 동시에 : A.GetHashCode () == B.GetHashCode () ==> false.

그러나 정확히 equals & hashcode-contract에 의해이 동작이 명시 적으로 금지됩니다.

두 번째 이유 : 물론-해시 코드의 변경으로 인해 해시 코드를 사용하여 해시 목록 및 기타 객체가 손상 될 수 있지만 그 반대도 마찬가지입니다. 해시 코드를 변경하지 않으면 최악의 경우 해시 목록을 얻습니다. 여기서 많은 다른 객체는 모두 동일한 해시 코드를 가지므로 동일한 해시 빈에 있습니다-예를 들어 객체가 표준 값으로 초기화 될 때 발생합니다.


글쎄요, 언뜻보기에 모순이있는 것처럼 보입니다. 어쨌든 코드가 깨질 것입니다. 그러나 변경되거나 변경되지 않은 해시 코드에서 발생하는 문제는 없습니다.

문제의 원인은 MSDN에 잘 설명되어 있습니다.

MSDN의 해시 테이블 항목에서 :

키 오브젝트는 Hashtable에서 키로 사용되는 한 변경 불가능해야합니다.

이것은 다음을 의미합니다.

해시 값을 생성하는 모든 객체는 해시 값을 변경해야하지만 객체가 변경 될 때 해시 값을 변경해야하지만 Hashtable (또는 다른 Hash 사용 객체) 내부에서 사용될 때 자체 변경을 허용해서는 안됩니다. .

첫 번째 방법은 물론 해시 테이블에서만 사용하기 위해 불변 객체를 디자인하는 것이 가장 쉬운 방법이며, 필요할 때 일반 가변 객체의 사본으로 만들어집니다. 불변 객체 내부에서 해시 코드는 불변이기 때문에 해시 코드를 캐시하는 것이 좋습니다.

두 번째 방법 또는 객체에 "지금 해시되었습니다"플래그를 지정하고 모든 객체 데이터가 비공개인지 확인하고 객체 데이터를 변경할 수있는 모든 함수에서 플래그를 확인하고 변경이 허용되지 않는 경우 예외 데이터를 throw합니다 (예 : 플래그가 설정 됨) ). 이제 해시 영역에 객체를 넣을 때 플래그를 설정하고 더 이상 필요하지 않은 경우 플래그를 설정 해제하십시오. 사용하기 쉽도록 "GetHashCode"메서드 내에서 플래그를 자동으로 설정하는 것이 좋습니다.이 방법으로 잊을 수 없습니다. 그리고 "ResetHashFlag"메소드를 명시 적으로 호출하면 프로그래머가 지금까지 오브젝트 데이터를 변경할 수 있는지 여부를 생각해야합니다.

좋아, 말해야 할 사항 : 등가 및 해시 코드 계약을 위반하지 않고 객체 데이터가 변경 될 때 해시 코드가 변경되지 않는 가변 데이터가있는 객체를 가질 수있는 경우가 있습니다.

그러나 등식 방법이 변경 가능한 데이터를 기반으로하지 않아야합니다. 따라서 객체를 작성하고 값을 한 번만 계산하고 나중에 호출 할 때 반환하기 위해 객체에 저장하는 GetHashCode 메소드를 작성하면 다시해야합니다. 절대적으로 Equals 메소드를 작성해야합니다. A.Equals (B)도 false에서 true로 변경되지 않도록 비교를 위해 저장된 값입니다. 그렇지 않으면 계약이 파기됩니다. 이것의 결과는 일반적으로 Equals 메소드가 의미가 없다는 것입니다. 원래 참조가 같지는 않지만 값도 같지 않습니다. 때로는 의도 된 동작 (예 : 고객 레코드) 일 수도 있지만 일반적으로 그렇지 않습니다.

따라서 객체 데이터가 변경 될 때 GetHashCode 결과를 변경하고 목록이나 객체를 사용하여 해시 내부에서 객체를 사용하려는 경우 (또는 가능한 경우) 객체를 변경할 수 없게 만들거나 읽기 전용 플래그를 만들어 객체를 포함하는 해시 목록의 수명.

(이 방법으로 :이 모든 것은 C # 또는 .NET에만 해당되는 것은 아닙니다. 모든 해시 테이블 구현 또는 더 일반적으로 색인화 된 목록의 특성상, 객체의 식별 데이터는 절대 변경되지 않아야하며 객체는 목록에 있습니다. 이 규칙이 위반되면 예상치 못한 예측할 수없는 동작이 발생합니다 어딘가에 목록 내부의 모든 요소를 ​​모니터링하고 목록을 자동으로 다시 색인화하는 목록 구현이있을 수 있습니다. 그러나 이들의 성능은 반드시 가장 끔찍할 것입니다.)


23
자세한 설명은 +1 (내가 할 수 있다면 더 줄 것이다)
Oliver

5
+1 이것은 장황한 설명 때문에 확실히 더 나은 대답입니다! :)
Joe

9

에서 MSDN

두 개체가 동일한 것으로 비교되면 각 개체에 대한 GetHashCode 메서드는 동일한 값을 반환해야합니다. 그러나 두 개체가 동일하게 비교되지 않으면 두 개체의 GetHashCode 메서드가 다른 값을 반환하지 않아도됩니다.

객체의 Equals 메소드의 반환 값을 결정하는 객체 상태가 수정되지 않는 한 객체의 GetHashCode 메소드는 동일한 해시 코드를 일관되게 반환해야합니다. 이는 현재 응용 프로그램 실행에 대해서만 적용되며 응용 프로그램을 다시 실행하면 다른 해시 코드가 반환 될 수 있습니다.

최상의 성능을 얻으려면 해시 함수가 모든 입력에 대해 무작위 분포를 생성해야합니다.

즉, 객체의 값이 변경되면 해시 코드가 변경되어야합니다. 예를 들어 "Name"속성이 "Tom"으로 설정된 "Person"클래스에는 이름을 "Jerry"로 변경하면 하나의 해시 코드와 다른 코드가 있어야합니다. 그렇지 않으면 Tom == Jerry입니다. 아마도 의도하지 않은 것입니다.


편집 :

또한 MSDN에서 :

GetHashCode를 재정의하는 파생 클래스도 Equals를 재정 의하여 동일하다고 간주되는 두 개체가 동일한 해시 코드를 갖도록해야합니다. 그렇지 않으면 해시 테이블 유형이 올바르게 작동하지 않을 수 있습니다.

에서 MSDN의 해시 테이블 항목 :

키 오브젝트는 Hashtable에서 키로 사용되는 한 변경 불가능해야합니다.

내가 읽는 방식은 가변 객체 해시 테이블에서 사용하도록 설계 되지 않은 한 값이 변경되면 다른 해시 코드를 반환 해야한다는 것 입니다.

System.Drawing.Point의 예에서, 오브젝트는 가변이며, 않는 다른 해시는 X 또는 Y 값의 변경을 반환한다. 이것은 해시 테이블에서 그대로 사용하기에 좋지 않은 후보입니다.


GetHashCode ()는이 함수의 유일한 지점 인 해시 테이블에서 사용하도록 설계되었습니다.
skolima

@ skolima-MSDN 설명서가 그와 일치하지 않습니다. 가변 객체는 GetHashCode ()를 구현할 수 있으며 객체 값이 변경 될 때 다른 값을 반환해야합니다. 해시 테이블은 변경 불가능한 키를 사용해야합니다. 따라서 해시 테이블 이외의 다른 용도로 GetHashCode ()를 사용할 수 있습니다.
Jon B

9

GetHashcode와 관련된 문서는 약간 혼란 스럽다고 생각합니다.

한편으로 MSDN은 개체의 해시 코드가 절대로 변경되어서는 안되며 일정해야한다고 말합니다. 한편, MSDN은 GetHashcode의 반환 값이 두 개체가 동일하다고 간주되는 경우 두 개체에 대해 동일해야한다고 말합니다.

MSDN :

해시 함수에는 다음 속성이 있어야합니다.

  • 두 개체가 동일한 것으로 비교되면 각 개체에 대한 GetHashCode 메서드는 동일한 값을 반환해야합니다. 그러나 두 개체가 동일하게 비교되지 않으면 두 개체의 GetHashCode 메서드가 다른 값을 반환하지 않아도됩니다.
  • 객체의 Equals 메소드의 반환 값을 결정하는 객체 상태가 수정되지 않는 한 객체의 GetHashCode 메소드는 동일한 해시 코드를 일관되게 반환해야합니다. 이는 현재 응용 프로그램 실행에 대해서만 적용되며 응용 프로그램을 다시 실행하면 다른 해시 코드가 반환 될 수 있습니다.
  • 최상의 성능을 얻으려면 해시 함수가 모든 입력에 대해 무작위 분포를 생성해야합니다.

그런 다음 모든 개체를 변경할 수 없거나 GetHashcode 메서드는 변경할 수없는 개체의 속성을 기반으로해야합니다. 예를 들어이 클래스 (순진한 구현)가 있다고 가정하십시오.

public class SomeThing
{
      public string Name {get; set;}

      public override GetHashCode()
      {
          return Name.GetHashcode();
      }

      public override Equals(object other)
      {
           SomeThing = other as Something;
           if( other == null ) return false;
           return this.Name == other.Name;
      }
}

이 구현은 이미 MSDN에서 찾을 수있는 규칙을 위반합니다. 이 클래스의 인스턴스가 2 개 있다고 가정합니다. instance1의 Name 속성은 'Pol'로 설정되고 instance2의 Name 속성은 'Piet'으로 설정됩니다. 두 인스턴스 모두 다른 해시 코드를 반환하며 동일하지 않습니다. 이제 instance2의 이름을 'Pol'로 변경 한 다음 Equals 메서드에 따라 두 인스턴스가 동일해야하며 MSDN 규칙 중 하나에 따라 동일한 해시 코드를 반환해야합니다.
그러나 instance2의 해시 코드가 변경되고 MSDN에서는이를 허용하지 않으므로이 작업을 수행 할 수 없습니다.

그런 다음 엔터티가있는 경우 해시 코드를 구현하여 해당 엔터티의 '기본 식별자'를 사용하도록 할 수 있습니다. 이는 대리 키 또는 불변 속성입니다. 값 개체가 있으면 해당 값 개체의 '속성'을 사용하도록 해시 코드를 구현할 수 있습니다. 이러한 속성은 값 개체의 '정의'를 구성합니다. 이것은 물론 가치 객체의 본질이다. 당신은 그것의 정체성에 관심이 아니라 오히려 가치에 관심이 있습니다.
따라서 가치 객체는 변경할 수 없어야합니다. (.NET 프레임 워크에있는 것처럼 문자열, 날짜 등은 모두 변경할 수없는 개체입니다).

명심해야 할 또 다른 사항 :
'GetHashCode'가 상수 값을 반환해야하는 '세션'(실제로 이것을 어떻게 호출해야하는지 모르겠다) 중에. 애플리케이션을 열고 DB (엔티티)에서 객체의 인스턴스를로드 한 다음 해시 코드를 가져 오십시오. 특정 숫자를 반환합니다. 애플리케이션을 닫고 동일한 엔티티를로드하십시오. 이번에는 해시 코드가 엔티티를 처음로드했을 때와 같은 값을 가져야합니까? IMHO가 아닙니다.


1
예를 들어 Jeff Yates는 해시 코드를 변경 가능한 데이터에 기반을 둘 수 없다고 말합니다. 해시 코드가 해당 객체의 변경 가능한 값을 기반으로하는 경우 사전에 변경 가능한 객체를 붙일 수 없으며 제대로 작동 할 것으로 예상 할 수 있습니다.
오우거 시편 33

3
MSDN 규칙이 어디에서 위반되는지 확인할 수 없습니다. 규칙은 다음과 같이 명확하게 말합니다. 객체의 Equals 메서드의 반환 값을 결정하는 객체 상태를 수정하지 않는 한 객체의 GetHashCode 메서드는 동일한 해시 코드를 일관되게 반환해야합니다 . 당신이 폴에 인스턴스 2의 이름을 변경할 때 인스턴스 2의 해시 코드를 변경하도록 허용되는이 수단
chikak

8

이것은 좋은 조언입니다. 이 문제에 대해 브라이언 페핀이 말한 내용은 다음과 같습니다.

이로 인해 두 번 이상 트립되었습니다. GetHashCode가 인스턴스 수명 전체에서 항상 동일한 값을 반환하는지 확인하십시오. 해시 코드는 대부분의 해시 테이블 구현에서 "버킷"을 식별하는 데 사용됩니다. 객체의 "버킷"이 변경되면 해시 테이블이 객체를 찾지 못할 수 있습니다. 이러한 버그는 찾기가 매우 어려울 수 있으므로 처음부터 올바르게 찾으십시오.


나는 그것을 투표하지 않았지만, 전체 문제를 다루지 않는 인용문이기 때문에 다른 사람들이 그렇게했다고 생각합니다. 척 문자열은 변경 가능했지만 해시 코드는 변경하지 않았습니다. "bob"을 만들어 해시 테이블에서 키로 사용한 다음 값을 "phil"으로 변경합니다. 그런 다음 새 문자열 "phil"을 만듭니다. "phil"키가있는 해시 테이블 항목을 찾으면 원래 넣은 항목을 찾을 수 없습니다. 누군가 "bob"을 검색하면 발견되지만 더 이상 정확하지 않은 값을 얻을 수 있습니다. 변경 가능한 키를 사용하지 않도록주의하거나 위험을 알고 있어야합니다.
Eric Tuttleman

@EricTuttleman : 나는 프레임 워크에 대한 규칙을 작성되었고, 나는 객체의 쌍에 대한 것을 지정한 것 X하고 Y, 한 번 X.Equals(Y)또는 Y.Equals(X)호출 된 이후의 모든 호출이 같은 결과를 산출한다. 평등의 다른 정의를 사용하려면을 사용하십시오 EqualityComparer<T>.
supercat

5

질문에 직접 대답하지는 않지만 Resharper를 사용하는 경우 합리적인 GetHashCode 구현 및 Equals 메서드를 생성하는 기능이 있다는 것을 잊지 마십시오. 물론 해시 코드를 계산할 때 클래스의 멤버를 고려해야 할 사항을 지정할 수 있습니다.


고마워, 실제로 나는 Resharper를 사용하지 않았지만 계속 자주 언급되는 것을 보았으므로 시도해야합니다.
Joan Venge

+1 Resharper가 있으면 멋진 GetHashCode 구현을 생성합니다.
ΩmegaMan

5

Marc Brooks의이 블로그 게시물을 확인하십시오.

VTO, RTO 및 GetHashCode ()-오 마이!

그런 다음 후속 게시물을 확인하십시오 (새로 링크 할 수는 없지만 initlal 기사에 링크가 있습니다).

이것은 GetHashCode () 구현을 만들기 위해 알아야 할 모든 것입니다. 그는 다른 유틸리티와 함께 ​​금메달로 메소드 다운로드를 제공합니다.


4

해시 코드는 절대 변경되지 않지만 해시 코드의 출처를 이해하는 것도 중요합니다.

객체가 값 의미론을 사용하는 경우, 즉 객체의 ID는 해당 값 (예 : 문자열, 색상, 모든 구조체)으로 정의됩니다. 객체의 ID가 모든 값과 독립적 인 경우 해시 코드는 해당 값의 하위 집합으로 식별됩니다. 예를 들어, StackOverflow 항목은 데이터베이스에 저장되어 있습니다. 이름이나 이메일을 변경하면 일부 값이 변경되었지만 고객 항목은 동일하게 유지됩니다 (일반적으로 일부 긴 고객 ID 번호로 식별 됨).

간단히 말해 :

값 유형 시맨틱-해시 코드는 값으로 정의 됨 참조 유형 시맨틱-해시 코드는 일부 ID로 정의 됨

Eric Evans의 Domain Driven Design을 읽어보십시오. 여전히 이해가 안된다면 엔터티 대 값 유형 (위에서 시도한 것보다 다소 적음)으로 이동합니다.


이것은 실제로 올바르지 않습니다. 해시 코드는 특정 인스턴스에 대해 일정하게 유지되어야합니다. 값 유형의 경우 각 값이 고유 한 인스턴스 인 경우가 많으므로 해시는 변경되는 것처럼 보이지만 실제로는 새 인스턴스입니다.
Jeff Yates

맞습니다. 값 유형은 변경할 수 없으므로 변경이 불가능합니다. 잘 잡았습니다.
DavidN

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