แนวทางปฏิบัติที่ดีที่สุดสำหรับการเอาชนะ isEqual: และ hash


267

คุณจะแทนที่อย่างถูกต้องisEqual:ใน Objective-C ได้อย่างไร? "การจับ" ดูเหมือนว่าถ้าวัตถุสองตัวเท่ากัน (ตามที่กำหนดโดยisEqual:วิธีการ) พวกเขาจะต้องมีค่าแฮชเดียวกัน

วิปัสสนาส่วนของคู่มือความรู้พื้นฐานโกโก้จะมีตัวอย่างเกี่ยวกับวิธีการแทนที่isEqual:คัดลอกดังต่อไปนี้สำหรับการเรียนที่ชื่อ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;
}

มันตรวจสอบความเท่าเทียมกันของตัวชี้จากนั้นความเท่าเทียมกันของคลาสและในที่สุดก็เปรียบเทียบวัตถุที่ใช้isEqualToWidget:ซึ่งจะตรวจสอบnameและdataคุณสมบัติเท่านั้น อะไรเช่นไม่hashแสดงคือวิธีการแทนที่

สมมติว่ามีคุณสมบัติอื่น ๆ ageที่ไม่ส่งผลกระทบต่อความเสมอภาคการพูด ไม่ควรhashแทนที่เมธอดดังกล่าวเท่านั้นnameและdataส่งผลต่อแฮช และถ้าเป็นเช่นนั้นคุณจะทำอย่างไร เพียงเพิ่มแฮชของnameและdata? ตัวอย่างเช่น:

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

เพียงพอหรือไม่ มีเทคนิคที่ดีกว่านี้ไหม? ถ้าคุณมีพื้นฐานเช่นintอะไร แปลงNSNumberเป็นแฮชของพวกเขาเหรอ? หรือ structs เป็นNSRectอย่างไร

( สมองผายลม : เดิมเขียนว่า "bitwise OR" ร่วมกับพวก|=มันหมายถึงเพิ่ม)


2
if (![other isKindOfClass:[self class]])- เทคนิคนี้หมายถึงความเท่าเทียมกันจะไม่สลับกัน Ie A = B ไม่ได้หมายถึง B = A (เช่นถ้ามีคลาสย่อยของอีกอัน)
Robert

ลิงก์เอกสารตายแล้วตอนนี้ถูกเก็บไว้ที่Introspection
jedwidz

คำตอบ:


111

เริ่มกับ

 NSUInteger prime = 31;
 NSUInteger result = 1;

จากนั้นสำหรับทุกครั้งที่คุณทำ

 result = prime * result + var

สำหรับวัตถุที่คุณใช้ 0 สำหรับศูนย์และอื่น ๆ แฮชรหัสของพวกเขา

 result = prime * result + [var hash];

สำหรับบูลีนคุณใช้สองค่าที่ต่างกัน

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

คำอธิบายและแสดงที่มา

นี่ไม่ใช่งานของ tcurdt และความคิดเห็นต่างก็ขอคำอธิบายเพิ่มเติมดังนั้นฉันเชื่อว่าการแก้ไขเพื่อระบุแหล่งที่มานั้นยุติธรรม

ขั้นตอนวิธีการนี้เป็นที่นิยมในหนังสือ "มีผลบังคับใช้ Java" และบทที่เกี่ยวข้องปัจจุบันสามารถพบได้ทั่วไปที่นี่ หนังสือเล่มนี้นิยมใช้อัลกอริธึมซึ่งตอนนี้เป็นค่าเริ่มต้นในแอปพลิเคชัน Java จำนวนมาก (รวมถึง Eclipse) มันมาจากการใช้งานที่เก่ากว่าซึ่งมีสาเหตุมาจาก Dan Bernstein หรือ Chris Torek อัลกอริทึมที่เก่ากว่านั้นลอยอยู่รอบ ๆ Usenet และการระบุแหล่งที่มาบางอย่างนั้นยาก ตัวอย่างเช่นมีคำอธิบายที่น่าสนใจในรหัส Apache นี้ (ค้นหาชื่อ) ที่อ้างอิงถึงแหล่งที่มาดั้งเดิม

บรรทัดล่างคือนี่เป็นอัลกอริทึมคร่ำครึแบบง่าย ๆ มันไม่ใช่นักแสดงที่เก่งที่สุดและมันก็ไม่ได้พิสูจน์ด้วยซ้ำว่าเป็นอัลกอริทึมที่ "ดี" แต่มันง่ายและผู้คนจำนวนมากใช้มันมาเป็นเวลานานด้วยผลลัพธ์ที่ดีดังนั้นมันจึงมีการสนับสนุนทางประวัติศาสตร์มากมาย


9
1231: 1237 มาจากไหน ฉันเห็นมันใน Java ของ Boolean.hashCode () ด้วย มันวิเศษไหม?
David Leonard

17
มันเป็นธรรมชาติของอัลกอริทึมคร่ำเครียดที่จะมีการชนกัน ดังนั้นฉันไม่เห็นประเด็นของคุณพอล
tcurdt

85
ในความเห็นของฉันคำตอบนี้ไม่ตอบคำถามจริง (แนวทางปฏิบัติที่ดีที่สุดสำหรับการเอาชนะแฮชของ NSObject) มันมีอัลกอริธึมแฮชหนึ่งอัน ยิ่งไปกว่านั้นความกระจัดกระจายของคำอธิบายทำให้เข้าใจยากโดยปราศจากความรู้อย่างลึกซึ้งเกี่ยวกับเรื่องนี้และอาจส่งผลให้คนใช้โดยไม่ทราบว่าพวกเขากำลังทำอะไร ฉันไม่เข้าใจว่าทำไมคำถามนี้มี upvotes มากมาย
Ricardo Sanchez-Saez

6
ปัญหาที่ 1 - (int) มีขนาดเล็กและล้นง่ายใช้ NSUInteger ปัญหาที่สอง - หากคุณคูณผลลัพธ์ด้วยแฮชตัวแปรแต่ละตัวผลลัพธ์ของคุณจะล้น เช่น. [แฮช NSString] สร้างค่าจำนวนมาก หากคุณมี 5+ ตัวแปรมันง่ายที่จะล้นด้วยอัลกอริทึมนี้ มันจะส่งผลในการแมปทุกอย่างไปยังแฮชเดียวกันซึ่งไม่ดี ดูคำตอบของฉัน: stackoverflow.com/a/4393493/276626
Paul Solt

10
@ PaulSolt - ล้นไม่ได้เป็นปัญหาในการสร้างแฮชการชนคือ แต่การโอเวอร์โฟลไม่จำเป็นต้องชนกันมากนักและคำสั่งของคุณเกี่ยวกับการโอเวอร์โฟลว์ทำให้ทุกอย่างเพื่อแมปแฮชเดียวกันนั้นไม่ถูกต้อง
DougW

81

ฉันแค่หยิบ Objective-C ขึ้นมาด้วยตัวเองดังนั้นฉันจึงไม่สามารถพูดสำหรับภาษานั้นโดยเฉพาะ แต่ในภาษาอื่น ๆ ที่ฉันใช้ถ้าสองอินสแตนซ์เป็น "เท่ากับ" พวกเขาจะต้องส่งคืนแฮชเดียวกัน - มิฉะนั้นคุณจะมีทั้งหมด เรียงลำดับปัญหาเมื่อพยายามใช้เป็นกุญแจใน hashtable (หรือคอลเล็กชันประเภทพจนานุกรม)

ในทางกลับกันหาก 2 อินสแตนซ์ไม่เท่ากันอาจมีแฮชที่เหมือนกันหรือดีกว่าก็ได้ นี่คือความแตกต่างระหว่างการค้นหา O (1) บนตารางแฮชและการค้นหา O (N) - หากแฮชทั้งหมดของคุณชนกันคุณอาจพบว่าการค้นหาตารางของคุณนั้นไม่ได้ดีไปกว่าการค้นหารายการ

ในแง่ของแนวปฏิบัติที่ดีที่สุดแฮชของคุณควรส่งคืนการแจกแจงแบบสุ่มของค่าสำหรับอินพุต ตัวอย่างเช่นถ้าคุณมีสองเท่า แต่ค่าส่วนใหญ่ของคุณมีแนวโน้มที่จะจัดกลุ่มระหว่าง 0 ถึง 100 คุณจะต้องตรวจสอบให้แน่ใจว่าแฮชที่ส่งคืนโดยค่าเหล่านั้นมีการกระจายอย่างสม่ำเสมอทั่วทั้งค่าแฮชที่เป็นไปได้ทั้งหมด . สิ่งนี้จะปรับปรุงประสิทธิภาพของคุณอย่างมีนัยสำคัญ

มีอัลกอริทึมการแปลงแป้นพิมพ์จำนวนมากรวมถึงรายการที่นี่ ฉันพยายามหลีกเลี่ยงการสร้างอัลกอริธึมการแฮชใหม่เนื่องจากอาจมีผลกระทบต่อประสิทธิภาพการทำงานขนาดใหญ่ดังนั้นการใช้วิธีแฮชที่มีอยู่และทำการผสมผสานแบบเรียงลำดับบางอย่างตามที่คุณทำในตัวอย่างของคุณเป็นวิธีที่ดี


4
+1 คำตอบที่ยอดเยี่ยมสมควรได้รับการโหวตมากขึ้นโดยเฉพาะอย่างยิ่งเมื่อเขาพูดถึง "แนวปฏิบัติที่ดีที่สุด" และทฤษฎีที่อยู่เบื้องหลังว่าทำไมแฮชที่ดี (มีเอกลักษณ์) จึงสำคัญ
Quinn Taylor

30

แฮคเกอร์ที่ง่ายกว่าค่าแฮชของคุณสมบัติที่สำคัญคือเพียงพอ 99% ของเวลา

ตัวอย่างเช่น:

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

พบโซลูชั่นที่http://nshipster.com/equality/โดย Mattt Thompson (ซึ่งยังส่งคำถามนี้ในโพสต์ของเขา!)


1
ปัญหาของคำตอบนี้คือไม่คำนึงถึงคุณค่าดั้งเดิมใด ๆ เลย และค่าดั้งเดิมอาจสำคัญสำหรับการแปลงแป้นพิมพ์ด้วย
Vive

@Vive ปัญหาเหล่านี้ส่วนใหญ่ได้รับการแก้ไขใน Swift แต่โดยปกติแล้วประเภทเหล่านี้จะเป็นตัวแทนของแฮชของตนเองเนื่องจากเป็นแบบดั้งเดิม
Yariv Nissim

1
ในขณะที่คุณเหมาะสมกับ Swift ยังมีอีกหลายโครงการที่เขียนด้วย objc เพราะคำตอบของคุณทุ่มเทเพื่อ objc มันมีค่าอย่างน้อยพูดถึง
Vive

ค่าแฮแฮร์ของ XOR ร่วมกันเป็นคำแนะนำที่ไม่ดีมันนำไปสู่การชนกันของแฮชจำนวนมาก ให้คูณด้วยจำนวนเฉพาะแล้วเพิ่มตามที่คำตอบอื่น ๆ ระบุ
fishinear

27

ฉันพบว่าเธรดนี้มีประโยชน์อย่างยิ่งในการจัดหาทุกสิ่งที่ฉันต้องการเพื่อนำไปใช้isEqual:และhashวิธีการที่นำไปใช้ด้วยการจับ เมื่อทดสอบตัวแปรอินสแตนซ์ของวัตถุในisEqual:รหัสตัวอย่างใช้:

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

สิ่งนี้ล้มเหลวซ้ำ ๆ ( เช่นส่งคืนNO ) โดยไม่มีและข้อผิดพลาดเมื่อฉันรู้ว่าวัตถุนั้นเหมือนกันในการทดสอบหน่วยของฉัน เหตุผลคือหนึ่งในNSStringตัวแปรอินสแตนซ์ไม่มีข้อมูลดังนั้นข้อความข้างต้นคือ:

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

และเนื่องจากศูนย์จะตอบสนองต่อวิธีการใด ๆ จึงเป็นสิ่งที่ถูกกฎหมายอย่างสมบูรณ์

[nil isEqual: nil]

ผลตอบแทนที่ศูนย์ซึ่งเป็นNOดังนั้นเมื่อทั้งวัตถุและเป็นหนึ่งในการทดสอบมีศูนย์วัตถุที่พวกเขาจะได้รับการพิจารณาไม่เท่ากัน ( เช่น , isEqual:จะกลับNO )

การแก้ไขง่ายๆนี้คือการเปลี่ยนคำสั่ง if เป็น:

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

ด้วยวิธีนี้ถ้าที่อยู่ของพวกเขาเหมือนกันก็จะข้ามวิธีการโทรไม่ว่าพวกเขาจะเป็นศูนย์หรือทั้งสองชี้ไปที่วัตถุเดียวกัน แต่ถ้าทั้งสองไม่ได้เป็นศูนย์หรือพวกเขาชี้ไปที่วัตถุที่แตกต่างกัน

ฉันหวังว่าจะช่วยให้ใครบางคนเกาหัวไม่กี่นาที


20

ฟังก์ชันแฮชควรสร้างค่ากึ่งพิเศษที่ไม่น่าจะขัดแย้งหรือตรงกับค่าแฮชของวัตถุอื่น

นี่คือฟังก์ชั่นแฮชเต็มรูปแบบซึ่งสามารถปรับให้เข้ากับตัวแปรอินสแตนซ์คลาสของคุณ มันใช้ NSUInteger มากกว่า int เพื่อความเข้ากันได้กับแอปพลิเคชัน 64/32 บิต

หากผลลัพธ์กลายเป็น 0 สำหรับวัตถุต่าง ๆ คุณจะเสี่ยงต่อการชนของแฮช การแฮชการชนอาจส่งผลให้เกิดการทำงานของโปรแกรมโดยไม่คาดคิดเมื่อทำงานกับคลาสการรวบรวมบางส่วนที่ขึ้นอยู่กับฟังก์ชันแฮช ตรวจสอบให้แน่ใจว่าได้ทดสอบฟังก์ชันแฮชของคุณก่อนใช้งาน

-(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
หนึ่ง gotcha ที่นี่: ฉันชอบที่จะหลีกเลี่ยงการไวยากรณ์จุดดังนั้นผมจึงเปลี่ยนคำสั่งบูลของคุณให้เป็น result = prime * result + [self isSelected] ? yesPrime : noPrime;(เช่น) ฉันพบว่านี่เป็นการตั้งค่าresultเป็น (เช่น) 1231ฉันถือว่าเนื่องจาก?ผู้ปฏิบัติงานมีความสำคัญกว่า ฉันแก้ไขปัญหาด้วยการเพิ่มตัวยึด:result = prime * result + ([self isSelected] ? yesPrime : noPrime);
แอชลีย์

12

วิธีที่ง่าย แต่ไม่มีประสิทธิภาพคือการส่งคืน-hashค่าเดียวกันสำหรับทุกอินสแตนซ์ มิฉะนั้นคุณต้องใช้แฮชตามวัตถุที่มีผลต่อความเท่าเทียมกันเท่านั้น นี่เป็นเรื่องยากหากคุณใช้การเปรียบเทียบแบบหละหลวมใน-isEqual:(เช่นการเปรียบเทียบสตริงตัวพิมพ์เล็กและตัวพิมพ์ใหญ่) สำหรับ ints คุณสามารถใช้ int เองได้เว้นแต่ว่าคุณจะเปรียบเทียบกับ NSNumbers

อย่าใช้ | = แต่จะทำให้อิ่มตัว ใช้ ^ = แทน

สุ่มจริงสนุก: แต่[[NSNumber numberWithInt:0] isEqual:[NSNumber numberWithBool:NO]] [[NSNumber numberWithInt:0] hash] != [[NSNumber numberWithBool:NO] hash](rdar: // 4538282 เปิดให้บริการตั้งแต่ 05 พฤษภาคม 2549)


1
คุณถูกต้องแล้วใน | = ไม่ได้หมายความอย่างนั้นจริงๆ :) + = และ ^ = มีความเท่าเทียมกัน คุณจะจัดการแบบดั้งเดิมที่ไม่ใช่จำนวนเต็มเช่น double และ float ได้อย่างไร
Dave Dribin

สุ่มสนุกจริง: การทดสอบบนเสือดาวหิมะ ... ;-)
ควินน์เทย์เลอร์

เขาถูกต้องเกี่ยวกับการใช้ XOR แทนหรือสำหรับการรวมเขตข้อมูลลงในแฮช อย่างไรก็ตามอย่าใช้คำแนะนำในการส่งคืนค่า -hash เดียวกันสำหรับทุกวัตถุแม้ว่าจะง่าย แต่ก็สามารถลดประสิทธิภาพการทำงานของสิ่งใดก็ตามที่ใช้แฮชของวัตถุได้อย่างรุนแรง แฮชไม่จำเป็นต้องแตกต่างกันไปสำหรับวัตถุที่ไม่เท่ากัน แต่ถ้าคุณสามารถทำสิ่งนั้นได้มันก็ไม่มีอะไรเหมือนกัน
Quinn Taylor

รายงานข้อผิดพลาดเรดาร์แบบเปิดถูกปิด openradar.me/4538282นั่นหมายความว่าอย่างไร
JJD

JJD ข้อผิดพลาดได้รับการแก้ไขใน Mac OS X 10.6 ตามที่ Quinn บอกใบ้ (โปรดทราบว่าความคิดเห็นนี้มีอายุสองปีแล้ว)
Jens Ayton

9

โปรดจำไว้ว่าคุณเพียงแค่ต้องมอบแฮชที่เท่ากันเมื่อisEqualเป็นจริง เมื่อisEqualใดก็ตามที่เป็นเท็จแฮชไม่จำเป็นต้องไม่เท่าเทียมกัน ดังนั้น:

แฮชง่าย ๆ เลือกตัวแปรสมาชิก (หรือสมาชิกไม่กี่คน) ที่โดดเด่นที่สุด

ตัวอย่างเช่นสำหรับ CLPlacemark ชื่อเท่านั้นก็เพียงพอแล้ว ใช่มี 2 หรือ 3 ข้อแตกต่าง CLPlacemark ที่มีชื่อเหมือนกันทุกประการ แต่หายาก ใช้แฮชนั้น

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

@implementation CLPlacemark (equal)

...

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


@end

แจ้งให้ทราบฉันไม่รำคาญที่จะระบุเมืองประเทศ ฯลฯ ชื่อก็เพียงพอแล้ว บางทีชื่อและ CLLocation

ควรแจกจ่ายแฮชอย่างสม่ำเสมอ ดังนั้นคุณสามารถรวมตัวแปรสมาชิกหลายคนโดยใช้เครื่องหมายรูปหมวก ^ (เครื่องหมาย xor)

ดังนั้นมันจึงเป็นเช่นนั้น

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

ด้วยวิธีนี้แฮชจะถูกกระจายอย่างเท่าเทียมกัน

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

ดังนั้นจะทำอย่างไรในอาร์เรย์?

ง่ายอีกครั้ง คุณไม่ต้องแฮชสมาชิกทั้งหมดของอาเรย์ พอที่จะแฮชองค์ประกอบแรกองค์ประกอบสุดท้ายการนับบางทีองค์ประกอบกลางและนั่นก็คือ


ค่าแฮแฮ XORing ไม่ได้เป็นการแจกแจงแบบสม่ำเสมอ
fishinear

7

แน่นอนว่าวิธีที่ง่ายกว่าในการทำเช่นนี้คือการแทนที่ครั้งแรก- (NSString )descriptionและให้การแสดงสตริงของสถานะวัตถุของคุณ (คุณต้องแสดงสถานะทั้งหมดของวัตถุในสตริงนี้)

จากนั้นให้ดำเนินการhashดังนี้:

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

สิ่งนี้ขึ้นอยู่กับหลักการที่ว่า "ถ้าวัตถุสตริงสองตัวมีค่าเท่ากัน (ตามที่กำหนดโดยเมธอด isEqualToString:) พวกเขาจะต้องมีค่าแฮชเดียวกัน"

ที่มา: การอ้างอิงระดับ NSString


1
นี่ถือว่าวิธีการอธิบายจะไม่ซ้ำกัน การใช้แฮชของคำอธิบายสร้างการพึ่งพาซึ่งอาจไม่ชัดเจนและมีความเสี่ยงสูงในการชน
Paul Solt

1
+1 โหวตแล้ว นี่เป็นความคิดที่ยอดเยี่ยม หากคุณกลัวว่าคำอธิบายทำให้เกิดการชนกันคุณก็สามารถแทนที่ได้
user4951

ขอบคุณ Jim ฉันจะไม่ปฏิเสธว่านี่เป็นบิตของการแฮ็ก แต่มันจะทำงานได้ในทุกกรณีที่ฉันสามารถนึกถึง - และอย่างที่ฉันบอกถ้าคุณแทนที่descriptionฉันไม่เห็นว่าทำไมมันถึงด้อยกว่า โซลูชันที่ได้รับคะแนนโหวตสูงกว่าใด ๆ อาจไม่ใช่วิธีการแก้ปัญหาทางคณิตศาสตร์ที่สวยงามที่สุด แต่ควรทำเคล็ดลับ ดังที่ Brian B. กล่าวไว้ (คำตอบที่ถูกโหวตสูงสุดในตอนนี้): "ฉันพยายามหลีกเลี่ยงการสร้างอัลกอริธึมการแฮชใหม่" - ตกลง! - ฉันเพิ่ง! hashNSString
Jonathan Ellis

โหวตขึ้นเนื่องจากเป็นแนวคิดที่เรียบร้อย ฉันจะไม่ใช้มันเพราะฉันกลัวการจัดสรร NSString เพิ่มเติม
karwag

1
นี่ไม่ใช่โซลูชันทั่วไปเนื่องจากสำหรับคลาสส่วนใหญ่จะdescriptionมีที่อยู่พอยน์เตอร์ นี่ทำให้สองอินสแตนซ์ที่ต่างกันของคลาสเดียวกันซึ่งเท่ากับแฮชที่แตกต่างกันซึ่งละเมิดสมมติฐานขั้นพื้นฐานที่ว่าสองวัตถุที่เท่ากันนั้นมีแฮชเดียวกัน!
Diogo T

5

สัญญาที่เท่าเทียมกันและแฮชมีการระบุไว้อย่างดีและมีการวิจัยอย่างละเอียดในโลก Java (ดูคำตอบของ @ mipardi) แต่การพิจารณาเดียวกันทั้งหมดควรนำไปใช้กับ Objective-C

Eclipse ทำงานที่เชื่อถือได้ในการสร้างวิธีการเหล่านี้ใน Java ดังนั้นนี่คือตัวอย่าง Eclipse ที่พอร์ตด้วยมือไปยัง 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;
}

และสำหรับคลาสย่อยYourWidgetที่เพิ่มคุณสมบัติ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;
}

การใช้งานนี้จะช่วยหลีกเลี่ยงข้อผิดพลาดในการทำคลาสย่อยในตัวอย่างisEqual:จาก Apple:

  • การทดสอบระดับของ Apple other isKindOfClass:[self class]นั้นไม่สมมาตรสำหรับคลาสย่อยที่แตกต่างกันสองMyWidgetรายการ ความเท่าเทียมกันจะต้องมีความสมมาตร: a = b หาก b = a สิ่งนี้สามารถแก้ไขได้อย่างง่ายดายโดยเปลี่ยนการทดสอบเป็นคลาสย่อยother isKindOfClass:[MyWidget class]ทั้งหมดMyWidgetจะเปรียบเทียบกัน
  • การใช้การisKindOfClass:ทดสอบคลาสย่อยป้องกันคลาสย่อยจากการแทนที่isEqual:ด้วยการทดสอบความเท่าเทียมที่ได้รับการปรับปรุง นี่เป็นเพราะความเสมอภาคจะต้องมีการถ่ายทอด: ถ้า a = b และ a = c ดังนั้น b = c หากMyWidgetอินสแตนซ์เปรียบเทียบเท่ากับสองYourWidgetอินสแตนซ์ดังนั้นYourWidgetอินสแตนซ์เหล่านั้นจะต้องเปรียบเทียบกันด้วยกันแม้ว่าจะมีความserialNoแตกต่างกันก็ตาม

ปัญหาที่สองสามารถแก้ไขได้โดยพิจารณาเฉพาะวัตถุที่จะเท่ากันหากพวกเขาอยู่ในระดับเดียวกันแน่นอนดังนั้นการ[self class] != [object class]ทดสอบที่นี่ สำหรับคลาสแอปพลิเคชันทั่วไปนี่น่าจะเป็นวิธีที่ดีที่สุด

อย่างไรก็ตามมีบางกรณีที่การisKindOfClass:ทดสอบดีกว่า นี่เป็นเรื่องปกติของคลาสเฟรมเวิร์กมากกว่าคลาสแอปพลิเคชัน ตัวอย่างเช่นใด ๆ ที่NSStringควรเปรียบเทียบกับอื่น ๆ ที่NSStringมีลำดับตัวละครพื้นฐานเดียวกันโดยไม่คำนึงถึงNSString/ NSMutableStringความแตกต่างและยังไม่คำนึงถึงสิ่งที่เรียนส่วนตัวในNSStringคลัสเตอร์ชั้นเรียน

ในกรณีเช่นนี้isEqual:ควรมีพฤติกรรมที่กำหนดไว้อย่างดีมีเอกสารชัดเจนและควรมีความชัดเจนว่าคลาสย่อยไม่สามารถแทนที่สิ่งนี้ได้ ใน Java ข้อ จำกัด 'no override' สามารถบังคับใช้โดยการตั้งค่าสถานะเท่ากับและวิธีแฮชโค้ดในขณะที่finalแต่ Objective-C นั้นไม่เทียบเท่ากัน


@adubr นั่นครอบคลุมในสองย่อหน้าสุดท้ายของฉัน มันไม่ได้โฟกัสตามที่MyWidgetเข้าใจกันว่าจะไม่เป็นกลุ่มคลัสเตอร์
jedwidz

5

นี่ไม่ได้ตอบคำถามของคุณโดยตรง (เลย) แต่ฉันเคยใช้ MurmurHash ก่อนที่จะสร้างแฮช: murmurhash

คิดว่าฉันควรอธิบายว่าทำไม: murmurhash เร็วกว่าเลือด ...


2
ไลบรารี C ++ ที่เน้นการแฮชที่ไม่ซ้ำกันสำหรับคีย์ void * โดยใช้หมายเลขสุ่ม (และยังไม่เกี่ยวข้องกับวัตถุ Objective-C) จริงๆแล้วมันไม่ได้เป็นคำแนะนำที่เป็นประโยชน์ที่นี่ เมธอดแฮชควรส่งคืนค่าที่สอดคล้องกันในแต่ละครั้งหรือจะไร้ประโยชน์อย่างเต็มที่ หากมีการเพิ่มวัตถุลงในคอลเลกชันที่เรียก -hash และส่งคืนค่าใหม่ทุกครั้งจะไม่มีการตรวจพบรายการที่ซ้ำกันและคุณไม่สามารถเรียกคืนวัตถุจากคอลเลกชันได้ ในกรณีนี้คำว่า "แฮช" นั้นแตกต่างจากความหมายในการรักษาความปลอดภัย / การเข้ารหัส
Quinn Taylor

3
murmurhash ไม่ได้เป็นฟังก์ชันแฮชการเข้ารหัสลับ โปรดตรวจสอบข้อเท็จจริงของคุณก่อนโพสต์ข้อมูลที่ไม่ถูกต้อง Murmurhash อาจมีประโยชน์สำหรับ hashing คลาส c-custom ที่กำหนดเอง (โดยเฉพาะถ้าคุณมี NSDatas จำนวนมากที่เกี่ยวข้อง) เพราะมันรวดเร็วมาก อย่างไรก็ตามฉันอนุญาตให้คุณรู้ว่าอาจจะแนะนำว่าไม่ใช่คำแนะนำที่ดีที่สุดที่จะมอบให้กับใครบางคน "แค่เลือกวัตถุประสงค์ -c" แต่โปรดสังเกตคำนำหน้าของฉันในการตอบคำถามเดิมของฉัน
schwa

5

ฉันพบว่าหน้านี้เป็นคู่มือที่เป็นประโยชน์ในการแทนที่วิธี equals- และ hash-type มันมีอัลกอริทึมที่เหมาะสมสำหรับการคำนวณรหัสแฮช หน้านี้มุ่งเน้นไปที่ Java แต่มันค่อนข้างง่ายที่จะปรับให้เข้ากับ Objective-C / Cocoa


1
ลิงค์แคชผ่าน archive.org: web.archive.org/web/20071013053633/http://www.geocities.com/…
cobbal

4

ผมเป็นมือใหม่วัตถุประสงค์ C เกินไป แต่ผมพบว่าบทความดีเกี่ยวกับความเท่าเทียมกันกับตัวตนในวัตถุประสงค์ C ที่นี่ จากการอ่านของฉันดูเหมือนว่าคุณอาจจะสามารถใช้ฟังก์ชันแฮชเริ่มต้น (ซึ่งควรให้ข้อมูลเฉพาะตัว) และใช้เมธอด isEqual เพื่อเปรียบเทียบค่าข้อมูล


ฉันเป็นสมาชิกใหม่ของ Cocoa / Objective C และคำตอบและลิงก์นี้ช่วยให้ฉันสามารถตัดสิ่งที่ก้าวหน้ากว่าทั้งหมดไปยังบรรทัดล่าง - ฉันไม่ต้องกังวลเกี่ยวกับแฮช - เพียงแค่ใช้วิธี isEqual: ขอบคุณ!
John Gallagher

อย่าพลาดลิงค์ของ @ ceperry บทความEquality vs Identityโดยคาร์ลคราฟท์นั้นดีจริงๆ
JJD

6
@ จอห์น: ฉันคิดว่าคุณควรอ่านบทความอีกครั้ง มันบอกอย่างชัดเจนว่า "อินสแตนซ์ที่เท่ากันต้องมีค่าแฮชเท่ากัน" หากคุณแทนที่isEqual:คุณต้องแทนที่hashด้วย
Steve Madsen

3

ควินน์เป็นเพียงผิดที่อ้างอิงถึงการแฮชบ่นไม่มีประโยชน์ที่นี่ ควินน์พูดถูกที่คุณต้องการเข้าใจทฤษฎีที่อยู่เบื้องหลังการคร่ำครวญ เสียงพึมพำกลั่นกรองทฤษฎีจำนวนมากไปสู่การปฏิบัติ การหาวิธีนำการใช้งานไปใช้กับแอปพลิเคชันนี้นั้นคุ้มค่าที่จะสำรวจ

ประเด็นสำคัญบางประการที่นี่:

ตัวอย่างฟังก์ชันจาก tcurdt แนะนำว่า '31' เป็นตัวคูณที่ดีเพราะมันยอดเยี่ยม เราต้องแสดงให้เห็นว่าการเป็นนายกเป็นเงื่อนไขที่จำเป็นและเพียงพอ ในความเป็นจริง 31 (และ 7) อาจไม่ใช่ช่วงเวลาที่ดีโดยเฉพาะอย่างยิ่งเพราะ 31 == -1% 32 ตัวคูณคี่ที่มีประมาณครึ่งบิตที่ตั้งค่าและครึ่งบิตที่ชัดเจนน่าจะดีกว่า (ค่าคงที่การคูณแฮชบ่นมีคุณสมบัตินั้น)

ฟังก์ชันแฮชประเภทนี้มีแนวโน้มว่าจะแข็งแกร่งขึ้นหากหลังจากการคูณค่าผลลัพธ์จะถูกปรับผ่านกะและ xor การคูณมีแนวโน้มที่จะสร้างผลลัพธ์ของการโต้ตอบจำนวนมากที่ส่วนท้ายสุดของการลงทะเบียนและผลการโต้ตอบที่ต่ำที่ด้านล่างสุดของการลงทะเบียน กะและ xor เพิ่มปฏิสัมพันธ์ที่ด้านล่างสุดของการลงทะเบียน

การตั้งค่าผลลัพธ์เริ่มต้นให้เป็นค่าโดยที่ประมาณครึ่งหนึ่งบิตเป็นศูนย์และประมาณครึ่งบิตเป็นค่าเดียวก็มีแนวโน้มที่จะเป็นประโยชน์เช่นกัน

มันอาจมีประโยชน์ที่จะต้องระมัดระวังเกี่ยวกับลำดับที่องค์ประกอบรวมกัน หนึ่งอาจเป็นครั้งแรกที่ควรดำเนินการบูลีนและองค์ประกอบอื่น ๆ ที่ค่าจะไม่กระจายอย่างยิ่ง

มันอาจจะมีประโยชน์ในการเพิ่มสองสามขั้นตอนในช่วงท้ายของการคำนวณ

แฮชบ่นไม่ว่าจริงหรือเร็วสำหรับแอปพลิเคชันนี้เป็นคำถามเปิดหรือไม่ เสียงบ่นแฮชผสมบิตของคำที่ป้อนแต่ละคำ คำที่ป้อนหลายคำสามารถประมวลผลแบบขนานซึ่งจะช่วยให้ซีพียู pipelined หลายปัญหา


3

เมื่อรวมคำตอบของ @ tcurdt เข้ากับคำตอบของ @ oscar-gomez เพื่อรับชื่อคุณสมบัติเราสามารถสร้างโซลูชันดร็อปอินแบบง่ายสำหรับ isEqual และแฮช:

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;
}

ตอนนี้ในคลาสที่คุณกำหนดเองคุณสามารถนำไปใช้isEqual:และhash:

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

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

2

โปรดทราบว่าหากคุณกำลังสร้างวัตถุที่สามารถกลายพันธุ์ได้หลังจากสร้างแล้วค่าแฮชจะต้องไม่เปลี่ยนแปลงหากมีการแทรกวัตถุลงในคอลเลกชัน พูดจริงหมายความว่าค่าแฮชต้องได้รับการแก้ไขจากจุดเริ่มต้นของการสร้างวัตถุ ดูเอกสารของ Apple เกี่ยวกับวิธีการแฮชของโปรโตคอล NSObjectสำหรับข้อมูลเพิ่มเติม:

หากมีการเพิ่มวัตถุที่ไม่แน่นอนลงในคอลเลกชันที่ใช้ค่าแฮชเพื่อกำหนดตำแหน่งของวัตถุในคอลเลกชันค่าที่ส่งคืนโดยเมธอดแฮชของวัตถุจะต้องไม่เปลี่ยนแปลงในขณะที่วัตถุนั้นอยู่ในคอลเล็กชัน ดังนั้นวิธีการแฮชต้องไม่พึ่งพาข้อมูลสถานะภายในของวัตถุใด ๆ หรือคุณต้องตรวจสอบให้แน่ใจว่าข้อมูลสถานะภายในของวัตถุไม่เปลี่ยนแปลงในขณะที่วัตถุอยู่ในคอลเลกชัน ยกตัวอย่างเช่นพจนานุกรมที่ไม่แน่นอนสามารถวางในตารางแฮช แต่คุณต้องไม่เปลี่ยนในขณะที่มันอยู่ในนั้น (โปรดทราบว่าอาจเป็นเรื่องยากที่จะรู้ว่าวัตถุที่กำหนดอยู่ในชุดสะสมหรือไม่)

ฟังดูเหมือนการตีสองหน้าที่สมบูรณ์สำหรับฉันเพราะอาจทำให้การค้นหาแฮชมีประสิทธิภาพน้อยลง แต่ฉันคิดว่ามันจะดีกว่าถ้าทำผิดด้านความระมัดระวังและปฏิบัติตามสิ่งที่เอกสารระบุไว้


1
คุณกำลังอ่านเอกสารแฮชอย่างไม่ถูกต้องซึ่งเป็นสถานการณ์ที่ "หรือ" หากวัตถุมีการเปลี่ยนแปลงแฮชโดยทั่วไปก็จะเปลี่ยนไปเช่นกัน นี่เป็นคำเตือนสำหรับโปรแกรมเมอร์จริงๆว่าหากแฮชเปลี่ยนไปเนื่องจากการกลายพันธุ์ของวัตถุการเปลี่ยนวัตถุในขณะที่มันอยู่ในคอลเล็กชันที่ใช้แฮชจะทำให้เกิดพฤติกรรมที่ไม่คาดคิด หากวัตถุนั้นต้อง "ปลอดภัยไม่แน่นอน" ในสถานการณ์เช่นนี้คุณไม่มีทางเลือกนอกจากต้องทำให้แฮชไม่เกี่ยวข้องกับสถานะที่ไม่แน่นอน สถานการณ์เฉพาะนั้นฟังดูแปลกสำหรับฉัน แต่มีสถานการณ์ที่ไม่บ่อยนักที่นำไปใช้
Quinn Taylor

1

ขออภัยถ้าฉันมีความเสี่ยงที่จะรับฟังโลงศพที่สมบูรณ์ที่นี่ แต่ ... ไม่มีใครใส่ใจที่จะกล่าวถึงการปฏิบัติตาม 'แนวทางปฏิบัติที่ดีที่สุด' คุณไม่ควรระบุวิธีการเท่ากับที่ไม่คำนึงถึงข้อมูลทั้งหมดที่เป็นวัตถุเป้าหมายของคุณ ข้อมูลจะถูกรวมเข้ากับวัตถุของคุณเมื่อเทียบกับการเชื่อมโยงของข้อมูลนั้นควรนำมาพิจารณาเมื่อมีการใช้งานเท่ากับ หากคุณไม่ต้องการใช้คำว่า 'อายุ' ในการเปรียบเทียบคุณควรเขียนตัวเปรียบเทียบและใช้เพื่อทำการเปรียบเทียบของคุณแทน isEqual:

หากคุณกำหนด isEqual: วิธีการที่ทำการเปรียบเทียบความเสมอภาคคุณมีความเสี่ยงที่วิธีการนี้ถูกใช้ในทางที่ผิดโดยผู้พัฒนารายอื่นหรือแม้แต่ตัวคุณเองเมื่อคุณลืม 'การบิด' ในการตีความเท่ากับ

ถึงแม้ว่านี่เป็นคำถามที่ยอดเยี่ยมเกี่ยวกับการแฮ็ก แต่คุณไม่จำเป็นต้องกำหนดวิธีการแฮชใหม่คุณควรกำหนด ad-hoc comparator แทน

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.