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

Cocoa Fundamentals Guide的Introspection部分确实有一个关于如何重写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:比较对象,后者只检查名称和数据属性。这个例子没有说明如何重写哈希。

让我们假设有其他属性不影响平等,比如年龄。难道不应该重写哈希方法,以便只有名称和数据影响哈希吗?如果是,你会怎么做?只是添加名称和数据的散列吗?例如:

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

这足够了吗?有更好的技术吗?如果你有基本类型,比如int呢?将它们转换为NSNumber以获得它们的散列?或者像NSRect这样的结构?

(脑屁:最初把“位或”和|=写在一起。意味着添加。)


当前回答

我自己只是在学习Objective-C,所以我不能特别地为这种语言说话,但在我使用的其他语言中,如果两个实例是“相等的”,它们必须返回相同的哈希值——否则当你试图将它们作为哈希表(或任何字典类型的集合)中的键时,你会遇到各种各样的问题。

另一方面,如果两个实例不相等,它们可能具有也可能不具有相同的哈希值——最好不是这样。这就是在哈希表上进行O(1)搜索和O(N)搜索的区别——如果你所有的哈希都发生冲突,你可能会发现搜索你的表并不比搜索一个列表好。

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

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

其他回答

这并没有直接回答你的问题,但我之前已经使用MurmurHash来生成哈希:MurmurHash

我想我应该解释一下原因:低语是非常快的……

我自己只是在学习Objective-C,所以我不能特别地为这种语言说话,但在我使用的其他语言中,如果两个实例是“相等的”,它们必须返回相同的哈希值——否则当你试图将它们作为哈希表(或任何字典类型的集合)中的键时,你会遇到各种各样的问题。

另一方面,如果两个实例不相等,它们可能具有也可能不具有相同的哈希值——最好不是这样。这就是在哈希表上进行O(1)搜索和O(N)搜索的区别——如果你所有的哈希都发生冲突,你可能会发现搜索你的表并不比搜索一个列表好。

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

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

请注意,如果创建的对象在创建后可以更改,则如果该对象插入到集合中,则哈希值不能更改。实际上,这意味着哈希值必须从初始对象创建时开始固定。更多信息请参阅Apple的NSObject协议的-hash方法文档:

If a mutable object is added to a collection that uses hash values to determine the object’s position in the collection, the value returned by the hash method of the object must not change while the object is in the collection. Therefore, either the hash method must not rely on any of the object’s internal state information or you must make sure the object’s internal state information does not change while the object is in the collection. Thus, for example, a mutable dictionary can be put in a hash table but you must not change it while it is in there. (Note that it can be difficult to know whether or not a given object is in a collection.)

对我来说,这听起来完全是无稽之谈,因为它可能会有效地降低哈希查找的效率,但我认为最好还是谨慎行事,并遵循文档所说的。

记住,你只需要在isEqual为真时提供相等的哈希值。当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。

散列应该是均匀分布的。所以你可以使用^ (xor号)来组合几个成员变量

这就像

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

这样哈希将被均匀分布。

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

那么在数组中要做什么呢?

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

结合@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);
}