Procedure consigliate per la sostituzione di isEqual: e hash


267

Come si sostituisce correttamente isEqual:in Objective-C? Il "catch" sembra essere che se due oggetti sono uguali (come determinato dal isEqual:metodo), devono avere lo stesso valore di hash.

La sezione Introspection della Cocoa Fundamentals Guide contiene un esempio su come eseguire l'override isEqual:, copiato come segue, per una classe denominata 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;
}

Verifica l'uguaglianza del puntatore, quindi l'uguaglianza di classe e infine confronta gli oggetti utilizzando isEqualToWidget:, che controlla solo le proprietà namee data. Ciò che l'esempio non mostra è come sovrascrivere hash.

Supponiamo che ci siano altre proprietà che non influiscono sull'uguaglianza, diciamo age. Non dovrebbe il hashmetodo di essere ignorato tale che solo namee datainfluenzano l'hash? E se sì, come lo faresti? Basta aggiungere gli hash di namee data? Per esempio:

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

È sufficiente? Esiste una tecnica migliore? E se avessi dei primitivi, come int? Convertirli in NSNumberper ottenere il loro hash? O strutture come NSRect?

( Scoreggia cerebrale : originariamente li scriveva "bit a bit OR" insieme a |=. Significa aggiungere.)


2
if (![other isKindOfClass:[self class]])- Ciò significa tecnicamente che l'uguaglianza non sarà commutativa. Vale a dire A = B non significa B = A (ad es. Se uno è una sottoclasse dell'altro)
Robert

Il collegamento alla documentazione è morto, ora archiviato in Introspection
jedwidz,

Risposte:


111

Iniziare con

 NSUInteger prime = 31;
 NSUInteger result = 1;

Quindi per ogni primitiva che fai

 result = prime * result + var

Per gli oggetti si usa 0 per zero e altrimenti il ​​loro hashcode.

 result = prime * result + [var hash];

Per i booleani si usano due valori diversi

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

Spiegazione e attribuzione

Questo non è il lavoro di tcurdt e i commenti chiedevano ulteriori spiegazioni, quindi credo che una modifica per l'attribuzione sia giusta.

Questo algoritmo è stato reso popolare nel libro "Effective Java" e il capitolo pertinente è attualmente disponibile online qui . Quel libro ha reso popolare l'algoritmo, che è ora predefinito in una serie di applicazioni Java (incluso Eclipse). Deriva, tuttavia, da un'implementazione ancora più antica che è variamente attribuita a Dan Bernstein o Chris Torek. Quell'algoritmo precedente originariamente fluttuava su Usenet e una certa attribuzione è difficile. Ad esempio, c'è un commento interessante in questo codice Apache (cerca i loro nomi) che fa riferimento alla fonte originale.

In conclusione, si tratta di un algoritmo di hashing molto vecchio e semplice. Non è il più performante e non è nemmeno provato matematicamente come un "buon" algoritmo. Ma è semplice e molte persone lo usano da molto tempo con buoni risultati, quindi ha molto supporto storico.


9
Da dove viene il 1231: 1237? Lo vedo anche nel Boolean.hashCode () di Java. È magico?
David Leonard,

17
È la natura di un algoritmo di hashing che ci saranno collisioni. Quindi non vedo il tuo punto, Paul.
tcurdt

85
A mio avviso, questa risposta non risponde alla domanda effettiva (migliori pratiche per ignorare l'hash di NSObject). Fornisce solo un particolare algoritmo di hash. Inoltre, la scarsità della spiegazione rende difficile la comprensione senza una profonda conoscenza della questione e può far sì che le persone la utilizzino senza sapere cosa stanno facendo. Non capisco perché questa domanda abbia così tanti voti positivi.
Ricardo Sanchez-Saez,

6
1 ° problema - (int) è piccolo e facile da traboccare, usa NSUInteger. 2 ° problema: se continui a moltiplicare il risultato per ogni hash variabile, il risultato traboccerà. per esempio. [NSString hash] crea valori di grandi dimensioni. Se hai 5+ variabili è facile overflow con questo algoritmo. Si tradurrà in tutto il mapping allo stesso hash, il che è male. Vedi la mia risposta: stackoverflow.com/a/4393493/276626
Paul Solt

10
@PaulSolt - Overflow non è un problema nella generazione di un hash, lo è la collisione. Ma l'overflow non rende necessariamente più probabile la collisione e la tua dichiarazione sull'overflow che fa sì che tutto si associ allo stesso hash è semplicemente errata.
DougW,

81

Sto solo raccogliendo Objective-C da solo, quindi non posso parlare in modo specifico per quella lingua, ma nelle altre lingue che uso se due istanze sono "uguali" devono restituire lo stesso hash - altrimenti avrai tutto una serie di problemi quando si tenta di usarli come chiavi in ​​una tabella hash (o in qualsiasi raccolta di tipo dizionario).

D'altra parte, se 2 istanze non sono uguali, possono o meno avere lo stesso hash - è meglio se non lo sono. Questa è la differenza tra una ricerca O (1) su una tabella hash e una ricerca O (N) - se tutti gli hash si scontrano potresti scoprire che cercare la tua tabella non è meglio che cercare un elenco.

In termini di best practice, l'hash dovrebbe restituire una distribuzione casuale di valori per il suo input. Ciò significa che, ad esempio, se si dispone di un doppio, ma la maggior parte dei valori tende a raggrupparsi tra 0 e 100, è necessario assicurarsi che gli hash restituiti da tali valori siano distribuiti uniformemente nell'intero intervallo di possibili valori hash . Ciò migliorerà significativamente le tue prestazioni.

Esistono numerosi algoritmi di hashing, inclusi alcuni elencati qui. Cerco di evitare la creazione di nuovi algoritmi hash in quanto può avere grandi implicazioni sulle prestazioni, quindi usare i metodi hash esistenti e fare una combinazione bit a bit di qualche tipo come fai nel tuo esempio è un buon modo per evitarlo.


4
+1 Risposta eccellente, merita più voti, soprattutto perché parla di "buone pratiche" e della teoria alla base del perché un hash buono (unico) è importante.
Quinn Taylor,

30

Un semplice XOR sui valori di hash delle proprietà critiche è sufficiente per il 99% delle volte.

Per esempio:

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

Soluzione trovata su http://nshipster.com/equality/ da Mattt Thompson (che ha anche posto questa domanda nel suo post!)


1
Il problema con questa risposta è che non tiene affatto conto dei valori primitivi. E i valori primitivi possono anche essere importanti per l'hashing.
Vive

@Vive Molti di questi problemi sono risolti in Swift, ma questi tipi di solito rappresentano il loro hash dato che sono primitivi.
Yariv Nissim,

1
Mentre hai ragione per Swift, ci sono ancora molti progetti scritti con objc. Perché la tua risposta è dedicata a objc, vale la pena almeno menzionare.
Vive

XORing insieme dei valori di hash è un cattivo consiglio, porta a molte collisioni di hash. Invece, moltiplicare per un numero primo e quindi aggiungere, come indicato da altre risposte.
Fishinearin

27

Ho trovato questo thread estremamente utile fornendo tutto ciò di cui avevo bisogno per ottenere il mio isEqual:e i hashmetodi implementati con una sola cattura. Quando si verificano le variabili dell'istanza dell'oggetto nel isEqual:codice di esempio si utilizza

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

Ciò ha ripetutamente fallito ( cioè restituito NO ) senza errori e, quando ho saputo che gli oggetti erano identici nel test dell'unità. Il motivo era che una delle NSStringvariabili di istanza era nulla, quindi l'istruzione precedente era:

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

e poiché zero risponderà a qualsiasi metodo, questo è perfettamente legale ma

[nil isEqual: nil]

restituisce zero , che è NO , quindi quando sia l'oggetto che quello testato avevano un oggetto zero sarebbero considerati non uguali ( cioè , isEqual:restituirebbero NO ).

Questa semplice correzione consisteva nel modificare l'istruzione if in:

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

In questo modo, se i loro indirizzi sono uguali, salta la chiamata del metodo, indipendentemente dal fatto che siano entrambi nulli o entrambi che puntano allo stesso oggetto, ma se o non è zero o puntano a oggetti diversi, il comparatore viene chiamato in modo appropriato.

Spero che questo risparmi a qualcuno qualche minuto di grattarsi la testa.


20

La funzione hash dovrebbe creare un valore semi-univoco che non è probabile che si scontri o corrisponda al valore hash di un altro oggetto.

Ecco la funzione di hash completa, che può essere adattata alle variabili di istanza delle classi. Utilizza NSUInteger piuttosto che int per la compatibilità su applicazioni a 64/32 bit.

Se il risultato diventa 0 per oggetti diversi, si corre il rischio di scontrarsi con hash. La collisione degli hash può comportare un comportamento imprevisto del programma quando si lavora con alcune delle classi di raccolta che dipendono dalla funzione hash. Assicurati di testare la tua funzione hash prima dell'uso.

-(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
Un esempio: preferisco evitare la sintassi dei punti, quindi ho trasformato la tua istruzione BOOL in (ad es result = prime * result + [self isSelected] ? yesPrime : noPrime;. ) . Ho quindi scoperto che questo era impostato resultsu (ad es.) 1231, Suppongo che l' ?operatore abbia la precedenza. Ho risolto il problema aggiungendo parentesi:result = prime * result + ([self isSelected] ? yesPrime : noPrime);
Ashley,

12

Il modo semplice ma inefficiente è di restituire lo stesso -hashvalore per ogni istanza. Altrimenti, sì, è necessario implementare l'hash basato solo su oggetti che influiscono sull'uguaglianza. Ciò è complicato se si utilizzano confronti lassisti -isEqual:(ad esempio confronti di stringhe senza distinzione tra maiuscole e minuscole). Per gli inte, puoi generalmente usare int stesso, a meno che non ti confronterai con NSNumbers.

Non usare | =, tuttavia, sarà saturo. Utilizzare invece ^ =.

Curiosità casuale: [[NSNumber numberWithInt:0] isEqual:[NSNumber numberWithBool:NO]]ma [[NSNumber numberWithInt:0] hash] != [[NSNumber numberWithBool:NO] hash]. (rdar: // 4538282, aperto dal 05 maggio 2006)


1
Hai esattamente ragione su | =. Non intendevo davvero questo. :) + = e ^ = sono abbastanza equivalenti. Come gestite le primitive non intere come double e float?
Dave Dribin,

Curiosità casuale: provalo su Snow Leopard ... ;-)
Quinn Taylor,

Ha ragione sull'usare XOR invece di OR per combinare i campi in un hash. Tuttavia, non usare il consiglio di restituire lo stesso valore -hash per ogni oggetto - sebbene sia facile, può degradare gravemente le prestazioni di tutto ciò che usa l'hash dell'oggetto. L'hash non deve essere distinto per oggetti che non sono uguali, ma se riesci a raggiungerlo, non c'è niente di simile.
Quinn Taylor,

Il report dei bug del radar aperto è chiuso. openradar.me/4538282 Cosa significa?
JJD

JJD, il bug è stato corretto in Mac OS X 10.6, come suggeriva Quinn. (Nota che il commento ha due anni.)
Jens Ayton,

9

Ricorda che devi solo fornire un hash uguale quando isEqualè vero. Quando isEqualè falso, l'hash non deve essere disuguale, sebbene presumibilmente lo sia. Quindi:

Mantieni l'hash semplice. Scegli una variabile membro (o pochi membri) che è la più distintiva.

Ad esempio, per CLPlacemark, solo il nome è sufficiente. Sì, ci sono 2 o 3 distinzioni CLPlacemark con lo stesso identico nome, ma quelle sono rare. Usa quell'hash.

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

@implementation CLPlacemark (equal)

...

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


@end

Nota che non mi preoccupo di specificare la città, il paese, ecc. Il nome è abbastanza. Forse il nome e CLLocation.

L'hash dovrebbe essere distribuito uniformemente. Quindi puoi combinare più variabili membro usando il cursore ^ (segno xor)

Quindi è qualcosa di simile

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

In questo modo l'hash verrà distribuito uniformemente.

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

Quindi cosa fare in array?

Ancora una volta, semplice. Non è necessario eseguire l'hashing di tutti i membri dell'array. Abbastanza per eseguire l'hashing del primo elemento, dell'ultimo elemento, del conteggio, forse di alcuni elementi intermedi, e basta.


I valori di hash XOR non forniscono una distribuzione uniforme.
Fishinearin

7

Aspetta, sicuramente un modo molto più semplice per farlo è quello di sovrascrivere - (NSString )descriptione fornire una rappresentazione in forma di stringa del tuo stato di oggetto (devi rappresentare l'intero stato del tuo oggetto in questa stringa).

Quindi, basta fornire la seguente implementazione di hash:

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

Questo si basa sul principio che "se due oggetti stringa sono uguali (come determinato dal metodo isEqualToString:), devono avere lo stesso valore hash".

Fonte: riferimento alla classe NSString


1
Ciò presuppone che il metodo di descrizione sarà univoco. L'uso dell'hash della descrizione crea una dipendenza, che potrebbe non essere ovvia, e un rischio maggiore di collisioni.
Paul Solt,

1
+1 potenziato. Questa è un'idea fantastica. Se temi che le descrizioni causino collisioni, puoi ignorarle.
user4951

Grazie Jim, non nego che questo sia un po 'un trucco, ma funzionerebbe in ogni caso che mi viene in mente - e come ho detto, a condizione che tu abbia la precedenza description, non vedo perché questo sia inferiore a una delle soluzioni più votate. Potrebbe non essere la soluzione matematicamente più elegante, ma dovrebbe fare il trucco. Come afferma Brian B. (la risposta più votata a questo punto): "Cerco di evitare di creare nuovi algoritmi di hash" - d'accordo! - Ho appena hashl' NSString!
Jonathan Ellis,

È stato votato perché è un'idea chiara. Non lo userò però perché temo le allocazioni aggiuntive di NSString.
Karwag,

1
Questa non è una soluzione generica poiché per la maggior parte delle classi descriptioninclude l'indirizzo del puntatore. Quindi, questo crea due diverse istanze della stessa classe che sono uguali a hash diversi, il che viola l'assunto di base che due oggetti uguali hanno lo stesso hash!
Diogo T,

5

I contratti uguali e hash sono ben specificati e studiati a fondo nel mondo Java (vedi la risposta di @ mipardi), ma tutte le stesse considerazioni dovrebbero valere per Objective-C.

Eclipse fa un lavoro affidabile nel generare questi metodi in Java, quindi ecco un esempio Eclipse portato a mano su 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;
}

E per una sottoclasse YourWidgetche aggiunge una proprietà 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;
}

Questa implementazione evita alcune insidie ​​della sottoclasse nell'esempio isEqual:di Apple:

  • Il test di classe di Apple other isKindOfClass:[self class]è asimmetrico per due diverse sottoclassi di MyWidget. L'uguaglianza deve essere simmetrica: a = b se e solo se b = a. Ciò potrebbe essere facilmente risolto modificando il test in other isKindOfClass:[MyWidget class], quindi tutte le MyWidgetsottoclassi sarebbero reciprocamente comparabili.
  • L'uso di un isKindOfClass:test di sottoclasse impedisce alle sottoclassi di scavalcare isEqual:con un test di uguaglianza raffinato. Questo perché l'uguaglianza deve essere transitiva: se a = b e a = c, allora b = c. Se MyWidgetun'istanza confronta uguale a due YourWidgetistanze, tali YourWidgetistanze devono comparare uguali tra loro, anche se le loro serialNodifferenze.

Il secondo problema può essere risolto considerando che gli oggetti sono uguali solo se appartengono alla stessa classe esatta, quindi il [self class] != [object class]test qui. Per le classi di applicazioni tipiche , questo sembra essere l'approccio migliore.

Tuttavia, ci sono certamente casi in cui il isKindOfClass:test è preferibile. Questo è più tipico delle classi di framework rispetto alle classi di applicazione. Ad esempio, ognuno NSStringdovrebbe confrontare uguale a qualsiasi altro NSStringcon la stessa sequenza di caratteri sottostante, indipendentemente dalla NSString/ NSMutableStringdistinzione, e anche indipendentemente dalle classi private nel NSStringcluster di classi.

In tali casi, isEqual:dovrebbe avere un comportamento ben definito e ben documentato e dovrebbe essere chiarito che le sottoclassi non possono ignorare questo. In Java, la restrizione "no override" può essere applicata contrassegnando i metodi egals e hashcode come final, ma Objective-C non ha equivalenti.


@adubr Questo è trattato nei miei ultimi due paragrafi. Non è focale in quanto MyWidgetsi ritiene che non sia un cluster di classe.
jedwidz,

5

Questo non risponde direttamente alla tua domanda (affatto) ma ho già usato MurmurHash per generare hash: murmurhash

Immagino che dovrei spiegare perché: murmurhash è velocissimo ...


2
Una libreria C ++ focalizzata su hash univoci per una chiave void * che utilizza un numero casuale (e non si riferisce nemmeno agli oggetti Objective-C) non è davvero un suggerimento utile qui. Il metodo -hash dovrebbe restituire un valore coerente ogni volta, o sarà completamente inutile. Se l'oggetto viene aggiunto a una raccolta che chiama -hash e restituisce un nuovo valore ogni volta, i duplicati non verranno mai rilevati e non sarà nemmeno possibile recuperare l'oggetto dalla raccolta. In questo caso, il termine "hash" è diverso dal significato di sicurezza / crittografia.
Quinn Taylor,

3
murmurhash non è una funzione hash crittografica. Si prega di controllare i fatti prima di pubblicare informazioni errate. Murmurhash potrebbe essere utile per eseguire l'hashing di classi personalizzate di obiettivi-c (specialmente se si hanno molti NSData coinvolti) perché è estremamente veloce. Tuttavia ti concedo che forse suggerendo che non è il miglior consiglio da dare a qualcuno "sto solo raccogliendo obiettivo-c", ma per favore nota il mio prefisso sulla mia risposta originale alla domanda.
schwa,


4

Sono anche un neofita dell'Obiettivo C, ma ho trovato un eccellente articolo sull'identità contro l'uguaglianza nell'Obiettivo C qui . Dalla mia lettura sembra che potresti essere in grado di mantenere la funzione hash predefinita (che dovrebbe fornire un'identità univoca) e implementare il metodo isEqual in modo da confrontare i valori dei dati.


Sono un principiante Cocoa / Objective C, e questa risposta e questo link mi hanno davvero aiutato a tagliare tutte le cose più avanzate sopra alla linea di fondo - Non ho bisogno di preoccuparmi degli hash - solo implementando il metodo isEqual :. Grazie!
John Gallagher,

Da non perdere il link di @ ceperry. L'articolo Equality vs Identitydi Karl Kraft è davvero buono.
JJD

6
@Giovanni: penso che dovresti rileggere l'articolo. Dice molto chiaramente che "le istanze uguali devono avere valori hash uguali". Se si esegue l'override isEqual:, è necessario eseguire l'override hash.
Steve Madsen,

3

Quinn ha semplicemente sbagliato che il riferimento all'hash del mormorio è inutile qui. Quinn ha ragione nel voler capire la teoria alla base dell'hashing. Il soffio distilla gran parte di quella teoria in un'implementazione. Vale la pena esplorare come applicare tale implementazione a questa particolare applicazione.

Alcuni dei punti chiave qui:

La funzione di esempio di tcurdt suggerisce che '31' è un buon moltiplicatore perché è primo. Bisogna dimostrare che essere primi è una condizione necessaria e sufficiente. In effetti 31 (e 7) probabilmente non sono numeri primi particolarmente buoni perché 31 == -1% 32. È probabile che un moltiplicatore dispari con circa metà dei bit impostati e metà dei bit liberi sia migliore. (La costante di moltiplicazione dell'hash del soffio ha quella proprietà.)

Questo tipo di funzione hash sarebbe probabilmente più forte se, dopo la moltiplicazione, il valore del risultato fosse regolato tramite shift e xor. La moltiplicazione tende a produrre i risultati di molte interazioni di bit all'estremità superiore del registro e risultati di interazione bassi all'estremità inferiore del registro. Lo spostamento e xor aumentano le interazioni all'estremità inferiore del registro.

L'impostazione del risultato iniziale su un valore in cui circa la metà dei bit sono zero e circa la metà dei bit sono uno tende ad essere utile.

Può essere utile fare attenzione all'ordine in cui gli elementi vengono combinati. Uno dovrebbe probabilmente prima elaborare booleani e altri elementi in cui i valori non sono fortemente distribuiti.

Potrebbe essere utile aggiungere un paio di fasi di rimescolamento dei bit extra alla fine del calcolo.

Se l'hash del soffio sia effettivamente veloce per questa applicazione è una domanda aperta. L'hash di soffio premiscela i bit di ogni parola di input. Più parole di input possono essere elaborate in parallelo, il che aiuta cpus pipeline con problemi multipli.


3

Combinando la risposta di @ tcurdt con la risposta di @ oscar-gomez per ottenere i nomi delle proprietà , possiamo creare una soluzione drop-in semplice sia per isEqual che per l'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;
}

Ora, nella tua classe personalizzata puoi facilmente implementare isEqual:e hash:

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

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

2

Se stai creando un oggetto che può essere mutato dopo la creazione, il valore di hash non deve cambiare se l'oggetto viene inserito in una raccolta. In pratica, ciò significa che il valore di hash deve essere fissato dal punto della creazione iniziale dell'oggetto. Consulta la documentazione di Apple sul metodo -hash del protocollo NSObject per ulteriori informazioni:

Se un oggetto mutabile viene aggiunto a una raccolta che utilizza valori hash per determinare la posizione dell'oggetto nella raccolta, il valore restituito dal metodo hash dell'oggetto non deve cambiare mentre l'oggetto si trova nella raccolta. Pertanto, il metodo hash non deve fare affidamento su nessuna delle informazioni sullo stato interno dell'oggetto oppure è necessario assicurarsi che le informazioni sullo stato interno dell'oggetto non cambino mentre l'oggetto si trova nella raccolta. Pertanto, ad esempio, un dizionario modificabile può essere inserito in una tabella hash ma non è necessario modificarlo mentre è presente. (Nota che può essere difficile sapere se un determinato oggetto si trova o meno in una raccolta.)

Questo mi sembra una completa perplessità poiché potenzialmente rende le ricerche di hash molto meno efficienti, ma suppongo che sia meglio sbagliare dal lato della cautela e seguire ciò che dice la documentazione.


1
Stai leggendo erroneamente i documenti hash - è essenzialmente una situazione "oo". Se l'oggetto cambia, generalmente cambia anche l'hash. Questo è davvero un avvertimento per il programmatore, che se l'hash cambia a causa della mutazione di un oggetto, la modifica dell'oggetto mentre risiede in una raccolta che utilizza l'hash causerà un comportamento imprevisto. Se l'oggetto deve essere "mutabile in sicurezza" in una situazione del genere, non hai altra scelta che rendere l'hash non correlato allo stato mutabile. Questa particolare situazione mi suona strana, ma ci sono sicuramente situazioni rare in cui si applica.
Quinn Taylor,

1

Scusate se rischio di suonare un boffin completo qui ma ... ... nessuno si è preoccupato di dire che per seguire le "migliori pratiche" non dovreste assolutamente specificare un metodo uguale che NON prenderebbe in considerazione tutti i dati posseduti dal vostro oggetto target, ad es. i dati vengono aggregati al tuo oggetto, rispetto a un suo associato, devono essere presi in considerazione quando si implementa uguale. Se non vuoi prenderne in considerazione, di 'un' età 'in considerazione in un confronto, allora dovresti scrivere un comparatore e usarlo per eseguire i tuoi confronti invece di isEqual :.

Se si definisce un metodo isEqual: che esegue arbitrariamente il confronto di uguaglianza, si incorre nel rischio che questo metodo venga utilizzato in modo improprio da un altro sviluppatore, o anche da se stessi, una volta dimenticata la "svolta" nella propria interpretazione uguale.

Ergo, anche se questo è un ottimo quesito sull'hash, di solito non è necessario ridefinire il metodo di hashing, ma probabilmente dovresti definire un comparatore ad hoc.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.