覆盖isEqual:和哈希的最佳实践


267

您如何isEqual:在Objective-C中正确覆盖?“捕获”似乎是如果两个对象相等(由isEqual:方法确定),则它们必须具有相同的哈希值。

可可基础知识指南》的内省”部分确实有一个示例,说明如何重写名为的类,如下所示:isEqual:MyWidget

- (BOOL)isEqual:(id)other {
    if (other == self)
        return YES;
    if (!other || ![other isKindOfClass:[self class]])
        return NO;
    return [self isEqualToWidget:other];
}

- (BOOL)isEqualToWidget:(MyWidget *)aWidget {
    if (self == aWidget)
        return YES;
    if (![(id)[self name] isEqual:[aWidget name]])
        return NO;
    if (![[self data] isEqualToData:[aWidget data]])
        return NO;
    return YES;
}

它先检查指针是否相等,然后再检查类是否相等,最后使用来比较对象isEqualToWidget:,后者仅检查namedata属性。该示例显示的是如何重写hash

假设还有其他不影响相等性的属性age。不应hash仅重写方法namedata影响哈希吗?如果是这样,您将如何做?只需添加的哈希namedata?例如:

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

够了吗?有更好的技术吗?如果您有原始类型,该int怎么办?转换它们以NSNumber获取其哈希值?还是类似的结构NSRect

(“ 大脑放屁”:最初将它们与一起写成“按位或” |=


2
if (![other isKindOfClass:[self class]])-从技术上讲,这意味着平等不会是可交换的。即A = B并不意味着B = A(例如,如果一个是另一个的子类)
罗伯特·罗伯特(Robert Robert

文档链接已死,现在存档至内省
jedwidz

Answers:


111

从...开始

 NSUInteger prime = 31;
 NSUInteger result = 1;

然后对于每个原始对象

 result = prime * result + var

对于对象,将0用作nil,否则将其哈希码使用。

 result = prime * result + [var hash];

对于布尔值,您可以使用两个不同的值

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

解释和归因

这不是tcurdt的工作,评论要求提供更多解释,所以我认为对署名的编辑是公平的。

该算法在《 Effective Java》一书中得到了普及,相关的章节目前可以在此处在线找到。该书普及了该算法,该算法现在是许多Java应用程序(包括Eclipse)中的默认算法。但是,它源自甚至更老的实现,该实现被不同地归因于Dan Bernstein或Chris Torek。该较旧的算法最初是在Usenet上浮动的,因此很难确定某些归因。例如,此Apache代码中有一些有趣的注释(搜索它们的名称),这些注释引用了原始源代码。

最重要的是,这是一种非常古老的简单哈希算法。它不是性能最高的,甚至在数学上都没有证明它是一种“好的”算法。但是它很简单,并且很多人已经使用了很长时间,并且效果很好,因此它具有很多历史支持。


9
1231:1237是从哪里来的?我也在Java的Boolean.hashCode()中看到它。神奇吗?
大卫·伦纳德

17
哈希算法的本质是会发生冲突。保罗,所以我看不出你的意思。
tcurdt 2011年

85
在我看来,这个答案并没有回答实际的问题(重写NSObject哈希的最佳做法)。它仅提供一种特定的哈希算法。最重要的是,解释的稀疏性使得如果不对问题有深入的了解就很难理解,并且可能导致人们在使用它的同时却不知道自己在做什么。我不明白为什么这个问题有这么多的反对意见。
里卡多·桑切斯·塞兹

6
第一个问题-(int)很小,很容易溢出,请使用NSUInteger。第二个问题-如果您将结果乘以每个变量哈希,结果将溢出。例如。[NSString hash]创建较大的值。如果您有5个以上的变量,则此算法很容易溢出。这将导致所有内容都映射到相同的哈希,这很糟糕。看到我的回复:stackoverflow.com/a/4393493/276626
Paul Solt 2012年

10
@PaulSolt-溢出不是生成哈希的问题,冲突是。但是溢出并不一定会使冲突发生的可能性更大,关于溢出会导致所有内容映射到同一哈希的陈述是完全错误的。
DougW 2012年

81

我本人只是拿起Objective-C,所以我不能专门针对该语言讲,但是在其他语言中,如果两个实例“相等”,它们必须返回相同的哈希值-否则,您将拥有全部尝试将它们用作哈希表(或任何字典类型的集合)中的键时会遇到各种问题。

另一方面,如果2个实例不相等,则它们可能具有或没有相同的哈希值-最好是不相同。这是哈希表上的O(1)搜索与O(N)搜索之间的区别-如果所有哈希值都发生冲突,您可能会发现搜索表并不比搜索列表更好。

根据最佳做法,您的散列应该为其输入返回值的随机分布。这意味着,例如,如果您有一个double,但是大多数值倾向于聚集在0到100之间,则需要确保这些值返回的哈希值均匀分布在所有可能的哈希值范围内。这将大大提高您的性能。

那里有很多哈希算法,包括这里列出的几种。我尝试避免创建新的哈希算法,因为它可能会对性能产生很大的影响,因此,如您在示例中所做的那样,使用现有的哈希方法并进行某种按位组合是避免这种情况的一种好方法。


4
+1出色的答案,值得更多批评,尤其是因为他实际上在谈论“最佳实践”以及为何良好(唯一)哈希很重要的背后的理论。
奎因·泰勒,

30

对关键属性的哈希值进行简单的XOR足以在99%的时间内完成。

例如:

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

Mattt Thompson 在http://nshipster.com/equality/上找到了解决方案(该解决方案在他的帖子中也提到了这个问题!)


1
这个答案的问题是它根本不考虑原始值。原始值对于哈希也可能很重要。
Vive

@Vive大多数这些问题都是在Swift中解决的,但是由于这些类型是原始类型,因此它们通常表示它们自己的哈希。
Yariv Nissim

1
虽然您适合使用Swift,但仍有许多使用objc编写的项目。因为您的答案专用于objc,所以至少值得一提。
Vive

对哈希值进行异或运算是不好的建议,它会导致许多哈希冲突。取而代之的是与质数相乘,然后加,作为其他答案。
fishinear

27

我发现此线程非常有用,它提供了使我的方法isEqual:hash方法实现一次捕获所需的一切。在isEqual:示例代码中测试对象实例变量时,使用:

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

当我知道单元测试中的对象相同时,这反复失败(,返回NO)而没有错误。原因是,实例变量之一为nil,因此上述语句为:NSString

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

而且由于nil会响应任何方法,因此这是完全合法的,但

[nil isEqual: nil]

返回nil,它是NO,因此当对象和被测试的对象都具有nil对象时,它们将被视为不相等(isEqual:将返回NO)。

这个简单的解决方法是将if语句更改为:

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

这样,如果它们的地址相同,则无论它们都是还是都指向同一个对象,如果它们都不为零或它们都指向不同的对象,则将跳过方法调用,然后将适当地调用比较器。

我希望这可以节省一些人抓头的时间。


20

哈希函数应该创建一个半唯一值,该值不太可能与另一个对象的哈希值冲突或匹配。

这是完整的哈希函数,可以适合您的类实例变量。它使用NSUInteger而不是int来实现对64/32位应用程序的兼容性。

如果不同对象的结果变为0,则存在散列冲突的风险。当使用某些依赖于哈希函数的集合类时,冲突散列可能导致程序异常。确保在使用前测试您的哈希函数。

-(NSUInteger)hash {
    NSUInteger result = 1;
    NSUInteger prime = 31;
    NSUInteger yesPrime = 1231;
    NSUInteger noPrime = 1237;

    // Add any object that already has a hash function (NSString)
    result = prime * result + [self.myObject hash];

    // Add primitive variables (int)
    result = prime * result + self.primitiveVariable; 

    // Boolean values (BOOL)
    result = prime * result + (self.isSelected?yesPrime:noPrime);

    return result;
}

3
这里有一个陷阱:我更喜欢避免点语法,因此我将您的BOOL语句转换为(eg)result = prime * result + [self isSelected] ? yesPrime : noPrime;。然后我发现这是设置result为(eg)1231,我认为是由于?操作员的优先权。我通过添加括号来解决此问题:result = prime * result + ([self isSelected] ? yesPrime : noPrime);
Ashley 2013年

12

一种简单但效率低下的方法是-hash为每个实例返回相同的值。否则,是的,您必须仅基于影响相等性的对象实现哈希。如果在其中使用松散比较-isEqual:(例如,不区分大小写的字符串比较),这将非常棘手。对于int,除非可以与NSNumbers进行比较,否则通常可以使用int本身。

但是,请勿使用| =,它会饱和。请改用^ =。

随机有趣的事实:[[NSNumber numberWithInt:0] isEqual:[NSNumber numberWithBool:NO]],但是[[NSNumber numberWithInt:0] hash] != [[NSNumber numberWithBool:NO] hash]。(rdar:// 4538282,自2006年5月5日开始营业)


1
您在| =上是正确的。真的不是那个意思。:) + =和^ =相当等效。如何处理double和float等非整数原语?
戴夫·德里宾

随机的有趣事实:在雪豹上测试... ;-)
奎因·泰勒

对于使用XOR而不是OR将字段组合成哈希,他是正确的。但是,不要使用为每个对象返回相同的-hash值的建议-尽管很容易,但是它会严重降低使用该对象的哈希值的任何对象的性能。哈希不具备成为不同的是不相等的对象,但如果你能做到这一点,没有什么喜欢它。
Quinn Taylor

打开的雷达错误报告已关闭。openradar.me/4538282是什么意思?
JJD

奎因暗示,JJD,此错误已在Mac OS X 10.6中修复。(请注意,该评论
已有

9

请记住,当isEqualtrue 时,您只需要提供相等的哈希即可。当isEqual为false时,尽管哈希值不一定相等,但不一定相等。因此:

保持哈希简单。选择一个(或几个成员)最有特色的变量。

例如,对于CLPlacemark,仅名称就足够了。是的,有2个或3个完全相同的名称CLPlacemark,但是很少。使用该哈希。

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

@implementation CLPlacemark (equal)

...

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


@end

注意,我不必指定城市,国家等。名称就足够了。名称和CLLocation。

哈希应平均分配。因此,您可以使用插入符号^(异或符号)组合多个成员变量

所以就像

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

这样,哈希将平均分配。

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

那么在数组中做什么?

再次,简单。您不必哈希数组的所有成员。足以散列第一个元素,最后一个元素,计数,也许是一些中间元素,仅此而已。


对哈希值进行XOR运算不会得到均匀的分布。
fishinear

7

稍等,当然,一种更简单的方法是首先覆盖- (NSString )description并提供对象状态的字符串表示形式(您必须在此字符串中表示对象的整个状态)。

然后,只需提供以下实现hash

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

这基于以下原则:“如果两个字符串对象相等(由isEqualToString:方法确定),则它们必须具有相同的哈希值。”

来源:NSString类参考


1
这假定描述方法将是唯一的。使用description的散列会创建一个依赖关系,这种依赖关系可能并不明显,并且存在更高的冲突风险。
Paul Solt 2012年

1
+1推荐。这是一个很棒的主意。如果您担心描述会引起冲突,则可以覆盖它。
user4951 2012年

谢谢Jim,我不会否认这有点hack,但是在我能想到的任何情况下它都可以工作-正如我说的,只要您覆盖description,我不明白为什么这不如任何投票率较高的解决方案。可能不是数学上最优雅的解决方案,但应该可以解决。正如Brian B.指出的那样(这是目前为止最受欢迎的答案):“我试图避免创建新的哈希算法”-同意!-我只是hashNSString
乔纳森·埃利斯

赞成,因为这是一个好主意。我不会使用它,因为我担心会有额外的NSString分配。
karwag

1
这不是通用解决方案,因为对于大多数类而言,都description包括指针地址。因此,这会使同一个类的两个不同实例具有不同的哈希值,这违背了两个相等的对象具有相同哈希值的基本假设!
Diogo T

5

在Java世界中,equals和hash合同已得到很好的指定和深入研究(请参阅@mipardi的答案),但是所有相同的考虑因素也应适用于Objective-C。

Eclipse在用Java生成这些方法方面做得很可靠,因此这是一个手工移植到Objective-C的Eclipse示例:

- (BOOL)isEqual:(id)object {
    if (self == object)
        return true;
    if ([self class] != [object class])
        return false;
    MyWidget *other = (MyWidget *)object;
    if (_name == nil) {
        if (other->_name != nil)
            return false;
    }
    else if (![_name isEqual:other->_name])
        return false;
    if (_data == nil) {
        if (other->_data != nil)
            return false;
    }
    else if (![_data isEqual:other->_data])
        return false;
    return true;
}

- (NSUInteger)hash {
    const NSUInteger prime = 31;
    NSUInteger result = 1;
    result = prime * result + [_name hash];
    result = prime * result + [_data hash];
    return result;
}

对于YourWidget添加属性的子类serialNo

- (BOOL)isEqual:(id)object {
    if (self == object)
        return true;
    if (![super isEqual:object])
        return false;
    if ([self class] != [object class])
        return false;
    YourWidget *other = (YourWidget *)object;
    if (_serialNo == nil) {
        if (other->_serialNo != nil)
            return false;
    }
    else if (![_serialNo isEqual:other->_serialNo])
        return false;
    return true;
}

- (NSUInteger)hash {
    const NSUInteger prime = 31;
    NSUInteger result = [super hash];
    result = prime * result + [_serialNo hash];
    return result;
}

此实现避免了isEqual:Apple 示例中的某些子类陷阱:

  • 苹果的类测试other isKindOfClass:[self class]对于的两个不同子类是不对称的MyWidget。平等必须是对称的:当且仅当b = a时,a = b。可以通过将test更改为来轻松解决此问题other isKindOfClass:[MyWidget class],然后所有MyWidget子类都可以相互比较。
  • 使用isKindOfClass:子类测试可防止子类isEqual:被精化的相等性测试覆盖。这是因为平等需要传递:如果a = b和a = c,则b = c。如果一个MyWidget实例等于两个YourWidget实例,则这些YourWidget实例即使彼此serialNo不同也必须彼此相等。

第二个问题可以通过仅考虑对象属于完全相同的类而相等来解决,因此在[self class] != [object class]这里进行测试。对于典型的应用程序类,这似乎是最好的方法。

但是,当然在某些情况下该isKindOfClass:测试更可取。框架类比应用程序类更典型。例如,任何一个NSString都应该比较其他任何NSString具有相同基础字符序列的其他字符,而不管NSString/的NSMutableString区别,也不管NSString涉及类集群中的哪些私有类。

在这种情况下,isEqual:应具有定义明确,记录良好的行为,并且应明确指出子类不能覆盖此行为。在Java中,可以通过将equals和hashcode方法标记为来强制实施“无覆盖”限制final,但是Objective-C没有等效项。


@adubr在我的后两段中已经介绍了。它不是重点,因为它不是MyWidget类集群。
jedwidz 2014年

5

(根本没有)直接回答您的问题,但是我之前使用过MurmurHash来生成哈希:murmurhash

猜猜我应该解释为什么:murmurhash血腥快速...


2
一个C ++库,它专注于使用随机数对void *键进行唯一哈希处理(并且也不与Objective-C对象相关),这实际上不是一个有用的建议。-hash方法每次应返回一个一致的值,否则将完全无用。如果将对象添加到调用-hash的集合中,并且每次都返回一个新值,则将永远不会检测到重复项,也永远无法从该集合中检索该对象。在这种情况下,术语“哈希”与安全/密码学中的含义不同。
奎因·泰勒,

3
murmurhash不是加密哈希函数。发布错误信息之前,请检查您的事实。murmur哈希可能是散列定制Objective-C类有用的(尤其是如果你有很多NSDatas的参与),因为它是非常快的。但是,我确实向您表示,也许暗示给某人“只是捡起物镜-c”不是最好的建议,但是请在我对问题的原始答复中注明我的前缀。
schwa,2010年


4

我是一个目标C新手太多,但是我发现有关目标C的身份与平等的优秀文章在这里。从我的阅读看来,您也许可以保留默认的哈希函数(应提供唯一标识)并实现isEqual方法,以便比较数据值。


我是Cocoa / Objective C的新手,这个答案和链接确实帮助我深入了以上所有更高级的内容-无需担心哈希-只需实现isEqual:方法即可。谢谢!
约翰·加拉格尔

不要错过@ceperry的链接。Equality vs IdentityKarl Kraft 的文章真的很好。
JJD

6
@约翰:我想你应该重新阅读这篇文章。它非常清楚地表明“相等的实例必须具有相等的哈希值”。如果覆盖isEqual:,则还必须覆盖hash
史蒂夫·马德森

3

奎因(Quinn)错了,因为这里没有提及杂项哈希。奎因是对的,您想了解散列背后的理论是对的。杂音把许多理论提炼成一种实现。弄清楚如何将该实现应用于此特定应用程序值得探讨。

这里的一些关键点:

来自tcurdt的示例函数表明'31'是一个很好的乘数,因为它是素数。一个人需要证明素人是必要和充分的条件。实际上31(和7)可能不是特别好的质数,因为31 == -1%32。奇乘数大约设置一半,清除一半则可能更好。(杂音哈希乘法常数具有该属性。)

如果在相乘后通过shift和xor调整结果值,则这种哈希函数可能会更强大。乘法往往会在寄存器的高端产生大量位交互的结果,而在寄存器的底端产生低交互作用的结果。移位和异或会增加寄存器底端的交互作用。

将初始结果设置为大约一半的位为零而大约一半的位为一的值也将很有用。

小心组合元素的顺序可能很有用。可能应该先处理布尔值和其他值分布不强的元素。

在计算结束时添加几个额外的位加扰阶段可能会很有用。

对于此应用程序而言,杂项哈希实际上是否快速是一个悬而未决的问题。杂音哈希将每个输入单词的位预混。可以并行处理多个输入字,这有助于多次发出流水线的cpus。


3

将@tcurdt的答案与@ oscar-gomez的答案相结合以获得属性名,我们可以为isEqual和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;
}

现在,在您的自定义类中,您可以轻松实现isEqual:hash

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

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

2

请注意,如果您要创建的对象可以在创建后进行更改,则如果将对象插入集合中,则哈希值不得更改。实际上,这意味着从初始对象创建的角度出发,哈希值必须是固定的。有关更多信息,请参阅有关NSObject协议的-hash方法的Apple文档

如果将可变对象添加到使用哈希值确定对象在集合中位置的集合中,则当对象在集合中时,对象的hash方法返回的值不得更改。因此,哈希方法必须不依赖于任何对象的内部状态信息,或者您必须确保对象在集合中时对象的内部状态信息不会改变。因此,例如,可以将一个可变字典放在哈希表中,但是当它在哈希表中时,您不能对其进行更改。(请注意,可能很难知道给定对象是否在集合中。)

对我来说,这听起来像是彻底的捣蛋,因为它可能有效地导致哈希查找的效率大大降低,但是我想最好还是谨慎一点,并遵循文档中的内容。


1
您读错了哈希文档,这实际上是一种“非此即彼”的情况。如果对象更改,则哈希通常也会更改。这实际上是对程序员的警告,如果哈希值由于对象的突变而发生更改,则在对象驻留在使用哈希值的集合中时更改对象将导致意外行为。如果在这种情况下对象必须“安全可变”,则别无选择,只能使散列与可变状态无关。那种特殊情况对我来说确实很奇怪,但是在某些情况下肯定不常见。
奎因·泰勒,

1

抱歉,如果我冒着风险听起来不妙,但是... ...没人愿意提及遵循“最佳实践”的做法,那么您绝对不应该指定一个不会考虑目标对象拥有的所有数据的equals方法,例如实施equals时,应考虑将数据聚合到您的对象,而不是对象的关联。如果您不想在比较中考虑“年龄”,则应编写一个比较器并使用它来执行比较,而不要使用isEqual:。

如果您定义了一个isEqual:方法,该方法可任意执行相等性比较,一旦您忘记了equals解释中的“扭曲”,您就有可能被另一名开发人员甚至您自己滥用该方法。

因此,尽管这是有关哈希的一个很好的问答,但是通常您不需要重新定义哈希方法,而应该定义一个临时比较器。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.