第8条:理解“对象等同性”这一概念
1. 对象等同性
“==”操作比较的是两个指针本身,而不是其所指的对象。
应该使用NSObject协议中声明的“isEqual:”方法来判断两个对象的等同性。其中,某些对象提供了特殊的“等同性判定方法”,如判断NSString类对象的“isEqualToString:”方法。
2. 判断等同性的关键方法
NSObject协议中有两个用于判断等同性的关键方法:
- (BOOL)isEqual:(id)object;
- (NSUInteger)hash;
NSObject类对这两个方法的默认实现是:当且仅当其“指针值”(pointer value)完全相等时,这两个对象才相等。
若想在自定义的对象中正确覆写这些方法,就必须先理解其约定。如果“isEqual:”方法判定两个对象相等,那么其hash方法也必须返回同一个值。但是,如果两个对象的hash方法返回同一个值,那么“isEqual:”方法未必会认为两者相等。
*** isEqual:方法的实现 ***
/* 某个类 */
@interface EOCPerson : NSObject
@property (nooatomic, copy) NSString *firstName;
@property (nooatomic, copy) NSString *lastName;
@property (nooatomic, assign) NSUInteger age;
@end/* 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;
}
*** hash方法的实现 ***
根据等同性约定:若两对象相等,则其hash值也相等,但是两个hash值相同的对象却未必相等。
/* 第一种实现方式 */
- (NSUInteger)hash{return 1337;
}
/*评价:在collection(集合类)中使用这种对象将产生性能问题,因为collection在检索哈希表(hash table)时,会用对象的哈希码做索引。*//* 第二种实现方式 */
- (NSUInteger)hash{NSString *stringToHash = [NSString stringWithFormat:@"%@:%@:%i", _firstName, _lastName, age];return [stringToHash hash];
}
/*评价:将NSString对象中的属性都塞入另外一个字符串中,然后令hash方法返回该字符串的hash值。这样做,符合“两个相等的对象返回相同的hash值”的约定,但是还需要负担创建字符串的开销,所以比返回单一值要慢。而且,把这种对象添加到collection中时,也会产生性能问题,因为要想添加,必须先计算其hash值。*//* 第三种实现方式 */
- (NSUInteger)hash{NSUInteger firstNameHash = [_firstName hash];NSUInteger lastNameHash = [_lastName hash];NSUInteger ageHash = _age;return firstNameHash ^ lastNameHash ^ ageHash;
}
/*评价:这种做法,既能保持较高效率,又能使生成的hash值至少位于一定范围之内,而不会过于频繁地重复。当然,此算法生成的hash值还是会碰撞(collision),不过至少可以保证hash值有多种可能的取值。*/
总结:编写hash方法时,应该用当前的对象做做实验,以便在减少碰撞频度与降低运算复杂程度之间取舍。
3. 特定类所具有的等同性判定方法
除了NSString之外,NSArray与NSDictionary类也具有特殊的等同性判定方法(分别为“isEqualToArray:”和“isEqualToDictionary:”方法)。
在编写特定类的判定方法时,也应一并覆写“isEqual:”方法。
/* EOCPerson类 */
// 在自己编写的判定方法中不用检测参数类型
- (BOOL)isEqualToPerson:(EOCPerson*)otherPerson{// 判断两个对象的指针if (self == object) return YES;// 检查两个对象的属性是否都相等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;
}// 覆写isEqual:方法
// 如果是两对象所属的类就调用自己编写的判定方法,否则交由超类来判断。
- (BOOL)isEqual:(id)object{if ([self class] == [object class]){return [self isEqualToPerson:(EOCPerson*)object];}else{return [super isEqual:object];}
}
4. 等同性判定的执行深度
创建等同性判定方法时,需要决定是根据整个对象来判断等同性,还是仅仅根据其中几个字段来判断。前者叫做“深度等同性判定”,后者由于有名为“唯一标识符”的属性,可以根据标识符来判断等同性,尤其是该属性声明为readonly的时候。
是否需要在等同性判定方法中检测全部字段取决于受测对象。只有类的编写者才可以判定两个对象实例在何种情况下应判定为相等。
5. 容器中可变类的等同性
在collection中放入可变类对象的时候,需要确保hash值不是根据对象的“可变部分”计算出来的,或是保证放入collection之后就不再改变可变类对象的内容了。
要点
- 若想检测对象的等同性,请提供“isEqual:”和“hash”方法。
- 相同的对象必须具有相同的hash值,但是两个hash值相同的对象却未必相同。
- 不要盲目地逐个检测每条属性,而是应该依照具体需求来制定检测方案。
- 编写hash方法时,应该使用计算速度快而且哈希码碰撞几率低的算法。