Nếu mã băm của null luôn bằng 0, trong .NET


87

Cho rằng các tập hợp như System.Collections.Generic.HashSet<>chấp nhận nullnhư một thành viên tập hợp, người ta có thể hỏi mã băm của nó nullnên là gì. Có vẻ như khung sử dụng 0:

// nullable struct type
int? i = null;
i.GetHashCode();  // gives 0
EqualityComparer<int?>.Default.GetHashCode(i);  // gives 0

// class type
CultureInfo c = null;
EqualityComparer<CultureInfo>.Default.GetHashCode(c);  // gives 0

Điều này có thể (một chút) có vấn đề với các enum nullable. Nếu chúng ta xác định

enum Season
{
  Spring,
  Summer,
  Autumn,
  Winter,
}

thì Nullable<Season>(còn được gọi là Season?) có thể chỉ nhận năm giá trị, nhưng hai trong số đó, cụ thể là nullSeason.Spring, có cùng mã băm.

Thật hấp dẫn khi viết một trình so sánh bình đẳng "tốt hơn" như thế này:

class NewNullEnumEqComp<T> : EqualityComparer<T?> where T : struct
{
  public override bool Equals(T? x, T? y)
  {
    return Default.Equals(x, y);
  }
  public override int GetHashCode(T? x)
  {
    return x.HasValue ? Default.GetHashCode(x) : -1;
  }
}

Nhưng có lý do gì khiến mã băm của nullphải như 0vậy không?

CHỈNH SỬA / BỔ SUNG:

Một số người dường như nghĩ rằng đây là về việc ghi đè Object.GetHashCode(). Nó thực sự không phải, thực sự. (Các tác giả của NET đã thực hiện một ghi đè GetHashCode()trong Nullable<>struct mà có liên quan, mặc dù.) Một thực hiện sử dụng bằng văn bản của parameterless GetHashCode()không bao giờ có thể xử lý tình huống nơi mà các đối tượng có mã băm chúng ta tìm kiếm là null.

Đây là về việc triển khai phương thức trừu tượng EqualityComparer<T>.GetHashCode(T)hoặc cách khác thực hiện phương thức giao diện IEqualityComparer<T>.GetHashCode(T). Bây giờ, trong khi tạo các liên kết này đến MSDN, tôi thấy rằng nó nói ở đó rằng các phương thức này ném một ArgumentNullExceptionđối số duy nhất của chúng nếu là null. Đây chắc chắn phải là một sai lầm trên MSDN? Không có triển khai riêng của .NET nào có ngoại lệ. Ném trong trường hợp đó sẽ phá vỡ hiệu quả bất kỳ nỗ lực nào để thêm nullvào a HashSet<>. Trừ khi HashSet<>làm điều gì đó phi thường khi giao dịch với một nullmặt hàng (tôi sẽ phải kiểm tra điều đó).

CHỈNH SỬA / BỔ SUNG MỚI:

Bây giờ tôi đã thử gỡ lỗi. Với HashSet<>, tôi có thể xác nhận rằng với trình so sánh bình đẳng mặc định, các giá trị Season.Springnull sẽ kết thúc trong cùng một nhóm. Điều này có thể được xác định bằng cách kiểm tra rất cẩn thận các thành viên mảng private m_bucketsm_slots. Lưu ý rằng theo thiết kế, các chỉ số luôn được bù trừ bởi một.

Tuy nhiên, đoạn mã tôi đưa ra ở trên không khắc phục được điều này. Hóa ra, HashSet<>thậm chí sẽ không bao giờ hỏi người so sánh bình đẳng khi giá trị là null. Đây là từ mã nguồn của HashSet<>:

    // Workaround Comparers that throw ArgumentNullException for GetHashCode(null).
    private int InternalGetHashCode(T item) {
        if (item == null) { 
            return 0;
        } 
        return m_comparer.GetHashCode(item) & Lower31BitMask; 
    }

Điều này có nghĩa là, ít nhất HashSet<>, thậm chí không thể thay đổi hàm băm của null. Thay vào đó, một giải pháp là thay đổi hàm băm của tất cả các giá trị khác, như sau:

class NewerNullEnumEqComp<T> : EqualityComparer<T?> where T : struct
{
  public override bool Equals(T? x, T? y)
  {
    return Default.Equals(x, y);
  }
  public override int GetHashCode(T? x)
  {
    return x.HasValue ? 1 + Default.GetHashCode(x) : /* not seen by HashSet: */ 0;
  }
}

1
Tôi thứ hai rằng - câu hỏi rất hay.
Sachin Kainth

26
Tại sao mã băm cho null không phải là 0? Bạn biết đấy, một vụ va chạm băm không phải là ngày tận thế.
Hot Licks

3
Ngoại trừ việc đó là một vụ va chạm nổi tiếng, khá phổ biến. Đó không phải là vấn đề tồi tệ hay thậm chí là vấn đề lớn, nó chỉ có thể dễ dàng tránh được
Chris Pfohl

8
lol tại sao tôi lại nghĩ "nếu .NET framework nhảy ra khỏi một cây cầu, bạn có làm theo nó không?" ...
Adam Houldsworth

3
Chỉ vì tò mò, một mùa trống sẽ là gì?
SwDevMan81

Câu trả lời:


25

Vì vậy, miễn là mã băm được trả về cho null phù hợp với loại, bạn sẽ ổn. Yêu cầu duy nhất đối với mã băm là hai đối tượng được coi là bằng nhau chia sẻ cùng một mã băm.

Trả về 0 hoặc -1 cho null, miễn là bạn chọn một và trả về mọi lúc, sẽ hoạt động. Rõ ràng, mã băm không phải null sẽ không trả về bất kỳ giá trị nào bạn sử dụng cho null.

Các câu hỏi tương tự:

GetHashCode trên các trường rỗng?

GetHashCode sẽ trả về điều gì khi mã định danh của đối tượng là rỗng?

"Chú thích" của mục MSDN này đi vào chi tiết hơn xung quanh mã băm. Sâu sắc, các tài liệu không cung cấp bất kỳ bảo hiểm hoặc thảo luận về giá trị null ở tất cả - không phải ngay cả trong nội dung cộng đồng.

Để giải quyết vấn đề của bạn với enum, hãy triển khai lại mã băm để trả về khác 0, thêm một mục nhập enum "không xác định" mặc định tương đương với null hoặc đơn giản là không sử dụng enum nullable.

Nhân tiện, tìm thấy thú vị.

Một vấn đề khác mà tôi thấy với điều này nói chung là mã băm không thể đại diện cho kiểu 4 byte hoặc lớn hơn có thể nullable mà không có ít nhất một va chạm (nhiều hơn khi kích thước kiểu tăng lên). Ví dụ, mã băm của một int chỉ là int, vì vậy nó sử dụng toàn bộ phạm vi int. Bạn chọn giá trị nào trong phạm vi đó cho null? Bất kỳ cái nào bạn chọn sẽ va chạm với chính mã băm của giá trị.

Các va chạm trong và ngoài bản thân chúng không nhất thiết là một vấn đề, nhưng bạn cần biết chúng ở đó. Mã băm chỉ được sử dụng trong một số trường hợp. Như đã nêu trong tài liệu trên MSDN, mã băm không được đảm bảo trả về các giá trị khác nhau cho các đối tượng khác nhau, vì vậy không nên mong đợi.


Tôi không nghĩ rằng các câu hỏi bạn liên kết là hoàn toàn giống nhau. Khi bạn đang ghi đè Object.GetHashCode()trong lớp (hoặc cấu trúc) của chính mình, bạn biết rằng mã này sẽ chỉ được truy cập khi mọi người thực sự có một phiên bản của lớp của bạn. Trường hợp đó không thể được null. Đó là lý do tại sao bạn không bắt đầu ghi đè Object.GetHashCode()bằng if (this == null) return -1;Có sự khác biệt giữa "hiện hữu null" và "là một đối tượng sở hữu một số trường null".
Jeppe Stig Nielsen

Bạn nói: Rõ ràng, mã băm không phải null sẽ không trả về bất kỳ giá trị nào bạn sử dụng cho null. Đó sẽ là lý tưởng, tôi đồng ý. Và đó là lý do tại sao tôi đặt câu hỏi của mình ngay từ đầu, bởi vì bất cứ khi nào chúng ta viết một enum T, thì (T?)null(T?)default(T)sẽ có cùng một mã băm (trong việc triển khai hiện tại của .NET). Điều đó có thể được thay đổi nếu những người triển khai .NET thay đổi mã băm của null hoặc thuật toán mã băm của System.Enum.
Jeppe Stig Nielsen

Tôi đồng ý các liên kết dành cho các trường nội bộ rỗng. Bạn đề cập đến nó là cho IEqualityComparer <T>, trong quá trình triển khai mã băm của bạn vẫn dành riêng cho một loại nên bạn vẫn ở trong tình trạng tương tự, tính nhất quán cho loại. Trả lại cùng một mã băm cho null thuộc bất kỳ loại nào sẽ không quan trọng vì null không có một loại.
Adam Houldsworth

1
Lưu ý: Tôi đã cập nhật câu hỏi của mình hai lần. Nó chỉ ra rằng (ít nhất là với HashSet<>) nó không hoạt động để thay đổi mã băm của null.
Jeppe Stig Nielsen

6

Hãy nhớ rằng mã băm chỉ được sử dụng như bước đầu tiên để xác định bình đẳng và [là / nên] không bao giờ (được) sử dụng như một xác định trên thực tế về việc liệu hai đối tượng có bằng nhau hay không.

Nếu mã băm của hai đối tượng không bằng nhau thì chúng được coi là không bằng nhau (vì chúng tôi giả định rằng việc triển khai cơ bản là đúng - tức là chúng tôi không đoán trước điều đó). Nếu chúng có cùng một mã băm, thì chúng sẽ được kiểm tra xem có bằng nhau thực sự hay không, trong trường hợp của bạn, nullgiá trị và giá trị enum sẽ không thành công.

Kết quả là - sử dụng số 0 cũng tốt như bất kỳ giá trị nào khác trong trường hợp chung.

Chắc chắn, sẽ có những trường hợp, như enum của bạn, trong đó số 0 này được chia sẻ với mã băm của giá trị thực . Câu hỏi đặt ra là đối với bạn, liệu chi phí rất nhỏ của một phép so sánh bổ sung có gây ra vấn đề hay không.

Nếu vậy, hãy xác định trình so sánh của riêng bạn cho trường hợp giá trị null cho kiểu cụ thể của bạn và đảm bảo rằng giá trị null luôn tạo ra mã băm luôn giống nhau (tất nhiên!) một giá trị không thể được tạo bởi giá trị bên dưới thuật toán mã băm riêng của loại. Đối với các loại của riêng bạn, điều này là có thể. Đối với những người khác - chúc may mắn :)


5

Nó không nhất thiết phải bằng 0 - bạn có thể làm cho nó 42 nếu bạn muốn.

Tất cả những gì quan trọng là tính nhất quán trong quá trình thực hiện chương trình.

Nó chỉ là đại diện rõ ràng nhất, bởi vì nullnó thường được biểu thị bằng 0 bên trong. Có nghĩa là, trong khi gỡ lỗi, nếu bạn thấy mã băm bằng 0, nó có thể khiến bạn nghĩ, "Hmm .. đây có phải là vấn đề tham chiếu rỗng không?"

Lưu ý rằng nếu bạn sử dụng một số như thế 0xDEADBEEF, thì ai đó có thể nói rằng bạn đang sử dụng một con số ma thuật ... và bạn sẽ như vậy. (Bạn có thể nói số 0 cũng là một con số kỳ diệu, và bạn đã đúng ... ngoại trừ việc nó được sử dụng rộng rãi đến mức có phần ngoại lệ đối với quy tắc.)


4

Câu hỏi hay.

Tôi vừa thử viết mã này:

enum Season
{
  Spring,
  Summer,
  Autumn,
  Winter,
}

và thực hiện như thế này:

Season? v = null;
Console.WriteLine(v);

nó trở lại null

nếu tôi làm, thay vì bình thường

Season? v = Season.Spring;
Console.WriteLine((int)v);

nó trở lại 0, như mong đợi, hoặc mùa xuân đơn giản nếu chúng ta tránh truyền sang int.

Vì vậy, .. nếu bạn làm như sau:

Season? v = Season.Spring;  
Season? vnull = null;   
if(vnull == v) // never TRUE

BIÊN TẬP

Của 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

Nói cách khác: nếu hai đối tượng có cùng mã băm không có nghĩa là chúng bằng nhau, nguyên nhân bằng nhau thực sự được xác định bởi Equals .

Từ MSDN một lần nữa:

Phương thức GetHashCode cho một đối tượng phải luôn trả về cùng một 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 đối với quá trình thực thi hiện tại của một ứng dụng và một mã băm khác có thể được trả về nếu ứng dụng được chạy lại.


6
Theo định nghĩa, xung đột có nghĩa là hai đối tượng không bằng nhau có cùng một mã băm. Bạn đã chứng minh rằng các đối tượng không bằng nhau. Bây giờ chúng có mã băm giống nhau không? Theo OP họ làm, có nghĩa là đây là một vụ va chạm. Bây giờ, không phải ngày tận thế để xảy ra va chạm, nó chỉ đơn giản là một vụ va chạm có nhiều khả năng xảy ra hơn là nếu null băm thành một thứ khác 0, điều này làm ảnh hưởng đến hiệu suất.
Phục vụ

1
Vậy câu trả lời của bạn thực sự nói lên điều gì? Bạn nói rằng Season.Spring không bằng null. Vâng, điều đó không sai, nhưng nó không thực sự trả lời câu hỏi theo bất kỳ cách nào bây giờ.
Phục vụ

2
@Servy: câu hỏi nói: đó là lý do tại sao tôi có cùng một mã băm cho 2 đối tượng khác nhau ( nullSpring ). Vì vậy, câu trả lời là không có nguyên nhân va chạm ngay cả khi có cùng một mã băm, nhân tiện, chúng không bằng nhau.
Tigran

3
"Trả lời: tại sao không?" Vâng, OP đã trả lời trước câu hỏi "tại sao không" của bạn. Nó dễ gây ra va chạm hơn một số khác. Anh ấy đang tự hỏi liệu có lý do nào khiến số 0 được chọn hay không, và cho đến nay vẫn chưa có ai trả lời được điều đó.
Servy

1
Câu trả lời này không có gì mà OP chưa biết, rõ ràng là cách đặt câu hỏi.
Konrad Rudolph

4

Nhưng có lý do gì khiến mã băm của null phải là 0 không?

Nó có thể là bất cứ điều gì ở tất cả. Tôi có xu hướng đồng ý rằng 0 không nhất thiết phải là lựa chọn tốt nhất, nhưng đó là lựa chọn có thể dẫn đến ít lỗi nhất.

Một hàm băm hoàn toàn phải trả về cùng một hàm băm cho cùng một giá trị. Khi tồn tại một thành phần thực hiện điều này, đây thực sự là giá trị hợp lệ duy nhất cho hàm băm của null. Nếu có một hằng số cho điều này, chẳng hạn như, hm object.HashOfNull, thì ai đó triển khai một IEqualityComparersẽ phải biết cách sử dụng giá trị đó. Nếu họ không nghĩ về nó, cơ hội họ sẽ sử dụng số 0 cao hơn một chút so với mọi giá trị khác, tôi nghĩ.

ít nhất là đối với HashSet <>, thậm chí không thể thay đổi hàm băm của null

Như đã đề cập ở trên, tôi nghĩ rằng nó hoàn toàn không thể dừng đầy đủ, chỉ vì tồn tại các loại đã tuân theo quy ước rằng băm của null là 0.


Khi một người triển khai phương thức EqualityComparer<T>.GetHashCode(T)cho một số kiểu cụ thể Tcho phép null, người ta phải làm gì đó khi đối số là null. Bạn có thể (1) ném một ArgumentNullException, (2) trả về 0, hoặc (3) trả lại thứ khác. Tôi lấy câu trả lời của bạn cho một khuyến nghị luôn luôn quay lại 0trong tình huống đó?
Jeppe Stig Nielsen

@JeppeStigNielsen Tôi không chắc chắn về việc ném và trả lại, nhưng nếu bạn chọn quay lại, thì chắc chắn bằng không.
Roman Starkov

2

Nó là 0 vì đơn giản. Không có yêu cầu khó như vậy. Bạn chỉ cần đảm bảo các yêu cầu chung của mã hóa băm.

Ví dụ: bạn cần đảm bảo rằng nếu hai đối tượng bằng nhau thì mã băm của chúng cũng phải bằng nhau. Do đó, các mã băm khác nhau phải luôn đại diện cho các đối tượng khác nhau (nhưng ngược lại không nhất thiết phải đúng: hai đối tượng khác nhau có thể có cùng một mã băm, mặc dù nếu điều này xảy ra thường xuyên thì đây không phải là một hàm băm chất lượng tốt - nó không có chống va chạm tốt).

Tất nhiên, tôi đã hạn chế câu trả lời của mình cho các yêu cầu của bản chất toán học. Có các điều kiện kỹ thuật, cụ thể cho .NET mà bạn có thể đọc tại đây . 0 cho giá trị null không nằm trong số đó.


1

Vì vậy, điều này có thể tránh được bằng cách sử dụng một Unknowngiá trị enum (mặc dù nó có vẻ hơi kỳ lạ đối với một Seasonẩn số). Vì vậy, một cái gì đó như thế này sẽ phủ nhận vấn đề này:

public enum Season
{
   Unknown = 0,
   Spring,
   Summer,
   Autumn,
   Winter
}

Season some_season = Season.Unknown;
int code = some_season.GetHashCode(); // 0
some_season = Season.Autumn;
code = some_season.GetHashCode(); // 3

Sau đó, bạn sẽ có các giá trị mã băm duy nhất cho mỗi phần.


1
có, nhưng điều này không thực sự làm cho câu hỏi. Theo cách này, theo câu hỏi, null sẽ đối chiếu với Uknown. Sự khác biệt là gì?
Tigran

@Tigran - Phiên bản này không sử dụng kiểu nullable
SwDevMan81

Tôi hiểu rồi, nhưng câu hỏi là về kiểu nullable.
Tigran

Tôi có cảnh hàng triệu lần trên SO mà mọi người đưa ra đề xuất để cải thiện như câu trả lời.
SwDevMan81

1

Cá nhân tôi thấy việc sử dụng các giá trị nullable hơi khó xử và cố gắng tránh chúng bất cứ khi nào tôi có thể. Vấn đề của bạn chỉ là một lý do khác. Đôi khi chúng rất tiện dụng nhưng quy tắc chung của tôi là không kết hợp các kiểu giá trị với null nếu có thể, đơn giản vì chúng đến từ hai thế giới khác nhau. Trong .NET framework, chúng dường như cũng làm như vậy - rất nhiều kiểu giá trị cung cấp TryParsephương thức là một cách để tách các giá trị khỏi không có giá trị ( null).

Trong trường hợp cụ thể của bạn, thật dễ dàng để thoát khỏi vấn đề vì bạn xử lý Seasonloại của riêng bạn .

(Season?)nullđối với tôi có nghĩa là 'mùa không được chỉ định' như khi bạn có biểu mẫu web trong đó một số trường không bắt buộc. Theo ý kiến ​​của tôi, tốt hơn là chỉ định 'giá trị' đặc biệt đó trong enumchính nó hơn là sử dụng một chút rườm rà Nullable<T>. Nó sẽ nhanh hơn (không có quyền anh) dễ đọc hơn ( Season.NotSpecifiedso với null) và sẽ giải quyết vấn đề của bạn với mã băm.

Tất nhiên đối với các loại khác, chẳng hạn như intbạn không thể mở rộng miền giá trị và để chỉ ra một trong các giá trị là đặc biệt không phải lúc nào cũng có thể. Nhưng với int?mã băm xung đột là một vấn đề nhỏ hơn nhiều, nếu có.


Khi bạn nói "quyền anh", tôi nghĩ bạn có nghĩa là "gói", tức là đặt một giá trị struct bên trong một Nullable<>cấu trúc (nơi HasValuethành viên sau đó sẽ được đặt thành true). Bạn có chắc vấn đề thực sự nhỏ hơn với int?? Rất nhiều lần người ta chỉ sử dụng một vài giá trị của int, và sau đó nó tương đương với một enum (về lý thuyết có thể có nhiều thành viên).
Jeppe Stig Nielsen

Nói chung, tôi muốn nói rằng enum được chọn khi yêu cầu số lượng giới hạn các giá trị đã biết (2-10). Nếu giới hạn lớn hơn hoặc không có, intsẽ có ý nghĩa hơn. Tất nhiên sở thích khác nhau.
Maciej

0
Tuple.Create( (object) null! ).GetHashCode() // 0
Tuple.Create( 0 ).GetHashCode() // 0
Tuple.Create( 1 ).GetHashCode() // 1
Tuple.Create( 2 ).GetHashCode() // 2

1
Đó là một cách tiếp cận thú vị. Sẽ rất hữu ích nếu bạn chỉnh sửa câu trả lời của mình để bao gồm một số giải thích bổ sung, và đặc biệt là dựa trên bản chất của câu hỏi.
Jeremy Caney
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.