覆盖isEqual:和hash的最佳实践

时间:2008-10-31 17:22:26

标签: objective-c equality

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

IntrospectionCocoa Fundamentals Guide部分确实有一个示例,说明如何为名为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:比较对象,name仅检查datahash属性。 没有显示的示例是如何覆盖age

假设有其他属性不影响平等,比如hash。是否应该覆盖name方法,以便仅dataname影响哈希?如果是这样,你会怎么做?只需添加data- (NSUInteger)hash { NSUInteger hash = 0; hash += [[self name] hash]; hash += [[self data] hash]; return hash; } 的哈希值?例如:

int

这还够吗?有更好的技术吗?如果你有基元,如NSNumber怎么办?将它们转换为NSRect以获取它们的哈希值?或结构如|=

脑屁:最初与{{1}}一起写“按位或”。添加。)

16 个答案:

答案 0 :(得分:110)

开始
 NSUInteger prime = 31;
 NSUInteger result = 1;

然后为你做的每一个原语

 result = prime * result + var

对于64位,你可能也想转移和xor。

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

对于对象,您使用0表示nil,否则使用其哈希码。

 result = prime * result + [var hash];

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

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

解释和归因

这不是tcurdt的工作,评论要求更多解释,所以我认为归因的编辑是公平的。

该算法在“Effective Java”和the relevant chapter can currently be found online here一书中得到了普及。那本书推广了算法,现在这个算法在许多Java应用程序(包括Eclipse)中都是默认的。然而,它来自一个更老的实现,这种实现不同地归功于Dan Bernstein或Chris Torek。那个较旧的算法最初浮在Usenet上,并且某些归因很难。例如,有一些引用原始来源的interesting commentary in this Apache code(搜索其名称)。

底线是,这是一个非常古老,简单的哈希算法。它不是最高效的,甚至在数学上也不是一个“好”的算法。但它很简单,很多人已经使用了很长时间并取得了良好的效果,因此它有很多历史支持。

答案 1 :(得分:81)

我只是自己拿起Objective-C,所以我不能专门用这种语言说话,但是在其他语言中我使用如果两个实例是“Equal”它们必须返回相同的哈希 - 否则你就要去了尝试将它们用作哈希表(或任何字典类型集合)中的键时出现各种问题。

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

就最佳实践而言,哈希应返回其输入值的随机分布。这意味着,例如,如果您有一个double,但是您的大多数值倾向于在0到100之间聚类,则需要确保这些值返回的哈希值均匀分布在整个可能的哈希值范围内。这将显着提高您的表现。

有许多哈希算法,包括这里列出的几个。我试图避免创建新的哈希算法,因为它可能会产生很大的性能影响,因此使用现有的哈希方法并按照您的示例中的某种方式进行按位组合是一种避免它的好方法。

答案 2 :(得分:31)

  

对关键属性的哈希值进行简单的XOR就足够了   99%的时间。

例如:

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

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

答案 3 :(得分:27)

我发现这个帖子非常有用,提供了我需要的一切,以便用一个catch实现我的isEqual:hash方法。在isEqual:中测试对象实例变量时,示例代码使用:

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

当我 知道 对象时,这反复失败( ie ,返回 NO )没有错误我的单元测试相同。原因是,NSString个实例变量之一是 nil 所以上面的陈述是:

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

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

[nil isEqual: nil]

返回 nil NO ,所以当对象和被测对象都有 nil 对象时,它们将被视为不等于( ie isEqual:将返回 NO )。

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

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

这样,如果它们的地址相同,它会跳过方法调用,无论它们是 nil 还是两者都指向同一个对象,但如果它们不是 nil 或者他们指向不同的对象,然后适当地调用比较器。

我希望这能让人节省几分钟的时间。

答案 4 :(得分:20)

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

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

如果不同对象的结果为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;
}

答案 5 :(得分:13)

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

不要使用| =,但它会饱和。使用^ =代替。

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

答案 6 :(得分:10)

请记住,当isEqual为真时,您只需要提供相等的哈希值。当isEqual为假时,哈希不必是不相等的,尽管可能是这样。因此:

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

例如,对于CLPlacemark,名称就足够了。是的,有两个或三个区别的CLPlacemark具有完全相同的名称,但这些很少见。使用那个哈希。

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

@implementation CLPlacemark (equal)

...

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


@end

请注意,我不打算指定城市,国家等。名称就足够了。也许是名字和CLLocation。

哈希应该均匀分布。因此,您可以使用插入符号^(xor符号)

组合多个成员变量

所以它就像

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

这样哈希就会均匀分布。

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

那么在数组中做什么?

再次,简单。您不必散列数组的所有成员。足以散列第一个元素,最后一个元素,计数,也许是一些中间元素,就是这样。

答案 7 :(得分:7)

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

然后,只提供hash的以下实现:

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

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

来源:NSString Class Reference

答案 8 :(得分:5)

我发现this page是覆盖equals-和hash-type方法的有用指南。它包括一个计算哈希码的合适算法。该页面面向Java,但很容易使其适应Objective-C / Cocoa。

答案 9 :(得分:5)

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

猜猜我应该解释原因:murmurhash血腥快......

答案 10 :(得分:5)

在Java世界中已经明确规定了相等和哈希契约并进行了彻底研究(参见@ 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;
}

这种实现避免了Apple样本isEqual:中的一些子类陷阱:

  • Apple的类测试other isKindOfClass:[self class]对于MyWidget的两个不同子类是不对称的。平等需要是对称的:当且仅当b = a时,a = b。通过将测试更改为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没有等效。

答案 11 :(得分:4)

我也是Objective C的新手,但我在Objective C here找到了一篇关于身份与平等的优秀文章。从我的阅读中看起来你可能只能保留默认的哈希函数(它应该提供唯一的标识)并实现isEqual方法,以便它比较数据值。

答案 12 :(得分:3)

Quinn是错误的,这里对murmur哈希的引用毫无用处。 Quinn是对的,你想要理解哈希背后的理论。杂音将很多理论提炼成一个实现。弄清楚如何将该实现应用于此特定应用程序是值得探索的。

这里有一些关键点:

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

如果在乘以后,通过shift和xor调整结果值,则此类散列函数可能会更强。乘法往往会在寄存器的高端产生大量位交互的结果,并在寄存器的底端产生低交互结果。 shift和xor增加了寄存器底端的相互作用。

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

注意元素组合的顺序可能很有用。人们应该首先处理布尔值和其他价值分布不均的元素。

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

这个应用程序的杂音哈希是否真的很快是一个悬而未决的问题。杂音散列预混合每个输入字的位。可以并行处理多个输入字,这有助于多个问题的流水线cpu。

答案 13 :(得分:3)

将@tcurdt的回答与@ oscar-gomez对getting property names的回答相结合,我们可以为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);
}

答案 14 :(得分:2)

请注意,如果您正在创建一个可在创建后进行变异的对象,则如果将对象插入到集合中,则哈希值必须不会更改。实际上,这意味着必须从初始对象创建的角度修复哈希值。有关详细信息,请参阅Apple's documentation on the NSObject protocol's -hash method

  

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

这对我来说听起来像是完全不可思议,因为它可能有效地使哈希查找效率低得多,但我认为最好在谨慎方面犯错并遵循文档所说的内容。

答案 15 :(得分:1)

对不起,如果我冒险在这里发出完整的棺材,但...... ...没有人提到要遵循“最佳实践”,你绝对不应该指定一个不考虑目标对象拥有的所有数据的equals方法,例如聚合到你的对象的任何数据,而不是它的关联,在实施等于时应该考虑到。 如果你不想服用,比较说“年龄”,那么你应该写一个比较器并用它来进行比较,而不是isEqual:。

如果你定义了一个任意执行相等比较的isEqual:方法,一旦你忘记了平等解释中的'扭曲',你就会冒这个方法被其他开发者甚至你自己滥用的风险。

Ergo,虽然这是一个很好的q&amp; a关于散列,你通常不需要重新定义散列方法,你可能应该定义一个ad-hoc比较器。

相关问题