Triển khai mặc định cho Object.GetHashCode ()


162

Làm thế nào để thực hiện mặc định cho GetHashCode()công việc? Và nó có xử lý các cấu trúc, lớp, mảng, vv một cách hiệu quả và đủ tốt không?

Tôi đang cố gắng quyết định trong trường hợp nào tôi nên tự đóng gói và trong trường hợp nào tôi có thể an toàn dựa vào việc triển khai mặc định để làm tốt. Tôi không muốn phát minh lại bánh xe, nếu có thể.


Hãy xem nhận xét tôi để lại trên bài viết: stackoverflow.com/questions/763731/gethashcode-extension-method
Paul Westcott


34
Ngoài ra: bạn có thể có được mã băm mặc định (ngay cả khi GetHashCode()đã bị ghi đè) bằng cách sử dụngSystem.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj)
Marc Gravell

@MarcGravell cảm ơn bạn đã đóng góp điều này, tôi đã tìm kiếm chính xác câu trả lời này.
Andrew Savinykh

@MarcGravell Nhưng tôi sẽ làm điều này với phương pháp khác như thế nào?
Tomáš Zato - Phục hồi Monica

Câu trả lời:


86
namespace System {
    public class Object {
        [MethodImpl(MethodImplOptions.InternalCall)]
        internal static extern int InternalGetHashCode(object obj);

        public virtual int GetHashCode() {
            return InternalGetHashCode(this);
        }
    }
}

InternalGetHashCode được ánh xạ tới hàm ObjectNative :: GetHashCode trong CLR, trông giống như sau:

FCIMPL1(INT32, ObjectNative::GetHashCode, Object* obj) {  
    CONTRACTL  
    {  
        THROWS;  
        DISABLED(GC_NOTRIGGER);  
        INJECT_FAULT(FCThrow(kOutOfMemoryException););  
        MODE_COOPERATIVE;  
        SO_TOLERANT;  
    }  
    CONTRACTL_END;  

    VALIDATEOBJECTREF(obj);  

    DWORD idx = 0;  

    if (obj == 0)  
        return 0;  

    OBJECTREF objRef(obj);  

    HELPER_METHOD_FRAME_BEGIN_RET_1(objRef);        // Set up a frame  

    idx = GetHashCodeEx(OBJECTREFToObject(objRef));  

    HELPER_METHOD_FRAME_END();  

    return idx;  
}  
FCIMPLEND

Việc triển khai đầy đủ GetHashCodeEx khá lớn, do đó việc liên kết với mã nguồn C ++ sẽ dễ dàng hơn .


5
Đó là trích dẫn tài liệu phải đến từ một phiên bản rất sớm. Nó không còn được viết như thế này trong các bài viết MSDN hiện tại, có lẽ vì nó khá sai.
Hans Passant

4
Họ đã thay đổi từ ngữ, vâng, nhưng về cơ bản vẫn nói điều tương tự: "Do đó, việc triển khai mặc định của phương thức này không được sử dụng như một định danh đối tượng duy nhất cho mục đích băm."
David Brown

7
Tại sao tài liệu cho rằng việc thực hiện không đặc biệt hữu ích cho việc băm? Nếu một đối tượng bằng chính nó và không có gì khác, bất kỳ phương thức mã băm nào sẽ luôn trả về cùng một giá trị cho một thể hiện đối tượng nhất định và thường sẽ trả về các giá trị khác nhau cho các trường hợp khác nhau, vấn đề là gì?
supercat

3
@ ta.speot.is: Nếu điều bạn muốn là xác định xem một trường hợp cụ thể đã được thêm vào từ điển hay chưa, thì đẳng thức tham chiếu là hoàn hảo. Với các chuỗi, như bạn lưu ý, người ta thường quan tâm nhiều hơn đến việc một chuỗi chứa cùng một chuỗi ký tự đã được thêm vào chưa. Đó là lý do tại sao stringghi đè GetHashCode. Mặt khác, giả sử bạn muốn giữ số lần kiểm soát Paintcác sự kiện khác nhau . Bạn có thể sử dụng một Dictionary<Object, int[]>(mỗi int[]lưu trữ sẽ giữ chính xác một mục).
supercat

6
@ ItNotALie. Sau đó, cảm ơn Archive.org vì đã có một bản sao ;-)
RobIII

88

Đối với một lớp, mặc định về cơ bản là tham chiếu bình đẳng và điều đó thường ổn. Nếu viết một cấu trúc, thông thường hơn là ghi đè lên sự bình đẳng (không nhất thiết là để tránh quyền anh), nhưng rất hiếm khi bạn viết một cấu trúc nào!

Khi ghi đè đẳng thức, bạn phải luôn có một kết quả khớp Equals()GetHashCode()(nghĩa là đối với hai giá trị, nếu Equals()trả về giá trị đúng, chúng phải trả về cùng mã băm, nhưng không bắt buộc phải có converse ) - và thông thường cũng cung cấp ==/ !=toán tử và thường thực hiện IEquatable<T>quá.

Để tạo mã băm, người ta thường sử dụng một tổng số có xác thực, vì điều này tránh va chạm vào các giá trị được ghép nối - ví dụ: đối với hàm băm 2 trường cơ bản:

unchecked // disable overflow, for the unlikely possibility that you
{         // are compiling with overflow-checking enabled
    int hash = 27;
    hash = (13 * hash) + field1.GetHashCode();
    hash = (13 * hash) + field2.GetHashCode();
    return hash;
}

Điều này có lợi thế là:

  • hàm băm của {1,2} không giống với hàm băm của {2.1}
  • hàm băm của {1,1} không giống với hàm băm của {2,2}

vv - có thể phổ biến nếu chỉ sử dụng tổng không có trọng số hoặc xor ( ^), v.v.


Điểm tuyệt vời về lợi ích của thuật toán tổng hợp; điều mà trước đây tôi chưa nhận ra!
Lỗ hổng

Đôi khi tổng số (như được viết ở trên) có gây ra ngoại lệ tràn không?
sinelaw

4
@sinelaw vâng, nó nên được thực hiện unchecked. May mắn thay, uncheckedlà mặc định trong C #, nhưng sẽ tốt hơn nếu làm cho nó rõ ràng; đã chỉnh sửa
Marc Gravell

7

Tài liệu về GetHashCodephương thức cho Object nói rằng "việc triển khai mặc định của phương thức này không được sử dụng như một định danh đối tượng duy nhất cho mục đích băm." và một cho ValueType nói "Nếu bạn gọi phương thức GetHashCode của loại dẫn xuất, giá trị trả về có thể không phù hợp để sử dụng làm khóa trong bảng băm." .

Các dữ liệu cơ bản như byte, short, int, long, charstringthực hiện một phương pháp tốt GetHashCode. Một số lớp và cấu trúc khác, như Pointví dụ, thực hiện một GetHashCodephương thức có thể phù hợp hoặc không phù hợp với nhu cầu cụ thể của bạn. Bạn chỉ cần dùng thử để xem nó có đủ tốt không.

Tài liệu cho mỗi lớp hoặc cấu trúc có thể cho bạn biết nếu nó ghi đè thực hiện mặc định hay không. Nếu nó không ghi đè lên nó, bạn nên sử dụng triển khai của riêng bạn. Đối với bất kỳ lớp hoặc cấu trúc nào bạn tự tạo ở nơi bạn cần sử dụng GetHashCodephương thức, bạn nên tự thực hiện bằng cách sử dụng các thành viên phù hợp để tính mã băm.


2
Tôi không đồng ý rằng bạn nên thường xuyên thêm triển khai của riêng bạn. Đơn giản, phần lớn các lớp (đặc biệt) sẽ không bao giờ được kiểm tra tính bằng - hoặc ở nơi nào, đẳng thức tham chiếu sẵn có là tốt. Trong dịp (đã hiếm) viết một cấu trúc, nó sẽ phổ biến hơn, đúng.
Marc Gravell

@Marc Gravel: Tất nhiên đó không phải là điều tôi muốn nói. Tôi sẽ điều chỉnh đoạn cuối. :)
Guffa

Các kiểu dữ liệu cơ bản không triển khai phương thức GetHashCode tốt, ít nhất là trong trường hợp của tôi. Ví dụ: GetHashCode cho int trả về chính số đó: (123) .GetHashCode () trả về 123.
fdermishin

5
@ user502144 Và điều đó có gì sai? Đó là một định danh duy nhất hoàn hảo, dễ tính toán, không có sự tích cực sai về sự bình đẳng ...
Richard Rast

@Richard Rast: Không sao trừ các khóa có thể được phân phối kém khi được sử dụng trong Hashtable. Hãy xem câu trả lời này: stackoverflow.com/a/1388329/502144
fdermishin

5

Vì tôi không thể tìm thấy câu trả lời giải thích lý do tại sao chúng ta nên ghi đè GetHashCodeEqualscho các cấu trúc tùy chỉnh và tại sao việc triển khai mặc định "không có khả năng phù hợp để sử dụng làm khóa trong bảng băm", tôi sẽ để lại liên kết đến blog này bài đăng , giải thích tại sao với một ví dụ thực tế về một vấn đề đã xảy ra.

Tôi khuyên bạn nên đọc toàn bộ bài viết, nhưng đây là một bản tóm tắt (nhấn mạnh và làm rõ thêm).

Lý do hàm băm mặc định cho cấu trúc chậm và không tốt lắm:

Cách CLR được thiết kế, mỗi cuộc gọi đến một thành viên được xác định trong System.ValueTypehoặc System.Enumloại [có thể] gây ra sự phân bổ quyền anh [...]

Một người triển khai hàm băm phải đối mặt với một vấn đề nan giải: thực hiện phân phối tốt hàm băm hoặc để làm cho nó nhanh. Trong một số trường hợp, nó có thể đạt được cả hai, nhưng nó là khó để làm được điều này quát trong ValueType.GetHashCode.

Hàm băm chính tắc của một mã băm "kết hợp" cấu trúc của tất cả các trường. Nhưng cách duy nhất để có được mã băm của một trường trong một ValueTypephương thức là sử dụng sự phản chiếu . Vì vậy, các tác giả CLR đã quyết định giao dịch tốc độ trên bản phân phối và GetHashCodephiên bản mặc định chỉ trả về mã băm của trường không null đầu tiên và "munges" với id loại [...] Đây là hành vi hợp lý trừ khi không phải vậy . Chẳng hạn, nếu bạn không đủ may mắn và trường đầu tiên trong cấu trúc của bạn có cùng giá trị cho hầu hết các trường hợp, thì hàm băm sẽ cung cấp kết quả tương tự mọi lúc. Và, như bạn có thể tưởng tượng, điều này sẽ gây ra tác động mạnh mẽ về hiệu suất nếu các trường hợp này được lưu trữ trong tập băm hoặc bảng băm.

[...] Việc thực hiện dựa trên phản xạ là chậm . Rất chậm.

[...] Cả hai ValueType.EqualsValueType.GetHashCodecó một tối ưu hóa đặc biệt. Nếu một loại không có "con trỏ" và được đóng gói đúng cách [...] thì các phiên bản tối ưu hơn sẽ được sử dụng: GetHashCodelặp lại qua một thể hiện và các khối XOR gồm 4 byte và Equalsphương thức so sánh hai trường hợp sử dụng memcmp. [...] Nhưng việc tối ưu hóa rất khó khăn. Đầu tiên, thật khó để biết khi nào tối ưu hóa được bật [...] Thứ hai, so sánh bộ nhớ sẽ không nhất thiết cho bạn kết quả đúng . Đây là một ví dụ đơn giản: [...] -0.0+0.0bằng nhau nhưng có các biểu diễn nhị phân khác nhau.

Vấn đề thực tế được mô tả trong bài:

private readonly HashSet<(ErrorLocation, int)> _locationsWithHitCount;
readonly struct ErrorLocation
{
    // Empty almost all the time
    public string OptionalDescription { get; }
    public string Path { get; }
    public int Position { get; }
}

Chúng tôi đã sử dụng một bộ chứa cấu trúc tùy chỉnh với triển khai bình đẳng mặc định. Và thật không may, struct có một trường đầu tiên tùy chọn gần như luôn luôn bằng [chuỗi rỗng] . Hiệu suất vẫn ổn cho đến khi số lượng phần tử trong tập hợp tăng đáng kể gây ra sự cố hiệu suất thực sự, mất vài phút để khởi tạo bộ sưu tập với hàng chục nghìn mục.

Vì vậy, để trả lời câu hỏi "trong trường hợp nào tôi nên tự đóng gói và trong trường hợp nào tôi có thể tin cậy vào việc triển khai mặc định", ít nhất là trong trường hợp cấu trúc , bạn nên ghi đè EqualsGetHashCodebất cứ khi nào cấu trúc tùy chỉnh của bạn có thể được sử dụng như một khóa trong bảng băm hoặc Dictionary.
Tôi cũng sẽ khuyên bạn nên thực hiện IEquatable<T>trong trường hợp này, để tránh đấm bốc.

Như các câu trả lời khác đã nói, nếu bạn đang viết một lớp , hàm băm mặc định sử dụng đẳng thức tham chiếu thường ổn, vì vậy tôi sẽ không bận tâm trong trường hợp này, trừ khi bạn cần ghi đè Equals(sau đó bạn sẽ phải ghi đè GetHashCodetương ứng).


1

Nói chung, nếu bạn ghi đè bằng, bạn muốn ghi đè GetHashCode. Lý do cho điều này là bởi vì cả hai được sử dụng để so sánh sự bình đẳng của lớp / struct của bạn.

Bằng được sử dụng khi kiểm tra Foo A, B;

nếu (A == B)

Vì chúng tôi biết con trỏ không có khả năng khớp, chúng tôi có thể so sánh các thành viên nội bộ.

Equals(obj o)
{
    if (o == null) return false;
    MyType Foo = o as MyType;
    if (Foo == null) return false;
    if (Foo.Prop1 != this.Prop1) return false;

    return Foo.Prop2 == this.Prop2;
}

GetHashCode thường được sử dụng bởi các bảng băm. Mã băm được tạo bởi lớp của bạn phải luôn giống nhau cho trạng thái cung cấp lớp.

Tôi thường làm,

GetHashCode()
{
    int HashCode = this.GetType().ToString().GetHashCode();
    HashCode ^= this.Prop1.GetHashCode();
    etc.

    return HashCode;
}

Một số người sẽ nói rằng mã băm chỉ nên được tính một lần cho mỗi vòng đời của đối tượng, nhưng tôi không đồng ý với điều đó (và có lẽ tôi đã sai).

Sử dụng triển khai mặc định được cung cấp bởi đối tượng, trừ khi bạn có cùng tham chiếu đến một trong các lớp của mình, chúng sẽ không bằng nhau. Bằng cách ghi đè Equals và GetHashCode, bạn có thể báo cáo đẳng thức dựa trên các giá trị bên trong thay vì tham chiếu các đối tượng.


2
Cách tiếp cận ^ = không phải là cách tiếp cận đặc biệt tốt để tạo ra hàm băm - nó có xu hướng dẫn đến nhiều va chạm phổ biến / có thể dự đoán được - ví dụ nếu Prop1 = Prop2 = 3.
Marc Gravell

Nếu các giá trị là như nhau, tôi không thấy sự cố với xung đột vì các đối tượng bằng nhau. 13 * Hash + NewHash có vẻ thú vị mặc dù.
Bennett Dill

2
Ben: hãy thử nó với Obj1 {Prop1 = 12, Prop2 = 12} và Obj2 {Prop1 = 13, Prop2 = 13}
Tomáš Kafka

0

Nếu bạn chỉ giao dịch với POCO, bạn có thể sử dụng tiện ích này để đơn giản hóa phần nào cuộc sống của mình:

var hash = HashCodeUtil.GetHashCode(
           poco.Field1,
           poco.Field2,
           ...,
           poco.FieldN);

...

public static class HashCodeUtil
{
    public static int GetHashCode(params object[] objects)
    {
        int hash = 13;

        foreach (var obj in objects)
        {
            hash = (hash * 7) + (!ReferenceEquals(null, obj) ? obj.GetHashCode() : 0);
        }

        return hash;
    }
}
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.