[.net] GetHashCode를 재정의하는 가장 좋은 알고리즘은 무엇입니까?

.NET에서 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는 훌륭합니다 … 그리고 작은 숫자로 본 대부분의 예제는 소수를 사용하는 경향이 있지만 비 프라임 숫자가 자주 사용되는 알고리즘은 적어도 유사합니다. 예를 들어, Fquit 가 아닌 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의 대안 마찬가지로 간단하지만, 다른 정수 및 사용 XOR대신을 ADD합성 동작한다. 그것은 보이는 뭔가 아래 코드 등이 있지만, 대신 32 비트 해시 값마다의, 바이트 당 하나의 반복을 수행하기 위해 수정 필요하므로 일반 FNV 알고리즘은, 개별 바이트에서 작동합니다. 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;
    }
}

알고 있어야 할 것은 해시 코드에 의존하는 컬렉션에 동등성에 민감한 (따라서 해시 코드에 민감한) 상태가 바뀌지 않도록하는 것이 이상적입니다.

당으로 문서 :

변경 불가능한 참조 유형에 대해 GetHashCode를 대체 할 수 있습니다. 일반적으로 변경 가능한 참조 유형의 경우 다음과 같은 경우에만 GetHashCode를 대체해야합니다.

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

답변

익명 유형

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

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

이것은 여러 속성에 적용됩니다. 권투를 사용하지 않습니다. 익명 형식의 프레임 워크에서 이미 구현 된 알고리즘 만 사용합니다.

ValueTuple-C # 7 업데이트

주석에서 @cactuaroid가 언급했듯이 값 튜플을 사용할 수 있습니다. 이렇게하면 몇 번의 키 입력이 절약되고 더 중요하게는 스택에서 순수하게 실행됩니다 (쓰레기 없음).

(PropA, PropB, PropC, PropD).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);
}


답변

이 목적으로 사용하는 도우미 라이브러리에 해싱 클래스가 있습니다.

/// <summary> 
/// This is a simple hashing function from Robert Sedgwicks Hashing in C book.
/// Also, some simple optimizations to the algorithm in order to speed up
/// its hashing process have been added. from: www.partow.net
/// </summary>
/// <param name="input">array of objects, parameters combination that you need
/// to get a unique hash code for them</param>
/// <returns>Hash code</returns>
public static int RSHash(params object[] input)
{
    const int b = 378551;
    int a = 63689;
    int hash = 0;

    // If it overflows then just wrap around
    unchecked
    {
        for (int i = 0; i < input.Length; i++)
        {
            if (input[i] != null)
            {
                hash = hash * a + input[i].GetHashCode();
                a = a * b;
            }
        }
    }

    return hash;
}

그런 다음 간단히 다음과 같이 사용할 수 있습니다.

public override int GetHashCode()
{
    return Hashing.RSHash(_field1, _field2, _field3);
}

성능을 평가하지 않았으므로 모든 의견을 환영합니다.


답변

Jon Skeet의 구현을 사용하는 도우미 클래스는 다음과 같습니다 .

public static class HashCode
{
    public const int Start = 17;

    public static int Hash<T>(this int hash, T obj)
    {
        var h = EqualityComparer<T>.Default.GetHashCode(obj);
        return unchecked((hash * 31) + h);
    }
}

용법:

public override int GetHashCode()
{
    return HashCode.Start
        .Hash(_field1)
        .Hash(_field2)
        .Hash(_field3);
}

System.Int32에 대한 확장 메서드를 작성하지 않으려는 경우 :

public readonly struct HashCode
{
    private readonly int _value;

    public HashCode(int value) => _value = value;

    public static HashCode Start { get; } = new HashCode(17);

    public static implicit operator int(HashCode hash) => hash._value;

    public HashCode Hash<T>(T obj)
    {
        var h = EqualityComparer<T>.Default.GetHashCode(obj);
        return unchecked(new HashCode((_value * 31) + h));
    }

    public override int GetHashCode() => _value;
}

여전히 힙 할당을 피하고 정확히 동일한 방식으로 사용됩니다.

public override int GetHashCode()
{
    // This time `HashCode.Start` is not an `Int32`, it's a `HashCode` instance.
    // And the result is implicitly converted to `Int32`.
    return HashCode.Start
        .Hash(_field1)
        .Hash(_field2)
        .Hash(_field3);
}

편집 (2018 년 5 월) : EqualityComparer<T>.Defaultgetter는 이제 JIT 고유 기능 입니다. 이 블로그 게시물 에서 Stephen Toub가 풀 요청 을 언급합니다 .


답변

.NET 표준 2.1 이상

.NET Standard 2.1 이상을 사용하는 경우 System.HashCode 구조체를 사용할 수 있습니다 . 그것을 사용하는 두 가지 방법이 있습니다 :

HashCode.Combine

Combine메소드는 최대 8 개의 오브젝트가 제공되는 해시 코드를 작성하는 데 사용될 수 있습니다.

public override int GetHashCode() => HashCode.Combine(this.object1, this.object2);

해시 코드

Add방법을 사용하면 컬렉션을 처리하는 데 도움이됩니다.

public override int GetHashCode()
{
    var hashCode = new HashCode();
    hashCode.Add(this.object1);
    foreach (var item in this.collection)
    {
        hashCode.Add(item);
    }
    return hashCode.ToHashCode();
}

손쉬운 GetHashCode

자세한 내용과 의견 은 전체 블로그 게시물 ‘ GetHashCode Made Easy ‘를 참조하십시오.

사용 예

public class SuperHero
{
    public int Age { get; set; }
    public string Name { get; set; }
    public List<string> Powers { get; set; }

    public override int GetHashCode() =>
        HashCode.Of(this.Name).And(this.Age).AndEach(this.Powers);
}

이행

public struct HashCode : IEquatable<HashCode>
{
    private const int EmptyCollectionPrimeNumber = 19;
    private readonly int value;

    private HashCode(int value) => this.value = value;

    public static implicit operator int(HashCode hashCode) => hashCode.value;

    public static bool operator ==(HashCode left, HashCode right) => left.Equals(right);

    public static bool operator !=(HashCode left, HashCode right) => !(left == right);

    public static HashCode Of<T>(T item) => new HashCode(GetHashCode(item));

    public static HashCode OfEach<T>(IEnumerable<T> items) =>
        items == null ? new HashCode(0) : new HashCode(GetHashCode(items, 0));

    public HashCode And<T>(T item) =>
        new HashCode(CombineHashCodes(this.value, GetHashCode(item)));

    public HashCode AndEach<T>(IEnumerable<T> items)
    {
        if (items == null)
        {
            return new HashCode(this.value);
        }

        return new HashCode(GetHashCode(items, this.value));
    }

    public bool Equals(HashCode other) => this.value.Equals(other.value);

    public override bool Equals(object obj)
    {
        if (obj is HashCode)
        {
            return this.Equals((HashCode)obj);
        }

        return false;
    }

    public override int GetHashCode() => this.value.GetHashCode();

    private static int CombineHashCodes(int h1, int h2)
    {
        unchecked
        {
            // Code copied from System.Tuple a good way to combine hashes.
            return ((h1 << 5) + h1) ^ h2;
        }
    }

    private static int GetHashCode<T>(T item) => item?.GetHashCode() ?? 0;

    private static int GetHashCode<T>(IEnumerable<T> items, int startHashCode)
    {
        var temp = startHashCode;

        var enumerator = items.GetEnumerator();
        if (enumerator.MoveNext())
        {
            temp = CombineHashCodes(temp, GetHashCode(enumerator.Current));

            while (enumerator.MoveNext())
            {
                temp = CombineHashCodes(temp, GetHashCode(enumerator.Current));
            }
        }
        else
        {
            temp = CombineHashCodes(temp, EmptyCollectionPrimeNumber);
        }

        return temp;
    }
}

좋은 알고리즘은 무엇입니까?

속도

해시 코드를 계산하는 알고리즘은 빨라야합니다. 간단한 알고리즘은 일반적으로 더 빠를 것입니다.

결정론

해싱 알고리즘은 결정 론적 이어야합니다. 즉, 동일한 입력이 주어지면 항상 동일한 출력을 생성해야합니다.

충돌 감소

해시 코드를 계산하는 알고리즘은 해시 충돌 을 최소 로 유지해야합니다 . 해시 충돌은 GetHashCode서로 다른 두 객체에 대한 두 번의 호출이 동일한 해시 코드를 생성 할 때 발생하는 상황입니다 . 충돌은 허용되지만 (일부는 잘못된 개념이 있지만) 최소한으로 유지해야합니다.

좋은 해시 함수는 예상 입력을 출력 범위에서 가능한 한 고르게 매핑해야합니다. 균일해야합니다.

예방의 DoS

.NET Core에서는 응용 프로그램을 다시 시작할 때마다 다른 해시 코드가 나타납니다. 이는 DoS (서비스 거부) 공격을 방지하기위한 보안 기능입니다. .NET Framework의 경우 다음 App.config 파일을 추가하여이 기능을 활성화 해야 합니다.

<?xml version ="1.0"?>
<configuration>
   <runtime>
      <UseRandomizedStringHashAlgorithm enabled="1" />
   </runtime>
</configuration>

이 기능으로 인해 해시 코드는 작성된 응용 프로그램 도메인 외부에서 사용해서는 안되며 컬렉션에서 키 필드로 사용해서는 안되며 지속해서는 안됩니다.

이에 대한 자세한 내용은 여기를 참조 하십시오 .

암호화 적으로 안전한가?

알고리즘은 암호화 해시 함수일 필요는 없습니다 . 다음 조건을 만족할 필요는 없습니다.

  • 주어진 해시 값을 생성하는 메시지를 생성하는 것은 불가능합니다
  • 해시 값이 동일한 두 개의 다른 메시지를 찾는 것은 불가능합니다
  • 메시지를 조금만 변경하면 해시 값이 광범위하게 변경되어 새 해시 값이 이전 해시 값과 관련이없는 것으로 나타납니다 (애벌랜치 효과).

답변

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

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

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