iOS中重写isEqual和hash

849 阅读6分钟

1、oc中如何判断两个对象是否相等?

使用isEqual方法判断两个对象是否相等。它和==判断相等有什么区别?

对于基本类型==比较的是值;对于对象类型,==比较的是对象的地址,即是否为同一个对象;

一个简单的例子:

UIColor *color1 = [UIColor colorWithRed:1 green:1 blue:1 alpha:1]; 
UIColor *color2 = [UIColor colorWithRed:1 green:1 blue:1 alpha:1]; 
result = [color1 isEqual:color2]; 
NSLog(@"color1 == color2--%d, [color1 isEqual:color2]---%d", color1==color2, result);

打印结果如下:

color1 == color2--0, [color1 isEqual:color2]---1

2、如何重写isEqual方法

为什么要重写?先来看看NSObject是怎么实现isEqual的

- (BOOL)isEqual:(id)anObject {
    return (self == anObject); 
}

参考的是gunStep的源码。如果不重写的话,任何两个对象的比较结果都是No,因为它比较的是两个对象的地址。

在cocoa framework中常见的NSString、NSDate、NSArray、NSDictionary、NSSet等都有重写isEqual方法。

对于自定义的类型来说,如果想要比较两个对象是否相同,就要重写isEqual方法。

我们自定义一个Person类来进行简单的说明。

@interface Person : NSObject 

@property (nonatomic, assign) NSInteger age; 
@property (nonatomic, copy) NSString *name; 

@end

在Person类中实现isEqual方法

- (BOOL)isEqual:(id)object { 
    // ==运算符判断是否是同一对象, 因为同一对象必然完全相同 
    if (self == object) { 
        return YES; 
    } 
    //判断是否是同一类型, 这样不仅可以提高判等的效率, 还可以避免隐式类型转换带来的潜在风险 
    if (![object isKindOfClass:[Person class]]) { 
        return NO; 
    } 
    return [self isEqualToPerson:object]; 
}

- (BOOL)isEqualToPerson:(Person *)person { 
    //判断person是否是nil, 做参数有效性检查 
    if (!person) { 
        return NO; 
    } 
    //对各个属性分别使用默认判等方法进行判断 
    BOOL equalNames = (!self.name && !person.name) || [self.name isEqualToString:person.name]; 
    BOOL equalAges = (!self.age && !person.age) || self.age == person.age; 
    //返回所有属性判等的与结果 
    return equalNames && equalAges; 
}

3、为什么要有hash方法?

这个问题要从Hash Table这种数据结构说起。

首先我们看下如何在数组中查找摸个成员:

  1. 遍历数组中的成员;
  2. 将取出的值与目标值比较,如果相等,则返回该成员;

数组在未排序的情况下,查找的时间复杂度是O(n)。

为了提高查找的速度,Hash Table出现了。

当成员被加入到Hash Table中是,会给它分配一个hash值,以标识该成员在集合中的位置。

通过这个位置标识可以将查找的时间复杂度优化到O(1),当然如果多个成员都是同一个位置标识,那么查找就不能达到O(1)了。

重点来了:

分配的这个hash值(即用于查找集合中成员的位置标识),就是通过hash方法计算得来的,且hash方法返回的hash值最好唯一。

和数组相比,基于hash值索引的Hash Table查找摸个成员的过程就是:

  1. 通过hash值直接找到查找目标的位置;
  2. 如果目标位置上有多个相同的hash值成员,此时再按照数据方式进行查找;

4、hash方法什么时候会被调用?

- (void)test2 { 
    Person *p1 = [[Person alloc] init]; 
    p1.name = @"张三"; p1.age = 11; 
    Person *p2 = [[Person alloc] init]; 
    p2.name = @"张三"; p2.age = 11;
    NSLog(@"array---------------");
    
    NSMutableArray *array = [NSMutableArray array]; 
    [array addObject:p1]; [array addObject:p2]; 
    NSLog(@"array--%ld", array.count); 
    NSLog(@"set-----------------"); 
    
    NSMutableSet *set = [NSMutableSet set]; 
    [set addObject:p1]; 
    [set addObject:p2];
    NSLog(@"set--%ld", set.count);
    NSLog(@"dict1----------------");
    
    NSMutableDictionary *dict1 = [NSMutableDictionary dictionary]; 
    [dict1 setObject:p1 forKey:@"p1"]; 
    [dict1 setObject:p2 forKey:@"p2"];
    NSLog(@"dict1--%ld", dict1.count); 
    NSLog(@"dict2-----------------"); 
    
    NSMutableDictionary *dict2 = [NSMutableDictionary dictionary]; 
    [dict2 setObject:@"p1" forKey:p1]; 
    [dict2 setObject:@"p2" forKey:p2]; 
    NSLog(@"dict2--%ld", dict2.count); 
 }

重写Person类的hash方法

- (NSUInteger)hash { 
    NSUInteger hash = [super hash];
    NSLog(@"hash=%ld", hash); return hash; 
  }

打印结果

 重写isEqual和hash1[68464:3862796] array--2 
 重写isEqual和hash1[68464:3862796] set----------------- 
 重写isEqual和hash1[68464:3862796] hash=105553131306432 
 重写isEqual和hash1[68464:3862796] hash=105553131306464 
 重写isEqual和hash1[68464:3862796] set--2 
 重写isEqual和hash1[68464:3862796] dict1---------------- 
 重写isEqual和hash1[68464:3862796] dict1--2 
 重写isEqual和hash1[68464:3862796] dict2----------------- 
 重写isEqual和hash1[68464:3862796] hash=105553131306432 
 重写isEqual和hash1[68464:3862796] hash=105553131306464 
 重写isEqual和hash1[68464:3862796] dict2--2

从打印的结果看

hash方法只有在对象被添加到NSSet和设置为NSDictionary的key时会被调用

NSSet添加新成员是需要根据hash值来快速查找成员,以保证集合中是否已经存在该成员。

NSDictionary在查找key是,也是利用key的hash值来提高查找的效率。

5、hash和isEqual的关系

为了优化判等的效率,基于hash的NSSet和NSDictionary在判段成员是否相等时,会这样做:

  1. 成员的hash值是否和目标hash值相等,如果相同进入2,如果不等,就直接判断不相等;
  2. hash值相同(即1)的情况下,再进行对象判等,作为判等条件;

6、如何重写hash方法

这么重写hash方法

- (NSUInteger)hash { 
    return [super hash]; 
  }

来看下[super hash] 的值是什么

Person *p1 = [[Person alloc] init]; 
p1.name = @"张三"; p1.age = 11;
Person *p2 = [[Person alloc] init]; 
p2.name = @"张三"; p2.age = 11; 
result = [p1 isEqual:p2]; 

NSLog(@"%d", result); 
NSLog(@"%ld---%ld", [p1 hash], (NSUInteger)p1);
NSLog(@"%ld---%ld", [p2 hash], (NSUInteger)p2);

打印结果如下

重写isEqual和hash1[68808:3878726] 105553123180000---105553123180000 
重写isEqual和hash1[68808:3878726] 105553123180032---105553123180032

可以看出,[super hash]返回的就是对象的内存地址

我们添加如下两个对象到NSSet中试试

Person *p1 = [[Person alloc] init]; 
p1.name = @"张三"; 
p1.age = 11; 
Person *p2 = [[Person alloc] init]; 
p2.name = @"张三"; 
p2.age = 11; 

NSMutableSet *set = [NSMutableSet set]; 
[set addObject:p1]; 
[set addObject:p2];
NSLog(@"count---%ld", set.count);

此时打印结果如下:

重写isEqual和hash1[69895:3927266] hash=105553129036576 
重写isEqual和hash1[69895:3927266] hash=105553129036352 
重写isEqual和hash1[69895:3927266] hash=105553129036384 
重写isEqual和hash1[69895:3927266] 调用了isEqual 
重写isEqual和hash1[69895:3927266] count---2 

isEqual相等的两个对象都加入到了NSSet中,所以直接返回[super hash]是不正确的。

那么hash方法的最佳实践是什么呢? 大神Mattt Thompson 给出的结论是:

对关键属性的hash值进行位异或运算作为hash

对于上面Person类的hash方法实现如下:

- (NSUInterger)hash {
    return [self.name hash] ^ [@(self.age) hash];
}