.net - 사용 - 재정의 된 System.Object.GetHashCode에 대한 최상의 알고리즘은 무엇입니까?




jpa hashcode (12)

.NET System.Object.GetHashCode 메서드는 .NET 기반 클래스 라이브러리에서 많은 곳에서 사용됩니다. 특히 컬렉션에서 항목을 빠르게 찾거나 평등을 결정할 때. 성능을 저하시키지 않도록 표준 사용자 정의 클래스에 대한 GetHashCode 재정의를 구현하는 방법에 대한 표준 알고리즘 / 모범 사례가 있습니까?


익명 유형

Microsoft는 이미 훌륭한 일반 HashCode 생성기를 제공합니다. 속성 / 필드 값을 익명 형식으로 복사하고 해시합니다.

new { PropA, PropB, PropC, PropD }.GetHashCode();

이 기능은 원하는 수만큼 사용할 수 있습니다. 그것은 권투를 사용하지 않습니다. 익명 형식의 프레임 워크에서 이미 구현 된 알고리즘을 사용합니다.

ValueTuple - C # 7 업데이트

@cactuaroid가 주석에서 언급했듯이 값 튜플을 사용할 수 있습니다. 이렇게하면 몇 번의 키 스트로크가 저장되고 더 중요한 것은 순전히 스택에서만 실행됩니다 (Garbage 없음).

(PropA, PropB, PropC, PropD).GetHashCode();

(참고 : 익명 형식을 사용하는 원래 기술은 힙에 개체 (예 : 가비지)를 만드는 것처럼 보이지만 익명 형식은 클래스로 구현되므로 컴파일러에서 최적화 할 수 있습니다. 이러한 옵션을 벤치마킹하는 것은 흥미로울 것입니다. 튜플 옵션이 우수해야합니다.)


Equals ()가 여러 필드를 비교하는 대부분의 경우 GetHash () 해시가 한 필드 또는 여러 필드에서 해시되는지 여부는 중요하지 않습니다. 해시 계산이 정말 저렴하고 ( 할당 없음 , 제발) 빠르고 ( 무거운 계산이 없고 확실히 데이터베이스 연결이 없음) 반드시 올바른 배포를 제공하는지 확인해야합니다.

무거운 리프트는 Equals () 메서드의 일부 여야합니다. 해시는 Equals ()를 가능한 한 적은 수의 항목에서 호출 할 수 있도록하는 매우 저렴한 작업이어야합니다.

마지막 팁 : GetHashCode ()를 여러 응용 프로그램 실행에서 안정적으로 사용하지 마십시오 . 많은 .Net 유형은 다시 시작한 후 해시 코드가 동일하게 유지되도록 보장하지 않으므로 메모리 데이터 구조에 GetHashCode () 값만 사용해야합니다.


나는 보통 Josh Bloch의 멋진 Effective Java 에서 제공되는 구현과 같은 것을 사용한다. 그것은 빠르며 충돌을 일으키지 않는 꽤 좋은 해시를 만듭니다. 두 개의 다른 소수 (예 : 17과 23)를 선택하고 다음을 수행하십시오.

public override int GetHashCode()
{
    unchecked // Overflow is fine, just wrap
    {
        int hash = 17;
        // Suitable nullity checks etc, of course :)
        hash = hash * 23 + field1.GetHashCode();
        hash = hash * 23 + field2.GetHashCode();
        hash = hash * 23 + field3.GetHashCode();
        return hash;
    }
}

주석에서 언급했듯이 큰 소수를 선택하여 대신 곱하면 좋습니다. 외관상으로는 486187739 좋은데 ... 작은 숫자로 본 대부분의 예제가 소수를 사용하는 경향이 있지만 적어도 소수가 아닌 숫자가 자주 사용되는 유사한 알고리즘이 있습니다. 예를 들어 FNV 예에서 나중에 잘 작동하는 숫자를 사용했지만 초기 값은 소수가 아닙니다. (곱셈 상수 소수입니다. 나는 그것이 얼마나 중요한지는 잘 모릅니다.)

이것은 두 가지 주요 이유로 해시 코드를 XOR 하는 일반적인 방법보다 낫습니다. 우리가 두 개의 int 필드를 가진 타입을 가지고 있다고 가정 해보자 :

XorHash(x, x) == XorHash(y, y) == 0 for all x, y
XorHash(x, y) == XorHash(y, x) for all x, y

그런데 이전 알고리즘은 익명 형식에 대해 C # 컴파일러에서 현재 사용되는 알고리즘입니다.

이 페이지 는 몇 가지 옵션을 제공합니다. 나는 대부분의 경우 위의 내용이 "충분 함"이라고 생각하며 기억하기 쉽고 옳은 것으로 간주합니다. FNV 대안은 비슷하게 간단하지만 ADD 대신 다른 상수와 XOR 을 결합 연산으로 사용합니다. 아래 코드와 비슷하지만 일반적인 FNV 알고리즘은 개별 바이트에서 작동하므로 32 비트 해시 값 대신 바이트 당 하나의 반복을 수행하도록 수정해야합니다. FNV는 가변 길이의 데이터를 위해 설계된 반면, 여기에서 우리가 사용하는 방식은 항상 같은 수의 필드 값을 사용합니다. 이 답변에 대한 의견은 위의 코드가 위의 추가 접근법으로 실제로 작동하지 않는다는 것을 보여줍니다 (테스트 된 샘플 사례에서).

// Note: Not quite FNV!
public override int GetHashCode()
{
    unchecked // Overflow is fine, just wrap
    {
        int hash = (int) 2166136261;
        // Suitable nullity checks etc, of course :)
        hash = (hash * 16777619) ^ field1.GetHashCode();
        hash = (hash * 16777619) ^ field2.GetHashCode();
        hash = (hash * 16777619) ^ field3.GetHashCode();
        return hash;
    }
}

한 가지주의해야 할 점은 해시 코드에 따라 콜렉션에 추가 한 후에 동등한 (따라서 해시 코드에 민감한) 상태가 변경되지 않는 것이 이상적입니다.

documentation :

변경할 수없는 참조 형식에 대해 GetHashCode를 재정의 할 수 있습니다. 일반적으로 가변 참조 유형의 경우 다음 경우에만 GetHashCode를 재정의해야합니다.

  • 변경할 수없는 필드의 해시 코드를 계산할 수 있습니다. 또는
  • 객체가 해시 코드에 의존하는 컬렉션에 포함되어있는 동안 변경 가능한 객체의 해시 코드가 변경되지 않도록 할 수 있습니다.

여기에 내 해시 코드 도우미가있다.
장점은 일반 유형 인수를 사용하므로 복싱이 발생하지 않는다는 것입니다.

public static class HashHelper
{
    public static int GetHashCode<T1, T2>(T1 arg1, T2 arg2)
    {
         unchecked
         {
             return 31 * arg1.GetHashCode() + arg2.GetHashCode();
         }
    }

    public static int GetHashCode<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3)
    {
        unchecked
        {
            int hash = arg1.GetHashCode();
            hash = 31 * hash + arg2.GetHashCode();
            return 31 * hash + arg3.GetHashCode();
        }
    }

    public static int GetHashCode<T1, T2, T3, T4>(T1 arg1, T2 arg2, T3 arg3, 
        T4 arg4)
    {
        unchecked
        {
            int hash = arg1.GetHashCode();
            hash = 31 * hash + arg2.GetHashCode();
            hash = 31 * hash + arg3.GetHashCode();
            return 31 * hash + arg4.GetHashCode();
        }
    }

    public static int GetHashCode<T>(T[] list)
    {
        unchecked
        {
            int hash = 0;
            foreach (var item in list)
            {
                hash = 31 * hash + item.GetHashCode();
            }
            return hash;
        }
    }

    public static int GetHashCode<T>(IEnumerable<T> list)
    {
        unchecked
        {
            int hash = 0;
            foreach (var item in list)
            {
                hash = 31 * hash + item.GetHashCode();
            }
            return hash;
        }
    }

    /// <summary>
    /// Gets a hashcode for a collection for that the order of items 
    /// does not matter.
    /// So {1, 2, 3} and {3, 2, 1} will get same hash code.
    /// </summary>
    public static int GetHashCodeForOrderNoMatterCollection<T>(
        IEnumerable<T> list)
    {
        unchecked
        {
            int hash = 0;
            int count = 0;
            foreach (var item in list)
            {
                hash += item.GetHashCode();
                count++;
            }
            return 31 * hash + count.GetHashCode();
        }
    }

    /// <summary>
    /// Alternative way to get a hashcode is to use a fluent 
    /// interface like this:<br />
    /// return 0.CombineHashCode(field1).CombineHashCode(field2).
    ///     CombineHashCode(field3);
    /// </summary>
    public static int CombineHashCode<T>(this int hashCode, T arg)
    {
        unchecked
        {
            return 31 * hashCode + arg.GetHashCode();   
        }
    }

또한 유창한 인터페이스를 제공하는 확장 메서드가 있으므로 다음과 같이 사용할 수 있습니다.

public override int GetHashCode()
{
    return HashHelper.GetHashCode(Manufacturer, PartN, Quantity);
}

또는 이와 같이 :

public override int GetHashCode()
{
    return 0.CombineHashCode(Manufacturer)
        .CombineHashCode(PartN)
        .CombineHashCode(Quantity);
}

최근까지 내 대답은 Jon Skeet과 매우 가까운 것이 었습니다. 그러나, 나는 최근에 내부 테이블의 크기가 8, 16, 32 등의 해시 테이블 인 2의 거듭 제곱 해시 테이블을 사용하는 프로젝트를 시작했습니다. 프라임 번호 크기를 선호하는 좋은 이유가 있습니다. 두 가지 크기의 파워에도 장점이 있습니다.

그리고 그것은 꽤 빨려 들었습니다. 그래서 약간의 실험과 연구 끝에 다음과 같이 해시를 다시 해싱하기 시작했습니다.

public static int ReHash(int source)
{
  unchecked
  {
    ulong c = 0xDEADBEEFDEADBEEF + (ulong)source;
    ulong d = 0xE2ADBEEFDEADBEEF ^ c;
    ulong a = d += c = c << 15 | c >> -15;
    ulong b = a += d = d << 52 | d >> -52;
    c ^= b += a = a << 26 | a >> -26;
    d ^= c += b = b << 51 | b >> -51;
    a ^= d += c = c << 28 | c >> -28;
    b ^= a += d = d << 9 | d >> -9;
    c ^= b += a = a << 47 | a >> -47;
    d ^= c += b << 54 | b >> -54;
    a ^= d += c << 32 | c >> 32;
    a += d << 25 | d >> -25;
    return (int)(a >> 1);
  }
}

그리고 나서 제 2의 힘의 해시 테이블은 더 이상 빨지 않았습니다.

위의 내용이 작동해서는 안되기 때문에 이것은 나를 혼란스럽게 만들었습니다. 또는 더 정확히 말하면 원래 GetHashCode() 가 매우 특별한 방식으로 가난하지 않으면 작동하지 않습니다.

해시 코드를 재 혼합하는 것은 큰 해시 코드를 향상시킬 수 없습니다. 가능한 유일한 효과는 더 많은 충돌을 도입하기 때문입니다.

해시 코드를 재 혼합하는 것은 끔찍한 해시 코드를 개선 할 수 없습니다. 가능한 유일한 효과는 값 53의 많은 수의 충돌과 값 18,3487,291의 많은 수의 변경 때문입니다.

해시 코드를 재 믹싱하면 해시 코드를 향상시킬 수 있지만 해시 코드를 개선하여 해 당 범위 (2 32 개의 가능한 값)에서 절대적인 충돌을 피할 수 있지만 해시 테이블에서 실제로 사용하기 위해 모듈화 할 때 충돌을 피할 수는 없습니다. 더 간단한 2 승수 테이블의 모듈러스가 더 명확 해졌지만 더 일반적인 소수 테이블에도 부정적인 영향을 미쳤지 만 재연의 추가 작업이 이점보다 큽니다. 그러나 이익은 여전히있을 것입니다.)

편집 : 나는 또한 오픈 - 어드레싱을 사용하고 있었고 충돌에 대한 민감성을 증가 시켰을 것입니다. 아마도 이것은 2의 거듭 제곱이었을 것입니다.

그리고 .NETstring.GetHashCode() 구현 (또는 here 공부)은이 방법 (충돌이 적어 20-30 배 빠른 실행 순서로)을 향상시킬 수있었습니다. 내 자신의 해시 코드가 많이 향상 될 수 있습니다 (그 이상).

과거에 코딩 한 GetHashCode () 구현체는 실제로이 사이트의 답변의 기초로 사용되었지만, 내가 생각했던 것보다 훨씬 나빴습니다 . 대부분의 경우에 "사용하기에 충분했다"는 것이었지만 나는 더 나은 것을 원했다.

그래서 저는이 프로젝트를 한 편으로 만들었습니다. (어쨌든 애완 동물 프로젝트였습니다.) .NET에서 좋고 잘 분산 된 해시 코드를 신속하게 생성하는 방법을 살펴보기 시작했습니다.

결국 SpookyHash 를 .NET으로 이식하는 SpookyHash 했습니다. 실제로 위의 코드는 SpookyHash를 사용하여 32 비트 입력에서 32 비트 출력을 생성하는 고속 경로 버전입니다.

이제 SpookyHash는 코드를 기억하기가 쉽지 않습니다. 그것의 나의 항구는 나가 더 나은 속도 *를 위해 그것의 많은 것을 hand-inlined 때문에 더 적은이다 *. 그러나 이것이 바로 코드 재사용을위한 것입니다.

그런 다음 원래 프로젝트가 더 나은 해시 코드를 생성하는 방법에 대한 질문을 생성 했으므로이 프로젝트를 한쪽에 넣으므로 프로젝트에서 .NET memcpy를 더 잘 생성하는 방법에 대한 질문이 생겼습니다.

그런 다음 다시 돌아와서 모든 네이티브 형식 ( decimal † 제외)을 해시 코드로 쉽게 전달할 수 있도록 많은 오버로드를 생성했습니다.

빠른 속도입니다. 원래 코드를 포팅 한 속도가 빠르기 때문에 특히 알고리즘이 ‡에 최적화 된 64 비트 시스템에서 밥 젠킨스 (Bob Jenkins)가 대부분의 신용을받을 자격이 있습니다.

전체 코드는 https://bitbucket.org/JonHanna/spookilysharp/src 에서 볼 수 있지만 위의 코드는 단순화 된 버전이라고 생각하십시오.

그러나 이제는 이미 작성 되었기 때문에 더 쉽게 사용할 수 있습니다.

public override int GetHashCode()
{
  var hash = new SpookyHash();
  hash.Update(field1);
  hash.Update(field2);
  hash.Update(field3);
  return hash.Final().GetHashCode();
}

또한 시드 값을 사용하기 때문에 신뢰할 수없는 입력을 처리해야하고 해시 DoS 공격으로부터 보호하려면 가동 시간 또는 유사 시간을 기반으로 시드를 설정하고 공격자가 예측할 수없는 결과를 낼 수 있습니다.

private static long hashSeed0 = Environment.TickCount;
private static long hashSeed1 = DateTime.Now.Ticks;
public override int GetHashCode()
{
  //produce different hashes ever time this application is restarted
  //but remain consistent in each run, so attackers have a harder time
  //DoSing the hash tables.
  var hash = new SpookyHash(hashSeed0, hashSeed1);
  hash.Update(field1);
  hash.Update(field2);
  hash.Update(field3);
  return hash.Final().GetHashCode();
}

*이 점에서 큰 놀라움은 반환 된 (x << n) | (x >> -n) 회전 메소드를 직접 인라인하는 것입니다. (x << n) | (x >> -n) 개선 된 것들. 나는 지터가 나를 위해 그것을 인라인 시켰을 것이라고 확신했을 것이다. 그러나 프로파일 링은 그렇지 않다는 것을 보여 주었다.

decimal 은 .NET에서 원천이 아니지만 C #에서 온 것입니다. 그것의 문제는 자신의 GetHashCode() 가 자신의 Equals() 가 중요하지 않은 것처럼 정밀도를 중요하게 취급한다는 것입니다. 둘다 유효한 선택이지만 그렇게 혼합되지는 않습니다. 자신의 버전을 구현할 때 하나 또는 다른 버전을 선택해야하지만 원하는 버전을 알 수는 없습니다.

• 비교 방법. 문자열에 사용하면 SpookyHash는 64 비트에서 string.GetHashCode() 보다 상당히 빠릅니다. 32 비트에서는 string.GetHashCode() 보다 약간 빠르며 64 비트에서 약간 빨라집니다. 32 비트에서 SpookyHash보다 상당히 빠르지 만 여전히 빠릅니다 합리적인 선택이 될만큼 충분히.


ReSharper 사용자는 GetHashCode, Equals 및 기타를 생성 할 수 있습니다 ReSharper -> Edit -> Generate Code -> Equality Members.

// ReSharper's GetHashCode looks like this
public override int GetHashCode() {
    unchecked {
        int hashCode = Id;
        hashCode = (hashCode * 397) ^ IntMember;
        hashCode = (hashCode * 397) ^ OtherIntMember;
        hashCode = (hashCode * 397) ^ (RefMember != null ? RefMember.GetHashCode() : 0);
        // ...
        return hashCode;
    }
}

Jon Skeet이 위에 게시 한 알고리즘 의 또 다른 유창한 구현이지만 할당이나 복싱 작업은 포함되지 않습니다.

public static class Hash
{
    public const int Base = 17;

    public static int HashObject(this int hash, object obj)
    {
        unchecked { return hash * 23 + (obj == null ? 0 : obj.GetHashCode()); }
    }

    public static int HashValue<T>(this int hash, T value)
        where T : struct
    {
        unchecked { return hash * 23 + value.GetHashCode(); }
    }
}

용법:

public class MyType<T>
{
    public string Name { get; set; }

    public string Description { get; set; }

    public int Value { get; set; }

    public IEnumerable<T> Children { get; set; }

    public override int GetHashCode()
    {
        return Hash.Base
            .HashObject(this.Name)
            .HashObject(this.Description)
            .HashValue(this.Value)
            .HashObject(this.Children);
    }
}

컴파일러는 제네릭 형식 제약 때문에 HashValue 가 클래스와 함께 호출되지 않도록합니다.그러나 HashObjectgeneric 인수를 추가하면 boxing 작업이 추가되기 때문에 컴파일러 지원은 없습니다 .


우리가 8 개 이상의 속성을 가지고 있다면 (희망 사항) 여기에 또 다른 대안이 있습니다.

ValueTuple구조체이며 견고한 GetHashCode구현 이있는 것 같습니다.

즉, 우리는 단순히 이것을 할 수 있음을 의미합니다 :

// Yay, no allocations and no custom implementations!
public override int GetHashCode() => (this.PropA, this.PropB).GetHashCode();

의를위한 .NET 코어의 현재 구현에 대해 살펴 보자 ValueTuple'들 GetHashCode.

이것은 출신입니다 ValueTuple:

    internal static int CombineHashCodes(int h1, int h2)
    {
        return HashHelpers.Combine(HashHelpers.Combine(HashHelpers.RandomSeed, h1), h2);
    }

    internal static int CombineHashCodes(int h1, int h2, int h3)
    {
        return HashHelpers.Combine(CombineHashCodes(h1, h2), h3);
    }

그리고 이것은 HashHelper:

    public static readonly int RandomSeed = Guid.NewGuid().GetHashCode();

    public static int Combine(int h1, int h2)
    {
        unchecked
        {
            // RyuJIT optimizes this to use the ROL instruction
            // Related GitHub pull request: dotnet/coreclr#1830
            uint rol5 = ((uint)h1 << 5) | ((uint)h1 >> 27);
            return ((int)rol5 + h1) ^ h2;
        }
    }

영어로:

  • h1을 5 위치만큼 왼쪽으로 돌립니다 (순환 이동).
  • 결과와 h1을 함께 더하십시오.
  • 결과를 h2로 XOR하십시오.
  • 먼저 {static random seed, h1}에서 위의 연산을 수행합니다.
  • 이후의 각 항목에 대해 이전 결과 및 다음 항목 (예 : h2)에 대한 작업을 수행하십시오.

이 ROL-5 해시 코드 알고리즘의 속성에 대해 더 알고 있으면 좋을 것입니다.

유감스럽게도 ValueTuple우리 자신 을 위해 연기하는 것은 우리 GetHashCode가 바라는만큼 빠르지 않을 수도 있습니다. 관련 토론 에서이 의견 은 직접적인 호출 HashHelpers.Combine이 더 효과적 이라는 것을 보여줍니다. 뒤집어 보면, 그 코드는 내부 코드이므로 코드를 복사해야합니다. 여기에서 얻은 많은 부분을 희생해야합니다. 또한, 우리는 처음 Combine에 랜덤 시드 를 기억할 책임이 있습니다 . 우리가 그 단계를 건너 뛰면 결과가 무엇인지 나는 모른다.


위의 대답으로 선택한 구현을 사용하여 부동 소수점 및 소수 자릿수 문제가 발생했습니다.

이 테스트는 실패합니다 (2 개의 값을 음수로 바꿨을지라도 해시는 동일합니다).

        var obj1 = new { A = 100m, B = 100m, C = 100m, D = 100m};
        var obj2 = new { A = 100m, B = 100m, C = -100m, D = -100m};
        var hash1 = ComputeHash(obj1.A, obj1.B, obj1.C, obj1.D);
        var hash2 = ComputeHash(obj2.A, obj2.B, obj2.C, obj2.D);
        Assert.IsFalse(hash1 == hash2, string.Format("Hashcode values should be different   hash1:{0}  hash2:{1}",hash1,hash2));

하지만이 테스트는 다음과 같이 전달됩니다 (int 포함).

        var obj1 = new { A = 100m, B = 100m, C = 100, D = 100};
        var obj2 = new { A = 100m, B = 100m, C = -100, D = -100};
        var hash1 = ComputeHash(obj1.A, obj1.B, obj1.C, obj1.D);
        var hash2 = ComputeHash(obj2.A, obj2.B, obj2.C, obj2.D);
        Assert.IsFalse(hash1 == hash2, string.Format("Hashcode values should be different   hash1:{0}  hash2:{1}",hash1,hash2));

프리미티브 유형에 GetHashCode를 사용하지 않기 위해 구현을 변경했으며 더 나은 것으로 보입니다.

    private static int InternalComputeHash(params object[] obj)
    {
        unchecked
        {
            var result = (int)SEED_VALUE_PRIME;
            for (uint i = 0; i < obj.Length; i++)
            {
                var currval = result;
                var nextval = DetermineNextValue(obj[i]);
                result = (result * MULTIPLIER_VALUE_PRIME) + nextval;

            }
            return result;
        }
    }



    private static int DetermineNextValue(object value)
    {
        unchecked
        {

                int hashCode;
                if (value is short
                    || value is int
                    || value is byte
                    || value is sbyte
                    || value is uint
                    || value is ushort
                    || value is ulong
                    || value is long
                    || value is float
                    || value is double
                    || value is decimal)
                {
                    return Convert.ToInt32(value);
                }
                else
                {
                    return value != null ? value.GetHashCode() : 0;
                }
        }
    }

해싱의 여러 가지 방법에 대한 Microsoft의 주도 ...

//for classes that contain a single int value
return this.value;

//for classes that contain multiple int value
return x ^ y;

//for classes that contain single number bigger than int    
return ((int)value ^ (int)(value >> 32)); 

//for classes that contain class instance fields which inherit from object
return obj1.GetHashCode();

//for classes that contain multiple class instance fields which inherit from object
return obj1.GetHashCode() ^ obj2.GetHashCode() ^ obj3.GetHashCode(); 

나는 당신이 이것을 사용할 수있는 다수의 큰 int를 위해 그것을 추측 할 수있다 :

int a=((int)value1 ^ (int)(value1 >> 32));
int b=((int)value2 ^ (int)(value2 >> 32));
int c=((int)value3 ^ (int)(value3 >> 32));
return a ^ b ^ c;

그리고 여러 종류의 같은 : 모두가 먼저 변환 int사용 GetHashCode()INT 값을 XOR 연산과 결과가 당신의 해시됩니다 다음.

ID로 해시를 사용하는 사용자 (고유 값)는 해시가 자연수로 제한되어 있으므로 해시 알고리즘에 대해 5 바이트라고 생각합니다. 최소 MD5.

여러 값을 해시 값으로 변환 할 수 있으며 그 중 일부는 동일하므로 식별자로 사용하지 마십시오. (어쩌면 언젠가 나는 당신의 구성 요소를 사용할 것입니다)


여기 내 단순한 접근 방식입니다. 나는 이것을 위해 고전적인 건축 양식을 사용하고있다. typesafe (boxing / unboxing 없음)이며 .NET 2.0 (확장 메서드 등 없음)과 호환됩니다.

이것은 다음과 같이 사용됩니다 :

public override int GetHashCode()
{
    HashBuilder b = new HashBuilder();
    b.AddItems(this.member1, this.member2, this.member3);
    return b.Result;
} 

그리고 여기 acutal 빌더 클래스가 있습니다 :

internal class HashBuilder
{
    private const int Prime1 = 17;
    private const int Prime2 = 23;
    private int result = Prime1;

    public HashBuilder()
    {
    }

    public HashBuilder(int startHash)
    {
        this.result = startHash;
    }

    public int Result
    {
        get
        {
            return this.result;
        }
    }

    public void AddItem<T>(T item)
    {
        unchecked
        {
            this.result = this.result * Prime2 + item.GetHashCode();
        }
    }

    public void AddItems<T1, T2>(T1 item1, T2 item2)
    {
        this.AddItem(item1);
        this.AddItem(item2);
    }

    public void AddItems<T1, T2, T3>(T1 item1, T2 item2, T3 item3)
    {
        this.AddItem(item1);
        this.AddItem(item2);
        this.AddItem(item3);
    }

    public void AddItems<T1, T2, T3, T4>(T1 item1, T2 item2, T3 item3, 
        T4 item4)
    {
        this.AddItem(item1);
        this.AddItem(item2);
        this.AddItem(item3);
        this.AddItem(item4);
    }

    public void AddItems<T1, T2, T3, T4, T5>(T1 item1, T2 item2, T3 item3, 
        T4 item4, T5 item5)
    {
        this.AddItem(item1);
        this.AddItem(item2);
        this.AddItem(item3);
        this.AddItem(item4);
        this.AddItem(item5);
    }        

    public void AddItems<T>(params T[] items)
    {
        foreach (T item in items)
        {
            this.AddItem(item);
        }
    }
}

현재 https://github.com/dotnet/coreclr/pull/14863 , 슈퍼 간단 그 해시 코드를 생성하는 새로운 방법이있다! 그냥 써

public override int GetHashCode()
    => HashCode.Combine(field1, field2, field3);

이것은 구현 세부 사항에 대해 걱정할 필요없이 품질 해시 코드를 생성합니다.





gethashcode