为什么要有isEqual方法
对于对象类型, ==运算符比较的是对象的地址,即是否为同一对象。
对象地址相等不代表对象相等,即对象地址相等是对象相等的必要非充分条件。
isEqual方法就是用来判断两个对象是否相等。
isEqual的默认实现
isEqual方法是NSObject中声明的,默认实现就是简单的比较对象地址。
@implementation NSObject (Approximate)
- (BOOL)isEqual:(id)object {
return self == object;
}
@end
NSObject的子类可以实现自己的isEqual:方法,一般方式如下:
- 实现新的
isEqualTo__ClassName__:
方法,用来做具体属性值的对比,我们叫做高层比较方法 - 重载
isEqual:
,先做对象地址的比较,以及对象类型的比较,然后调用高层比较方法 - 重载
hash:
,这个方法后面再讲
如果是集合类型的话,还应该做深度判等,即所有成员一一进行比较。
综合举例如下:
@implementation NSArray (Approximate)
- (BOOL)isEqualToArray:(NSArray *)array {
if (!array || [self count] != [array count]) {
return NO;
}
for (NSUInteger idx = 0; idx < [array count]; idx++) {
if (![self[idx] isEqual:array[idx]]) {
return NO;
}
}
return YES;
}
- (BOOL)isEqual:(id)object {
if (self == object) {
return YES;
}
if (![object isKindOfClass:[NSArray class]]) {
return NO;
}
return [self isEqualToArray:(NSArray *)object];
}
@end
在Foundation中,很多NSObject的子类已经定义好了自己的isEqual,及其高层比较方法:
- NSAttributedString -isEqualToAttributedString:
- NSData -isEqualToData:
- NSDate -isEqualToDate:
- NSDictionary -isEqualToDictionary:
- NSHashTable -isEqualToHashTable:
- NSIndexSet -isEqualToIndexSet:
- NSNumber -isEqualToNumber:
- NSOrderedSet -isEqualToOrderedSet:
- NSSet -isEqualToSet:
- NSString -isEqualToString:
- NSTimeZone -isEqualToTimeZone:
- NSValue -isEqualToValue:
NSArray中isEqual相关实践
看一段示例代码
NSString* name1 = @"小明";
NSMutableArray* array = [NSMutableArray arrayWithObjects:name1,nil];
NSString* name2 = [NSString stringWithFormat:@"小明"];
NSLog(@"name1:%p,name2:%p",name1,name2);
NSLog(@"[array contain:name2]=%d",[array containsObject:name2]);
[array removeObject:name2];
NSLog(@"[array remove:name2].count=%zd",array.count);
打印结果
name1:0x108e58200,name2:0x600000436780
[array contain:name2]=1
[array remove:name2].count=0
可见,NSArray的containsObject:
和removeObject:
方法都是使用了isEqual来判断成员是否相等的。removeObject:
会把所有相等的成员都移除掉。
为什么要有hash方法
我们在数组中查找某个成员,在数组未排序的情况下, 查找的时间复杂度是O(array_length)
为了提高查找的速度, Hash Table出现了。当成员被加入到Hash Table中时, 会给它分配一个hash值, 以标识该成员在集合中的位置,通过这个位置标识可以将查找的时间复杂度优化到O(1), 当然如果多个成员都是同一个位置标识, 那么查找就不能达到O(1)了
重点来了:
分配的这个hash值(即用于查找集合中成员的位置标识), 就是通过hash方法计算得来的, 且hash方法返回的hash值最好唯一
和数组相比, 基于hash值索引的Hash Table查找某个成员的过程就是
Step 1: 通过hash值直接找到查找目标的位置
Step 2: 如果目标位置上有多个相同hash值得成员, 此时再按照数组方式进行查找
hash方法什么时候被调用?
HashTable是一种基本的数据结构,NSSet和NSDictionary都是使用了HashTable存储数据,因此可以确保它们查询成员的速度为O(1)。而NSArray使用了顺序表存储数据,查询速度为O(n)。
hash方法只在对象被添加至NSSet和设置为NSDictionary的key时会调用
NSSet添加新成员时, 需要根据hash值来快速查找成员, 以保证集合中是否已经存在该成员
NSDictionary在查找key时, 也利用了key的hash值来提高查找的效率
hash和isEqual的关系
- 对象的判等是相互的 ([a isEqual:b] ⇒ [b isEqual:a])
- 如果两个对象相等,那么它们的hash值一定相等 ([a isEqual:b] ⇒ [a hash] == [b hash])
- 值相等,两个对象不一定相等 ([a hash] == [b hash] ¬⇒ [a isEqual:b])
总之,hash值是对象判等的必要非充分条件
为了优化判等的效率, 基于hash的NSSet和NSDictionary在判断成员是否相等时, 会这样做
Step 1: 集成成员的hash值是否和目标hash值相等, 如果相同进入Step 2, 如果不等, 直接判断不相等
Step 2: hash值相同(即Step 1)的情况下, 再进行对象判等, 作为判等的结果
如何重写自己的hash方法
hash方法是NSObject中声明的,默认实现是返回对象的内存地址。
那么hash方法的最佳实践到底是什么呢?
大神Mattt Thompson在Equality中给出的结论就是
In reality, a simple XOR over the hash values of critical properties is sufficient 99% of the time(对关键属性的hash值进行位异或运算作为hash值)
比如对于Person类的hash方法实现如下
- (NSUInteger)hash {
return [self.name hash] ^ [self.birthday hash];
}