在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这样的结构?

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


当前回答

记住,你只需要在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数组的所有成员。足以散列第一个元素,最后一个元素,计数,也许还有一些中间元素,就这样。

其他回答

坚持,当然一个更简单的方法来做到这一点是首先覆盖- (NSString)描述,并提供一个字符串表示你的对象状态(你必须在这个字符串中表示你的对象的整个状态)。

然后,只需提供以下哈希的实现:

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

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

来源:NSString类参考

等号契约和散列契约在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;
}

这个实现避免了isEqual: from Apple示例中的一些子类化陷阱:

Apple's class test other isKindOfClass:[self class] is asymmetric for two different subclasses of MyWidget. Equality needs to be symmetric: a=b if and only if b=a. This could easily be fixed by changing the test to other isKindOfClass:[MyWidget class], then all MyWidget subclasses would be mutually comparable. Using an isKindOfClass: subclass test prevents subclasses from overriding isEqual: with a refined equality test. This is because equality needs to be transitive: if a=b and a=c then b=c. If a MyWidget instance compares equal to two YourWidget instances, then those YourWidget instances must compare equal to each other, even if their serialNo differs.

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

然而,在某些情况下,isKindOfClass: test更可取。这在框架类中比在应用程序类中更典型。例如,不管NSString/NSMutableString的区别如何,也不管NSString类集群中涉及哪些私有类,任何NSString都应该与其他具有相同底层字符序列的NSString进行比较。

在这种情况下,isEqual:应该具有定义良好的、记录良好的行为,并且应该清楚地说明子类不能重写此行为。在Java中,“不重写”限制可以通过将equals和hashcode方法标记为final来强制执行,但Objective-C没有等效的方法。

记住,你只需要在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数组的所有成员。足以散列第一个元素,最后一个元素,计数,也许还有一些中间元素,就这样。

我发现本页是重写等号和哈希类型方法的有用指南。它包括一个计算哈希码的不错的算法。这个页面是面向Java的,但是很容易适应Objective-C/Cocoa。

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