Objective-C中“对象等同性”的判定方法
根据“等同性”来比较对象是一个非常有用的功能。不过通过==操作符比较出来的结果未必是我们想要的,因为该操作比较的是两个指针本身,而不是其所指的对象。应该使用NSObject协议中声明的“isEqual:”方法来判定两个对象的等同性。一般来说,两个类型不同的对象总是不相等的。某些对象对象提供了特殊的“等同性判定方法”,如果已经知道两个受测对象都属于同一个类,那么就可以使用这种方法。以下述代码为例:
NSString *foo = @"Badger 123";
NSString *bar = [NSString stringWithFormat:@"Badger %i",123];
BOOL equalA = (foo == bar);// NO
BOOL equalB = [foo isEqual:bar];//YES
BOOL equalC = [foo isEqualToString:bar];//YES
可以看到==与等同性判定方法之间的差别。NSString类实现了一个自己所独有的等同性判定方法,名为“isEqualToString:”。传递给该方法的对象必须是NSString,否则结果为未定义。调用该方法比调用isEqual:方法快,后者还要执行额外的步骤,因为要判断受测对象的类型。
NSObject协议中有两个用于判定等同性的关键方法:
- (BOOL)isEqual:(id)object; - (NSUInteger)hash;
NSObject类对这两个方法的默认实现是:当且仅当“指针指”完全相等时,这两个对象才相等。若想在自定义的对象中正确覆写这些方法,就必须先理解其中的约定。如果“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;// 1.判断两个对象的指针是否相同
if ([self class] != [object class]) return NO;//2.判断两个对象所属的类
EOCPerson *otherPerson = (EOCPerson *)object;
if (![_lastName isEqualToString:otherPerson.lastName]) return NO;
if (_age != otherPerson.age) return NO;//3.判断对象的属性值
return YES;
}
- (NSUInteger)hash {
NSUInteger firstNameHash = [_firstName hash];
NSUInteger lastNameHash = [_lastName hash];
NSUInteger ageHash = _age;
return firstNameHash ^ lastNameHash ^ ageHash;
}
这种做法能够保证生成哈希码的效率。又能使生成的哈希码至少位于一定范围之内,减少碰撞的次数。
特定类所具有的等同性判定方法
系统中一些类的等同性判定方法:
NSString,等同性判定方法:isEqualToString:
NSArray, 等同性判定方法:isEqualToArray:
NSDictionary:等同性判定方法:isEqualToDictionary:
这些方法如果如果受测对象类型不正确,将会抛出异常。
在编写自定类的判定方法时,也应一并覆写“isEqual:”方法。常见的实现方式为:如果受测的对象与接受消息的对象都属于同一个类,那么就调用自己编写的判定方法,否则就交由父类来判定。如下代码实现:
- (BOOL)isEqualToPerson:(EOCPerson *)otherPerson {
if (self == otherPerson) return YES;
if (![_firstName isEqualToString:otherPerson.firstName]) {
return NO;
}
if (![_lastName isEqualToString:otherPerson.lastName]) {
return NO;
}
if (_age != otherPerson.age) {
return NO;
}
return YES;
}
- (BOOL)isEqual:(id)object {
if ([self class] == [object class]) {
return [self isEqualToPerson:(EOCPerson *)object];
} else {
return [super isEqual:object];
}
}
等同性判定的执行深度
创建等同性判定方法时,需要决定是根据整个对象来判断等同性,还是仅根据其中几个字段来判断。
深度等同性判定:以NSArray为例,NSArray检测方式为先看两个数组所含对象个数是否相等,若相等,则在每个对应位置的两个对象身上调用其“isEqual:”方法。如果对应位置上的对象均相等,那么这两个数组就相等。不过有时候,无须所有所有的数据都逐个比较。只根据部分数据即可判定,如对象中含有唯一标识符。
容器中可变类的等同性
在容器中放入可变类对象的时候,如果把某个对象放入集合中之后,就不应该在改变其哈希码了。集合会根据各个对象的哈希码来分装数据在不同的位置,如果某个对象在放入数组之后,哈希码又变了,那么其现在所在数组对这个对象来说就是错误的。更具体的介绍请参考书《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》中第八条的介绍。
要点总结:
1. 若想检测对象的等同性,请提供“isEqual:”与hash方法。
2. 相同的对象必具有相同的哈希码,但是两个哈希码相同的对象却未必相同。
3. 不要盲目的逐个检测每条属性,而是应该根据具体的需求来定制检测方案。
4. 编写hash方法时,应该使用计算速度快而且哈希码碰撞几率低的算法。
注:根据《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》进行摘抄和整理。