etc./StackOverFlow

Equals 메서드가 재정의될 때 GetHashCode를 재정의하는 것이 중요한 이유는 무엇입니까?

청렴결백한 만능 재주꾼 2022. 2. 7. 06:19
반응형

질문자 :David Basarab


다음 클래스가 주어졌을 때

 public class Foo { public int FooId { get; set; } public string FooName { get; set; } public override bool Equals(object obj) { Foo fooItem = obj as Foo; if (fooItem == null) { return false; } return fooItem.FooId == this.FooId; } public override int GetHashCode() { // Which is preferred? return base.GetHashCode(); //return this.FooId.GetHashCode(); } }

Foo Foo 테이블의 행을 나타내기 Equals 메서드를 재정의했습니다. GetHashCode 를 재정의하는 데 선호되는 방법은 무엇입니까?

GetHashCode 를 재정의하는 것이 중요한 이유는 무엇입니까?



HashSet<T> 등의 키로 사용되는지 여부가 중요합니다. 이는 항목을 버킷으로 그룹화하기 위해 ( IEqualityComparer<T> 가 없는 경우) 사용되기 때문입니다. 두 항목에 대한 해시 코드가 일치하지 않으면 결코 동일한 것으로 간주 되지 않을 수 있습니다( Equals 는 단순히 호출되지 않음).

GetHashCode() Equals 논리를 반영해야 합니다. 규칙은 다음과 같습니다.

  • 두 항목이 같으면( Equals(...) == true ) GetHashCode() 대해 동일한 값을 반환 해야 합니다.
  • GetHashCode() 가 같으면 같을 필요 는 없습니다. 이것은 충돌이며 Equals 가 실제 평등인지 여부를 확인하기 위해 호출됩니다.

이 경우 " return FooId; "가 적절한 GetHashCode() 구현처럼 보입니다. 여러 속성을 테스트하는 경우 대각선 충돌을 줄이기 위해 아래와 같은 코드를 사용하여 속성을 결합하는 것이 일반적입니다(즉, new Foo(3,5) new Foo(5,3) 과 다른 해시 코드를 갖도록)

 unchecked // only needed if you're compiling with arithmetic checks enabled { // (the default compiler behaviour is *disabled*, so most folks won't need this) int hash = 13; hash = (hash * 7) + field1.GetHashCode(); hash = (hash * 7) + field2.GetHashCode(); ... return hash; }

EqualsGetHashCode 재정의할 때 ==!= 연산자를 제공하는 것도 고려할 수 있습니다.


이것을 틀리면 어떻게 되는지에 대한 데모가 여기 있습니다 .


Marc Gravell

Marc가 이미 언급한 규칙 외에도 해시 코드는 개체의 수명 동안 변경되지 않아야 하기 때문에 GetHashCode() 올바르게 구현하는 것은 실제로 매우 어렵습니다. 따라서 해시 코드를 계산하는 데 사용되는 필드는 변경 불가능해야 합니다.

나는 NHibernate로 작업할 때 마침내 이 문제에 대한 해결책을 찾았습니다. 내 접근 방식은 개체의 ID에서 해시 코드를 계산하는 것입니다. ID는 생성자를 통해서만 설정할 수 있으므로 ID를 변경하려는 경우(매우 가능성이 낮음) 새 ID와 새 해시 코드가 있는 새 개체를 만들어야 합니다. 이 접근 방식은 ID를 무작위로 생성하는 매개 변수가 없는 생성자를 제공할 수 있기 때문에 GUID에서 가장 잘 작동합니다.


Albic

Equals를 재정의함으로써 기본적으로 주어진 유형의 두 인스턴스를 비교하는 방법을 더 잘 알고 있는 사람이 자신이라고 말하므로 최상의 해시 코드를 제공할 수 있는 가장 좋은 후보가 될 가능성이 높습니다.

다음은 ReSharper가 GetHashCode() 함수를 작성하는 방법의 예입니다.

 public override int GetHashCode() { unchecked { var result = 0; result = (result * 397) ^ m_someVar1; result = (result * 397) ^ m_someVar2; result = (result * 397) ^ m_someVar3; result = (result * 397) ^ m_someVar4; return result; } }

보시다시피 클래스의 모든 필드를 기반으로 좋은 해시 코드를 추측하려고 시도하지만 개체의 도메인 또는 값 범위를 알고 있기 때문에 여전히 더 나은 것을 제공할 수 있습니다.


Trap

Equals() 재정의할 때 null 에 대해 obj 매개변수를 확인하는 것을 잊지 마십시오. 또한 유형을 비교하십시오.

 public override bool Equals(object obj) { Foo fooItem = obj as Foo; if (fooItem == null) { return false; } return fooItem.FooId == this.FooId; }

그 이유는 다음과 같습니다. Equals null 과 비교할 때 false 를 반환해야 합니다. http://msdn.microsoft.com/en-us/library/bsc2ak47.aspx 도 참조하십시오.


huha

어때요:

 public override int GetHashCode() { return string.Format("{0}_{1}_{2}", prop1, prop2, prop3).GetHashCode(); }

성능이 문제가 아니라고 가정합니다 :)


Ludmil Tinkov

위의 답변을 추가하기 만하면됩니다.

Equals를 재정의하지 않으면 기본 동작은 개체의 참조가 비교되는 것입니다. 해시코드에도 동일하게 적용됩니다. 기본 구현은 일반적으로 참조의 메모리 주소를 기반으로 합니다. Equals를 재정의했기 때문에 올바른 동작은 참조가 아닌 Equals에서 구현한 항목을 비교하는 것이므로 해시코드에 대해서도 동일한 작업을 수행해야 합니다.

클래스의 클라이언트는 해시 코드가 equals 메서드와 유사한 논리를 가질 것으로 예상합니다. 예를 들어 IEqualityComparer를 사용하는 linq 메서드는 먼저 해시 코드를 비교하고 동일한 경우에만 더 비쌀 수 있는 Equals() 메서드를 비교합니다. 실행하려면 해시 코드를 구현하지 않은 경우 equals()는 다른 해시 코드를 가질 것이며(메모리 주소가 다르기 때문에) 같지 않은 것으로 잘못 결정됩니다(Equals()는 적중되지 않음).

또한 사전에서 사용하는 경우 개체를 찾을 수 없다는 문제를 제외하고(하나의 해시 코드에 의해 삽입되었고 찾을 때 기본 해시 코드가 다를 수 있고 다시 Equals() Marc Gravell이 그의 답변에서 설명하는 것처럼 호출되지도 않습니다. 또한 동일한 키를 허용하지 않아야 하는 사전 또는 해시 집합 개념 위반을 도입합니다. Equals를 재정의할 때 해당 객체가 본질적으로 동일하다고 이미 선언했기 때문에 고유 키가 있다고 가정하는 데이터 구조에서 둘 다 다른 키로 원하지 않지만 해시 코드가 다르기 때문에 "동일한" 키가 다른 것으로 삽입됩니다.


BornToCode

우리는 대처해야 할 두 가지 문제가 있습니다.

  1. 개체의 필드가 변경될 수 있는 경우 GetHashCode() 제공할 수 없습니다. GetHashCode() 에 의존하는 컬렉션에서 절대 사용되지 않습니다. GetHashCode() 를 구현하는 비용은 종종 가치가 없거나 불가능합니다.

  2. GetHashCode() 를 호출하는 컬렉션에 개체를 넣고 GetHashCode() 가 올바른 방식으로 작동하지 않고 Equals() 를 재정의했다면 그 사람은 문제를 추적하는 데 며칠을 보낼 수 있습니다.

따라서 기본적으로 나는 합니다.

 public class Foo { public int FooId { get; set; } public string FooName { get; set; } public override bool Equals(object obj) { Foo fooItem = obj as Foo; if (fooItem == null) { return false; } return fooItem.FooId == this.FooId; } public override int GetHashCode() { // Some comment to explain if there is a real problem with providing GetHashCode() // or if I just don't see a need for it for the given class throw new Exception("Sorry I don't know what GetHashCode should do for this class"); } }

Ian Ringrose

프레임워크에서는 동일한 두 개체가 동일한 해시코드를 가져야 한다고 요구하기 때문입니다. equals 메소드를 재정의하여 두 객체의 특수 비교를 수행하고 두 객체가 메소드에서 동일한 것으로 간주되면 두 객체의 해시 코드도 동일해야 합니다. (사전과 해시테이블은 이 원칙에 의존합니다).


kemiller2002

.NET 4.7 GetHashCode() 를 재정의하는 기본 방법은 아래와 같습니다. 이전 .NET 버전을 대상으로 하는 경우 System.ValueTuple nuget 패키지를 포함합니다.

 // C# 7.0+ public override int GetHashCode() => (FooId, FooName).GetHashCode();

성능 면에서 이 방법은 대부분의 복합 해시 코드 구현보다 성능이 뛰어납니다. ValueTuplestruct 이므로 쓰레기가 없으며 기본 알고리즘은 최대한 빠릅니다.


l33t

해시 코드는 Dictionary, Hashtable, HashSet 등과 같은 해시 기반 컬렉션에 사용됩니다. 이 코드의 목적은 특정 개체를 특정 그룹(버킷)에 넣어 매우 빠르게 사전 정렬하는 것입니다. 이 사전 정렬은 코드가 포함된 모든 객체가 아니라 하나의 버킷에서만 객체를 검색해야 하기 때문에 해시 컬렉션에서 객체를 다시 검색해야 할 때 이 객체를 찾는 데 크게 도움이 됩니다. 해시 코드의 더 나은 배포(더 나은 고유성) 더 빠른 검색. 각 객체에 고유한 해시 코드가 있는 이상적인 상황에서 이를 찾는 것은 O(1) 작업입니다. 대부분의 경우 O(1)에 접근합니다.


Maciej

반드시 중요한 것은 아닙니다. 컬렉션의 크기와 성능 요구 사항, 그리고 성능 요구 사항을 모를 수도 있는 라이브러리에서 클래스를 사용할지 여부에 따라 다릅니다. 나는 종종 내 컬렉션 크기가 그다지 크지 않으며 내 시간이 완벽한 해시 코드를 생성하여 얻은 몇 마이크로초의 성능보다 더 가치 있다는 것을 알고 있습니다. 그래서 (컴파일러의 성가신 경고를 없애기 위해) 간단히 다음을 사용합니다.

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

(물론 #pragma를 사용하여 경고를 끌 수도 있지만 이 방법을 선호합니다.)

물론 여기에서 다른 사람들이 언급한 모든 문제보다 성능 필요한 위치에 있을 때 적용됩니다. 가장 중요한 것 - 그렇지 않으면 해시 세트 또는 사전에서 항목을 검색할 때 잘못된 결과를 얻을 수 있습니다. 해시 코드는 개체의 수명에 따라 변하지 않아야 합니다 (더 정확하게는 해시 코드가 필요할 때와 같이 a key in a dictionary): 예를 들어, 다음은 Value가 public이므로 잘못된 것이므로 인스턴스의 수명 동안 클래스 외부에서 변경될 수 있으므로 이를 해시 코드의 기초로 사용해서는 안 됩니다.

 class A { public int Value; public override int GetHashCode() { return Value.GetHashCode(); //WRONG! Value is not constant during the instance's life time } }

반면에 값을 변경할 수 없으면 다음을 사용해도 됩니다.

 class A { public readonly int Value; public override int GetHashCode() { return Value.GetHashCode(); //OK Value is read-only and can't be changed during the instance's life time } }

ILoveFortran

Equals()에 정의된 대로 두 객체가 같으면 동일한 해시 코드를 반환해야 한다는 것을 항상 보장해야 합니다. 다른 의견 중 일부가 말하듯이, 이론적으로 객체가 HashSet 또는 Dictionary와 같은 해시 기반 컨테이너에서 사용되지 않는 경우 이는 필수 사항이 아닙니다. 그래도 항상 이 규칙을 따르라고 조언합니다. 그 이유는 단순히 누군가가 실제로 성능을 향상시키거나 더 나은 방식으로 코드 의미를 전달하려는 좋은 의도로 컬렉션을 한 유형에서 다른 유형으로 변경하는 것이 너무 쉽기 때문입니다.

예를 들어 목록에 일부 개체를 보관한다고 가정합니다. 언젠가 누군가는 예를 들어 더 나은 검색 특성 때문에 HashSet이 훨씬 더 나은 대안이라는 것을 실제로 깨닫습니다. 이것은 우리가 곤경에 빠질 수 있는 때입니다. List는 내부적으로 HashSet이 GetHashCode()를 사용하는 동안 귀하의 경우 Equals를 의미하는 유형에 대한 기본 같음 비교자를 사용합니다. 두 가지가 다르게 동작하면 프로그램도 마찬가지입니다. 그리고 그러한 문제는 해결하기가 가장 쉽지 않다는 점을 명심하십시오.

추가 예제와 설명을 찾을 수 있는 블로그 게시물 에서 다른 GetHashCode() 함정과 함께 이 동작을 요약했습니다.


Vasil Kosturski

C# 9 (.net 5 또는 .net core 3.1)부터 Value Based Equality 처럼 레코드 를 사용할 수 있습니다.


Community Wiki

원래 GetHashCode()는 개체의 메모리 주소를 반환하므로 두 개의 서로 다른 개체를 비교하려면 이를 재정의하는 것이 중요합니다.

편집됨: 그것은 정확하지 않습니다. 원래 GetHashCode() 메서드는 2개의 값의 동일성을 보장할 수 없습니다. 동일한 객체는 동일한 해시 코드를 반환합니다.


user2855602

아래에서 리플렉션을 사용하는 것이 공용 속성을 고려하는 더 나은 옵션인 것 같습니다(일반적인 시나리오는 아니지만). 속성 추가/제거에 대해 걱정할 필요가 없습니다. 이것은 또한 더 나은 성능을 발견했습니다.(Diagonistics 스톱워치를 사용한 비교 시간).

 public int getHashCode() { PropertyInfo[] theProperties = this.GetType().GetProperties(); int hash = 31; foreach (PropertyInfo info in theProperties) { if (info != null) { var value = info.GetValue(this,null); if(value != null) unchecked { hash = 29 * hash ^ value.GetHashCode(); } } } return hash; }

Guanxi

출처 : http:www.stackoverflow.com/questions/371328/why-is-it-important-to-override-gethashcode-when-equals-method-is-overridden

반응형