Nguyên tắc GetHashCode trong C #


136

Tôi đọc trong cuốn sách Essential C # 3.0 và .NET 3.5:

Lợi nhuận của GetHashCode () trong vòng đời của một đối tượng cụ thể phải không đổi (cùng giá trị), ngay cả khi dữ liệu của đối tượng thay đổi. Trong nhiều trường hợp, bạn nên lưu lại bộ đệm phương thức để thực thi điều này.

Đây có phải là một hướng dẫn hợp lệ?

Tôi đã thử một vài loại tích hợp trong .NET và chúng không hoạt động như thế này.


Bạn có thể muốn xem xét thay đổi câu trả lời được chấp nhận, nếu có thể.
Giffyguy

Câu trả lời:


93

Câu trả lời chủ yếu là, nó là một hướng dẫn hợp lệ, nhưng có lẽ không phải là một quy tắc hợp lệ. Nó cũng không nói lên toàn bộ câu chuyện.

Vấn đề được đưa ra là đối với các loại có thể thay đổi, bạn không thể căn cứ mã băm vào dữ liệu có thể thay đổi vì hai đối tượng bằng nhau phải trả về cùng mã băm và mã băm phải có giá trị trong suốt vòng đời của đối tượng. Nếu mã băm thay đổi, bạn kết thúc với một đối tượng bị mất trong bộ sưu tập băm vì nó không còn tồn tại trong thùng băm chính xác.

Ví dụ, đối tượng A trả về hàm băm của 1. Vì vậy, nó đi vào thùng 1 của bảng băm. Sau đó, bạn thay đổi đối tượng A sao cho nó trả về hàm băm là 2. Khi bảng băm tìm kiếm nó, nó sẽ tìm trong thùng 2 và không thể tìm thấy nó - đối tượng bị mồ côi trong thùng 1. Đây là lý do tại sao mã băm phải không thay đổi trong suốt cuộc đời của đối tượng và chỉ một lý do tại sao viết các triển khai GetHashCode là một nỗi đau ở mông.

Cập nhật
Eric Lippert đã đăng một blog cung cấp thông tin tuyệt vời trên GetHashCode.

Cập nhật bổ sung
Tôi đã thực hiện một vài thay đổi ở trên:

  1. Tôi đã phân biệt giữa hướng dẫn và quy tắc.
  2. Tôi đánh xuyên qua "suốt đời của đối tượng".

Một hướng dẫn chỉ là một hướng dẫn, không phải là một quy tắc. Trong thực tế, GetHashCodechỉ phải tuân theo các hướng dẫn này khi mọi thứ mong muốn đối tượng tuân theo các nguyên tắc, chẳng hạn như khi nó được lưu trữ trong bảng băm. Nếu bạn không bao giờ có ý định sử dụng các đối tượng của mình trong các bảng băm (hoặc bất cứ điều gì khác dựa trên các quy tắc GetHashCode), thì việc thực hiện của bạn không cần phải tuân theo các hướng dẫn.

Khi bạn thấy "suốt đời của đối tượng", bạn nên đọc "trong thời gian đối tượng cần hợp tác với các bảng băm" hoặc tương tự. Giống như hầu hết mọi thứ, GetHashCodelà về việc biết khi nào nên phá vỡ các quy tắc.


1
Làm thế nào để bạn xác định sự bình đẳng giữa các loại đột biến?
Jon B

9
Bạn không nên sử dụng GetHashCode để xác định sự bình đẳng.
JSB

4
@JS Bangs - Từ MSDN: Các lớp dẫn xuất ghi đè GetHashCode cũng phải ghi đè Bằng để đảm bảo hai đối tượng được coi là bằng nhau có cùng mã băm; mặt khác, loại Hashtable có thể không hoạt động chính xác.
Jon B

3
@Joan Venge: Hai điều. Đầu tiên, ngay cả Microsoft cũng không có GetHashCode ngay mỗi khi thực hiện. Thứ hai, các loại giá trị thường không thay đổi với mỗi giá trị là một thể hiện mới thay vì sửa đổi một thể hiện hiện có.
Jeff Yates

17
Vì a.Equals (b) phải có nghĩa là a.GetHashCode () == b.GetHashCode (), mã băm thường phải thay đổi nếu dữ liệu được sử dụng để so sánh bằng được thay đổi. Tôi muốn nói rằng vấn đề không phải là GetHashCode dựa trên dữ liệu có thể thay đổi. Vấn đề là sử dụng các đối tượng có thể thay đổi làm khóa bảng băm (và thực sự làm biến đổi chúng). Liệu tôi có sai?
Niklas

120

Đó là một thời gian dài, tuy nhiên tôi nghĩ rằng vẫn cần phải đưa ra một câu trả lời chính xác cho câu hỏi này, bao gồm cả những lời giải thích về các whys và hows. Câu trả lời tốt nhất cho đến nay là câu trích dẫn MSDN một cách mệt mỏi - đừng cố gắng đưa ra các quy tắc của riêng bạn, các anh chàng MS biết họ đang làm gì.

Nhưng điều đầu tiên trước tiên: Hướng dẫn như được trích dẫn trong câu hỏi là sai.

Bây giờ các whys - có hai trong số họ

Đầu tiên tại sao : Nếu mã băm được tính theo cách, thì nó không thay đổi trong suốt vòng đời của một đối tượng, ngay cả khi chính đối tượng đó thay đổi, hơn là nó sẽ phá vỡ hợp đồng bằng.

Hãy nhớ rằng: "Nếu hai đối tượng so sánh bằng nhau, phương thức GetHashCode cho mỗi đối tượng phải trả về cùng một giá trị. Tuy nhiên, nếu hai đối tượng không so sánh bằng nhau, thì các phương thức GetHashCode cho hai đối tượng không phải trả về các giá trị khác nhau."

Câu thứ hai thường bị hiểu sai là "Quy tắc duy nhất là, tại thời điểm tạo đối tượng, mã băm của các đối tượng bằng nhau phải bằng nhau". Không thực sự biết tại sao, nhưng đó là về bản chất của hầu hết các câu trả lời ở đây là tốt.

Hãy nghĩ về hai đối tượng chứa một tên, trong đó tên được sử dụng trong phương thức bằng: Tên giống nhau -> cùng một thứ. Tạo sơ thẩm A: Name = Joe Tạo sơ thẩm B: Name = Peter

Hashcode A và Hashcode B rất có thể sẽ không giống nhau. Điều gì sẽ xảy ra, khi Tên của trường hợp B được đổi thành Joe?

Theo hướng dẫn từ câu hỏi, mã băm của B sẽ không thay đổi. Kết quả của việc này sẽ là: A.Equals (B) ==> true Nhưng đồng thời: A.GetHashCode () == B.GetHashCode () ==> false.

Nhưng chính xác hành vi này bị cấm rõ ràng bởi hợp đồng bằng & hashcode-hợp đồng.

Thứ hai tại sao : Mặc dù - dĩ nhiên - đúng, những thay đổi trong mã băm có thể phá vỡ danh sách băm và các đối tượng khác bằng cách sử dụng mã băm, điều ngược lại cũng đúng. Không thay đổi mã băm trong trường hợp xấu nhất sẽ nhận được danh sách băm, trong đó tất cả rất nhiều đối tượng khác nhau sẽ có cùng mã băm và do đó trong cùng một thùng băm - xảy ra khi các đối tượng được khởi tạo với giá trị tiêu chuẩn, chẳng hạn.


Bây giờ đến với các cung, Vâng, thoạt nhìn, dường như có một mâu thuẫn - dù bằng cách nào, mã sẽ bị phá vỡ. Nhưng không có vấn đề gì đến từ mã băm thay đổi hoặc không thay đổi.

Nguồn gốc của các vấn đề được mô tả tốt trong MSDN:

Từ mục nhập hashtable của MSDN:

Các đối tượng chính phải là bất biến miễn là chúng được sử dụng làm khóa trong Hashtable.

Điều này không có nghĩa là:

Bất kỳ đối tượng nào tạo ra giá trị băm đều phải thay đổi giá trị băm, khi đối tượng thay đổi, nhưng nó không được - tuyệt đối không được - cho phép mọi thay đổi đối với chính nó, khi nó được sử dụng bên trong Hashtable (hoặc bất kỳ đối tượng sử dụng Hash nào khác), tất nhiên) .

Đầu tiên, cách dễ nhất dĩ nhiên là thiết kế các đối tượng bất biến chỉ để sử dụng trong hashtables, sẽ được tạo ra như bản sao của các đối tượng bình thường, có thể thay đổi khi cần. Bên trong các đối tượng không thay đổi, thật tuyệt vời khi lưu trữ mã băm, vì nó không thay đổi.

Cách thứ hai Hoặc cung cấp cho đối tượng một "bạn đã được băm ngay bây giờ" -flag, đảm bảo tất cả dữ liệu đối tượng là riêng tư, kiểm tra cờ trong tất cả các chức năng có thể thay đổi dữ liệu đối tượng và ném dữ liệu ngoại lệ nếu không cho phép thay đổi (ví dụ: cờ được đặt ). Bây giờ, khi bạn đặt đối tượng vào bất kỳ khu vực băm nào, hãy đảm bảo đặt cờ và - cũng như - bỏ đặt cờ, khi không còn cần thiết nữa. Để dễ sử dụng, tôi khuyên bạn nên đặt cờ tự động bên trong phương thức "GetHashCode" - theo cách này không thể quên được. Và cuộc gọi rõ ràng của phương thức "ResetHashFlag" sẽ đảm bảo rằng lập trình viên sẽ phải suy nghĩ, khi đó nó có hoặc không được phép thay đổi dữ liệu đối tượng.

Ok, cũng nên nói: Có những trường hợp, trong đó có thể có các đối tượng có dữ liệu có thể thay đổi, trong đó mã băm vẫn không thay đổi, khi dữ liệu đối tượng bị thay đổi, mà không vi phạm hợp đồng bằng & hashcode-hợp đồng.

Tuy nhiên, điều này không yêu cầu, phương thức đẳng thức cũng không dựa trên dữ liệu có thể thay đổi. Vì vậy, nếu tôi viết một đối tượng và tạo phương thức GetHashCode chỉ tính toán một giá trị một lần và lưu trữ bên trong đối tượng để trả về nó trong các cuộc gọi sau, thì tôi phải, một lần nữa: hoàn toàn phải, tạo phương thức Equals, sẽ sử dụng các giá trị được lưu trữ để so sánh, do đó A.Equals (B) sẽ không bao giờ thay đổi từ sai thành đúng. Nếu không, hợp đồng sẽ bị phá vỡ. Kết quả của điều này thường sẽ là phương thức Equals không có ý nghĩa gì - nó không phải là tham chiếu ban đầu bằng, nhưng nó cũng không phải là một giá trị bằng. Đôi khi, đây có thể là hành vi dự định (tức là hồ sơ khách hàng), nhưng thường thì không.

Vì vậy, chỉ cần thay đổi kết quả GetHashCode, khi dữ liệu đối tượng thay đổi và nếu việc sử dụng đối tượng bên trong hàm băm sử dụng danh sách hoặc đối tượng được dự định (hoặc chỉ có thể) thì làm cho đối tượng bất biến hoặc tạo cờ chỉ đọc để sử dụng cho vòng đời của một danh sách băm chứa đối tượng.

(Nhân tiện: Tất cả những thứ này không phải là C # oder .NET cụ thể - đó là bản chất của tất cả các triển khai có thể băm, hoặc nói chung là của bất kỳ danh sách được lập chỉ mục nào, việc xác định dữ liệu của các đối tượng sẽ không bao giờ thay đổi, trong khi đối tượng nằm trong danh sách Hành vi bất ngờ và không thể đoán trước sẽ xảy ra, nếu quy tắc này bị phá vỡ. Ở đâu đó, có thể có các triển khai danh sách, theo dõi tất cả các yếu tố trong danh sách và tự động lập lại danh sách - nhưng hiệu suất của những điều đó chắc chắn sẽ rất kinh khủng.)


23
+1 cho lời giải thích chi tiết này (sẽ cung cấp thêm nếu tôi có thể)
Oliver

5
+1 đây chắc chắn là câu trả lời tốt hơn vì giải thích dài dòng! :)
Joe

9

Từ MSDN

Nếu hai đối tượng so sánh bằng nhau, phương thức GetHashCode cho mỗi đối tượng phải trả về cùng một giá trị. Tuy nhiên, nếu hai đối tượng không so sánh bằng nhau, các phương thức GetHashCode cho hai đối tượng không phải trả về các giá trị khác nhau.

Phương thức GetHashCode cho một đối tượng phải luôn trả về cùng mã băm miễn là không có sửa đổi nào đối với trạng thái đối tượng xác định giá trị trả về của phương thức Equals của đối tượng. Lưu ý rằng điều này chỉ đúng với thực thi hiện tại của một ứng dụng và có thể trả về một mã băm khác nếu ứng dụng được chạy lại.

Để có hiệu suất tốt nhất, hàm băm phải tạo phân phối ngẫu nhiên cho tất cả đầu vào.

Điều này có nghĩa là nếu (các) giá trị của đối tượng thay đổi, mã băm sẽ thay đổi. Ví dụ: một lớp "Người" với thuộc tính "Tên" được đặt thành "Tom" sẽ có một mã băm và một mã khác nếu bạn đổi tên thành "Jerry". Nếu không, Tom == Jerry, có lẽ không phải là những gì bạn dự định.


Chỉnh sửa :

Cũng từ MSDN:

Các lớp dẫn xuất ghi đè GetHashCode cũng phải ghi đè Bằng để đảm bảo hai đối tượng được coi là bằng nhau có cùng mã băm; mặt khác, loại Hashtable có thể không hoạt động chính xác.

Từ mục nhập hashtable của MSDN :

Các đối tượng chính phải là bất biến miễn là chúng được sử dụng làm khóa trong Hashtable.

Cách tôi đọc điều này là các đối tượng có thể thay đổi sẽ trả về các mã băm khác nhau khi giá trị của chúng thay đổi, trừ khi chúng được thiết kế để sử dụng trong hàm băm.

Trong ví dụ của System.Drawing.Point, đối tượng là có thể thay đổi, và không trả lại một hashcode khác nhau khi X hoặc Y thay đổi giá trị. Điều này sẽ làm cho nó trở thành một ứng cử viên nghèo được sử dụng như trong một hashtable.


GetHashCode () được thiết kế để sử dụng trong hàm băm, đó là điểm duy nhất của hàm này.
skolima

@skolima - tài liệu MSDN không phù hợp với điều đó. Các đối tượng có thể thay đổi có thể triển khai GetHashCode () và sẽ trả về các giá trị khác nhau khi giá trị của đối tượng thay đổi. Hashtables phải sử dụng các khóa bất biến. Do đó, bạn có thể sử dụng GetHashCode () cho mục đích khác ngoài hashtable.
Jon B

9

Tôi nghĩ rằng các tài liệu liên quan đến GetHashcode hơi khó hiểu.

Một mặt, MSDN tuyên bố rằng mã băm của một đối tượng sẽ không bao giờ thay đổi và không đổi Mặt khác, MSDN cũng nói rằng giá trị trả về của GetHashcode phải bằng 2 đối tượng, nếu 2 đối tượng đó được coi là bằng nhau.

MSDN:

Hàm băm phải có các thuộc tính sau:

  • Nếu hai đối tượng so sánh bằng nhau, phương thức GetHashCode cho mỗi đối tượng phải trả về cùng một giá trị. Tuy nhiên, nếu hai đối tượng không so sánh bằng nhau, các phương thức GetHashCode cho hai đối tượng không phải trả về các giá trị khác nhau.
  • Phương thức GetHashCode cho một đối tượng phải luôn trả về cùng mã băm miễn là không có sửa đổi nào đối với trạng thái đối tượng xác định giá trị trả về của phương thức Equals của đối tượng. Lưu ý rằng điều này chỉ đúng với thực thi hiện tại của một ứng dụng và có thể trả về một mã băm khác nếu ứng dụng được chạy lại.
  • Để có hiệu suất tốt nhất, hàm băm phải tạo phân phối ngẫu nhiên cho tất cả đầu vào.

Sau đó, điều này có nghĩa là tất cả các đối tượng của bạn phải là bất biến hoặc phương thức GetHashcode phải dựa trên các thuộc tính của đối tượng của bạn là bất biến. Ví dụ, giả sử bạn có lớp này (triển khai ngây thơ):

public class SomeThing
{
      public string Name {get; set;}

      public override GetHashCode()
      {
          return Name.GetHashcode();
      }

      public override Equals(object other)
      {
           SomeThing = other as Something;
           if( other == null ) return false;
           return this.Name == other.Name;
      }
}

Việc triển khai này đã vi phạm các quy tắc có thể tìm thấy trong MSDN. Giả sử bạn có 2 trường hợp của lớp này; thuộc tính Name của instance1 được đặt thành 'Pol' và thuộc tính Name của instance2 được đặt thành 'Piet'. Cả hai trường hợp trả về một mã băm khác nhau và chúng cũng không bằng nhau. Bây giờ, giả sử rằng tôi thay đổi Tên của instance2 thành 'Pol', thì theo phương thức Equals của tôi, cả hai trường hợp phải bằng nhau và theo một trong các quy tắc của MSDN, chúng sẽ trả về cùng một mã băm.
Tuy nhiên, điều này không thể được thực hiện, vì mã băm của instance2 sẽ thay đổi và MSDN nói rằng điều này không được phép.

Sau đó, nếu bạn có một thực thể, bạn có thể triển khai mã băm để nó sử dụng 'định danh chính' của thực thể đó, có thể lý tưởng là khóa thay thế hoặc thuộc tính bất biến. Nếu bạn có một đối tượng giá trị, bạn có thể triển khai Hashcode để nó sử dụng 'thuộc tính' của đối tượng giá trị đó. Các thuộc tính này tạo nên 'định nghĩa' của đối tượng giá trị. Tất nhiên đây là bản chất của một đối tượng giá trị; bạn không quan tâm đến danh tính của nó, mà là giá trị của nó.
Và, do đó, các đối tượng giá trị nên bất biến. (Giống như chúng nằm trong .NET framework, chuỗi, Date, v.v ... đều là các đối tượng bất biến).

Một điều nữa xuất hiện trong tâm trí:
Trong đó 'phiên' (tôi thực sự không biết nên gọi nó như thế nào) nên 'GetHashCode' trả về một giá trị không đổi. Giả sử bạn mở ứng dụng của mình, tải một thể hiện của một đối tượng ra khỏi DB (một thực thể) và lấy mã băm của nó. Nó sẽ trả về một số nhất định. Đóng ứng dụng và tải cùng một thực thể. Có yêu cầu rằng mã băm lần này có cùng giá trị như khi bạn tải thực thể lần đầu tiên không? IMHO, không phải.


1
Ví dụ của bạn là lý do tại sao Jeff Yates nói rằng bạn không thể căn cứ mã băm vào dữ liệu có thể thay đổi. Bạn không thể gắn một đối tượng có thể thay đổi trong Từ điển và mong muốn nó hoạt động tốt nếu mã băm dựa trên các giá trị có thể thay đổi của đối tượng đó.
Ogre Psalm33

3
Tôi không thể thấy quy tắc MSDN bị vi phạm ở đâu? Quy tắc nói rõ: Phương thức GetHashCode cho một đối tượng phải luôn trả về cùng mã băm miễn là không có sửa đổi nào đối với trạng thái đối tượng xác định giá trị trả về của phương thức Equals của đối tượng . Điều này có nghĩa là mã băm của instance2 được phép thay đổi khi bạn thay đổi Tên của instance2 thành Pol
chikak

8

Đây là lời khuyên tốt. Đây là những gì Brian Pepin đã nói về vấn đề này:

Điều này đã khiến tôi tăng gấp nhiều lần: Đảm bảo GetHashCode luôn trả về cùng một giá trị trong suốt vòng đời của một thể hiện. Hãy nhớ rằng mã băm được sử dụng để xác định "xô" trong hầu hết các triển khai băm. Nếu "xô" của đối tượng thay đổi, hashtable có thể không thể tìm thấy đối tượng của bạn. Đây có thể là những lỗi rất khó tìm, vì vậy hãy xử lý ngay lần đầu tiên.


Tôi đã không bỏ phiếu, nhưng tôi đoán rằng những người khác đã làm vì đó là một trích dẫn không bao gồm toàn bộ vấn đề. Chuỗi giả vờ là có thể thay đổi, nhưng không thay đổi mã băm. Bạn tạo "bob", sử dụng nó làm khóa trong hashtable và sau đó thay đổi giá trị của nó thành "phil". Tiếp theo tạo một chuỗi mới "phil". nếu sau đó bạn tìm kiếm một mục băm với khóa "phil", mục bạn đặt ban đầu sẽ không được tìm thấy. Nếu ai đó đã tìm kiếm trên "bob" thì nó sẽ được tìm thấy, nhưng bạn sẽ nhận được một giá trị có thể không còn đúng nữa. Hoặc là siêng năng để không sử dụng các khóa có thể thay đổi, hoặc nhận thức được các nguy hiểm.
Eric Tuttman

@EricTuttman: Tôi đã viết các quy tắc cho khung, tôi sẽ chỉ định rằng đối với bất kỳ cặp đối tượng nào XY, một khi X.Equals(Y)hoặc Y.Equals(X)đã được gọi, tất cả các cuộc gọi trong tương lai sẽ mang lại kết quả tương tự. Nếu một người muốn sử dụng một số định nghĩa khác về bình đẳng, sử dụng một EqualityComparer<T>.
supercat

5

Không trả lời trực tiếp câu hỏi của bạn, nhưng - nếu bạn sử dụng Resharper, đừng quên nó có một tính năng tạo ra triển khai GetHashCode hợp lý (cũng như phương thức Equals) cho bạn. Tất nhiên bạn có thể chỉ định thành viên nào của lớp sẽ được tính đến khi tính toán mã băm.


Cảm ơn, thực sự tôi không bao giờ sử dụng Resharper nhưng tôi vẫn thấy nó được đề cập khá thường xuyên, vì vậy tôi nên dùng thử.
Joan Venge

+1 Chia sẻ lại nếu có, nó tạo ra một triển khai GetHashCode đẹp.
ΩmegaMan

5

Kiểm tra bài đăng blog này từ Marc Brooks:

VTO, RTO và GetHashCode () - oh, của tôi!

Và sau đó kiểm tra bài đăng tiếp theo (không thể liên kết là tôi mới, nhưng có một liên kết trong bài viết khởi đầu) sẽ thảo luận thêm và đề cập đến một số điểm yếu nhỏ trong triển khai ban đầu.

Đây là tất cả mọi thứ tôi cần biết về việc tạo triển khai GetHashCode (), anh ta thậm chí còn cung cấp bản tải xuống phương thức của mình cùng với một số tiện ích khác, bằng vàng ngắn.


4

Mã băm không bao giờ thay đổi, nhưng điều quan trọng là phải hiểu Hashcode đến từ đâu.

Nếu đối tượng của bạn đang sử dụng ngữ nghĩa giá trị, tức là danh tính của đối tượng được xác định bởi các giá trị của nó (như Chuỗi, Màu, tất cả các cấu trúc). Nếu danh tính của đối tượng của bạn độc lập với tất cả các giá trị của nó, thì Hashcode được xác định bởi một tập hợp con các giá trị của nó. Ví dụ: mục StackOverflow của bạn được lưu trữ trong cơ sở dữ liệu ở đâu đó. Nếu bạn thay đổi tên hoặc email, mục nhập khách hàng của bạn vẫn giữ nguyên, mặc dù một số giá trị đã thay đổi (cuối cùng bạn thường được xác định bởi một số id khách hàng dài #).

Vì vậy, trong ngắn hạn:

Ngữ nghĩa loại giá trị - Hashcode được xác định bởi các giá trị Ngữ nghĩa loại tham chiếu - Hashcode được xác định bởi một số id

Tôi khuyên bạn nên đọc Thiết kế hướng miền của Eric Evans, nơi anh ấy đi vào các thực thể so với các loại giá trị (ít nhiều là những gì tôi đã cố gắng làm ở trên) nếu điều này vẫn không có ý nghĩa.


Điều này không thực sự chính xác. Mã băm phải không đổi cho một trường hợp cụ thể. Trong trường hợp của các loại giá trị, thường thì mỗi giá trị là một thể hiện duy nhất và do đó, hàm băm dường như thay đổi, nhưng thực tế nó là một thể hiện mới.
Jeff Yates

Bạn nói đúng, các loại giá trị là bất biến nên chúng ngăn cản sự thay đổi. Nắm bắt tốt.
DavidN

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.