文章目录
一、==
、isEqual
、hash
首先需要了解==
符号:比较的是两个对象的指针本身,而不是其所指向的对象。因此我们需要用到NSObject
协议中声明的isEqual
方法来判断两个对象的等同性。然而其默认的实现却是跟==
一样的。因此需要子类们覆写该方法,实现自身内容的比较。
NSObject
协议中有两个用于判断等同性的关键方法:
// 基类的默认实现:isEqual 等同 ==
- (BOOL)isEqual:(id)object {
return self == object; // 比较两`指针对象`的地址
}
- (NSUInteger)hash; // 哈希值
NSObject
类对这两个方法的默认实现是:当且仅当其指针值
(pointer value
)完全相等时,这两个对象才相等。若想在自定义的对象中正确覆写这些方法,就必须先理解其约定(contract)。如果isEqual:
方法判断两个对象相等,那么其hash方法也必须返回同一个值。相反 如果两个对象的hash方法返回同一个值,那么isEuqal:
方法未必会认为两者相等。
二、重写isEqual:
和hash
isEqual:
:先对比对象地址,然后对比类型,再调用高层比较方法hash
:因为collection
(如:NSSet
、NSDictionary
等)都使用了HashTable
存储数据,添加和查找时都是使用对象的哈希值做索引的。如:set
会根据哈希值把对象分装到不同的数组中。在向set
中添加新对象时,要根据其哈希值找到与之相关的那个数组,一次检查其中各个元素,看数组中已有对象与之相等。如果相等则说明要添加的对象已经在set
里了。(由此可见,如果多个对象返回相同的哈希值时,那么在set中已经有100w个对象的情况下,继续加时则需要将这100w个对象都扫描一遍)所以我们需要尽量降低哈希值的碰撞率。(对散列表HashTable
不太熟的同学,自行去补补课)
举例实现如下:
- (BOOL)isEqual:(id)object {
if (self == object) return YES; // 指针是否相等
if ([self class] != [object class]) return NO; // 类是否相等
MOPerson *otherPerson = (MOPerson *)object;
// 其他属性判断,相等返回YES,否则返回NO(看项目需求)
}
- (NSUInteger)hash {
NSUInteger identify = [identify hash];
NSUInteger age = _age;
return identify ^ age;
// 保持高效,限制位数,降低碰撞率
}
三、isEuqalTo__ClassName__
:
一般来说,两个类型不同的对象总是不想等的。某些对象提供了特殊的等同性判定方法
,如果已经知道两个对象都属于同一个类,就可以使用这种方法。该类方法传递的对象必须跟当前对象一致,因此比调用isEqual:
方法快,后者还要执行额外的步骤(因为它不知道受测对象的类型)。Foundation
中,很多NSObject
的子类已经定义好了自己类的等同性方法:
NSData
->isEqualToData:
NSDate
->isEqualToDate:
NSValue
->isEqualToValue:
NSNumber
->isEqualToNumber:
NSString
->isEqualToString:
NSAttributedString:
->isEqualToAttributedString:
NSSet
->isEqualToSet:
NSIndexSet
->isEqualToIndexSet:
NSDictionary
->isEqualToDictionary:
NSHashTable
->isEqualToHashTable:
NSOrderedSet
->isEqualToOrderedSet:
NSTimerZone
->isEqualToTimeZone:
- …
举例实现如下:
- (BOOL)isEqualToPerson:(MOPerson *)otherPerson {
if (self == otherPerson) return YES;
// 其他属性判断,相等返回YES,否则返回NO(看项目需求)
}
// 也应覆写isEqual方法,实现如下:
- (BOOL)isEqual:(id)object {
if ([self class] == [object class]) {
// 是该类则调用自己的判定方法
return [self isEqualToPerson:(MOPerson *)object];
} else {
// 否则交由超类来判定
return [super isEqual:object];
}
}
四、等同性判定的执行深度
创建等同性判定方法时,需要决定是根据整个对象来判断等同性,还是仅根据其中几个字段来判断。NSArray
的判定方式:先看两数组的count
是否相同;若相同,再在每个对应位置的两个对象上调用其isEqual:
方法。如果均相等,那么这两个数组相等,这叫做深度等同性判定
(deep equality)。
不过有些时候无须将所有数据逐个比较,只根据其中部分数据即可判定二者是否等同。(如:若EOCPerson
类的实例是根据数据库里的数据创建而来的,那么其中就可能会含有一个属性是唯一标识符
(unique indentifier)),在数据库中用作主键
(primary key):@property NSUInteger identifier;
,尤其是此属性声明为readonly
时,那么只判断标识符就可。因为这两对象是由同一个数据源所创建的,据此断定其余数据也相同。
当然具体情况还是得具体分析,根据你项目的特点和需求来做判断。
五、容器中可变类的等同性
注意当把一个对象放入容器(collection)之后,就不应该改变其哈希值了。如上所说的collection
会把各个对象按照其哈希值分装到不同的“箱子数组”中。如果某对象放入“箱子”之后哈希值又变了,那么其现在所处的箱子对它来说就是“错误”的。要想解决这个问题,需要确保哈希值不是根据对象的“可变部分”计算出来的,或是保证放入collection
之后就不再改变对象内容了。
看下面一个现象:(尽量将对象做成“不可变的”(immutable))
NSMutableSet *set = [NSMutableSet new];
[set addObject:@[@1, @2]];
[set addObject:@[@1, @2]];
NSLog(@"set: %@", set); // set: {((1, 2))} 集合里不能存两个相等的数组
NSMutableArray *arr = [@[@1] mutableCopy];
[set addObject:arr];
NSLog(@"set: %@", set); // set: {((1), (1, 2))}
[arr addObject:@2];
NSLog(@"set: %@", set); // set: {((1, 2), (1, 2))} 集合里却存了两个相等的数组
// (根据set的语义是不允许这样的,现在却无法保证这一点了,因为我们修改了set中已有的对象)
NSSet *setB = [set copy]; // 如果拷贝此set,那就更糟糕了
NSLog(@"setB: %@", setB); // setB: {((1, 2))} 又只剩一个对象了
对于这种现象大家都有争议,可能符合你的需求,也可能不符合。因此得出结论:如果把某对象放入set之后又修改其内容,那么后面的行为将很难预料。
参考: