Thực tiễn tốt nhất để ghi đè isEqual: và băm


267

Làm thế nào để bạn ghi đè đúng isEqual:trong Objective-C? "Bắt" dường như là nếu hai đối tượng bằng nhau (như được xác định bởi isEqual:phương thức), chúng phải có cùng giá trị băm.

Phần Introspection của Hướng dẫn cơ bản về ca cao có một ví dụ về cách ghi đè isEqual:, sao chép như sau, đối với một lớp có tên MyWidget:

- (BOOL)isEqual:(id)other {
    if (other == self)
        return YES;
    if (!other || ![other isKindOfClass:[self class]])
        return NO;
    return [self isEqualToWidget:other];
}

- (BOOL)isEqualToWidget:(MyWidget *)aWidget {
    if (self == aWidget)
        return YES;
    if (![(id)[self name] isEqual:[aWidget name]])
        return NO;
    if (![[self data] isEqualToData:[aWidget data]])
        return NO;
    return YES;
}

Nó kiểm tra sự bằng nhau của con trỏ, sau đó là đẳng thức lớp và cuối cùng so sánh các đối tượng sử dụng isEqualToWidget:, chỉ kiểm tra các thuộc tính namedata. Những gì ví dụ không hiển thị là làm thế nào để ghi đè hash.

Giả sử có những tính chất khác không ảnh hưởng đến sự bình đẳng, nói age. Không nên hashghi đè phương thức đó chỉ namedataảnh hưởng đến hàm băm? Và nếu vậy, bạn sẽ làm điều đó như thế nào? Chỉ cần thêm băm của namedata? Ví dụ:

- (NSUInteger)hash {
    NSUInteger hash = 0;
    hash += [[self name] hash];
    hash += [[self data] hash];
    return hash;
}

Như vậy đã đủ chưa? Có một kỹ thuật tốt hơn? Điều gì nếu bạn có người nguyên thủy, như thế intnào? Chuyển đổi chúng để NSNumbercó được băm của họ? Hay cấu trúc như thế NSRectnào?

( Brain fart : Ban đầu được viết "bitwise HOẶC" chúng cùng với |=. Thêm vào.)


2
if (![other isKindOfClass:[self class]])- Về mặt kỹ thuật này có nghĩa là bình đẳng sẽ không được giao hoán. Tức là A = B không có nghĩa là B = A (ví dụ: nếu một người là lớp con của người khác)
Robert

Liên kết tài liệu đã chết, hiện được lưu trữ vào Introspection
jedwidz

Câu trả lời:


111

Bắt đầu với

 NSUInteger prime = 31;
 NSUInteger result = 1;

Sau đó, cho mỗi nguyên thủy bạn làm

 result = prime * result + var

Đối với các đối tượng bạn sử dụng 0 cho nil và nếu không thì mã băm của chúng.

 result = prime * result + [var hash];

Đối với booleans, bạn sử dụng hai giá trị khác nhau

 result = prime * result + ((var)?1231:1237);

Giải thích và ghi công

Đây không phải là công việc của tcurdt và các ý kiến ​​đã yêu cầu giải thích thêm, vì vậy tôi tin rằng một chỉnh sửa để ghi nhận là công bằng.

Thuật toán này đã được phổ biến trong cuốn sách "Java hiệu quả" và chương hiện tại có thể được tìm thấy trực tuyến tại đây . Cuốn sách đó đã phổ biến thuật toán, hiện là mặc định trong một số ứng dụng Java (bao gồm cả Eclipse). Tuy nhiên, nó bắt nguồn từ một triển khai thậm chí cũ hơn được quy cho Dan Bernstein hoặc Chris Torek. Thuật toán cũ hơn ban đầu trôi nổi trên Usenet và việc ghi nhận nhất định là khó khăn. Ví dụ, có một số bình luận thú vị trong mã Apache này (tìm kiếm tên của chúng) tham chiếu nguồn gốc.

Điểm mấu chốt là, đây là một thuật toán băm rất cũ, đơn giản. Nó không phải là hiệu suất cao nhất và thậm chí nó còn không được chứng minh về mặt toán học là một thuật toán "tốt". Nhưng nó đơn giản, và rất nhiều người đã sử dụng nó trong một thời gian dài với kết quả tốt, vì vậy nó có rất nhiều hỗ trợ lịch sử.


9
1231: 1237 đến từ đâu? Tôi cũng thấy nó trong Boolean.hashCode () của Java. Nó có ma thuật không?
David Leonard

17
Đó là bản chất của một thuật toán băm sẽ có va chạm. Vì vậy, tôi không thấy quan điểm của bạn, Paul.
tcurdt

85
Theo tôi, câu trả lời này không trả lời cho câu hỏi thực tế (cách thực hành tốt nhất để ghi đè hàm băm của NSObject). Nó chỉ cung cấp một thuật toán băm cụ thể. Trên hết, sự thưa thớt của lời giải thích làm cho khó hiểu nếu không có kiến ​​thức sâu về vấn đề này, và có thể dẫn đến việc mọi người sử dụng nó mà không biết họ đang làm gì. Tôi không hiểu tại sao câu hỏi này có quá nhiều câu hỏi.
Ricardo Sanchez-Saez

6
Vấn đề thứ nhất - (int) nhỏ và dễ tràn, sử dụng NSUInteger. Vấn đề thứ 2 - Nếu bạn tiếp tục nhân kết quả với mỗi biến băm, kết quả của bạn sẽ tràn ra. ví dụ. [Băm NSString] tạo ra các giá trị lớn. Nếu bạn có hơn 5 biến, bạn có thể dễ dàng vượt qua thuật toán này. Nó sẽ dẫn đến mọi thứ ánh xạ tới cùng một hàm băm, điều này thật tệ. Xem phản hồi của tôi: stackoverflow.com/a/4393493/276626
Paul Solt

10
@PaulSolt - Tràn không phải là vấn đề trong việc tạo ra hàm băm, xung đột là. Nhưng tràn không nhất thiết có thể tạo ra xung đột nhiều hơn và tuyên bố của bạn về tràn khiến mọi thứ ánh xạ tới cùng một hàm băm đơn giản là không chính xác.
DougW

81

Tôi chỉ tự mình chọn Objective-C, vì vậy tôi không thể nói ngôn ngữ đó một cách cụ thể, nhưng trong các ngôn ngữ khác tôi sử dụng nếu hai trường hợp là "Bằng nhau" thì chúng phải trả về cùng một hàm băm - nếu không bạn sẽ có tất cả các loại vấn đề khi cố gắng sử dụng chúng làm khóa trong hàm băm (hoặc bất kỳ bộ sưu tập kiểu từ điển nào).

Mặt khác, nếu 2 trường hợp không bằng nhau, chúng có thể có hoặc không có cùng hàm băm - tốt nhất là nếu chúng không có. Đây là sự khác biệt giữa tìm kiếm O (1) trên bảng băm và tìm kiếm O (N) - nếu tất cả các giá trị băm của bạn va chạm, bạn có thể thấy rằng tìm kiếm bảng của mình không tốt hơn tìm kiếm danh sách.

Về mặt thực hành tốt nhất, hàm băm của bạn sẽ trả về phân phối ngẫu nhiên các giá trị cho đầu vào của nó. Điều này có nghĩa là, ví dụ, nếu bạn có gấp đôi, nhưng phần lớn các giá trị của bạn có xu hướng phân cụm từ 0 đến 100, bạn cần đảm bảo rằng các giá trị băm được trả về bởi các giá trị đó được phân bổ đều trên toàn bộ phạm vi của các giá trị băm có thể . Điều này sẽ cải thiện đáng kể hiệu suất của bạn.

Có một số thuật toán băm ngoài kia, bao gồm một số thuật toán được liệt kê ở đây. Tôi cố gắng tránh tạo ra các thuật toán băm mới vì nó có thể có ý nghĩa hiệu suất lớn, vì vậy sử dụng các phương thức băm hiện có và thực hiện kết hợp bitwise của một số loại như bạn làm trong ví dụ của mình là một cách tốt để tránh nó.


4
+1 Câu trả lời tuyệt vời, xứng đáng nhận được nhiều sự ủng hộ hơn, đặc biệt là khi anh ấy thực sự nói về "thực tiễn tốt nhất" và lý thuyết đằng sau lý do tại sao một hàm băm tốt (duy nhất) là quan trọng.
Quinn Taylor

30

Một XOR đơn giản trên các giá trị băm của các thuộc tính quan trọng là đủ 99% thời gian.

Ví dụ:

- (NSUInteger)hash
{
    return [self.name hash] ^ [self.data hash];
}

Giải pháp được tìm thấy tại http://nshipster.com/equality/ của Mattt Thompson (cũng đề cập đến câu hỏi này trong bài đăng của anh ấy!)


1
Vấn đề với câu trả lời này là nó hoàn toàn không xem xét các giá trị nguyên thủy. Và giá trị nguyên thủy cũng có thể quan trọng đối với băm.
Vive

@Vive Hầu hết các vấn đề này đều được giải quyết trong Swift, nhưng các loại này thường đại diện cho hàm băm của riêng chúng vì chúng là nguyên thủy.
Yariv Nissim

1
Mặc dù bạn phù hợp với Swift, nhưng vẫn có nhiều dự án được viết bằng objc. Bởi vì câu trả lời của bạn là dành riêng cho objc, nó ít nhất là một đề cập.
Vive

XORing các giá trị băm với nhau là lời khuyên tồi, nó dẫn đến nhiều va chạm băm. Thay vào đó, nhân với một số nguyên tố và sau đó thêm vào, như các câu trả lời khác.
fishinear

27

Tôi thấy chủ đề này cực kỳ hữu ích khi cung cấp mọi thứ tôi cần để thực hiện isEqual:hashcác phương thức của tôi chỉ với một lần nắm bắt. Khi kiểm tra các biến đối tượng trong isEqual:mã ví dụ sử dụng:

if (![(id)[self name] isEqual:[aWidget name]])
    return NO;

Điều này liên tục thất bại ( nghĩa là trả về NO ) mà không có lỗi và khi tôi biết các đối tượng giống hệt nhau trong thử nghiệm đơn vị của mình. Lý do là, một trong các NSStringbiến thể hiện là con số không nên câu lệnh trên là:

if (![nil isEqual: nil])
    return NO;

và vì nil sẽ đáp ứng với bất kỳ phương pháp nào, điều này là hoàn toàn hợp pháp nhưng

[nil isEqual: nil]

lợi nhuận Nil , mà là KHÔNG , vì vậy khi cả hai đối tượng và một đang được thử nghiệm đã có một con số không đối tượng họ sẽ được coi là không bằng nhau ( ví dụ , isEqual:sẽ trở lại NO ).

Cách khắc phục đơn giản này là thay đổi câu lệnh if thành:

if ([self name] != [aWidget name] && ![(id)[self name] isEqual:[aWidget name]])
    return NO;

Bằng cách này, nếu địa chỉ của họ đều giống nhau nó sẽ bỏ qua các lời gọi phương thức không có vấn đề nếu họ là cả hai con số không hoặc cả hai trỏ đến cùng một đối tượng nhưng nếu một trong hai không phải là con số không hoặc chúng trỏ tới đối tượng khác nhau thì so sánh được một cách thích hợp gọi.

Tôi hy vọng điều này sẽ cứu ai đó vài phút gãi đầu.


20

Hàm băm sẽ tạo ra một giá trị bán duy nhất không có khả năng va chạm hoặc khớp với giá trị băm của đối tượng khác.

Đây là hàm băm đầy đủ, có thể được điều chỉnh phù hợp với các biến thể hiện của lớp. Nó sử dụng NSUInteger chứ không phải int để tương thích trên các ứng dụng 64 / 32bit.

Nếu kết quả trở thành 0 cho các đối tượng khác nhau, bạn có nguy cơ va chạm băm. Băm va chạm có thể dẫn đến hành vi chương trình không mong muốn khi làm việc với một số lớp bộ sưu tập phụ thuộc vào hàm băm. Hãy chắc chắn để kiểm tra chức năng băm của bạn trước khi sử dụng.

-(NSUInteger)hash {
    NSUInteger result = 1;
    NSUInteger prime = 31;
    NSUInteger yesPrime = 1231;
    NSUInteger noPrime = 1237;

    // Add any object that already has a hash function (NSString)
    result = prime * result + [self.myObject hash];

    // Add primitive variables (int)
    result = prime * result + self.primitiveVariable; 

    // Boolean values (BOOL)
    result = prime * result + (self.isSelected?yesPrime:noPrime);

    return result;
}

3
Một gotcha ở đây: Tôi thích tránh cú pháp dấu chấm, vì vậy tôi đã chuyển đổi câu lệnh BOOL của bạn thành (ví dụ) result = prime * result + [self isSelected] ? yesPrime : noPrime;. Sau đó tôi thấy điều này được đặt resultthành (ví dụ) 1231, tôi giả sử do ?toán tử được ưu tiên. Tôi đã khắc phục sự cố bằng cách thêm dấu ngoặc:result = prime * result + ([self isSelected] ? yesPrime : noPrime);
Ashley

12

Cách dễ dàng nhưng không hiệu quả là trả về cùng một -hashgiá trị cho mọi trường hợp. Mặt khác, có, bạn phải thực hiện băm chỉ dựa trên các đối tượng có ảnh hưởng đến sự bình đẳng. Điều này là khó khăn nếu bạn sử dụng so sánh lỏng lẻo trong -isEqual:(ví dụ so sánh chuỗi không phân biệt chữ hoa chữ thường). Đối với ints, bạn thường có thể sử dụng chính int, trừ khi bạn sẽ so sánh với NSNumbers.

Đừng sử dụng | =, mặc dù vậy, nó sẽ bão hòa. Sử dụng ^ = thay vào đó.

Thực tế vui vẻ ngẫu nhiên : [[NSNumber numberWithInt:0] isEqual:[NSNumber numberWithBool:NO]], nhưng [[NSNumber numberWithInt:0] hash] != [[NSNumber numberWithBool:NO] hash]. (rdar: // 4538282, mở cửa từ ngày 5 tháng 5 năm 2006)


1
Bạn hoàn toàn đúng trên | =. Không thực sự có nghĩa là. :) + = và ^ = tương đối giống nhau. Làm thế nào để bạn xử lý các nguyên thủy không nguyên như double và float?
Dave Dribin

Sự thật thú vị ngẫu nhiên: Thử nghiệm nó trên Snow Leopard ... ;-)
Quinn Taylor

Anh ấy đúng về việc sử dụng XOR thay vì OR để kết hợp các trường thành hàm băm. Tuy nhiên, không sử dụng lời khuyên trả về cùng một giá trị -hash cho mọi đối tượng - mặc dù dễ dàng, nó có thể làm giảm nghiêm trọng hiệu suất của bất kỳ thứ gì sử dụng hàm băm của đối tượng. Hàm băm không nhất thiết phải khác biệt đối với các đối tượng không bằng nhau, nhưng nếu bạn có thể đạt được điều đó, thì không có gì giống như vậy.
Quinn Taylor

Báo cáo lỗi radar mở được đóng lại. openradar.me/4538282 Điều đó có nghĩa là gì?
JJD

JJD, lỗi đã được sửa trong Mac OS X 10.6, như Quinn gợi ý. (Lưu ý rằng nhận xét là hai tuổi.)
Jens Ayton

9

Hãy nhớ rằng bạn chỉ cần cung cấp hàm băm bằng nhau khi isEqualđúng. Khi isEqualsai, hàm băm không nhất thiết phải bằng nhau mặc dù có lẽ là như vậy. Vì thế:

Giữ băm đơn giản. Chọn một thành viên (hoặc một vài thành viên) biến đặc biệt nhất.

Ví dụ, đối với CLPlacemark, chỉ tên là đủ. Có, có 2 hoặc 3 phân biệt CLPlacemark với cùng tên chính xác nhưng chúng rất hiếm. Sử dụng hàm băm đó.

@interface CLPlacemark (equal)
- (BOOL)isEqual:(CLPlacemark*)other;
@end

@implementation CLPlacemark (equal)

...

-(NSUInteger) hash
{
    return self.name.hash;
}


@end

Lưu ý tôi không bận tâm chỉ định thành phố, quốc gia, vv Tên là đủ. Có lẽ tên và CLLocation.

Hash nên được phân phối đều. Vì vậy, bạn có thể kết hợp nhiều biến thành viên bằng cách sử dụng dấu mũ ^ (dấu xor)

Vì vậy, nó là một cái gì đó như

hash = self.member1.hash ^ self.member2.hash ^ self.member3.hash

Bằng cách đó, hàm băm sẽ được phân phối đều.

Hash must be O(1), and not O(n)

Vậy phải làm gì trong mảng?

Một lần nữa, đơn giản. Bạn không phải băm tất cả các thành viên của mảng. Đủ để băm phần tử đầu tiên, phần tử cuối cùng, số đếm, có thể một số phần tử ở giữa, và đó là nó.


Giá trị băm XORing không cung cấp phân phối đồng đều.
fishinear

7

Xin chờ, chắc chắn một cách dễ dàng hơn để làm điều này là trước tiên ghi đè - (NSString )descriptionvà cung cấp một chuỗi đại diện cho trạng thái đối tượng của bạn (bạn phải thể hiện toàn bộ trạng thái của đối tượng trong chuỗi này).

Sau đó, chỉ cần cung cấp việc thực hiện sau đây hash:

- (NSUInteger)hash {
    return [[self description] hash];
}

Điều này dựa trên nguyên tắc "nếu hai đối tượng chuỗi bằng nhau (như được xác định bởi phương thức isEqualToString:), thì chúng phải có cùng giá trị băm."

Nguồn: Tham khảo lớp NSString


1
Điều này giả định rằng phương pháp mô tả sẽ là duy nhất. Sử dụng hàm băm mô tả tạo ra sự phụ thuộc, điều đó có thể không rõ ràng và nguy cơ va chạm cao hơn.
Paul Solt

1
+1 nâng cấp. Đây là một ý tưởng tuyệt vời. Nếu bạn sợ rằng các mô tả gây ra va chạm, thì bạn có thể ghi đè lên nó.
dùng4951

Cảm ơn Jim, tôi sẽ không phủ nhận rằng đây là một chút hack, nhưng nó sẽ hoạt động trong mọi trường hợp tôi có thể nghĩ đến - và như tôi đã nói, với điều kiện bạn ghi đè description, tôi không hiểu tại sao điều này lại thua kém bất kỳ giải pháp bình chọn cao hơn. Có thể không phải là giải pháp thanh lịch nhất về mặt toán học, nhưng nên thực hiện các mẹo. Như Brian B. tuyên bố (hầu hết câu trả lời được đưa ra vào thời điểm này): "Tôi cố gắng tránh tạo ra các thuật toán băm mới" - đã đồng ý! - Tôi chỉ hashNSString!
Jonathan Ellis

Ủng hộ vì đó là một ý tưởng gọn gàng. Tôi sẽ không sử dụng nó bởi vì tôi sợ phân bổ NSString bổ sung.
karwag

1
Đây không phải là một giải pháp chung vì hầu hết các lớp descriptionbao gồm địa chỉ con trỏ. Vì vậy, điều này làm cho hai trường hợp khác nhau của cùng một lớp bằng với hàm băm khác nhau, vi phạm giả định cơ bản rằng hai đối tượng bằng nhau có cùng hàm băm!
Diogo T

5

Các hợp đồng bằng và băm được xác định rõ và được nghiên cứu kỹ lưỡng trong thế giới Java (xem câu trả lời của @ mipardi's), nhưng tất cả các cân nhắc tương tự nên áp dụng cho Objective-C.

Eclipse thực hiện một công việc đáng tin cậy để tạo các phương thức này trong Java, vì vậy đây là một ví dụ về Eclipse được chuyển bằng tay sang Objective-C:

- (BOOL)isEqual:(id)object {
    if (self == object)
        return true;
    if ([self class] != [object class])
        return false;
    MyWidget *other = (MyWidget *)object;
    if (_name == nil) {
        if (other->_name != nil)
            return false;
    }
    else if (![_name isEqual:other->_name])
        return false;
    if (_data == nil) {
        if (other->_data != nil)
            return false;
    }
    else if (![_data isEqual:other->_data])
        return false;
    return true;
}

- (NSUInteger)hash {
    const NSUInteger prime = 31;
    NSUInteger result = 1;
    result = prime * result + [_name hash];
    result = prime * result + [_data hash];
    return result;
}

Và đối với một lớp con YourWidgetcó thêm thuộc tính serialNo:

- (BOOL)isEqual:(id)object {
    if (self == object)
        return true;
    if (![super isEqual:object])
        return false;
    if ([self class] != [object class])
        return false;
    YourWidget *other = (YourWidget *)object;
    if (_serialNo == nil) {
        if (other->_serialNo != nil)
            return false;
    }
    else if (![_serialNo isEqual:other->_serialNo])
        return false;
    return true;
}

- (NSUInteger)hash {
    const NSUInteger prime = 31;
    NSUInteger result = [super hash];
    result = prime * result + [_serialNo hash];
    return result;
}

Việc triển khai này tránh được một số cạm bẫy phân lớp trong mẫu isEqual:từ Apple:

  • Kiểm tra lớp của Apple other isKindOfClass:[self class]là không đối xứng cho hai lớp con khác nhau của MyWidget. Bình đẳng cần phải đối xứng: a = b khi và chỉ khi b = a. Điều này có thể dễ dàng được sửa bằng cách thay đổi thử nghiệm thành other isKindOfClass:[MyWidget class], sau đó tất cả các MyWidgetlớp con sẽ có thể so sánh lẫn nhau.
  • Sử dụng một isKindOfClass:bài kiểm tra lớp con ngăn chặn các lớp con ghi đè isEqual:bằng một bài kiểm tra đẳng thức tinh chế. Điều này là do đẳng thức cần phải có tính bắc cầu: if a = b và a = c thì b = c. Nếu một MyWidgetthể hiện so sánh bằng hai YourWidgetthể hiện, thì những YourWidgetthể hiện đó phải so sánh bằng nhau, ngay cả khi chúng serialNokhác nhau.

Vấn đề thứ hai có thể được khắc phục bằng cách chỉ xem xét các đối tượng là bằng nhau nếu chúng thuộc cùng một lớp chính xác, do đó [self class] != [object class]thử nghiệm ở đây. Đối với các lớp ứng dụng điển hình , đây dường như là cách tiếp cận tốt nhất.

Tuy nhiên, chắc chắn có những trường hợp isKindOfClass:thử nghiệm là thích hợp hơn. Đây là điển hình của các lớp khung hơn các lớp ứng dụng. Ví dụ, bất kỳ NSStringnên so sánh bằng với bất kỳ khác NSStringvới cùng một chuỗi ký tự cơ bản, bất kể NSString/ NSMutableStringphân biệt và bất kể các lớp riêng tư trong NSStringcụm lớp có liên quan.

Trong các trường hợp như vậy, isEqual:cần có hành vi được xác định rõ ràng, được ghi chép rõ ràng và cần làm rõ rằng các lớp con không thể ghi đè lên điều này. Trong Java, hạn chế 'không ghi đè' có thể được thi hành bằng cách gắn cờ các phương thức bằng và mã băm như final, nhưng Objective-C không có tương đương.


@adubr Điều đó được đề cập trong hai đoạn cuối của tôi. Nó không tập trung vì MyWidgetđược hiểu không phải là một cụm lớp.
jedwidz

5

Điều này không trực tiếp trả lời câu hỏi của bạn (tất cả) nhưng tôi đã sử dụng MurmurHash trước đây để tạo băm: murmurhash

Đoán tôi nên giải thích tại sao: murmurhash đẫm máu nhanh ...


2
Thư viện C ++ tập trung vào các giá trị băm duy nhất cho khóa void * sử dụng số ngẫu nhiên (và cũng không liên quan đến các đối tượng Objective-C) thực sự không phải là một gợi ý hữu ích ở đây. Phương thức -hash sẽ trả về một giá trị nhất quán mỗi lần, hoặc nó sẽ hoàn toàn vô dụng. Nếu đối tượng được thêm vào bộ sưu tập gọi -hash và trả về giá trị mới mỗi lần, các bản sao sẽ không bao giờ được phát hiện và bạn cũng không bao giờ có thể truy xuất đối tượng từ bộ sưu tập. Trong trường hợp này, thuật ngữ "băm" khác với ý nghĩa trong bảo mật / mật mã.
Quinn Taylor

3
murmurhash không phải là hàm băm mật mã. Vui lòng kiểm tra sự thật của bạn trước khi đăng thông tin không chính xác. Murmurhash thể hữu ích cho việc băm các lớp object -c tùy chỉnh (đặc biệt nếu bạn có nhiều NSDatas tham gia) vì nó cực kỳ nhanh. Tuy nhiên, tôi cho bạn biết rằng có thể đề nghị đó không phải là lời khuyên tốt nhất để đưa ra cho ai đó "chỉ cần chọn mục tiêu-c", nhưng xin lưu ý tiền tố của tôi trong câu trả lời ban đầu của tôi cho câu hỏi.
schwa

5

Tôi đã tìm thấy trang này là một hướng dẫn hữu ích trong việc ghi đè các phương thức bằng và loại băm. Nó bao gồm một thuật toán khá để tính mã băm. Trang này hướng tới Java, nhưng thật dễ dàng để điều chỉnh nó thành Objective-C / Ca cao.


1
liên kết được lưu trong bộ nhớ cache qua archive.org: web.archive.org/web/20071013053633/http://www.geocities.com/
cobbal

4

Tôi cũng là một người mới của Objective C, nhưng tôi đã tìm thấy một bài viết tuyệt vời về danh tính so với sự bình đẳng trong Objective C ở đây . Từ cách đọc của tôi, có vẻ như bạn có thể chỉ cần giữ hàm băm mặc định (sẽ cung cấp một danh tính duy nhất) và thực hiện phương thức isEqual để so sánh các giá trị dữ liệu.


Tôi là người mới sử dụng Cacao / Objective C, và câu trả lời và liên kết này thực sự đã giúp tôi vượt qua tất cả những thứ tiên tiến hơn ở trên đến điểm mấu chốt - Tôi không cần phải lo lắng về băm - chỉ cần thực hiện phương thức isEqual :. Cảm ơn!
John Gallagher

Đừng bỏ lỡ liên kết của @ ceperry. Bài viết Equality vs Identitycủa Karl Kraft thực sự tốt.
JJD

6
@ John: Tôi nghĩ bạn nên đọc lại bài viết. Nó nói rất rõ ràng rằng "các trường hợp bằng nhau phải có giá trị băm bằng nhau". Nếu bạn ghi đè isEqual:, bạn cũng phải ghi đè hash.
Steve Madsen

3

Quinn chỉ sai khi tham chiếu đến băm thầm thì vô dụng ở đây. Quinn nói đúng rằng bạn muốn hiểu lý thuyết đằng sau việc băm. Tiếng xì xào chắt lọc rất nhiều lý thuyết đó vào một triển khai. Chỉ ra làm thế nào để áp dụng thực hiện đó cho ứng dụng cụ thể này là đáng để khám phá.

Một số điểm chính ở đây:

Hàm ví dụ từ tcurdt cho thấy rằng '31' là một số nhân tốt vì nó là số nguyên tố. Người ta cần chỉ ra rằng trở thành số nguyên tố là điều kiện cần và đủ. Trong thực tế 31 (và 7) có lẽ không phải là số nguyên tố đặc biệt tốt vì 31 == -1% 32. Một số nhân lẻ với khoảng một nửa bit được đặt và một nửa số bit rõ ràng có khả năng tốt hơn. (Hằng số nhân băm của murmur có thuộc tính đó.)

Loại hàm băm này có thể sẽ mạnh hơn nếu sau khi nhân, giá trị kết quả được điều chỉnh thông qua một ca và xor. Phép nhân có xu hướng tạo ra kết quả của rất nhiều tương tác bit ở đầu cao của thanh ghi và kết quả tương tác thấp ở đầu dưới của thanh ghi. Sự thay đổi và xor làm tăng các tương tác ở cuối dưới cùng của thanh ghi.

Đặt kết quả ban đầu thành một giá trị trong đó khoảng một nửa số bit bằng 0 và khoảng một nửa số bit sẽ có xu hướng hữu ích.

Nó có thể hữu ích để cẩn thận về thứ tự kết hợp các yếu tố. Đầu tiên có lẽ nên xử lý các booleans và các phần tử khác trong đó các giá trị không được phân phối mạnh.

Nó có thể hữu ích để thêm một vài giai đoạn tranh giành bit bổ sung vào cuối tính toán.

Liệu băm murmur có thực sự nhanh cho ứng dụng này hay không là một câu hỏi mở. Hàm băm murmur trộn sẵn các bit của mỗi từ đầu vào. Nhiều từ đầu vào có thể được xử lý song song, giúp xử lý nhiều vấn đề.


3

Kết hợp câu trả lời của @ tcurdt với câu trả lời của @ oscar-gomez để lấy tên thuộc tính , chúng tôi có thể tạo ra một giải pháp thả vào dễ dàng cho cả isEqual và hàm băm:

NSArray *PropertyNamesFromObject(id object)
{
    unsigned int propertyCount = 0;
    objc_property_t * properties = class_copyPropertyList([object class], &propertyCount);
    NSMutableArray *propertyNames = [NSMutableArray arrayWithCapacity:propertyCount];

    for (unsigned int i = 0; i < propertyCount; ++i) {
        objc_property_t property = properties[i];
        const char * name = property_getName(property);
        NSString *propertyName = [NSString stringWithUTF8String:name];
        [propertyNames addObject:propertyName];
    }
    free(properties);
    return propertyNames;
}

BOOL IsEqualObjects(id object1, id object2)
{
    if (object1 == object2)
        return YES;
    if (!object1 || ![object2 isKindOfClass:[object1 class]])
        return NO;

    NSArray *propertyNames = PropertyNamesFromObject(object1);
    for (NSString *propertyName in propertyNames) {
        if (([object1 valueForKey:propertyName] != [object2 valueForKey:propertyName])
            && (![[object1 valueForKey:propertyName] isEqual:[object2 valueForKey:propertyName]])) return NO;
    }

    return YES;
}

NSUInteger MagicHash(id object)
{
    NSUInteger prime = 31;
    NSUInteger result = 1;

    NSArray *propertyNames = PropertyNamesFromObject(object);

    for (NSString *propertyName in propertyNames) {
        id value = [object valueForKey:propertyName];
        result = prime * result + [value hash];
    }

    return result;
}

Bây giờ, trong lớp tùy chỉnh của bạn, bạn có thể dễ dàng thực hiện isEqual:hash:

- (NSUInteger)hash
{
    return MagicHash(self);
}

- (BOOL)isEqual:(id)other
{
    return IsEqualObjects(self, other);
}

2

Lưu ý rằng nếu bạn đang tạo một đối tượng có thể bị thay đổi sau khi tạo, giá trị băm không được thay đổi nếu đối tượng được chèn vào bộ sưu tập. Thực tế, điều này có nghĩa là giá trị băm phải được cố định từ điểm tạo đối tượng ban đầu. Xem tài liệu của Apple về phương pháp -hash của giao thức NSObject để biết thêm thông tin:

Nếu một đối tượng có thể thay đổi được thêm vào bộ sưu tập sử dụng giá trị băm để xác định vị trí của đối tượng trong bộ sưu tập, giá trị được trả về bởi phương thức băm của đối tượng không được thay đổi trong khi đối tượng nằm trong bộ sưu tập. Do đó, phương thức băm không được dựa vào bất kỳ thông tin trạng thái bên trong nào của đối tượng hoặc bạn phải đảm bảo thông tin trạng thái bên trong của đối tượng không thay đổi trong khi đối tượng nằm trong bộ sưu tập. Vì vậy, ví dụ, một từ điển có thể thay đổi có thể được đặt trong bảng băm nhưng bạn không được thay đổi nó khi nó ở trong đó. (Lưu ý rằng có thể khó biết liệu một đối tượng nhất định có trong bộ sưu tập hay không.)

Điều này nghe có vẻ như hoàn toàn đánh vào tôi vì nó có khả năng khiến việc tra cứu băm trở nên kém hiệu quả hơn rất nhiều, nhưng tôi cho rằng tốt hơn hết là nên tránh lỗi và làm theo những gì tài liệu nói.


1
Bạn đang đọc các tài liệu băm sai - về cơ bản đó là một tình huống "hoặc - hoặc". Nếu đối tượng thay đổi, hàm băm nói chung cũng thay đổi. Đây thực sự là một cảnh báo cho lập trình viên, rằng nếu hàm băm thay đổi do thay đổi đối tượng, thì việc thay đổi đối tượng trong khi nó nằm trong bộ sưu tập sử dụng hàm băm sẽ gây ra hành vi không mong muốn. Nếu đối tượng phải "có thể thay đổi an toàn" trong tình huống như vậy, bạn không có lựa chọn nào khác ngoài việc làm cho hàm băm không liên quan đến trạng thái có thể thay đổi. Tình huống đặc biệt đó nghe có vẻ lạ đối với tôi, nhưng chắc chắn có những tình huống không thường xuyên xảy ra.
Quinn Taylor

1

Xin lỗi nếu tôi có nguy cơ phát ra một boffin hoàn chỉnh ở đây nhưng ... ... không ai bận tâm đến việc tuân theo 'các thực tiễn tốt nhất', bạn chắc chắn không nên chỉ định một phương thức bằng sẽ KHÔNG tính đến tất cả dữ liệu do đối tượng mục tiêu của bạn sở hữu, ví dụ như bất cứ điều gì dữ liệu được tổng hợp vào đối tượng của bạn, so với một liên kết của nó, nên được tính đến khi thực hiện bằng. Nếu bạn không muốn xem, hãy nói 'tuổi' trong một so sánh, sau đó bạn nên viết một bộ so sánh và sử dụng nó để thực hiện so sánh của bạn thay vì isEqual :.

Nếu bạn định nghĩa một phương thức isEqual: thực hiện so sánh bình đẳng một cách tùy tiện, bạn sẽ gặp rủi ro khi phương thức này bị lạm dụng bởi nhà phát triển khác, hoặc thậm chí chính bạn, một khi bạn đã quên 'xoắn' trong cách diễn giải bằng.

Ergo, mặc dù đây là một câu hỏi hay về băm, thông thường bạn không cần xác định lại phương pháp băm, thay vào đó bạn có thể nên xác định một bộ so sánh đặc biệt.

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.