web-dev-qa-db-fra.com

Les meilleures méthodes pour outrepasser isEqual: and hash

Comment remplacez-vous correctement isEqual: en Objective-C? Le "piège" semble être que si deux objets sont égaux (comme déterminé par le isEqual: méthode), ils doivent avoir la même valeur de hachage.

La section Introspection du Guide de base du cacao contient un exemple sur la façon de remplacer isEqual:, copié comme suit, pour une classe nommée 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;
}

Il vérifie l’égalité du pointeur, puis l’égalité de classe et enfin compare les objets à l’aide de isEqualToWidget:, qui ne vérifie que les propriétés name et data. Ce que l'exemple ne montre pas montre comment remplacer hash.

Supposons que d’autres propriétés n’affectent pas l’égalité, disons age. La méthode hash ne devrait-elle pas être remplacée de telle sorte que seuls name et data affectent le hachage? Et si oui, comment feriez-vous cela? Ajoutez simplement les hachages de name et data? Par exemple:

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

Est-ce suffisant? Y a-t-il une meilleure technique? Et si vous avez des primitives, comme int? Les convertir en NSNumber pour obtenir leur hash? Ou des structures comme NSRect?

( Cerveau péter : Initialement écrit "bitwise OU" eux avec |=. Signifié add.)

264
Dave Dribin

Commencer avec

 NSUInteger prime = 31;
 NSUInteger result = 1;

Ensuite, pour chaque primitif que vous faites

 result = prime * result + var

Pour 64 bits, vous pouvez également vouloir décaler et xor.

 result = prime * result + (int) (var ^ (var >>> 32));

Pour les objets, utilisez 0 pour nil et sinon leur hashcode.

 result = prime * result + [var hash];

Pour les booléens, vous utilisez deux valeurs différentes

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

Explication et attribution

Ce n’est pas le travail de Cuddt, et les commentaires demandaient plus d’explications, alors je pense qu’une édition pour l’attribution est juste.

Cet algorithme a été popularisé dans le livre "Effective Java", et le chapitre correspondant est actuellement disponible en ligne ici . Ce livre a popularisé l’algorithme, qui est maintenant utilisé par défaut dans un certain nombre d’applications Java (y compris Eclipse). Il repose toutefois sur une implémentation encore plus ancienne, attribuée de manière différente à Dan Bernstein ou Chris. Torek. Cet ancien algorithme flottait à l'origine sur Usenet, et certaines attributions sont difficiles, par exemple, il y a quelques commentaire intéressant dans ce code Apache (recherche de leurs noms) qui fait référence à la source originale.

En bout de ligne, c’est un très ancien algorithme de hachage simple. Ce n'est pas le plus performant, et il n'est même pas prouvé mathématiquement qu'il est un "bon" algorithme. Mais c’est simple, et beaucoup de gens l’utilisent depuis longtemps avec de bons résultats, c’est pourquoi il a beaucoup de soutien historique.

110
tcurdt

Je ne fais que prendre moi-même Objective-C, je ne peux donc pas parler pour cette langue en particulier, mais dans les autres langues que j'utilise si deux instances sont "égales", elles doivent renvoyer le même hachage - sinon vous aurez toutes sortes de problèmes lorsque vous essayez de les utiliser comme clés dans une table de hachage (ou dans une collection de type dictionnaire).

D'autre part, si 2 instances ne sont pas égales, elles peuvent ou non avoir le même hash - il est préférable qu'elles ne le soient pas. C'est la différence entre une O(1) recherche sur une table de hachage et une O(N) recherche - si tous vos hachages se rencontrent, vous constaterez peut-être que rechercher votre table n'est pas meilleur que chercher une liste.

En termes de meilleures pratiques, votre hash devrait renvoyer une distribution aléatoire de valeurs pour son entrée. Cela signifie que, par exemple, si vous avez un double, mais que la majorité de vos valeurs ont tendance à se regrouper entre 0 et 100, vous devez vous assurer que les hachages renvoyés par ces valeurs sont répartis de manière égale dans toute la plage de valeurs de hachage possibles. . Cela améliorera considérablement vos performances.

Il existe un certain nombre d'algorithmes de hachage, dont plusieurs sont énumérés ici. J'essaie d'éviter de créer de nouveaux algorithmes de hachage, car cela peut avoir d'importantes répercussions sur les performances. Par conséquent, utiliser les méthodes de hachage existantes et effectuer une combinaison de bits comme vous le faites dans votre exemple est un bon moyen de l'éviter.

81
Brian B.

Un simple XOR sur les valeurs de hachage des propriétés critiques est suffisant 99% du temps.

Par exemple:

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

Solution trouvée sur http://nshipster.com/equality/ par Mattt Thompson (qui a également évoqué cette question dans son message!)

31
Yariv Nissim

J'ai trouvé ce fil extrêmement utile, fournissant tout ce dont j'avais besoin pour obtenir mon isEqual: et hash méthodes implémentées avec une capture. Lors du test des variables d'instance d'objet dans isEqual: l'exemple de code utilise:

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

Cela a échoué à plusieurs reprises (c'est à dire., revenu NON) sans et erreur, quand je savait les objets étaient identiques dans mes tests unitaires. La raison en était qu’une des variables d’instance NSString était néant donc la déclaration ci-dessus était:

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

et depuis néant répondra à n'importe quelle méthode, ceci est parfaitement légal mais

[nil isEqual: nil]

résultats néant, lequel est NONdonc quand l’objet testé et celui testé ont un néant objet, ils seraient considérés non égaux (c'est à dire., isEqual: retournerais NON).

Ce correctif simple consistait à modifier l'instruction if en:

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

De cette façon, si leurs adresses sont les mêmes, l'appel de méthode est ignoré, peu importe qu'ils soient tous les deux néant ou les deux pointant sur le même objet mais si l'un ou l'autre n'est pas néant ou bien ils pointent vers des objets différents, le comparateur est appelé de manière appropriée.

J'espère que cela épargnera à quelqu'un quelques minutes de grattage de la tête.

27
LavaSlider

La fonction de hachage doit créer une valeur semi-unique qui ne risque pas d'entrer en collision ou de correspondre à la valeur de hachage d'un autre objet.

Voici la fonction de hachage complet, qui peut être adaptée aux variables d'instance de vos classes. Il utilise NSUInteger plutôt que int pour la compatibilité sur les applications 64/32 bits.

Si le résultat devient 0 pour différents objets, vous risquez une collision de hachages. La collision de hachages peut entraîner un comportement inattendu du programme lors de l'utilisation de certaines classes de collection qui dépendent de la fonction de hachage. Assurez-vous de tester votre fonction de hachage avant de l'utiliser.

-(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;
}
20
Paul Solt

Cela m'a beaucoup aidé! Peut-être la réponse que vous cherchez. implémentant l'égalité et le hachage

18
Steve M

La manière simple mais peu efficace est de retourner le même -hash valeur pour chaque instance. Sinon, oui, vous devez implémenter un hachage basé uniquement sur des objets qui affectent l'égalité. C'est délicat si vous utilisez des comparaisons laxistes dans -isEqual: (comparaisons de chaînes ne respectant pas la casse, par exemple). Pour ints, vous pouvez généralement utiliser int lui-même, à moins que vous ne compariez avec NSNumbers.

N'utilisez pas | =, cependant, cela saturera. Utilisez ^ = à la place.

Fait amusant amusant: [[NSNumber numberWithInt:0] isEqual:[NSNumber numberWithBool:NO]], mais [[NSNumber numberWithInt:0] hash] != [[NSNumber numberWithBool:NO] hash]. (rdar: // 4538282, ouvert depuis le 5 mai 2006)

13
Jens Ayton

Rappelez-vous que vous devez uniquement fournir un hash égal lorsque isEqual est vrai. Lorsque isEqual est faux, le hachage ne doit pas nécessairement être inégal, bien qu'il le soit vraisemblablement. Par conséquent:

Keep hash simple. Choisissez une variable membre (ou quelques membres) qui soit la plus distinctive.

Par exemple, pour CLPlacemark, le nom suffit. Oui, il y a 2 ou 3 CLPlacemark distincts portant exactement le même nom, mais ceux-ci sont rares. Utilisez ce hash.

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

@implementation CLPlacemark (equal)

...

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


@end

Remarquez que je ne me donne pas la peine de spécifier la ville, le pays, etc. Le nom suffit. Peut-être le nom et CLLocation.

Le hachage doit être uniformément distribué. Vous pouvez donc combiner plusieurs membres en utilisant le caret ^ (signe xor)

Donc, c'est quelque chose comme

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

De cette façon, le hachage sera distribué de manière égale.

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

Alors, que faire en tableau?

Encore une fois, simple. Vous n'êtes pas obligé de hacher tous les membres du tableau. Assez pour hacher le premier élément, le dernier élément, le compte, peut-être quelques éléments centraux, et c'est tout.

10
user4951

Attendez, un moyen beaucoup plus facile de faire cela est d’abord de remplacer - (NSString )description et de fournir une représentation sous forme de chaîne de votre état d’objet (vous devez représenter l’ensemble de l’état de votre objet dans cette chaîne).

Ensuite, fournissez simplement l’implémentation suivante de hash:

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

Ceci est basé sur le principe que "si deux objets de chaîne sont égaux (comme déterminé par la méthode isEqualToString:), ils doivent avoir la même valeur de hachage".

Source: référence de classe NSString

7
Jonathan Ellis

Les contrats d'égalité et de hachage sont bien spécifiés et minutieusement étudiés dans le monde Java (voir la réponse de @ mipardi)), mais les mêmes considérations devraient s'appliquer à Objective-C.

Eclipse effectue un travail fiable de génération de ces méthodes en Java. Voici donc un exemple Eclipse transféré manuellement vers 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;
}

Et pour une sous-classe YourWidget qui ajoute une propriété 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;
}

Cette implémentation évite certains écueils de sous-classes dans l’échantillon isEqual: de Apple:

  • Test de classe d'Apple other isKindOfClass:[self class] est asymétrique pour deux sous-classes différentes de MyWidget. L'égalité doit être symétrique: a = b si et seulement si b = a. Cela pourrait facilement être corrigé en modifiant le test en other isKindOfClass:[MyWidget class] _, toutes les sous-classes MyWidget seraient alors comparables.
  • Utiliser un isKindOfClass: test de sous-classe empêche les sous-classes de remplacer isEqual: avec un test d’égalité raffiné. En effet, l'égalité doit être transitive: si a = b et a = c, alors b = c. Si une instance MyWidget se compare à deux instances YourWidget, ces instances YourWidget doivent être égales, même si leur serialNo diffère.

Le deuxième problème peut être résolu en considérant que les objets sont égaux s'ils appartiennent exactement à la même classe, d'où le [self class] != [object class] test ici. Pour les types classes d'application, cela semble être la meilleure approche.

Cependant, il y a certainement des cas où le isKindOfClass: test est préférable. Ceci est plus typique de classes de structure que des classes d'application. Par exemple, tout NSString doit être égal à tout autre NSString avec la même séquence de caractères sous-jacente, quelle que soit la distinction NSString/NSMutableString, ainsi que quels que soient les classes privées du cluster de classe NSString qui sont impliquées.

Dans ces cas, isEqual: devrait avoir un comportement bien défini et bien documenté, et il devrait être clairement indiqué que les sous-classes ne peuvent pas remplacer ce comportement. En Java, la restriction "pas de substitution" peut être appliquée en signalant les méthodes equals et hashcode comme final, mais Objective-C n'a pas d'équivalent.

5
jedwidz

J'ai trouvé que cette page était un guide utile pour le remplacement des méthodes de type égalité et hachage. Il inclut un algorithme décent pour calculer les codes de hachage. La page est orientée vers Java, mais il est assez facile de l’adapter à Objective-C/Cocoa.

5
mipadi

Cela ne répond pas directement à votre question (du tout) mais j'ai déjà utilisé MurmurHash pour générer des hachages: murmurhash

Je suppose que je devrais expliquer pourquoi: le murmurhash est vachement rapide ...

5
schwa

Je suis aussi un novice de l’Objectif C, mais j’ai trouvé un excellent article sur l’identité contre l’égalité dans l’objectif C ici . D'après mes lectures, il semblerait que vous puissiez simplement conserver la fonction de hachage par défaut (qui devrait fournir une identité unique) et implémenter la méthode isEqual afin de comparer les valeurs de données.

4
ceperry

Quinn a tout simplement tort de dire que la référence au murmure est inutile ici. Quinn a raison de vouloir comprendre la théorie derrière le hachage. Le murmure distille beaucoup de cette théorie dans une implémentation. Il est intéressant d'explorer comment appliquer cette implémentation à cette application particulière.

Quelques points clés ici:

L'exemple de fonction de tcurdt suggère que "31" est un bon multiplicateur, car il est premier. Il faut montrer que la prime est une condition nécessaire et suffisante. En fait, 31 (et 7) ne sont probablement pas des nombres premiers particulièrement bons, car 31 == -1% 32. Un multiplicateur impair avec environ la moitié des bits définis et la moitié des bits effacés est susceptible d'être meilleur. (La constante de multiplication de hachage de murmure a cette propriété.)

Ce type de fonction de hachage serait probablement plus fort si, après multiplication, la valeur du résultat était ajustée via un décalage et xor. La multiplication a tendance à produire les résultats de nombreuses interactions de bits à l'extrémité supérieure du registre et les résultats d'interaction faibles à l'extrémité inférieure du registre. Le décalage et xor augmentent les interactions en bas du registre.

Fixer le résultat initial à une valeur où environ la moitié des bits sont nuls et environ la moitié des bits sont un aurait également tendance à être utile.

Il peut être utile de faire attention à l'ordre dans lequel les éléments sont combinés. Il faut probablement d’abord traiter les booléens et d’autres éléments où les valeurs ne sont pas fortement distribuées.

Il peut être utile d’ajouter quelques étapes d’embrouillage supplémentaires à la fin du calcul.

Que le murmure soit rapide ou non pour cette application est une question ouverte. Le murmure hash prémélange les bits de chaque mot d'entrée. Plusieurs mots d'entrée peuvent être traités en parallèle, ce qui facilite les processeurs en pipeline à problèmes multiples.

3
Ces

En combinant la réponse de @ tcurdt avec la réponse de @ oscar-gomez pour obtenir les noms de propriété , nous pouvons créer une solution simple à mettre en place pour isEqual et le hachage:

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

Maintenant, dans votre classe personnalisée, vous pouvez facilement implémenter isEqual: et hash:

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

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

Notez que si vous créez un objet pouvant être muté après la création, la valeur de hachage ne doit pas changer si l'objet est inséré dans une collection. Concrètement, cela signifie que la valeur de hachage doit être fixée à partir du point de la création initiale de l'objet. Reportez-vous à la section Documentation Apple sur la méthode -hash du protocole NSObject pour plus d'informations:

Si un objet modifiable est ajouté à une collection qui utilise des valeurs de hachage pour déterminer la position de l’objet dans la collection, la valeur renvoyée par la méthode de hachage de l’objet ne doit pas changer tant que l’objet est dans la collection. Par conséquent, la méthode de hachage ne doit pas s'appuyer sur les informations d'état interne de l'objet ou vous devez vous assurer que les informations d'état interne de l'objet ne changent pas tant qu'il se trouve dans la collection. Ainsi, par exemple, un dictionnaire mutable peut être placé dans une table de hachage mais vous ne devez pas le changer tant qu'il est dans la table. (Notez qu'il peut être difficile de savoir si un objet donné se trouve ou non dans une collection.)

Cela me semble tout à fait ridicule, car cela rend potentiellement efficacement les recherches de hachage beaucoup moins efficaces, mais je suppose qu'il est préférable de faire preuve de prudence et de suivre les indications de la documentation.

2
user10345

Désolé si je risque de produire ici un boffin complet, mais ... ... personne ne prend la peine de mentionner que pour suivre les "meilleures pratiques", vous ne devez absolument pas spécifier une méthode d'égalité qui NE TENIRA PAS tenir compte de toutes les données appartenant à votre objet cible, par exemple. les données sont agrégées à votre objet, par opposition à un associé, doivent être prises en compte lors de l'implémentation d'égal à égal. Si vous ne voulez pas prendre en compte, dites "age" dans une comparaison, écrivez un comparateur et utilisez-le pour effectuer vos comparaisons au lieu de isEqual :.

Si vous définissez une méthode isEqual: qui effectue une comparaison d'égalité de manière arbitraire, vous courez le risque que cette méthode soit utilisée de manière abusive par un autre développeur, ou même par vous-même, une fois que vous avez oublié la "torsion" de votre interprétation d'égal à égal.

Ainsi, bien qu’il s’agisse d’une excellente méthode de hachage, vous n’avez normalement pas besoin de redéfinir la méthode de hachage, vous devez probablement définir un comparateur ad-hoc à la place.

1
Thibaud de Souza