NSObject协议中有两个用于判断等同性的关键方法:
- (BOOL)isEqual:(id)object; - (NSUInteger)hash;
NSObject类对这两个方法的默认实现是:当且仅当其“指针值”(pointer value)完全相等时,这两个对象才相等。如果“isEqual:”方法判定两个对象相等,那么其hash方法也必须返回同一个值。但是,如果两个对象的hash方法返回同一个值,那么“isEqual:”方法未必会认为两者相等。
比如有下面这个类:
@interface EOCPerson : NSObject @property (nonatomic, copy) NSString *firstName; @property (nonatomic, copy) NSString *lastName; @property (nonatomic, assign) NSUInteger age; @end
我们认为,如果两个EOCPerson的所有字段均相等,那么这两个对象就相等。于是“isEqual:”方法可以写成:
- (BOOL)isEqual:(id)object { if (self == object) return YES; if ([self class] != [object class]) return NO; EOCPerson *otherPerson = (EOCPerson*)object; if (![_firstName isEqualToString:otherPerson.firstName]) return NO; if (![_lastName isEqualToString:otherPerson.lastName]) return NO; if (_age != otherPerson.age) return NO; return YES; }
首先,直接判断两个指针是否相等。若相等,则其均指向同一对象,所以受测的对象也必定相等。接下来,比较两对象所属的类。若不属于同一个类,则两对象不相等。EOCPerson对象当然不可能与EOCDog对象相等。不过,有时我们可能认为:一个EOCPerson实例可以与其子类(比如EOCSmithPerson)实例相等。在继承体系(inheritance hierarchy)中判断等同性时,经常遭遇此类问题。所以实现“isEqual:”方法时要考虑到这种情况。最后,检测每个属性是否相等。只要其中有不相等的属性,就判定两对象不等,否则两对象相等。
接下来该实现hash方法了。回想一下,根据等同性约定:若两对象相等,则其哈希码(hash)也相等,但是两个哈希码相同的对象却未必相等。这是能否正确覆写“isEqual:”方法的关键所在。下面这种写法完全可行:
- (NSUInteger)hash { return 1337; }
不过若是这么写的话,在collection中使用这种对象将产生性能问题,因为collection在检索哈希表(hash table)时,会用对象的哈希码做索引。假如某个collection是用set实现的,那么set可能会根据哈希码把对象分装到不同的数组中。在向set中添加新对象时,要根据其哈希码找到与之相关的那个数组,依次检查其中各个元素,看数组中已有的对象是否和将要添加的新对象相等。如果相等,那就说明要添加的对象已经在set里面了。由此可知,如果令每个对象都返回相同的哈希码,那么在set中已有1?000?000个对象的情况下,若是继续向其中添加对象,则需将这1?000?000个对象全部扫描一遍。
hash方法也可以这样来实现:
- (NSUInteger)hash { NSString *stringToHash = [NSStringstringWithFormat:@"%@:%@:%i", _firstName, _lastName, _age]; return [stringToHash hash]; }
这次所用的办法是将NSString对象中的属性都塞入另一个字符串中,然后令hash方法返回该字符串的哈希码。这么做符合约定,因为两个相等的EOCPerson对象总会返回相同的哈希码。但是这样做还需负担创建字符串的开销,所以比返回单一值要慢。把这种对象添加到collection中时,也会产生性能问题,因为要想添加,必须先计算其哈希码。
再来看最后一种计算哈希码的办法:
- (NSUInteger)hash { NSUInteger firstNameHash = [_firstName hash]; NSUInteger lastNameHash = [_lastName hash]; NSUInteger ageHash = _age; return firstNameHash ^ lastNameHash ^ ageHash; }
这种做法既能保持较高效率,又能使生成的哈希码至少位于一定范围之内,而不会过于频繁地重复。当然,此算法生成的哈希码还是会碰撞(collision),不过至少可以保证哈希码有多种可能的取值。编写hash方法时,应该用当前的对象做做实验,以便在减少碰撞频度与降低运算复杂程度之间取舍。