Praktik terbaik untuk mengesampingkan isEqual: dan hash

267

Bagaimana Anda menimpa dengan benar isEqual:di Objective-C? "Tangkapan" tampaknya bahwa jika dua objek sama (seperti yang ditentukan oleh isEqual:metode), mereka harus memiliki nilai hash yang sama.

Bagian Introspeksi dari Panduan Dasar-Dasar Kakao memang memiliki contoh tentang cara menimpa isEqual:, disalin sebagai berikut, untuk kelas bernama 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;
}

Ini memeriksa kesetaraan pointer, kemudian kelas kesetaraan, dan akhirnya membandingkan objek menggunakan isEqualToWidget:, yang hanya memeriksa namedan dataproperti. Apa yang tidak ditunjukkan contoh ini adalah cara menimpa hash.

Anggaplah ada properti lain yang tidak memengaruhi kesetaraan, katakanlah age. Bukankah seharusnya hashmetode diganti sehingga hanya namedan datamempengaruhi hash? Dan jika demikian, bagaimana Anda akan melakukannya? Cukup tambahkan hash dari namedan data? Sebagai contoh:

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

Apakah itu cukup? Apakah ada teknik yang lebih baik? Bagaimana jika Anda memiliki primitif, seperti int? Konversikan mereka ke NSNumberuntuk mendapatkan hash mereka? Atau struct suka NSRect?

( Brain fart : Awalnya menulis "bitwise OR" bersama dengan mereka |=. Dimaksudkan menambahkan.)

Dave Dribin
sumber
2
if (![other isKindOfClass:[self class]])- Ini secara teknis berarti kesetaraan tidak akan bersifat komutatif. Yaitu A = B tidak berarti B = A (misalnya jika satu adalah subkelas yang lain)
Robert
Tautan dokumentasi sudah mati, sekarang diarsipkan ke Introspeksi
jedwidz

Jawaban:

111

Dimulai dari

 NSUInteger prime = 31;
 NSUInteger result = 1;

Lalu untuk setiap primitif yang Anda lakukan

 result = prime * result + var

Untuk objek yang Anda gunakan 0 untuk nihil dan jika tidak, kode hashnya.

 result = prime * result + [var hash];

Untuk boolean Anda menggunakan dua nilai yang berbeda

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

Penjelasan dan Atribusi

Ini bukan pekerjaan tcurdt, dan komentar meminta penjelasan lebih lanjut, jadi saya percaya bahwa edit untuk atribusi adalah adil.

Algoritma ini dipopulerkan dalam buku "Java Efektif", dan bab yang relevan saat ini dapat ditemukan online di sini . Buku itu mempopulerkan algoritma, yang sekarang menjadi default di sejumlah aplikasi Java (termasuk Eclipse). Namun, ini berasal dari implementasi yang bahkan lebih lama yang dikaitkan dengan berbagai variasi dengan Dan Bernstein atau Chris Torek. Algoritma yang lebih tua itu awalnya beredar di Usenet, dan atribusi tertentu sulit. Misalnya, ada beberapa komentar menarik dalam kode Apache ini (cari nama mereka) yang merujuk sumber aslinya.

Intinya adalah, ini adalah algoritma hashing yang sangat tua dan sederhana. Ini bukan yang paling performan, dan bahkan tidak terbukti secara matematis sebagai algoritma "baik". Tapi itu sederhana, dan banyak orang telah menggunakannya untuk waktu yang lama dengan hasil yang baik, sehingga memiliki banyak dukungan historis.

tcurdt
sumber
9
Dari mana datangnya 1231: 1237? Saya melihatnya di Java Boolean.hashCode () juga. Apakah ini ajaib?
David Leonard
17
Itu sifat dari algoritma hashing bahwa akan ada tabrakan. Jadi saya tidak mengerti maksud Anda, Paul.
tcurdt
85
Menurut pendapat saya jawaban ini tidak menanggapi pertanyaan aktual (praktik terbaik untuk mengesampingkan hash NSObject). Itu hanya menyediakan satu algoritma hash tertentu. Selain itu, kurangnya penjelasan membuat sulit untuk memahami tanpa pengetahuan yang mendalam tentang masalah ini, dan dapat mengakibatkan orang menggunakannya tanpa mengetahui apa yang mereka lakukan. Saya tidak mengerti mengapa pertanyaan ini memiliki begitu banyak masalah.
Ricardo Sanchez-Saez
6
Masalah 1 - (int) kecil dan mudah meluap, gunakan NSUInteger. Masalah 2 - Jika Anda terus mengalikan hasil dengan masing-masing variabel hash hasil Anda akan melimpah. misalnya. [NSString hash] menciptakan nilai besar. Jika Anda memiliki 5+ variabel, mudah untuk dipenuhi dengan algoritma ini. Ini akan menghasilkan semua pemetaan ke hash yang sama, yang buruk. Lihat respons saya: stackoverflow.com/a/4393493/276626
Paul Solt
10
@ PaulSolt - Overflow bukan masalah dalam menghasilkan hash, tabrakan adalah. Tetapi overflow tidak selalu membuat tabrakan lebih mungkin, dan pernyataan Anda tentang overflow menyebabkan semuanya memetakan ke hash yang sama tidak benar.
DougW
81

Saya hanya mengambil Objective-C sendiri, jadi saya tidak dapat berbicara untuk bahasa itu secara khusus, tetapi dalam bahasa lain saya gunakan jika dua contoh adalah "Sama" mereka harus mengembalikan hash yang sama - jika tidak, Anda akan memiliki semua macam masalah ketika mencoba menggunakannya sebagai kunci dalam hashtable (atau koleksi jenis kamus).

Di sisi lain, jika 2 instance tidak sama, mereka mungkin atau mungkin tidak memiliki hash yang sama - yang terbaik adalah jika tidak. Ini adalah perbedaan antara pencarian O (1) pada tabel hash dan pencarian O (N) - jika semua hash Anda bertabrakan, Anda mungkin menemukan bahwa mencari meja Anda tidak lebih baik daripada mencari daftar.

Dalam hal praktik terbaik, hash Anda harus mengembalikan distribusi nilai acak untuk inputnya. Ini berarti bahwa, misalnya, jika Anda memiliki dua kali lipat, tetapi sebagian besar nilai Anda cenderung mengelompok antara 0 dan 100, Anda perlu memastikan bahwa hash yang dikembalikan oleh nilai-nilai tersebut didistribusikan secara merata di seluruh rentang kemungkinan nilai hash . Ini akan secara signifikan meningkatkan kinerja Anda.

Ada sejumlah algoritma hashing di luar sana, termasuk beberapa yang tercantum di sini. Saya mencoba untuk menghindari membuat algoritma hash baru karena dapat memiliki implikasi kinerja yang besar, jadi menggunakan metode hash yang ada dan melakukan kombinasi bitwise dari beberapa jenis seperti yang Anda lakukan dalam contoh Anda adalah cara yang baik untuk menghindarinya.

Brian B.
sumber
4
+1 Jawaban luar biasa, patut mendapat lebih banyak pujian, terutama karena ia benar-benar berbicara tentang "praktik terbaik" dan teori di balik mengapa hash yang baik (unik) penting.
Quinn Taylor
30

XOR sederhana atas nilai hash properti kritis sudah cukup 99% dari waktu.

Sebagai contoh:

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

Solusi ditemukan di http://nshipster.com/equality/ oleh Mattt Thompson (yang juga merujuk pertanyaan ini di posnya!)

Yariv Nissim
sumber
1
Masalah dengan jawaban ini adalah bahwa ia sama sekali tidak mempertimbangkan nilai-nilai primitif. Dan nilai-nilai primitif juga penting untuk hashing.
Vive
@Vive Sebagian besar masalah ini diselesaikan di Swift, tetapi tipe ini biasanya mewakili hash mereka sendiri karena bersifat primitif.
Yariv Nissim
1
Meskipun Anda benar untuk Swift, masih ada banyak proyek yang ditulis dengan objc. Karena jawaban Anda didedikasikan untuk objc, setidaknya perlu disebutkan.
Vive
Nilai-nilai hash XORing bersama-sama adalah saran yang buruk, itu mengarah ke banyak tabrakan hash. Sebagai gantinya, gandakan dengan bilangan prima dan kemudian tambahkan, seperti yang dinyatakan oleh jawaban lainnya.
ikan dekat
27

Saya menemukan utas ini sangat membantu memasok semua yang saya butuhkan untuk mendapatkan metode saya isEqual:dan hashdiimplementasikan dengan satu tangkapan. Saat menguji variabel instance objek dalam isEqual:kode contoh menggunakan:

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

Ini berulang kali gagal ( yaitu , mengembalikan TIDAK ) tanpa dan kesalahan, ketika saya tahu benda itu identik dalam pengujian unit saya. Alasannya adalah, salah satu NSStringvariabel instan adalah nil sehingga pernyataan di atas adalah:

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

dan karena nihil akan menanggapi metode apa pun, ini sah tetapi

[nil isEqual: nil]

mengembalikan nil , yang TIDAK , jadi ketika kedua objek dan yang diuji memiliki objek nihil mereka akan dianggap tidak sama ( yaitu , isEqual:akan mengembalikan TIDAK ).

Perbaikan sederhana ini adalah untuk mengubah pernyataan if ke:

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

Dengan cara ini, jika alamatnya sama maka metode akan dilewati panggilan tidak peduli apakah keduanya nihil atau keduanya menunjuk ke objek yang sama tetapi jika salah satu tidak nihil atau menunjuk ke objek yang berbeda maka komparator dipanggil dengan tepat.

Saya harap ini menghemat beberapa menit dari goresan kepala.

LavaSlider
sumber
20

Fungsi hash harus membuat nilai semi-unik yang tidak mungkin bertabrakan atau cocok dengan nilai hash objek lain.

Berikut adalah fungsi hash penuh, yang dapat disesuaikan dengan variabel instance kelas Anda. Ia menggunakan NSUInteger's daripada int untuk kompatibilitas pada aplikasi 64 / 32bit.

Jika hasilnya menjadi 0 untuk objek yang berbeda, Anda berisiko menabrak hash. Mengumpulkan hash dapat menghasilkan perilaku program yang tidak terduga saat bekerja dengan beberapa kelas koleksi yang bergantung pada fungsi hash. Pastikan untuk menguji fungsi hash Anda sebelum digunakan.

-(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;
}
Paul Solt
sumber
3
Satu gotcha di sini: Saya lebih suka menghindari sintaksis titik, jadi saya mengubah pernyataan BOOL Anda menjadi (misalnya) result = prime * result + [self isSelected] ? yesPrime : noPrime;. Saya kemudian menemukan ini pengaturan resultke (misalnya) 1231, saya berasumsi karena ?operator diutamakan. Saya memperbaiki masalah ini dengan menambahkan tanda kurung:result = prime * result + ([self isSelected] ? yesPrime : noPrime);
Ashley
12

Cara mudah namun tidak efisien adalah mengembalikan nilai yang sama -hashuntuk setiap instance. Jika tidak, ya, Anda harus menerapkan hash hanya berdasarkan objek yang memengaruhi kesetaraan. Ini rumit jika Anda menggunakan perbandingan longgar dalam -isEqual:(misalnya perbandingan string case-insensitive). Untuk int, umumnya Anda bisa menggunakan int itu sendiri, kecuali jika Anda akan membandingkannya dengan NSNumber.

Jangan gunakan | =, meskipun, itu akan jenuh. Gunakan ^ = sebagai gantinya.

Fakta menyenangkan acak:, [[NSNumber numberWithInt:0] isEqual:[NSNumber numberWithBool:NO]]tapi [[NSNumber numberWithInt:0] hash] != [[NSNumber numberWithBool:NO] hash]. (rdar: // 4538282, buka sejak 05-Mei-2006)

Jens Ayton
sumber
1
Anda benar pada | =. Itu tidak benar-benar berarti. :) + = dan ^ = cukup setara. Bagaimana Anda menangani primitif non-integer seperti double dan float?
Dave Dribin
Fakta asyik acak: Uji di Snow Leopard ... ;-)
Quinn Taylor
Dia benar tentang menggunakan XOR, bukan OR untuk menggabungkan bidang menjadi hash. Namun, jangan gunakan saran untuk mengembalikan nilai hash yang sama untuk setiap objek - meskipun mudah, itu dapat sangat menurunkan kinerja apa pun yang menggunakan hash objek. Hash tidak memiliki untuk menjadi berbeda untuk benda-benda yang tidak sama, tetapi jika Anda bisa mencapai itu, tidak ada yang seperti itu.
Quinn Taylor
Laporan bug radar terbuka ditutup. openradar.me/4538282 Apa artinya itu?
JJD
JJD, bug diperbaiki di Mac OS X 10.6, seperti yang Quinn mengisyaratkan. (Perhatikan bahwa komentarnya berumur dua tahun.)
Jens Ayton
9

Ingat bahwa Anda hanya perlu memberikan hash yang sama ketika isEqualbenar. Ketika isEqualsalah, hash tidak harus tidak sama meskipun mungkin itu. Karenanya:

Jaga agar hash tetap sederhana. Pilih variabel anggota (atau beberapa anggota) yang paling khas.

Misalnya, untuk CLPlacemark, nama saja sudah cukup. Ya ada 2 atau 3 perbedaan CLPlacemark dengan nama yang persis sama tetapi itu jarang terjadi. Gunakan hash itu.

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

@implementation CLPlacemark (equal)

...

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


@end

Perhatikan saya tidak repot menentukan kota, negara, dll. Nama sudah cukup. Mungkin nama dan CLLocation.

Hash harus didistribusikan secara merata. Jadi, Anda dapat menggabungkan beberapa variabel anggota menggunakan tanda sisipan ^ (tanda xor)

Jadi itu seperti

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

Dengan begitu hash akan didistribusikan secara merata.

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

Jadi apa yang harus dilakukan dalam array?

Sekali lagi, sederhana. Anda tidak perlu hash semua anggota array. Cukup dengan hash elemen pertama, elemen terakhir, hitungan, mungkin beberapa elemen tengah, dan hanya itu.

pengguna4951
sumber
Nilai hash XORing tidak memberikan distribusi yang merata.
Fishinear
7

Tunggu sebentar, pasti cara yang jauh lebih mudah untuk melakukan ini adalah dengan terlebih dahulu menimpanya - (NSString )description dan memberikan representasi string dari keadaan objek Anda (Anda harus mewakili seluruh keadaan objek Anda di string ini).

Kemudian, cukup berikan implementasi sebagai berikut hash:

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

Ini didasarkan pada prinsip bahwa "jika dua objek string sama (seperti yang ditentukan oleh metode isEqualToString:), mereka harus memiliki nilai hash yang sama."

Sumber: Referensi Kelas NSString

Jonathan Ellis
sumber
1
Ini mengasumsikan bahwa metode deskripsi akan unik. Menggunakan hash deskripsi menciptakan ketergantungan, yang mungkin tidak jelas, dan risiko tabrakan yang lebih tinggi.
Paul Solt
1
+1 Terpilih. Ini ide yang luar biasa. Jika Anda takut uraian menyebabkan benturan, maka Anda dapat menimpanya.
user4951
Terima kasih Jim, saya tidak akan menyangkal bahwa ini adalah sedikit peretasan, tapi itu akan berhasil dalam hal apa pun yang dapat saya pikirkan - dan seperti yang saya katakan, asalkan Anda menimpa description, saya tidak melihat mengapa ini lebih rendah daripada salah satu solusi dengan suara lebih tinggi. Mungkin bukan solusi yang paling elegan secara matematis, tetapi harus melakukan trik. Seperti yang dinyatakan oleh Brian B. (jawaban yang paling banyak dipilih saat ini): "Saya mencoba menghindari membuat algoritma hash baru" - setuju! - Aku hanya hashitu NSString!
Jonathan Ellis
Terpilih karena ide yang bagus. Saya tidak akan menggunakannya karena saya takut alokasi NSString tambahan.
karwag
1
Ini bukan solusi umum karena sebagian besar kelas descriptionmenyertakan alamat pointer. Jadi ini membuat dua contoh berbeda dari kelas yang sama yang sama dengan hash yang berbeda, yang melanggar asumsi dasar bahwa dua objek yang sama memiliki hash yang sama!
Diogo T
5

Kontrak equals dan hash dirinci dengan baik dan diteliti secara menyeluruh di dunia Java (lihat jawaban @ mipardi), tetapi semua pertimbangan yang sama harus berlaku untuk Objective-C.

Eclipse melakukan pekerjaan yang dapat diandalkan untuk menghasilkan metode ini di Jawa, jadi inilah contoh Eclipse yang diangkut dengan tangan ke 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;
}

Dan untuk subkelas YourWidgetyang menambahkan properti 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;
}

Implementasi ini menghindari beberapa jebakan subclassing dalam sampel isEqual:dari Apple:

  • Tes kelas Apple other isKindOfClass:[self class]asimetris untuk dua subclass berbeda MyWidget. Kesetaraan harus simetris: a = b jika dan hanya jika b = a. Ini dapat dengan mudah diperbaiki dengan mengubah tes other isKindOfClass:[MyWidget class], maka semua MyWidgetsubclass akan dapat dibandingkan satu sama lain.
  • Menggunakan isKindOfClass:tes subclass mencegah subclass dari mengesampingkan isEqual:dengan tes kesetaraan yang disempurnakan. Ini karena persamaan perlu transitif: jika a = b dan a = c maka b = c. Jika sebuah MyWidgetinstance membandingkan sama dengan dua YourWidgetinstance, maka YourWidgetinstance tersebut harus membandingkan sama satu sama lain, bahkan jika mereka serialNoberbeda.

Masalah kedua dapat diperbaiki dengan hanya mempertimbangkan objek menjadi sama jika mereka termasuk kelas yang sama persis, maka [self class] != [object class]pengujian di sini. Untuk kelas aplikasi tipikal , ini tampaknya menjadi pendekatan terbaik.

Namun, tentu saja ada kasus di mana isKindOfClass:tes lebih disukai. Ini lebih tipikal kelas kerangka kerja daripada kelas aplikasi. Sebagai contoh, siapa pun NSStringharus membandingkan sama dengan yang lain NSStringdengan urutan karakter mendasar yang sama, terlepas dari NSString/ NSMutableStringperbedaan, dan juga terlepas dari apa kelas privat dalam NSStringgugus kelas yang terlibat.

Dalam kasus seperti itu, isEqual:harus memiliki perilaku yang terdefinisi dengan baik, dan harus diperjelas bahwa subkelas tidak dapat mengesampingkan ini. Di Jawa, pembatasan 'tanpa pengesampingan' dapat ditegakkan dengan menandai metode equals dan hashcode final, tetapi Objective-C tidak memiliki padanan.

jedwidz
sumber
@ Adubr Itu tercakup dalam dua paragraf terakhir saya. Itu tidak fokus karena MyWidgetdipahami tidak menjadi kluster kelas.
jedwidz
5

Ini tidak langsung menjawab pertanyaan Anda (sama sekali) tetapi saya telah menggunakan MurmurHash sebelumnya untuk menghasilkan hash: murmurhash

Kira saya harus menjelaskan mengapa: murmurhash berdarah cepat ...

schwa
sumber
2
Pustaka C ++ yang berfokus pada hash unik untuk kunci * kosong menggunakan angka acak (dan juga tidak berhubungan dengan objek Objective-C) benar-benar bukan saran yang membantu di sini. Metode -hash harus mengembalikan nilai yang konsisten setiap kali, atau itu akan sama sekali tidak berguna. Jika objek ditambahkan ke koleksi yang memanggil -hash, dan mengembalikan nilai baru setiap kali, duplikat tidak akan pernah terdeteksi, dan Anda tidak akan pernah bisa mengambil objek dari koleksi juga. Dalam hal ini, istilah "hash" berbeda dari arti dalam keamanan / kriptografi.
Quinn Taylor
3
murmurhash bukanlah fungsi hash kriptografis. Silakan periksa fakta Anda sebelum memposting informasi yang salah. Murmurhash dapat berguna untuk hashing custom objektif-c kelas (khususnya jika Anda memiliki banyak NSDatas terlibat) karena sangat cepat. Namun saya memberikan Anda bahwa mungkin menyarankan itu bukan saran terbaik untuk diberikan kepada seseorang "hanya mengambil tujuan-c", tapi tolong catat awalan saya pada balasan asli saya untuk pertanyaan itu.
schwa
4

Saya seorang pemula Objective C juga, tetapi saya menemukan artikel yang bagus tentang identitas vs kesetaraan di Objective C di sini . Dari bacaan saya, sepertinya Anda mungkin bisa mempertahankan fungsi hash default (yang seharusnya memberikan identitas unik) dan mengimplementasikan metode isEqual sehingga membandingkan nilai data.

ceperry
sumber
Saya seorang pemula Cocoa / Objective C, dan tautan serta jawaban ini sangat membantu saya memotong semua hal yang lebih maju di atas ke garis bawah - saya tidak perlu khawatir tentang hash - cukup menerapkan metode isEqual:. Terima kasih!
John Gallagher
Jangan lewatkan tautan @ ceperry. Artikel Equality vs Identitykarya Karl Kraft benar-benar bagus.
JJD
6
@ John: Saya pikir Anda harus membaca kembali artikel itu. Ia mengatakan dengan sangat jelas bahwa "instance yang sama harus memiliki nilai hash yang sama." Jika Anda menimpa isEqual:, Anda juga harus menimpa hash.
Steve Madsen
3

Quinn salah bahwa referensi ke hash murmur tidak berguna di sini. Quinn benar bahwa Anda ingin memahami teori di balik hashing. Bising menyaring banyak teori itu menjadi sebuah implementasi. Mencari tahu bagaimana menerapkan implementasi itu untuk aplikasi khusus ini perlu ditelusuri.

Beberapa poin penting di sini:

Contoh fungsi dari tcurdt menunjukkan bahwa '31' adalah pengganda yang baik karena ini adalah prima. Orang perlu menunjukkan bahwa menjadi yang utama adalah kondisi yang diperlukan dan memadai. Faktanya 31 (dan 7) mungkin bukan bilangan prima yang sangat baik karena 31 == -1% 32. Pengganda ganjil dengan sekitar setengah bit yang diset dan setengah bit jelas cenderung lebih baik. (Konstanta multiplikasi murmur hash memiliki properti itu.)

Jenis fungsi hash ini kemungkinan akan lebih kuat jika, setelah dikalikan, nilai hasil disesuaikan melalui shift dan xor. Penggandaan cenderung menghasilkan hasil dari banyak interaksi bit di bagian atas register dan hasil interaksi yang rendah di bagian bawah register. Pergeseran dan xor meningkatkan interaksi di bagian bawah register.

Menyetel hasil awal ke nilai di mana sekitar setengah bit nol dan sekitar setengah bit juga cenderung berguna.

Mungkin bermanfaat untuk berhati-hati tentang urutan unsur-unsur digabungkan. Seseorang mungkin pertama-tama harus memproses boolean dan elemen lain di mana nilainya tidak terdistribusi dengan kuat.

Mungkin bermanfaat untuk menambahkan beberapa tahap pengacakan bit tambahan di akhir perhitungan.

Apakah hash murmur sebenarnya cepat untuk aplikasi ini adalah pertanyaan terbuka. Murmur hash mem-preix bit dari setiap kata input. Kata input ganda dapat diproses secara paralel yang membantu CPU pipelined multi-masalah.


sumber
3

Menggabungkan jawaban @ tcurdt dengan jawaban @ oscar-gomez untuk mendapatkan nama properti , kita dapat membuat solusi drop-in yang mudah untuk kedua isEqual dan hash:

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

Sekarang, di kelas khusus Anda, Anda dapat dengan mudah menerapkan isEqual:dan hash:

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

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

Perhatikan bahwa jika Anda membuat objek yang dapat dimutasi setelah pembuatan, nilai hash tidak boleh berubah jika objek dimasukkan ke dalam koleksi. Secara praktis, ini berarti bahwa nilai hash harus diperbaiki dari titik pembuatan objek awal. Lihat dokumentasi Apple tentang metode hash-protokol NSObject untuk informasi lebih lanjut:

Jika objek yang dapat diubah ditambahkan ke koleksi yang menggunakan nilai hash untuk menentukan posisi objek dalam koleksi, nilai yang dikembalikan oleh metode hash objek tidak boleh berubah saat objek berada di koleksi. Oleh karena itu, metode hash tidak boleh bergantung pada informasi status internal objek apa pun atau Anda harus memastikan informasi status internal objek tidak berubah saat objek dalam koleksi. Jadi, misalnya, kamus yang bisa diubah dapat diletakkan di tabel hash tetapi Anda tidak harus mengubahnya saat itu ada di sana. (Perhatikan bahwa mungkin sulit untuk mengetahui apakah suatu objek diberikan dalam koleksi atau tidak.)

Ini kedengarannya seperti perombakan total bagi saya karena berpotensi membuat pencarian hash jauh lebih efisien, tapi saya kira lebih baik berbuat salah di sisi hati-hati dan mengikuti apa yang dikatakan dokumentasi.

pengguna10345
sumber
1
Anda membaca hash docs salah - ini pada dasarnya situasi "baik-atau". Jika objek berubah, hash umumnya juga berubah. Ini benar-benar peringatan bagi programmer, bahwa jika hash berubah sebagai hasil dari mutasi objek, maka mengubah objek saat berada dalam koleksi yang menggunakan hash akan menyebabkan perilaku yang tidak terduga. Jika objek tersebut harus "dapat dirubah dengan aman" dalam situasi seperti itu, Anda tidak punya pilihan selain membuat hash tidak terkait dengan keadaan bisa berubah. Situasi khusus itu terdengar aneh bagiku, tetapi tentu saja ada situasi yang jarang terjadi di mana itu berlaku.
Quinn Taylor
1

Maaf jika saya beresiko terdengar peti mati lengkap di sini tapi ... ... tidak ada yang mau repot-repot menyebutkan bahwa untuk mengikuti 'praktik terbaik' Anda tidak boleh menentukan metode yang sama yang TIDAK akan memperhitungkan semua data yang dimiliki oleh objek target Anda, misalnya apa pun data dikumpulkan ke objek Anda, versus rekannya, harus diperhitungkan saat menerapkan sama. Jika Anda tidak ingin mengambil, katakan 'usia' ke dalam perbandingan, maka Anda harus menulis komparator dan menggunakannya untuk melakukan perbandingan, bukan isEqual :.

Jika Anda mendefinisikan metode isEqual: yang melakukan perbandingan kesetaraan secara sewenang-wenang, Anda menanggung risiko bahwa metode ini disalahgunakan oleh pengembang lain, atau bahkan diri Anda sendiri, setelah Anda lupa 'twist' dalam interpretasi yang sama dengan Anda.

Ergo, meskipun ini adalah q & a yang bagus tentang hashing, Anda biasanya tidak perlu mendefinisikan ulang metode hashing, Anda mungkin harus mendefinisikan komparator ad-hoc sebagai gantinya.

Thibaud de Souza
sumber