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 đè GetHashCode
và Equals
cho 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.ValueType
hoặc System.Enum
loạ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 ValueType
phươ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à GetHashCode
phiê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.Equals
và ValueType.GetHashCode
có 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: GetHashCode
lặp lại qua một thể hiện và các khối XOR gồm 4 byte và Equals
phươ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
và +0.0
bằ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 đè Equals
và GetHashCode
bấ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 đè GetHashCode
tương ứng).