Thuật toán tốt nhất để ghi đè GetHashCode là gì?


1449

Trong .NET, GetHashCodephương thức này được sử dụng ở rất nhiều nơi trong các thư viện lớp cơ sở .NET. Thực hiện nó đúng cách đặc biệt quan trọng để tìm thấy các mục một cách nhanh chóng trong một bộ sưu tập hoặc khi xác định sự bình đẳng.

Có một thuật toán tiêu chuẩn hoặc thực tiễn tốt nhất về cách triển khai GetHashCodecho các lớp tùy chỉnh của tôi để tôi không làm giảm hiệu suất?


38
Sau khi đọc câu hỏi này và bài viết dưới đây, tôi có thể thực hiện ghi đè GetHashCode. Tôi hy vọng nó sẽ hữu ích cho những người khác. Nguyên tắc và quy tắc cho GetHashCode được viết bởi Eric Lippert
rene 22/03 '

4
"Hoặc để xác định sự bình đẳng": không! Hai đối tượng có cùng mã băm không nhất thiết phải bằng nhau.
Thomas Levesque

1
@ThomasLevesque Bạn nói đúng, hai đối tượng có cùng mã băm không nhất thiết phải bằng nhau. Nhưng vẫn GetHashCode()được sử dụng trong rất nhiều triển khai Equals(). Đó là những gì tôi muốn nói với câu nói đó. GetHashCode()bên trong Equals()thường được sử dụng như một phím tắt để xác định bất đẳng thức , bởi vì nếu hai đối tượng có mã băm khác nhau thì chúng phải là các đối tượng không bằng nhau và phần còn lại của kiểm tra đẳng thức không phải thực thi.
bitbonk

3
@bitbonk Thông thường, cả hai GetHashCode()Equals()cần xem xét tất cả các trường của cả hai đối tượng (Bằng phải thực hiện điều này nếu mã băm bằng hoặc không được kiểm tra). Bởi vì điều này, một cuộc gọi đến GetHashCode()bên trong Equals()thường là dư thừa và có thể làm giảm hiệu suất. Equals()cũng có thể ngắn mạch, làm cho nó nhanh hơn nhiều - tuy nhiên trong một số trường hợp, mã băm có thể được lưu vào bộ nhớ cache, làm cho việc GetHashCode()kiểm tra nhanh hơn và rất đáng giá. Xem câu hỏi này để biết thêm.
NotEnoughData

CẬP NHẬT JAN 2020: Blog của Eric Lippert tại: docs.microsoft.com/en-us/archive/bloss/ericlippert/ (
Davin

Câu trả lời:


1604

Tôi thường đi với một cái gì đó giống như việc triển khai được đưa ra trong Java hiệu quả tuyệt vời của Josh Bloch . Nó nhanh và tạo ra một hàm băm khá tốt, không có khả năng gây ra va chạm. Chọn hai số nguyên tố khác nhau, ví dụ 17 và 23 và làm:

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;
    }
}

Như đã lưu ý trong các nhận xét, bạn có thể thấy tốt hơn là chọn một số nguyên tố lớn để nhân lên thay vào đó. Rõ ràng 486187739 là tốt ... và mặc dù hầu hết các ví dụ tôi đã thấy với số lượng nhỏ có xu hướng sử dụng các số nguyên tố, có ít nhất các thuật toán tương tự trong đó các số không phải là số nguyên tố thường được sử dụng. Ví dụ, trong ví dụ không hoàn toàn về FNV , tôi đã sử dụng các số có vẻ hoạt động tốt - nhưng giá trị ban đầu không phải là số nguyên tố. (Hằng số nhân số nguyên tố. Tôi không biết điều đó quan trọng đến mức nào.)

Điều này tốt hơn so với thực tế phổ biến của mã XORbăm ing vì hai lý do chính. Giả sử chúng ta có một loại có hai inttrường:

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

Nhân tiện, thuật toán trước đó là thuật toán hiện đang được trình biên dịch C # sử dụng cho các loại ẩn danh.

Trang này cung cấp khá nhiều tùy chọn. Tôi nghĩ đối với hầu hết các trường hợp ở trên là "đủ tốt" và nó cực kỳ dễ nhớ và đúng. Sự thay thế FNV tương tự đơn giản, nhưng sử dụng các hằng số khác nhau và XORthay vì ADDnhư một hoạt động kết hợp. Nó trông giống một cái gì đó giống như mã dưới đây, nhưng các thuật toán FNV bình thường hoạt động trên byte cá nhân, vì vậy đây sẽ yêu cầu sửa đổi để thực hiện một sự lặp lại mỗi byte, thay vì mỗi giá trị hash 32-bit. FNV cũng được thiết kế cho độ dài dữ liệu thay đổi, trong khi cách chúng tôi sử dụng ở đây luôn có cùng số lượng giá trị trường. Nhận xét về câu trả lời này cho thấy rằng mã ở đây không thực sự hoạt động tốt (trong trường hợp mẫu được thử nghiệm) như cách tiếp cận bổ sung ở trên.

// 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;
    }
}

Lưu ý rằng một điều cần lưu ý là lý tưởng nhất là bạn nên ngăn trạng thái nhạy cảm bình đẳng (và do đó nhạy cảm với mã băm) thay đổi sau khi thêm nó vào bộ sưu tập phụ thuộc vào mã băm.

Theo tài liệu :

Bạn có thể ghi đè GetHashCode cho các loại tham chiếu không thay đổi. Nói chung, đối với các loại tham chiếu có thể thay đổi, bạn chỉ nên ghi đè GetHashCode nếu:

  • Bạn có thể tính mã băm từ các trường không thể thay đổi; hoặc là
  • Bạn có thể đảm bảo rằng mã băm của một đối tượng có thể thay đổi không thay đổi trong khi đối tượng được chứa trong một bộ sưu tập dựa trên mã băm của nó.

8
Thuật toán được mô tả trong cuốn sách mà bạn đề cập không hoàn toàn chi tiết hơn một chút, nó đặc biệt mô tả những gì cần làm cho các loại dữ liệu khác nhau của các trường. Ví dụ: đối với các trường loại sử dụng dài (int) (trường ^ f >>> 32) thay vì chỉ gọi GetHashcode. Là lâu.GetHashCodes thực hiện theo cách đó?
bitbonk

13
Yup, Int64.GetHashCode thực hiện chính xác điều đó. Trong Java sẽ yêu cầu quyền anh, tất nhiên. Điều đó nhắc nhở tôi - thời gian để thêm một liên kết đến cuốn sách ...
Jon Skeet

77
23 không phải là lựa chọn tốt, vì (kể từ .net 3.5 SP1) Dictionary<TKey,TValue>giả định các số nguyên tố modulo phân phối tốt. Và 23 là một trong số đó. Vì vậy, nếu bạn có một từ điển với Công suất 23 thì chỉ có đóng góp cuối cùng GetHashCodeảnh hưởng đến mã băm tổng hợp. Vì vậy, tôi muốn sử dụng 29 thay vì 23.
CodeInChaos

23
@CodeInChaos: Chỉ đóng góp cuối cùng ảnh hưởng đến nhóm - vì vậy, tệ nhất, có thể phải xem qua tất cả 23 mục trong từ điển. Nó vẫn sẽ kiểm tra mã băm thực tế của mỗi mục, sẽ rẻ. Nếu bạn có một cuốn từ điển nhỏ, nó không có vấn đề gì nhiều.
Jon Skeet

20
@Vajda: Tôi thường sử dụng 0 làm mã băm hiệu quả cho null- không giống như bỏ qua trường.
Jon Skeet

431

Loại ẩn danh

Microsoft đã cung cấp một trình tạo HashCode chung tốt: Chỉ cần sao chép các giá trị thuộc tính / trường của bạn sang một loại ẩn danh và băm nó:

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

Điều này sẽ làm việc cho bất kỳ số lượng tài sản. Nó không sử dụng quyền anh. Nó chỉ sử dụng thuật toán đã được triển khai trong khung cho các loại ẩn danh.

ValueTuple - Cập nhật cho C # 7

Như @cactuaroid đề cập trong các bình luận, một bộ giá trị có thể được sử dụng. Điều này giúp tiết kiệm một vài tổ hợp phím và quan trọng hơn là thực hiện hoàn toàn trên ngăn xếp (không có Rác):

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

(Lưu ý: Kỹ thuật ban đầu sử dụng các loại ẩn danh dường như tạo ra một đối tượng trên heap, tức là rác, vì các loại ẩn danh được triển khai dưới dạng các lớp, mặc dù điều này có thể được tối ưu hóa bởi trình biên dịch. tùy chọn tuple nên vượt trội.)


85
Đúng, GetHashCodeviệc triển khai ẩn danh rất hiệu quả (BTW giống như câu trả lời của Jon Skeet), nhưng vấn đề duy nhất với giải pháp này là bạn tạo ra một trường hợp mới trong bất kỳ GetHashCodecuộc gọi nào . Nó có thể là một chút chi phí cụ thể trong trường hợp truy cập mạnh vào các bộ sưu tập băm lớn ...
digEmAll

5
@digEmAll Điểm hay, tôi đã không nghĩ về chi phí tạo ra một đối tượng mới. Câu trả lời của Jon Skeet là hiệu quả nhất và sẽ không sử dụng quyền anh. (@Kumba Để giải quyết vấn đề không được kiểm tra trong VB, chỉ cần sử dụng Int64 (dài) và cắt bớt nó sau khi tính toán.)
Rick Love

42
chỉ có thể nói new { PropA, PropB, PropC, PropD }.GetHashCode()quá
sehe

17
VB.NET phải sử dụng Khóa trong tạo kiểu ẩn danh: New With {Key PropA}.GetHashCode()Nếu không, GetHashCode sẽ không trả lại cùng một mã băm cho các đối tượng khác nhau có cùng thuộc tính 'nhận dạng'.
David Ostern

4
@Keith trong trường hợp đó, tôi sẽ xem xét việc lưu IEnumerable dưới dạng giá trị danh sách ở đâu đó thay vì liệt kê nó mỗi khi mã băm được tính. Caclulation ToList mỗi lần bên trong GetHashCode có thể ảnh hưởng đến hiệu suất trong nhiều tình huống.
Rick Yêu

105

Đây là người trợ giúp mã băm của tôi.
Ưu điểm của nó là nó sử dụng các đối số kiểu chung và do đó sẽ không gây ra quyền anh:

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();   
        }
    }

Ngoài ra, nó có phương thức mở rộng để cung cấp một giao diện trôi chảy, vì vậy bạn có thể sử dụng nó như thế này:

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

hoặc như thế này:

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

5
Không cần T[]riêng biệt như đã cóIEnumerable<T>
nawfal

5
Bạn có thể cấu trúc lại các phương thức đó và hạn chế logic cốt lõi thành một hàm
nawfal

12
Ngẫu nhiên, 31 là một sự thay đổi và trừ trên CPU, cực kỳ nhanh.
Chui Tey

4
@nightcoder bạn có thể sử dụng params .
ANeves

6
@ChuiTey Đây là điểm chung của Mersenne Primes .
Pharap

63

Tôi có một lớp Hashing trong thư viện Helper mà tôi sử dụng nó cho mục đích này.

/// <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;
}

Sau đó, đơn giản là bạn có thể sử dụng nó như:

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

Tôi đã không đánh giá hiệu suất của nó, vì vậy bất kỳ thông tin phản hồi đều được hoan nghênh.


26
Vâng, nó sẽ gây ra quyền anh, nếu các lĩnh vực là loại giá trị.
nightcoder

5
"Có thể được tăng cường sau này bằng cách bắt OverflowException" Toàn bộ quan điểm của việc uncheckednày là để tránh các ngoại lệ đối với tràn tràn mong muốn GetHashCode. Vì vậy, nó không chính xác nếu giá trị tràn ra intvà nó hoàn toàn không gây hại.
Tim Schmelter

1
Một vấn đề với thuật toán này là bất kỳ mảng nào có giá trị null sẽ luôn trả về 0, bất kể độ dài của nó
Nathan Adams

2
Phương pháp trợ giúp này cũng phân bổ một đối tượng mới []
James Newton-King

1
Như @NathanAdams đề cập, việc nullbỏ qua hoàn toàn có thể mang lại cho bạn kết quả bất ngờ. Thay vì bỏ qua chúng, bạn chỉ nên sử dụng một số giá trị không đổi thay vì input[i].GetHashCode()khi nào input[i]là null.
David Schwartz

58

Đây là lớp người trợ giúp của tôi sử dụng triển khai của 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);
    }
}

Sử dụng:

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

Nếu bạn muốn tránh viết phương thức mở rộng cho 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;
}

Nó vẫn tránh mọi phân bổ heap và được sử dụng chính xác theo cùng một cách:

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);
}

Chỉnh sửa (tháng 5 năm 2018): EqualityComparer<T>.Defaultgetter hiện là nội tại của JIT - yêu cầu kéo được đề cập bởi Stephen Toub trong bài đăng trên blog này .


1
Tôi sẽ thay đổi dòng với nhà điều hành đại học thành:var h = Equals(obj, default(T)) ? 0 : obj.GetHashCode();
Bill Barry

Tôi tin rằng toán tử ternary với obj != nullsẽ biên dịch thành một boxlệnh sẽ phân bổ bộ nhớ nếu Tlà một loại giá trị. Thay vào đó, bạn có thể sử dụng obj.Equals(null)nó sẽ biên dịch thành một cuộc gọi ảo của Equalsphương thức.
Martin Liversage

Bởi vì this.hashCode != h. Nó sẽ không trả lại cùng một giá trị.
Şafak Gür

Xin lỗi, quản lý để xóa bình luận của tôi thay vì chỉnh sửa nó. Có ích hơn khi tạo một cấu trúc mới sau đó thay đổi hashCode thành không chỉ đọc và thực hiện: "unchecked {this.hashCode ^ = h * 397;} trả lại cái này;" ví dụ?
Erik Karlsson

Bất biến có lợi ích của nó ( Tại sao các cấu trúc đột biến xấu xa? ). Về hiệu suất, những gì tôi làm là khá rẻ vì nó không phân bổ bất kỳ không gian nào trong heap.
Şafak Gür

30

.NET Standard 2.1 trở lên

Nếu bạn đang sử dụng .NET Standard 2.1 trở lên, bạn có thể sử dụng cấu trúc System.HashCode . Có hai phương pháp sử dụng nó:

HashCode.Combine

Các Combinephương pháp có thể được sử dụng để tạo một mã băm, từ bỏ đến tám đối tượng.

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

HashCode. Thêm

Các Addphương pháp giúp bạn đối phó với các bộ sưu tập:

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 thực hiện dễ dàng

Bạn có thể đọc toàn bộ bài đăng trên blog ' GetHashCode Made Easy ' để biết thêm chi tiết và nhận xét.

Ví dụ sử dụng

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);
}

Thực hiện

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;
    }
}

Điều gì tạo nên một thuật toán tốt?

Tốc độ

Thuật toán tính toán mã băm cần phải nhanh. Một thuật toán đơn giản thường sẽ là một thuật toán nhanh hơn.

Quyết đoán

Thuật toán băm cần phải có tính xác định tức là được cung cấp cùng một đầu vào, nó phải luôn tạo ra cùng một đầu ra.

Giảm va chạm

Thuật toán tính toán mã băm cần giữ các va chạm băm đến mức tối thiểu. Xung đột băm là tình huống xảy ra khi hai lệnh gọi đến GetHashCodehai đối tượng khác nhau tạo ra mã băm giống hệt nhau. Lưu ý rằng va chạm được cho phép (một số có quan niệm sai lầm rằng chúng không phải) nhưng chúng nên được giữ ở mức tối thiểu.

Hàm băm tốt sẽ ánh xạ các đầu vào dự kiến ​​càng đều càng tốt trên phạm vi đầu ra của nó. Nó nên có sự đồng nhất.

DoS

Trong .NET Core mỗi lần bạn khởi động lại một ứng dụng, bạn sẽ nhận được các mã băm khác nhau. Đây là một tính năng bảo mật để ngăn chặn các cuộc tấn công từ chối dịch vụ (DoS). Đối với .NET Framework, bạn nên kích hoạt tính năng này bằng cách thêm tệp App.config sau:

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

Do tính năng này, mã băm không bao giờ được sử dụng bên ngoài miền ứng dụng mà chúng được tạo, chúng không bao giờ được sử dụng làm trường chính trong bộ sưu tập và chúng không bao giờ được tồn tại.

Tìm hiểu thêm về điều này ở đây .

Mật mã an toàn?

Thuật toán không phải là hàm băm mật mã . Có nghĩa là nó không phải đáp ứng các điều kiện sau:

  • Không thể tạo ra một thông báo mang lại giá trị băm nhất định
  • Không thể tìm thấy hai thông báo khác nhau có cùng giá trị băm
  • Một thay đổi nhỏ đối với một thông báo sẽ thay đổi giá trị băm rộng rãi đến mức giá trị băm mới xuất hiện không tương thích với giá trị băm cũ (hiệu ứng tuyết lở).

29

Trong hầu hết các trường hợp khi Equals () so sánh nhiều trường, điều đó không thực sự quan trọng nếu GetHash () của bạn băm trên một trường hoặc trên nhiều trường. Bạn chỉ cần đảm bảo rằng tính toán hàm băm thực sự rẻ ( Không phân bổ , vui lòng) và nhanh chóng ( Không tính toán nặng và chắc chắn không có kết nối cơ sở dữ liệu) và cung cấp phân phối tốt.

Việc nâng vật nặng phải là một phần của phương thức Equals (); hàm băm phải là một thao tác rất rẻ để cho phép gọi Equals () trên càng ít mục càng tốt.

Và một mẹo cuối cùng: Đừng dựa vào GetHashCode () ổn định qua nhiều lần chạy ứng dụng . Nhiều loại .Net không đảm bảo mã băm của chúng giữ nguyên sau khi khởi động lại, vì vậy bạn chỉ nên sử dụng giá trị của GetHashCode () cho các cấu trúc dữ liệu bộ nhớ.


10
"Trong hầu hết các trường hợp khi Equals () so sánh nhiều trường, điều đó không thực sự quan trọng nếu GetHash () của bạn băm trên một trường hoặc trên nhiều trường." Đây là lời khuyên nguy hiểm, vì đối với các đối tượng chỉ khác nhau trong các trường không băm, bạn sẽ nhận được các va chạm băm. Nếu điều này xảy ra thường xuyên, hiệu suất của các bộ sưu tập dựa trên hàm băm (HashMap, Hashset, v.v.) sẽ giảm (tối đa O (n) trong trường hợp xấu nhất).
sleske

10
Điều này thực sự đã xảy ra trong Java: Trong các phiên bản đầu tiên của JDK String.hashCode () chỉ được coi là phần đầu của chuỗi; điều này dẫn đến các vấn đề về hiệu suất nếu bạn sử dụng Chuỗi làm khóa trong HashMaps chỉ khác nhau ở cuối (điều này phổ biến, ví dụ như đối với URL). Do đó, thuật toán đã được thay đổi (theo JDK 1.2 hoặc 1.3 tôi tin).
sleske

3
Nếu một trường đó 'cung cấp phân phối tốt' (phần cuối câu trả lời của tôi), thì một trường là đủ .. Nếu nó không cung cấp phân phối tốt , thì (và sau đó) bạn cần một phép tính khác. (Ví dụ: chỉ cần sử dụng một lĩnh vực mà không cung cấp một phân phối tốt, hoặc sử dụng nhiều lĩnh vực)
Bert Huijben

Tôi không nghĩ rằng có vấn đề với việc GetHashCodethực hiện phân bổ bộ nhớ, với điều kiện là nó chỉ được sử dụng lần đầu tiên (với các lần gọi tiếp theo chỉ đơn giản là trả về kết quả được lưu trong bộ nhớ cache). Điều quan trọng không phải là người ta phải cố gắng hết sức để tránh va chạm, mà là người ta nên tránh va chạm "có hệ thống". Nếu một loại có hai inttrường oldXnewXthường khác nhau bởi một trường, giá trị băm oldX^newXsẽ gán 90% giá trị băm của các bản ghi đó là 1, 2, 4 hoặc 8. Sử dụng oldX+newX[số học không được kiểm tra] có thể tạo ra nhiều va chạm hơn ...
supercat

1
... Hơn chức năng tinh vi hơn, nhưng một bộ sưu tập 1.000.000 thứ có 500.000 giá trị băm khác nhau sẽ rất tốt nếu mỗi giá trị băm có hai thứ liên quan và rất tệ nếu một giá trị băm có 500.001 thứ và những thứ khác có một giá trị.
supercat

23

Cho đến gần đây, câu trả lời của tôi rất gần với Jon Skeet ở đây. Tuy nhiên, gần đây tôi đã bắt đầu một dự án sử dụng các bảng băm có hai sức mạnh, đó là các bảng băm trong đó kích thước của bảng nội bộ là 8, 16, 32, v.v ... Có một lý do chính đáng để ưu tiên kích thước số nguyên tố, nhưng có là một số lợi thế cho sức mạnh của hai kích thước quá.

Và nó bị hút khá nhiều. Vì vậy, sau một chút thử nghiệm và nghiên cứu, tôi bắt đầu băm lại băm của mình bằng cách sau:

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);
  }
}

Và rồi bảng băm hai sức mạnh của tôi không còn hút nữa.

Điều này làm tôi băn khoăn, vì những điều trên không nên làm việc. Hay chính xác hơn, nó không hoạt động trừ khi bản gốc GetHashCode()kém theo một cách rất riêng.

Trộn lại mã băm không thể cải thiện mã băm tuyệt vời, bởi vì hiệu quả duy nhất có thể là chúng tôi giới thiệu thêm một vài va chạm.

Trộn lại mã băm không thể cải thiện mã băm khủng khiếp, bởi vì hiệu ứng duy nhất có thể là chúng ta thay đổi, ví dụ như một số lượng lớn các va chạm trên giá trị 53 thành một số lượng lớn giá trị 18.3487.291.

Trộn lại mã băm chỉ có thể cải thiện mã băm ít nhất là khá tốt trong việc tránh va chạm tuyệt đối trong phạm vi của nó (2 32 giá trị có thể) nhưng rất tệ trong việc tránh va chạm khi modulo xuống sử dụng thực tế trong bảng băm. Mặc dù mô-đun đơn giản hơn của bảng hai sức mạnh làm cho điều này rõ ràng hơn, nhưng nó cũng có tác động tiêu cực với các bảng số nguyên tố phổ biến hơn, điều đó không rõ ràng (công việc bổ sung trong việc luyện lại sẽ vượt trội hơn lợi ích , nhưng lợi ích vẫn còn đó).

Chỉnh sửa: Tôi cũng đang sử dụng địa chỉ mở, điều này cũng sẽ làm tăng độ nhạy cảm với va chạm, có lẽ nhiều hơn thực tế là nó có sức mạnh hai.

Và tốt, điều đáng lo ngại là string.GetHashCode()việc triển khai .NET (hoặc nghiên cứu ở đây ) có thể được cải thiện theo cách này (theo thứ tự các bài kiểm tra chạy nhanh hơn khoảng 20-30 lần do ít va chạm hơn) và làm phiền nhiều hơn bao nhiêu mã băm của riêng tôi có thể được cải thiện (nhiều hơn thế).

Tất cả các triển khai GetHashCode () mà tôi đã mã hóa trong quá khứ và thực sự được sử dụng làm cơ sở của các câu trả lời trên trang web này, tồi tệ hơn nhiều so với trước đây . Phần lớn thời gian là "đủ tốt" cho phần lớn mục đích sử dụng, nhưng tôi muốn thứ gì đó tốt hơn.

Vì vậy, tôi đặt dự án đó sang một bên (dù sao đó cũng là một dự án thú cưng) và bắt đầu xem xét cách tạo ra mã băm tốt, được phân phối tốt trong .NET một cách nhanh chóng.

Cuối cùng, tôi quyết định chuyển SpookyHash sang .NET. Thật vậy, đoạn mã trên là phiên bản đường dẫn nhanh của việc sử dụng SpookyHash để tạo đầu ra 32 bit từ đầu vào 32 bit.

Bây giờ, SpookyHash không phải là một đoạn mã nhanh để nhớ. Cổng của tôi thậm chí còn ít hơn bởi vì tôi đã nhúng tay rất nhiều để có tốc độ tốt hơn *. Nhưng đó là những gì mã tái sử dụng là dành cho.

Sau đó, tôi đặt dự án đó sang một bên, vì giống như dự án ban đầu đã tạo ra câu hỏi làm thế nào để tạo ra mã băm tốt hơn, để dự án đó tạo ra câu hỏi về cách sản xuất một memcpy .NET tốt hơn.

Sau đó, tôi trở lại và tạo ra rất nhiều tình trạng quá tải để dễ dàng cung cấp tất cả các loại bản địa (trừ decimal†) vào mã băm.

Thật nhanh, vì Bob Jenkins xứng đáng nhận phần lớn tín dụng vì mã gốc mà tôi chuyển đến vẫn nhanh hơn, đặc biệt là trên các máy 64 bit mà thuật toán được tối ưu hóa cho.

Mã đầy đủ có thể được xem tại https://bitbucket.org/JonHanna/spookilysharp/src nhưng xem xét rằng mã ở trên là phiên bản đơn giản hóa của nó.

Tuy nhiên, vì hiện tại nó đã được viết, người ta có thể sử dụng nó dễ dàng hơn:

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

Nó cũng lấy các giá trị hạt giống, vì vậy nếu bạn cần xử lý đầu vào không đáng tin cậy và muốn bảo vệ chống lại các cuộc tấn công Hash DoS, bạn có thể đặt hạt giống dựa trên thời gian hoạt động hoặc tương tự, và làm cho kết quả không thể đoán trước bởi những kẻ tấn công:

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();
}

* Một bất ngờ lớn ở đây là việc đưa ra một phương pháp xoay vòng đã trả lại (x << n) | (x >> -n)những thứ được cải thiện. Tôi đã chắc chắn rằng jitter sẽ đưa ra điều đó cho tôi, nhưng hồ sơ cho thấy khác.

decimallà không có nguồn gốc từ quan điểm NET mặc dù nó là từ C #. Vấn đề với nó là chính nó GetHashCode()coi độ chính xác là quan trọng trong khi chính nó Equals()thì không. Cả hai đều là lựa chọn hợp lệ, nhưng không trộn lẫn như thế. Khi thực hiện phiên bản của riêng bạn, bạn cần chọn thực hiện cái này hoặc cái kia, nhưng tôi không thể biết bạn muốn cái nào.

Bằng cách so sánh. Nếu được sử dụng trên một chuỗi, SpookyHash trên 64 bit nhanh hơn đáng kể so với string.GetHashCode()trên 32 bit, nhanh hơn một chút so với string.GetHashCode()trên 64 bit, nhanh hơn đáng kể so với SpookyHash trên 32 bit, mặc dù vẫn đủ nhanh để là một lựa chọn hợp lý.


Khi kết hợp nhiều giá trị băm thành một, tôi có xu hướng sử dụng longcác giá trị cho kết quả trung gian và sau đó chuyển kết quả cuối cùng xuống một int. Điều đó có vẻ như là một ý tưởng tốt? Mối quan tâm của tôi là người ta sử dụng ví dụ: hash = (hash * 31) + nextField, sau đó các cặp giá trị khớp sẽ chỉ ảnh hưởng đến 27 bit trên của hàm băm. Để phép tính mở rộng thành một longvà gói các thứ vào sẽ giảm thiểu nguy hiểm đó.
supercat

@supercat nó phụ thuộc vào việc phân phối munging cuối cùng của bạn. Thư viện SpookilySharp sẽ đảm bảo rằng bản phân phối tốt, lý tưởng (vì nó sẽ không cần tạo đối tượng) bằng cách chuyển một con trỏ đến một loại có thể xóa được hoặc chuyển một trong số các liệt kê mà nó xử lý trực tiếp, nhưng nếu bạn không có khả năng chịu trách nhiệm dữ liệu hoặc một bảng liệt kê phù hợp, sau đó gọi .Update()với nhiều giá trị theo câu trả lời ở trên sẽ thực hiện thủ thuật.
Jon Hanna

@JonHanna bạn có sẵn sàng chính xác hơn với hành vi có vấn đề bạn gặp phải không? Tôi đang cố gắng triển khai một thư viện làm cho việc thực hiện các đối tượng giá trị trở nên tầm thường ( ValueUtils ) và tôi thích một thử nghiệm chứng minh khả năng nhầm lẫn băm kém trong các hàm băm có sức mạnh của hai.
Eamon Nerbonne

@EamonNerbonne Tôi thực sự không có gì chính xác hơn "thời gian nói chung chậm hơn theo cách đó". Như tôi đã thêm trong một chỉnh sửa, thực tế là tôi đang sử dụng địa chỉ mở có thể quan trọng hơn yếu tố sức mạnh hai. Tôi có kế hoạch thực hiện một số trường hợp thử nghiệm cho một dự án cụ thể trong đó tôi sẽ so sánh một vài cách tiếp cận khác nhau, vì vậy tôi có thể có câu trả lời tốt hơn cho bạn sau đó, mặc dù đó không phải là ưu tiên cao (dự án cá nhân không có nhu cầu cấp bách , vì vậy tôi sẽ nhận được nó khi tôi nhận được nó ...)
Jon Hanna

@JonHanna: yeah Tôi biết lịch trình dự án cá nhân diễn ra như thế nào - chúc may mắn! Trong mọi trường hợp, tôi thấy tôi đã không bình luận tốt về nhận xét cuối cùng: Tôi muốn hỏi về đầu vào có vấn đề và không nhất thiết phải là chi tiết về các vấn đề dẫn đến. Tôi muốn sử dụng nó như một bộ thử nghiệm (hoặc nguồn cảm hứng cho một bộ thử nghiệm). Trong mọi trường hợp - chúc may mắn với dự án thú cưng của bạn :-).
Eamon Nerbonne

13

Đây là một người tốt:

/// <summary>
/// Helper class for generating hash codes suitable 
/// for use in hashing algorithms and data structures like a hash table. 
/// </summary>
public static class HashCodeHelper
{
    private static int GetHashCodeInternal(int key1, int key2)
    {
        unchecked
        {
           var num = 0x7e53a269;
           num = (-1521134295 * num) + key1;
           num += (num << 10);
           num ^= (num >> 6);

           num = ((-1521134295 * num) + key2);
           num += (num << 10);
           num ^= (num >> 6);

           return num;
        }
    }

    /// <summary>
    /// Returns a hash code for the specified objects
    /// </summary>
    /// <param name="arr">An array of objects used for generating the 
    /// hash code.</param>
    /// <returns>
    /// A hash code, suitable for use in hashing algorithms and data 
    /// structures like a hash table. 
    /// </returns>
    public static int GetHashCode(params object[] arr)
    {
        int hash = 0;
        foreach (var item in arr)
            hash = GetHashCodeInternal(hash, item.GetHashCode());
        return hash;
    }

    /// <summary>
    /// Returns a hash code for the specified objects
    /// </summary>
    /// <param name="obj1">The first object.</param>
    /// <param name="obj2">The second object.</param>
    /// <param name="obj3">The third object.</param>
    /// <param name="obj4">The fourth object.</param>
    /// <returns>
    /// A hash code, suitable for use in hashing algorithms and
    /// data structures like a hash table.
    /// </returns>
    public static int GetHashCode<T1, T2, T3, T4>(T1 obj1, T2 obj2, T3 obj3,
        T4 obj4)
    {
        return GetHashCode(obj1, GetHashCode(obj2, obj3, obj4));
    }

    /// <summary>
    /// Returns a hash code for the specified objects
    /// </summary>
    /// <param name="obj1">The first object.</param>
    /// <param name="obj2">The second object.</param>
    /// <param name="obj3">The third object.</param>
    /// <returns>
    /// A hash code, suitable for use in hashing algorithms and data 
    /// structures like a hash table. 
    /// </returns>
    public static int GetHashCode<T1, T2, T3>(T1 obj1, T2 obj2, T3 obj3)
    {
        return GetHashCode(obj1, GetHashCode(obj2, obj3));
    }

    /// <summary>
    /// Returns a hash code for the specified objects
    /// </summary>
    /// <param name="obj1">The first object.</param>
    /// <param name="obj2">The second object.</param>
    /// <returns>
    /// A hash code, suitable for use in hashing algorithms and data 
    /// structures like a hash table. 
    /// </returns>
    public static int GetHashCode<T1, T2>(T1 obj1, T2 obj2)
    {
        return GetHashCodeInternal(obj1.GetHashCode(), obj2.GetHashCode());
    }
}

Và đây là cách sử dụng nó:

private struct Key
{
    private Type _type;
    private string _field;

    public Type Type { get { return _type; } }
    public string Field { get { return _field; } }

    public Key(Type type, string field)
    {
        _type = type;
        _field = field;
    }

    public override int GetHashCode()
    {
        return HashCodeHelper.GetHashCode(_field, _type);
    }

    public override bool Equals(object obj)
    {
        if (!(obj is Key))
            return false;
        var tf = (Key)obj;
        return tf._field.Equals(_field) && tf._type.Equals(_type);
    }
}

1
Chìa khóa được xác định như thế nào? GetHashCode () không lấy bất kỳ tham số nào, vì vậy nó cần gọi hàm này bằng hai Khóa cần được xác định bằng cách nào đó. Xin lỗi, không giải thích thêm điều này chỉ có vẻ thông minh, nhưng không tốt.
Michael Stum

Và tại sao bạn cần quá tải chung? Loại này không quan trọng (và không được sử dụng trong mã của bạn) vì tất cả các đối tượng đều có một GetHashCode()phương thức, vì vậy bạn luôn có thể sử dụng phương thức với paramstham số mảng. Hay tôi đang thiếu một cái gì đó ở đây?
gehho

4
Khi bạn sử dụng đối tượng thay vì thuốc generic, bạn sẽ nhận được quyền anh và phân bổ bộ nhớ, điều mà bạn không muốn trong GetHashCode. Vì vậy, thuốc generic là con đường để đi.
CodeInChaos

1
Các bước dịch chuyển / xor kéo dài ( h += (h << 10); h ^= (h >> 6); h += (h << 3); h ^= (h >> 11); h += (h << 15);có mã số: chúng không phụ thuộc vào bất kỳ đầu vào nào và trông cực kỳ dư thừa đối với tôi.
sehe

1
@Magnus đúng, tôi sẽ xóa nhận xét ban đầu của tôi. Chỉ cần lưu ý một chút rằng điều này có thể không nhanh như một số giải pháp khác ở đây, nhưng như bạn nói không quan trọng. Phân phối rất tuyệt, tốt hơn hầu hết các giải pháp ở đây, vì vậy +1 từ tôi! :)
nawfal

11

Kể từ https://github.com/dotnet/coreclr/pull/14863 , có một cách mới để tạo mã băm siêu đơn giản! Chỉ viết

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

Điều này sẽ tạo ra một mã băm chất lượng mà bạn không phải lo lắng về các chi tiết thực hiện.


Trông giống như một sự bổ sung ngọt ngào ... bất kỳ cách nào để biết phiên bản .NET Core nào sẽ được phát hành?
Dan J

1
@DanJ Thật là một sự trùng hợp vui vẻ, những HashCodethay đổi cho corefx đã được hợp nhất chỉ một vài giờ trước khi nhận xét của bạn :) Loại này dự kiến ​​sẽ được gửi trong .NET Core 2.1.
James Ko

Đó là tuyệt vời - và khá thời gian quay vòng. Nâng cao. :)
Dan J

@DanJ Thậm chí có tin tốt hơn-- nó sẽ có sẵn ngay bây giờ trên các bản dựng hàng đêm của CoreFX được lưu trữ trên nguồn cấp dữ liệu MyGet lõi dotnet.
James Ko

Ngọt ngào - điều đó không giúp tôi trong công việc, vì chúng tôi không hoàn toàn dễ hiểu , nhưng thật tốt khi biết điều đó. Chúc mừng!
Dan J

9

Dưới đây là một triển khai thông thạo thuật toán được đăng bởi Jon Skeet ở trên , nhưng không bao gồm phân bổ hoặc hoạt động đấm bốc:

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(); }
    }
}

Sử dụng:

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);
    }
}

Trình biên dịch sẽ đảm bảo HashValuekhông được gọi với một lớp do ràng buộc kiểu chung. Nhưng không có hỗ trợ trình biên dịch HashObjectvì việc thêm một đối số chung cũng thêm một thao tác đấm bốc.


8

Đây là cách tiếp cận đơn giản của tôi. Tôi đang sử dụng mô hình xây dựng cổ điển cho việc này. Đó là loại an toàn (không có quyền anh / unboxing) và cũng tương thích với .NET 2.0 (không có phương thức mở rộng, v.v.).

Nó được sử dụng như thế này:

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

Và đây là lớp xây dựng chính:

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);
        }
    }
}

bạn có thể tránh việc tạo đối tượng bên trong hàm gethashcode như trong câu trả lời của Mangus. Chỉ cần gọi các hàm băm tĩnh chết tiệt (ai quan tâm đến hàm băm khởi động). Ngoài ra, bạn có thể sử dụng AddItems<T>(params T[] items)phương thức thường xuyên hơn trong lớp người trợ giúp (hơn là gọi AddItem(T)mỗi lần).
nawfal

Và lợi ích nào bạn thấy làm this.result * Prime2 * item.GetHashCode()khi thường xuyên sử dụng là this.result * Prime2 + item.GetHashCode()?
nawfal

Tôi không thể sử dụng AddItems<T>(params T[] items)thường xuyên hơn vì typeof(T1) != typeof(T2)vv
bitbonk

oh vâng tôi đã bỏ lỡ điều đó
nawfal

5

Người dùng ReSharper có thể tạo GetHashCode, Equals và những người khác với 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;
    }
}

4

Nếu chúng ta có không quá 8 thuộc tính (hy vọng), đây là một lựa chọn khác.

ValueTuplelà một cấu trúc và dường như có một GetHashCodethực hiện vững chắc .

Điều đó có nghĩa là chúng ta có thể làm điều này một cách đơn giản:

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

Chúng ta hãy nhìn vào thực hiện NET Core cho ValueTuple's GetHashCode.

Đây là từ 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);
    }

Và đây là từ 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;
        }
    }

Bằng tiếng Anh:

  • Xoay trái (dịch chuyển tròn) h1 bằng 5 vị trí.
  • Cộng kết quả và h1 lại với nhau.
  • XOR kết quả với h2.
  • Bắt đầu bằng cách thực hiện thao tác trên trên {hạt giống ngẫu nhiên tĩnh, h1}.
  • Đối với mỗi mục tiếp theo, thực hiện thao tác trên kết quả trước đó và mục tiếp theo (ví dụ: h2).

Sẽ thật tuyệt khi biết thêm về các thuộc tính của thuật toán mã băm ROL-5 này.

Đáng tiếc, việc trì hoãn ValueTuplecho riêng chúng ta GetHashCodecó thể không nhanh như chúng ta mong muốn. Nhận xét này trong một cuộc thảo luận liên quan minh họa rằng việc gọi trực tiếp HashHelpers.Combinesẽ hiệu quả hơn. Mặt khác, cái đó là nội bộ, vì vậy chúng tôi phải sao chép mã, hy sinh phần lớn những gì chúng tôi đã đạt được ở đây. Ngoài ra, chúng tôi sẽ chịu trách nhiệm ghi nhớ đầu tiên Combinevới hạt giống ngẫu nhiên. Tôi không biết hậu quả là gì nếu chúng ta bỏ qua bước đó.


Giả sử h1 >> 27là 0 để bỏ qua nó, h1 << 5bằng h1 * 32do đó nó giống như h1 * 33 ^ h2. Theo trang này , nó được gọi là "Bernstein đã sửa đổi".
cactuaroid

3

Hầu hết các công việc của tôi được thực hiện với kết nối cơ sở dữ liệu, điều đó có nghĩa là tất cả các lớp của tôi đều có một định danh duy nhất từ ​​cơ sở dữ liệu. Tôi luôn sử dụng ID từ cơ sở dữ liệu để tạo mã băm.

// Unique ID from database
private int _id;

...    
{
  return _id.GetHashCode();
}

Điều đó có nghĩa là nếu bạn có các đối tượng Person và Account và cả hai đều có ID = 1, chúng sẽ có cùng mã băm. Và điều đó không ổn.
pero

15
Trên thực tế các nhận xét trên là không chính xác. Sẽ luôn có khả năng xảy ra xung đột mã băm (mã băm chỉ định vị nhóm, không phải đối tượng riêng lẻ). Vì vậy, việc triển khai như vậy - đối với mã băm chứa các đối tượng hỗn hợp - sẽ dẫn đến rất nhiều xung đột, điều không mong muốn, nhưng sẽ hoàn toàn ổn nếu bạn chỉ có các đối tượng thuộc một loại duy nhất trong hashtables của mình. Ngoài ra, nó không phân phối đồng đều, tuy nhiên việc triển khai cơ sở trên system.object cũng không, vì vậy tôi sẽ không lo lắng về điều đó quá nhiều ...
piers7

2
Mã băm chỉ có thể là id, vì id là một số nguyên. Không cần phải gọi GetHashCode trên một số nguyên (đó là chức năng nhận dạng)
Darrel Lee

2
@DarrelLee nhưng tomo của anh ấy có thể là một Guid. Đó là một thực hành mã hóa tốt để làm _id.GetHashCodenhư ý định là rõ ràng.
nawfal

2
@ 1224 tùy thuộc vào mô hình sử dụng, nó có thể là khủng khiếp vì lý do bạn đưa ra, nhưng nó cũng có thể là tuyệt vời; nếu bạn có một chuỗi các số như vậy không có lỗ, thì bạn đã có một hàm băm hoàn hảo, tốt hơn bất kỳ thuật toán nào có thể tạo ra. Nếu bạn biết đó là trường hợp, bạn thậm chí có thể tin tưởng vào nó và bỏ qua kiểm tra bình đẳng.
Jon Hanna

3

Khá giống với giải pháp của nightcoder ngoại trừ việc dễ dàng tăng số nguyên tố nếu bạn muốn.

Tái bút: Đây là một trong những lần bạn ngậm một chút trong miệng, biết rằng điều này có thể được tái cấu trúc thành một phương thức với 9 mặc định nhưng nó sẽ chậm hơn, vì vậy bạn chỉ cần nhắm mắt lại và cố gắng quên nó đi.

/// <summary>
/// Try not to look at the source code. It works. Just rely on it.
/// </summary>
public static class HashHelper
{
    private const int PrimeOne = 17;
    private const int PrimeTwo = 23;

    public static int GetHashCode<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();
            hash = hash * PrimeTwo + arg6.GetHashCode();
            hash = hash * PrimeTwo + arg7.GetHashCode();
            hash = hash * PrimeTwo + arg8.GetHashCode();
            hash = hash * PrimeTwo + arg9.GetHashCode();
            hash = hash * PrimeTwo + arg10.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4, T5, T6, T7, T8, T9>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();
            hash = hash * PrimeTwo + arg6.GetHashCode();
            hash = hash * PrimeTwo + arg7.GetHashCode();
            hash = hash * PrimeTwo + arg8.GetHashCode();
            hash = hash * PrimeTwo + arg9.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4, T5, T6, T7, T8>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();
            hash = hash * PrimeTwo + arg6.GetHashCode();
            hash = hash * PrimeTwo + arg7.GetHashCode();
            hash = hash * PrimeTwo + arg8.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4, T5, T6, T7>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();
            hash = hash * PrimeTwo + arg6.GetHashCode();
            hash = hash * PrimeTwo + arg7.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4, T5, T6>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();
            hash = hash * PrimeTwo + arg6.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4, T5>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();

            return hash;
        }
    }

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

            return hash;
        }
    }

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

            return hash;
        }
    }

    public static int GetHashCode<T1, T2>(T1 arg1, T2 arg2)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();

            return hash;
        }
    }
}

2
Không xử lý null.
JJS

1

Tôi gặp phải một vấn đề với số float và số thập phân bằng cách sử dụng triển khai được chọn làm câu trả lời ở trên.

Thử nghiệm này không thành công (float; hash là như nhau mặc dù tôi đã chuyển 2 giá trị thành âm):

        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));

Nhưng bài kiểm tra này đã vượt qua (với ints):

        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));

Tôi đã thay đổi triển khai của mình để không sử dụng GetHashCode cho các kiểu nguyên thủy và có vẻ như nó hoạt động tốt hơn

    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;
                }
        }
    }

1
Trong trường hợp bạn dự định khác uncheckedkhông ảnh hưởng đến Convert.ToInt32: uint, long, float, doubledecimalđều có thể tràn vào đây.
Đánh dấu

1

Microsoft dẫn đầu cho một số cách băm ...

//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(); 

Tôi có thể đoán rằng đối với nhiều int lớn, bạn có thể sử dụng điều này:

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;

Và tương tự đối với nhiều loại: tất cả được chuyển đổi đầu tiên sang intsử dụng GetHashCode() sau đó các giá trị int sẽ được xor'ed và kết quả là hàm băm của bạn.

Đối với những người sử dụng hàm băm làm ID (ý tôi là một giá trị duy nhất), hàm băm tự nhiên bị giới hạn ở một số chữ số, tôi nghĩ rằng đó là 5 byte cho thuật toán băm, ít nhất là MD5.

Bạn có thể biến nhiều giá trị thành giá trị băm và một số trong số chúng giống nhau, vì vậy đừng sử dụng nó làm định danh. (có thể một ngày nào đó tôi sẽ sử dụng thành phần của bạn)


7
Số nguyên Xoring để tạo mã băm là một antipotype nổi tiếng có xu hướng dẫn đến số lượng va chạm đặc biệt cao với các giá trị trong thế giới thực.
Jon Hanna

Mọi người ở đây đều sử dụng số nguyên và không bao giờ có bất kỳ loại đảm bảo nào cho hàm băm giống nhau, nó chỉ cố gắng thay đổi nhiều như có một vài va chạm xảy ra.
deadManN

Có, nhưng thứ hai và thứ năm của bạn không cố gắng tránh va chạm.
Jon Hanna

1
Vâng, đó là antipotype khá phổ biến.
Jon Hanna

2
Có một sự cân bằng để đạt được. Sử dụng mã băm thực sự tốt như Spookyhash và bạn sẽ tránh được va chạm tốt hơn nhiều, nhưng nó sẽ có nhiều thời gian tính toán hơn bất kỳ thứ nào trong số này (nhưng khi nói đến việc băm một lượng dữ liệu rất lớn, Spookyhash cực kỳ nhanh chóng). Một sự thay đổi đơn giản trên một trong các giá trị trước khi xored chỉ là chi phí phụ cận biên để giảm va chạm tốt. Nhân số nguyên tố tăng cả thời gian và chất lượng một lần nữa. Do đó tốt hơn giữa shift hoặc mult là do tranh cãi. Plain xor mặc dù rất thường xuyên có rất nhiều va chạm trên dữ liệu thực và tốt nhất nên tránh
Jon Hanna

1

Đây là lớp người trợ giúp tĩnh thực hiện triển khai Josh Bloch; và cung cấp quá tải rõ ràng để "ngăn chặn" quyền anh, và cũng để thực hiện băm đặc biệt cho các nguyên thủy dài.

Bạn có thể vượt qua một so sánh chuỗi phù hợp với việc thực hiện bằng của bạn.

Vì đầu ra Hash luôn là một int, bạn chỉ có thể xâu chuỗi các cuộc gọi Hash.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;


namespace Sc.Util.System
{
    /// <summary>
    /// Static methods that allow easy implementation of hashCode. Example usage:
    /// <code>
    /// public override int GetHashCode()
    ///     => HashCodeHelper.Seed
    ///         .Hash(primitiveField)
    ///         .Hsh(objectField)
    ///         .Hash(iEnumerableField);
    /// </code>
    /// </summary>
    public static class HashCodeHelper
    {
        /// <summary>
        /// An initial value for a hashCode, to which is added contributions from fields.
        /// Using a non-zero value decreases collisions of hashCode values.
        /// </summary>
        public const int Seed = 23;

        private const int oddPrimeNumber = 37;


        /// <summary>
        /// Rotates the seed against a prime number.
        /// </summary>
        /// <param name="aSeed">The hash's first term.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static int rotateFirstTerm(int aSeed)
        {
            unchecked {
                return HashCodeHelper.oddPrimeNumber * aSeed;
            }
        }


        /// <summary>
        /// Contributes a boolean to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aBoolean">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, bool aBoolean)
        {
            unchecked {
                return HashCodeHelper.rotateFirstTerm(aSeed)
                        + (aBoolean
                                ? 1
                                : 0);
            }
        }

        /// <summary>
        /// Contributes a char to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aChar">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, char aChar)
        {
            unchecked {
                return HashCodeHelper.rotateFirstTerm(aSeed)
                        + aChar;
            }
        }

        /// <summary>
        /// Contributes an int to the developing HashCode seed.
        /// Note that byte and short are handled by this method, through implicit conversion.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aInt">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, int aInt)
        {
            unchecked {
                return HashCodeHelper.rotateFirstTerm(aSeed)
                        + aInt;
            }
        }

        /// <summary>
        /// Contributes a long to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aLong">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, long aLong)
        {
            unchecked {
                return HashCodeHelper.rotateFirstTerm(aSeed)
                        + (int)(aLong ^ (aLong >> 32));
            }
        }

        /// <summary>
        /// Contributes a float to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aFloat">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, float aFloat)
        {
            unchecked {
                return HashCodeHelper.rotateFirstTerm(aSeed)
                        + Convert.ToInt32(aFloat);
            }
        }

        /// <summary>
        /// Contributes a double to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aDouble">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, double aDouble)
            => aSeed.Hash(Convert.ToInt64(aDouble));

        /// <summary>
        /// Contributes a string to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aString">The value to contribute.</param>
        /// <param name="stringComparison">Optional comparison that creates the hash.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(
                this int aSeed,
                string aString,
                StringComparison stringComparison = StringComparison.Ordinal)
        {
            if (aString == null)
                return aSeed.Hash(0);
            switch (stringComparison) {
                case StringComparison.CurrentCulture :
                    return StringComparer.CurrentCulture.GetHashCode(aString);
                case StringComparison.CurrentCultureIgnoreCase :
                    return StringComparer.CurrentCultureIgnoreCase.GetHashCode(aString);
                case StringComparison.InvariantCulture :
                    return StringComparer.InvariantCulture.GetHashCode(aString);
                case StringComparison.InvariantCultureIgnoreCase :
                    return StringComparer.InvariantCultureIgnoreCase.GetHashCode(aString);
                case StringComparison.OrdinalIgnoreCase :
                    return StringComparer.OrdinalIgnoreCase.GetHashCode(aString);
                default :
                    return StringComparer.Ordinal.GetHashCode(aString);
            }
        }

        /// <summary>
        /// Contributes a possibly-null array to the developing HashCode seed.
        /// Each element may be a primitive, a reference, or a possibly-null array.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aArray">CAN be null.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, IEnumerable aArray)
        {
            if (aArray == null)
                return aSeed.Hash(0);
            int countPlusOne = 1; // So it differs from null
            foreach (object item in aArray) {
                ++countPlusOne;
                if (item is IEnumerable arrayItem) {
                    if (!object.ReferenceEquals(aArray, arrayItem))
                        aSeed = aSeed.Hash(arrayItem); // recursive call!
                } else
                    aSeed = aSeed.Hash(item);
            }
            return aSeed.Hash(countPlusOne);
        }

        /// <summary>
        /// Contributes a possibly-null array to the developing HashCode seed.
        /// You must provide the hash function for each element.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aArray">CAN be null.</param>
        /// <param name="hashElement">Required: yields the hash for each element
        /// in <paramref name="aArray"/>.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash<T>(this int aSeed, IEnumerable<T> aArray, Func<T, int> hashElement)
        {
            if (aArray == null)
                return aSeed.Hash(0);
            int countPlusOne = 1; // So it differs from null
            foreach (T item in aArray) {
                ++countPlusOne;
                aSeed = aSeed.Hash(hashElement(item));
            }
            return aSeed.Hash(countPlusOne);
        }

        /// <summary>
        /// Contributes a possibly-null object to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aObject">CAN be null.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, object aObject)
        {
            switch (aObject) {
                case null :
                    return aSeed.Hash(0);
                case bool b :
                    return aSeed.Hash(b);
                case char c :
                    return aSeed.Hash(c);
                case int i :
                    return aSeed.Hash(i);
                case long l :
                    return aSeed.Hash(l);
                case float f :
                    return aSeed.Hash(f);
                case double d :
                    return aSeed.Hash(d);
                case string s :
                    return aSeed.Hash(s);
                case IEnumerable iEnumerable :
                    return aSeed.Hash(iEnumerable);
            }
            return aSeed.Hash(aObject.GetHashCode());
        }


        /// <summary>
        /// This utility method uses reflection to iterate all specified properties that are readable
        /// on the given object, excluding any property names given in the params arguments, and
        /// generates a hashcode.
        /// </summary>
        /// <param name="aSeed">The developing hash code, or the seed: if you have no seed, use
        /// the <see cref="Seed"/>.</param>
        /// <param name="aObject">CAN be null.</param>
        /// <param name="propertySelector"><see cref="BindingFlags"/> to select the properties to hash.</param>
        /// <param name="ignorePropertyNames">Optional.</param>
        /// <returns>A hash from the properties contributed to <c>aSeed</c>.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int HashAllProperties(
                this int aSeed,
                object aObject,
                BindingFlags propertySelector
                        = BindingFlags.Instance
                        | BindingFlags.Public
                        | BindingFlags.GetProperty,
                params string[] ignorePropertyNames)
        {
            if (aObject == null)
                return aSeed.Hash(0);
            if ((ignorePropertyNames != null)
                    && (ignorePropertyNames.Length != 0)) {
                foreach (PropertyInfo propertyInfo in aObject.GetType()
                        .GetProperties(propertySelector)) {
                    if (!propertyInfo.CanRead
                            || (Array.IndexOf(ignorePropertyNames, propertyInfo.Name) >= 0))
                        continue;
                    aSeed = aSeed.Hash(propertyInfo.GetValue(aObject));
                }
            } else {
                foreach (PropertyInfo propertyInfo in aObject.GetType()
                        .GetProperties(propertySelector)) {
                    if (propertyInfo.CanRead)
                        aSeed = aSeed.Hash(propertyInfo.GetValue(aObject));
                }
            }
            return aSeed;
        }


        /// <summary>
        /// NOTICE: this method is provided to contribute a <see cref="KeyValuePair{TKey,TValue}"/> to
        /// the developing HashCode seed; by hashing the key and the value independently. HOWEVER,
        /// this method has a different name since it will not be automatically invoked by
        /// <see cref="Hash(int,object)"/>, <see cref="Hash(int,IEnumerable)"/>,
        /// or <see cref="HashAllProperties"/> --- you MUST NOT mix this method with those unless
        /// you are sure that no KeyValuePair instances will be passed to those methods; or otherwise
        /// the generated hash code will not be consistent. This method itself ALSO will not invoke
        /// this method on the Key or Value here if that itself is a KeyValuePair.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="keyValuePair">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int HashKeyAndValue<TKey, TValue>(this int aSeed, KeyValuePair<TKey, TValue> keyValuePair)
            => aSeed.Hash(keyValuePair.Key)
                    .Hash(keyValuePair.Value);

        /// <summary>
        /// NOTICE: this method is provided to contribute a collection of <see cref="KeyValuePair{TKey,TValue}"/>
        /// to the developing HashCode seed; by hashing the key and the value independently. HOWEVER,
        /// this method has a different name since it will not be automatically invoked by
        /// <see cref="Hash(int,object)"/>, <see cref="Hash(int,IEnumerable)"/>,
        /// or <see cref="HashAllProperties"/> --- you MUST NOT mix this method with those unless
        /// you are sure that no KeyValuePair instances will be passed to those methods; or otherwise
        /// the generated hash code will not be consistent. This method itself ALSO will not invoke
        /// this method on a Key or Value here if that itself is a KeyValuePair or an Enumerable of
        /// KeyValuePair.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="keyValuePairs">The values to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int HashKeysAndValues<TKey, TValue>(
                this int aSeed,
                IEnumerable<KeyValuePair<TKey, TValue>> keyValuePairs)
        {
            if (keyValuePairs == null)
                return aSeed.Hash(null);
            foreach (KeyValuePair<TKey, TValue> keyValuePair in keyValuePairs) {
                aSeed = aSeed.HashKeyAndValue(keyValuePair);
            }
            return aSeed;
        }
    }
}

Yipes: Tôi tìm thấy một lỗi! Các HashKeysAndValuesphương pháp đã được cố định: nó gọi HashKeyAndValue.
Steven Coco

0

Trong trường hợp bạn muốn polyfill HashCodetừnetstandard2.1

public static class HashCode
{
    public static int Combine(params object[] instances)
    {
        int hash = 17;

        foreach (var i in instances)
        {
            hash = unchecked((hash * 31) + (i?.GetHashCode() ?? 0));
        }

        return hash;
    }
}

Lưu ý: Nếu được sử dụng cùng struct, nó sẽ phân bổ bộ nhớ do quyền anh

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.