开发细节之 hash 与 isEqual 方法

isEuqal 与 == 的区别

相信大家都知道在OC中有两种比较是否相等的方法,第一种是直接用==符号比较,第二种是使用isEqual来比较,它们的区别如下:

  1. == 如果是用于基本数据类型的比较,那么直接比较数值,isEqual只能用于OC对象比较
  2. == 如果是用于OC对象比较,那么是判断他们是不是同一个对象,也就是指针所指向的地址是否一致。而isEqual则是比较两个对象是否相同。

接下来用一个颜色的示例来看看:

1
2
3
4
5
UIColor *color1 = [UIColor colorWithRed:120/255.0 green:120/255.0 blue:120/255.0 alpha:1];
UIColor *color2 = [UIColor colorWithRed:120/255.0 green:120/255.0 blue:120/255.0 alpha:1];
NSLog(@"color1.address = %p\ncolor2.address = %p",color1,color2);
NSLog(@"color1 == color2 ? %@ ",color1 == color2 ? @"是" : @"否");
NSLog(@"color1 isEqual color2 ? %@ ",[color1 isEqual:color2] ? @"是" : @"否");

结果输出:

1
2
3
4
color1.address = 0x60400046af40
color2.address = 0x60400046b280
color1 == color2 ? 否
color1 isEqual color2 ? 是

通过上面的例子我们可以看出,color1和color2是两个不同的对象,所以使用==来比较的时候他们不相等。但是color1和color2颜色的值都是一样的,所以使用isEuqal来比较的时候他们是相等的。

isEuqal 用于自定义对象的比较

刚刚我们是使用系统的UIColor的对象来比较,如果是我们是自定义的对象,如果需要判断两个对象是否相等的时候,使用isEqual方法来比较是否还起作用呢?

看看以下代码,我们有一个LMPerson类,里面有两个属性,一个name,一个age,我们创建两个不同的对象,然后给他们赋予同样的name和age,看看使用isEuqal来比较是否相等。

1
2
3
4
5
LMPerson *person1 = [LMPerson personWithName:@"lemon" age:18];
LMPerson *person2 = [LMPerson personWithName:@"lemon" age:18];
NSLog(@"person1.address = %p\nperson2.address = %p",person1,person2);
NSLog(@"person1 == person2 ? %@ ",person1==person2?@"是":@"否");
NSLog(@"person1 isEqual person2 ? %@ ",[person1 isEqual:person2]?@"是":@"否");

结果如下:

1
2
3
4
person1.address = 0x60400022b500
person2.address = 0x60400022b200
person1 == person2 ? 否
person1 isEqual person2 ? 否

通过上面的结果可以看出,当isEqual是用于我们自定义对象的比较的时候,即使我们赋予两个对象属性相同的值,但是返回的却是NO。这是为什么呢?

这是因为UIColor,NSArray,NSdictonary 系统已经帮我们实现了对应的isEqual或者isEqualTo的方法,所以我们如果要用于自定义对象比较,那么也需要实现对应的isEqual方法,接下来我们给LMPerson添加以下实现方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (BOOL)isEqual:(id)object{
if (self == object) {
return YES;
}

if (![object isKindOfClass:LMPerson.class]) {
return NO;
}

LMPerson *person2 = (LMPerson*)object;
BOOL isSameName = (!self.name && !person2.name) || [self.name isEqualToString:person2.name];
BOOL isSameAge = self.age == person2.age ;
return isSameName && isSameAge;
}

测试结果如下:

1
2
3
4
person1.address = 0x60000022bac0
person2.address = 0x60000022c460
person1 == person2 ? 否
person1 isEqual person2 ? 是

综上,如果我们要比较两个自定义对象是否相等的时候我们需要重写isEqual方法,给该方法提供一个实现。

什么是hash方法

这个要从hashTable说起,因为hashTabe是无序的集合,并且查找的时间复杂度是O(1),数组是O(array_lenth),为什么hashTable可以做到O(1)呢,因为当一个元素加到hashTable里面的时候,会有一个默认的hash值,用于标记元素在table中的位置,后面如果需要查找该元素,通过hash值可以直接找到该元素。

那么问题来了,这个hash值是怎样得来的呢?

这个hash值其实就是通过- (NSUInteger)hash方法提供的,并且系统默认的实现就是返回该对象的地址。下面我们来验证这个说法:

我们增加以下方法,并且打印出hash的值。

1
2
3
4
5
6
7
8
9
10
11
//LMPerson.m
- (NSUInteger)getSuperHash{
NSUInteger superHash = [super hash];
return superHash;
}

//ViewController
LMPerson *person1 = [LMPerson personWithName:@"lemon" age:18];
LMPerson *person2 = [LMPerson personWithName:@"lemon" age:18];
NSLog(@"person1.address = %ld\nperson2.address = %ld",(NSUInteger)person1,(NSUInteger)person2);
NSLog(@"person1.hash = %ld\nperson2.hash = %ld",[person1 getSuperHash],[person2 getSuperHash]);

测试结果如下:

1
2
3
4
person1.address = 105553118496736
person2.address = 105553116524864
person1.hash = 105553118496736
person2.hash = 105553116524864

通过结果我们可以知道,其实系统默认的hash方法就是返回对象地址的十进制。

什么时候会调用hash 方法

这里我们直接说结论,如果一个集合中不能出现重复的元素那么就会调用hash方法来判断两个元素是否相等。什么意思呢?

NSMutableArray和NSArray是允许添加重复元素的,所以将一个元素放到该容器中的时候是不会调用hash方法,像NSSet,NSMutableSet元素不能重复,在添加和删除的时候会调用hash方法。当一个元素作为NSDictonary的key的时候,因为key也不能重复,所以也会调用hash方法。大家可以通过将上述创建的两个person对象分别放到不同的集合中进行验证。

值得注意的是,就算hash方法相等也不能判断两个元素就一定是相等,还会调用isEqual来进行判断。也就是说,会优先判断hash是否相等,如果hash不相等那么这两个元素一定不相等,如果hash相等,那么就调用isEqual判断两个元素是否相等,如果返回NO,那么两个元素也不相等, 如果返回YES那么两个元素相等。

也就是说当我们把自定义对象加到NSSet中的或者作为NSDictonary的key的时候 会同时调用hash方法和isEqual方法来判断两个元素是否相等,因此我们需要重写isEqual方法和hash方法。

hash 的正确使用姿势

我们在上面已经验证过如果我们使用系统默认的hash方法来比较两个自定义对象是否相等是不正确的了,那么正确的使用姿势是什么呢?

Equality这篇文章中,matt大神给了方法,也就是对属性的hash进行异或运算。在LMPerson.m

1
2
3
- (NSUInteger)hash{
return ([self.name hash] ^ [[NSNumber numberWithInteger:self.age] hash]);
}

下面我们编写以下代码来测试一下相同的元素是否还能加到hashTable里面

1
2
3
4
5
LMPerson *person1 = [LMPerson personWithName:@"lemon" age:18];
LMPerson *person2 = [LMPerson personWithName:@"lemon" age:18];
LMPerson *person3 = [LMPerson personWithName:@"lemon" age:19];
NSSet *set = [NSSet setWithObjects:person1,person2, person3,nil];
NSLog(@"set.count = %ld",[set count]);

测试结果:

1
2018-08-14 11:08:13.250550+0800 testImageSourceCode[47478:7219161] set.count = 2

可以看到我们往hashTable里面添加了三个元素,但是第一和第二个元素是相同的,所以最后加到集合里面的只有两个元素,证明hash方法起作用了。

-------评论系统采用disqus,如果看不到需要翻墙-------------