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 name
dan data
properti. Apa yang tidak ditunjukkan contoh ini adalah cara menimpa hash
.
Anggaplah ada properti lain yang tidak memengaruhi kesetaraan, katakanlah age
. Bukankah seharusnya hash
metode diganti sehingga hanya name
dan data
mempengaruhi hash? Dan jika demikian, bagaimana Anda akan melakukannya? Cukup tambahkan hash dari name
dan 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 NSNumber
untuk mendapatkan hash mereka? Atau struct suka NSRect
?
( Brain fart : Awalnya menulis "bitwise OR" bersama dengan mereka |=
. Dimaksudkan menambahkan.)
sumber
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)Jawaban:
Dimulai dari
Lalu untuk setiap primitif yang Anda lakukan
Untuk objek yang Anda gunakan 0 untuk nihil dan jika tidak, kode hashnya.
Untuk boolean Anda menggunakan dua nilai yang berbeda
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.
sumber
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.
sumber
Sebagai contoh:
Solusi ditemukan di http://nshipster.com/equality/ oleh Mattt Thompson (yang juga merujuk pertanyaan ini di posnya!)
sumber
Saya menemukan utas ini sangat membantu memasok semua yang saya butuhkan untuk mendapatkan metode saya
isEqual:
danhash
diimplementasikan dengan satu tangkapan. Saat menguji variabel instance objek dalamisEqual:
kode contoh menggunakan:Ini berulang kali gagal ( yaitu , mengembalikan TIDAK ) tanpa dan kesalahan, ketika saya tahu benda itu identik dalam pengujian unit saya. Alasannya adalah, salah satu
NSString
variabel instan adalah nil sehingga pernyataan di atas adalah:dan karena nihil akan menanggapi metode apa pun, ini sah tetapi
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:
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.
sumber
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.
sumber
result = prime * result + [self isSelected] ? yesPrime : noPrime;
. Saya kemudian menemukan ini pengaturanresult
ke (misalnya)1231
, saya berasumsi karena?
operator diutamakan. Saya memperbaiki masalah ini dengan menambahkan tanda kurung:result = prime * result + ([self isSelected] ? yesPrime : noPrime);
Cara mudah namun tidak efisien adalah mengembalikan nilai yang sama
-hash
untuk 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)sumber
Ingat bahwa Anda hanya perlu memberikan hash yang sama ketika
isEqual
benar. KetikaisEqual
salah, 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.
...
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
Dengan begitu hash akan didistribusikan secara merata.
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.
sumber
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
: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
sumber
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 hanyahash
ituNSString
!description
menyertakan 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!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:
Dan untuk subkelas
YourWidget
yang menambahkan propertiserialNo
:Implementasi ini menghindari beberapa jebakan subclassing dalam sampel
isEqual:
dari Apple:other isKindOfClass:[self class]
asimetris untuk dua subclass berbedaMyWidget
. Kesetaraan harus simetris: a = b jika dan hanya jika b = a. Ini dapat dengan mudah diperbaiki dengan mengubah tesother isKindOfClass:[MyWidget class]
, maka semuaMyWidget
subclass akan dapat dibandingkan satu sama lain.isKindOfClass:
tes subclass mencegah subclass dari mengesampingkanisEqual:
dengan tes kesetaraan yang disempurnakan. Ini karena persamaan perlu transitif: jika a = b dan a = c maka b = c. Jika sebuahMyWidget
instance membandingkan sama dengan duaYourWidget
instance, makaYourWidget
instance tersebut harus membandingkan sama satu sama lain, bahkan jika merekaserialNo
berbeda.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 punNSString
harus membandingkan sama dengan yang lainNSString
dengan urutan karakter mendasar yang sama, terlepas dariNSString
/NSMutableString
perbedaan, dan juga terlepas dari apa kelas privat dalamNSString
gugus 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 hashcodefinal
, tetapi Objective-C tidak memiliki padanan.sumber
MyWidget
dipahami tidak menjadi kluster kelas.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 ...
sumber
Saya menemukan halaman ini sebagai panduan bermanfaat dalam mengesampingkan metode equals- dan hash. Ini termasuk algoritma yang layak untuk menghitung kode hash. Halaman diarahkan ke Jawa, tetapi cukup mudah untuk menyesuaikannya dengan Objective-C / Cocoa.
sumber
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.
sumber
Equality vs Identity
karya Karl Kraft benar-benar bagus.isEqual:
, Anda juga harus menimpahash
.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
Menggabungkan jawaban @ tcurdt dengan jawaban @ oscar-gomez untuk mendapatkan nama properti , kita dapat membuat solusi drop-in yang mudah untuk kedua isEqual dan hash:
Sekarang, di kelas khusus Anda, Anda dapat dengan mudah menerapkan
isEqual:
danhash
:sumber
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:
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.
sumber
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.
sumber