isEqual & hash

96
upworld
2017.01.13 09:30* 字数 2537

闲话少说,先说本编博客的核心

iOS系统API给我们提供一个自动过滤重复元素的容器 NSMutableSet/NSSet。我们可能经常用NSMutableSet/NSSet过滤相同的字符串(NSSring实例)。因为NSMutableSet/NSSet内部一些实现机制要比我们自己写的滤重方法效率高。但是对于自定义一个类如Person,如果想利用NSMutableSet/NSSet来过滤重复元素(如多个Person实例的uid相同),我们必须要同时实现- (BOOL)isEqual:- (NSUInteger)hash两个方法。这里先简单介绍他们的关系:两个相等的实例,他们的hash值一定相等。但是hash值相等的两个实例,不一定相等。在重写- (BOOL)isEqual:- (NSUInteger)hash两个方法 的时候,切记一定要遵循上述规则。后面我们会详细分析只实现- (BOOL)isEqual:会遇到一些什么问题。

如果我们对两个实例相同或者- (BOOL)isEqual:概念不是很清楚。可以看下博客iOS开发 之 不要告诉我你真的懂isEqual与hash!。然后再回过头来,继续下面的一些深入的分析。

1 用NSMutableSet/NSSet 过滤相同字符串

下面就是利用NSMutableSet/NSSet 过滤相同字符串的代码实现。对于一些系统类如NSString,NSData等已经默认支持NSMutableSet/NSSet滤重 。

self.mutSet = [NSMutableSet set];
[self.mutSet addObject:@"123"];
[self.mutSet addObject:@"1234"];
[self.mutSet addObject:@"123"];
NSArray *filterArr = self.mutSet.allObjects;
//fiterArr:只包含@"123",@"1234"两个元素。

2 用NSMutableSet/NSSet 过滤自定义类的相同实例

更多的情况下我们是想利用NSMutableSet/NSSet来过滤自定义类(如Person)相同实例。别再问我为什么不自己实现一个过滤相同值的方法,因为前面已经说过NSMutableSet/NSSet内部实现机制要比我们自己写的效率高。那么我们需要做什么呢?很简单,上面已经说过。

必须同时实现- (BOOL)isEqual:- (NSUInteger)hash两个方法

下面先简单介绍下- (BOOL)isEqual:。这个从字面上方法很好理解:就是比较两个值相等不相等。具体何为相等,我们可以根据需求决定(如uid相等就认为相等或者uid和name同时相等才相等)。要想过滤相同元素,那必须提供一个比较两个元素是否相等的函数,那就是- (BOOL)isEqual:

有人会说“如果让我自己实现一个过滤相同元素的功能,一个- (BOOL)isEqual:方法就够我用了"。是的,如果按下面的滤重算法去实现:“弄一个数组,先不考虑性能问题,每addObject之前都调用- (BOOL)isEqual:判断是否和数组某个元素值相等,如果都不相等调用addObject,否则不做处理”。一个- (BOOL)isEqual:确实能搞定。但是如果只实现- (BOOL)isEqual:NSMutableSet/NSSet能搞定吗???带着这个问题,我们继续上路

我们来看下面只实现- (BOOL)isEqual:没有实现- (NSUInteger)hash的代码。本博客测试代码(同时推荐EqualAndHashDemo

2.1 只实现- (BOOL)isEqual:没有实现- (NSUInteger)hash

如下代码:
///声明:这个是不完善的实现案例。用于对比用
@interface Person : NSObject
@property (nonatomic, assign) NSInteger uid;
@property (nonatomic, strong) NSString *name;
@end

@implementation Person
- (instancetype)initWithID:(NSInteger)uid name:(NSString *)name{
    if (self = [super init]) {
        self.uid = uid;
        self.name = name;
    }
    return self;
}

- (BOOL)isEqual:(Person *)object{
    BOOL result;
    if (self == object) {
        result = YES;
    }else{
        if (object.uid == self.uid) {
            result = YES;
        }else{
            result = NO;
        }
    }
    NSLog(@"%@ compare with %@ result = %@",self,object,result ? @"Equal":@"NO Equal");
    return result;
}

- (NSString *)description{
    return [NSString stringWithFormat:@"%p(%ld,%@)",self,self.uid,self.name];
}
@end

上面定义了一个Person类只实现了- (BOOL)isEqual:。这里- (NSString *)description只是用于log输出。后面我们看具体调用

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    self.mutSet = [NSMutableSet set];
    Person *person1 = [[Person alloc] initWithID:1 name:@"nihao"];
    Person *person2 = [[Person alloc] initWithID:2 name:@"nihao2"];
    [self.mutSet addObject:person1];
    NSLog(@"add %@",person1);
    [self.mutSet addObject:person2];
    NSLog(@"add %@",person2);
    NSLog(@"count = %ld",self.mutSet.count);
    
    Person *person3 = [[Person alloc] initWithID:1 name:@"nihao"];
    [self.mutSet addObject:person3];
    NSLog(@"add %@",person3);
    NSLog(@"count = %d",self.mutSet.count);
 }

下面是一个输出log:
运行第X遍输出:

add 0x60800002bb40(1,nihao)
0x60800002bb40(1,nihao) compare with 0x60800002bde0(2,nihao2) result = NO Equal
add 0x60800002bde0(2,nihao2)
count = 2
add 0x60800002be00(1,nihao)
count = 3

运行第Y遍输出:

add 0x61000003c160(1,nihao)
0x61000003c160(1,nihao) compare with 0x61000003c520(2,nihao2) result = NO Equal
add 0x61000003c520(2,nihao2)
count = 2
0x61000003c160(1,nihao) compare with 0x60000003d7c0(1,nihao) result = Equal
add 0x60000003d7c0(1,nihao)
count = 2

同样的代码,运行结果竟然不一致(一定要多次测试,输出的结果有时候正确有时候不正确)。根据测试案例person3 和 person1 显然是相同的,正常情况下person3应该被滤掉。为啥有时候执行结果正确,有时候不正确呢?

其实吧NSMutableSet/NSSet,是一个无序集合容器,不像我们上面想的那么简单。仅仅实现- (BOOL)isEqual:而不实现- (NSUInteger)hash没门。 NSMutableSet/NSSet在数据存储和比较元素相等都和- (NSUInteger)hash方法息息相关。内部高效滤重机制有- (NSUInteger)hash的很大功劳。- (NSUInteger)hash究竟有什么用???带着一些疑问,我继续上路。

2.2 只实现- (BOOL)isEqual:调用默认实现- (NSUInteger)hash

下面代码我们虽然实现了- (NSUInteger)hash,但是我们只调用了[super hash]并输出了一些日志,其行为完全和系统默认实现一致。继续完善一上面的案例,增加一些log。根据log输出,理清- (BOOL)isEqual:- (NSUInteger)hash何时会被触发及调用顺序。

///下面主要增加了log输出。我重写了hash,但是只调用[super hash],增加log输出。实际功能和上面代码完全一致。
@interface Person : NSObject
@property (nonatomic, assign) NSInteger uid;
@property (nonatomic, strong) NSString *name;
@end

@implementation Person
- (instancetype)initWithID:(NSInteger)uid name:(NSString *)name{
    if (self = [super init]) {
        self.uid = uid;
        self.name = name;
    }
    return self;
}

- (BOOL)isEqual:(Person *)object{
    BOOL result;
    if (self == object) {
        result = YES;
    }else{
        if (object.uid == self.uid) {
            result = YES;
        }else{
            result = NO;
        }
    }
    NSLog(@"%@ compare with %@ result = %@",self,object,result ? @"Equal":@"NO Equal");
    return result;
}

- (NSString *)description{
    return [NSString stringWithFormat:@"%p(%ld,%@)",self,self.uid,self.name];
}

- (NSUInteger)hash{
    NSUInteger hashValue = [super hash];
    NSLog(@"hash = %lu,addressValue = %lu,address = %p",(NSUInteger)hashValue,(NSUInteger)self,self);
    return hashValue;
}
@end

同时调用处我也添加了一些log。帮助分析- (BOOL)isEqual:- (NSUInteger)hash如何默契协调工作的。

- (void)viewDidLoad {
    [super viewDidLoad];
    self.mutSet = [NSMutableSet set];
    Person *person1 = [[Person alloc] initWithID:1 name:@"nihao"];
    Person *person2 = [[Person alloc] initWithID:2 name:@"nihao2"];
    NSLog(@"begin add %@",person1);
    [self.mutSet addObject:person1];
    NSLog(@"after add %@",person1);
    
    NSLog(@"begin add %@",person2);
    [self.mutSet addObject:person2];
    NSLog(@"after add %@",person2);
    
    NSLog(@"count = %d",self.mutSet.count);
    
    Person *person3 = [[Person alloc] initWithID:1 name:@"nihao"];
    NSLog(@"begin add %@",person3);
    [self.mutSet addObject:person3];
    NSLog(@"after add %@",person3);
    
    NSLog(@"count = %d",self.mutSet.count);
}

运行第X遍输出:

begin add 0x60000003efc0(1,nihao)
hash = 105553116524480,addressValue = 105553116524480,address = 0x60000003efc0
hash = 105553116524480,addressValue = 105553116524480,address = 0x60000003efc0
after add 0x60000003efc0(1,nihao)
begin add 0x60000003f1a0(2,nihao2)
hash = 105553116524960,addressValue = 105553116524960,address = 0x60000003f1a0
0x60000003efc0(1,nihao) compare with 0x60000003f1a0(2,nihao2) result = NO Equal
after add 0x60000003f1a0(2,nihao2)
count = 2
begin add 0x61800003f9c0(1,nihao)
hash = 107202383968704,addressValue = 107202383968704,address = 0x61800003f9c0
0x60000003f1a0(2,nihao2) compare with 0x61800003f9c0(1,nihao) result = NO Equal
after add 0x61800003f9c0(1,nihao)
count = 3

运行第Y遍输出:

begin add 0x600000023520(1,nihao)
hash = 105553116411168,addressValue = 105553116411168,address = 0x600000023520
hash = 105553116411168,addressValue = 105553116411168,address = 0x600000023520
after add 0x600000023520(1,nihao)
begin add 0x600000023620(2,nihao2)
hash = 105553116411424,addressValue = 105553116411424,address = 0x600000023620
after add 0x600000023620(2,nihao2)
count = 2
begin add 0x610000023a20(1,nihao)
hash = 106652628040224,addressValue = 106652628040224,address = 0x610000023a20
0x600000023520(1,nihao) compare with 0x610000023a20(1,nihao) result = Equal
after add 0x610000023a20(1,nihao)
count = 2

Person继承自NSObject。2.1代码中Person自然也就继承(NSObject)- (NSUInteger)hash实现。2.2代码虽然重写hash但是调用的是[super hash],其他log输出可以忽略。所以2.1代码和2.2代码,实现功能完全一致。梳理下log我们可以得出以下结论:

  • [super hash]是系统默认实现,其返回值和实例所在内存地址值完全一致(注意十六进制和十进制转换后相等)。
  • 当把一个实例假设为personA添加到NSMutableSet/NSSet中的时候一定会调用- (NSUInteger)hash
  • 当把一个实例假设为personA添加到NSMutableSet/NSSet中的时候,如果mutSet中存在>=1个元素,调用- (NSUInteger)hash后,可能会继续调用- (BOOL)isEqual:

了解上面的一些结论,不必深入,因为上面的案例,不是正确的案例,输出的结果,存在偶然性(有时候输出一样,有时候不一样)。下面我们步入正轨,如果我们同时 - (BOOL)isEqual:- (NSUInteger)hash和上面2.2会有何不同???

2.3 同时 - (BOOL)isEqual:- (NSUInteger)hash

2.1和2.2代码都是错误实现,是为了对比用。下面才是正确实现!!!

///正确的测试案例
@interface Person : NSObject
@property (nonatomic, assign) NSInteger uid;
@property (nonatomic, strong) NSString *name;
@end

@implementation Person
- (instancetype)initWithID:(NSInteger)uid name:(NSString *)name{
    if (self = [super init]) {
        self.uid = uid;
        self.name = name;
    }
    return self;
}

- (BOOL)isEqual:(Person *)object{
    BOOL result;
    if (self == object) {
        result = YES;
    }else{
        if (object.uid == self.uid) {
            result = YES;
        }else{
            result = NO;
        }
    }
    NSLog(@"%@ compare with %@ result = %@",self,object,result ? @"Equal":@"NO Equal");
    return result;
}

- (NSString *)description{
    return [NSString stringWithFormat:@"%p(%ld,%@)",self,self.uid,self.name];
}

- (NSUInteger)hash{
    NSUInteger hashValue = self.uid; //在这里只需要比较uid就行。这样的话就满足如果两个实例相等,那么他们的hash一定相等,但反过来hash值相等,那么两个实例不一定相等。但是在Person这个实例中,hash值相等那么实例一定相等。(不考虑继承之类的)
    NSLog(@"hash = %lu,addressValue = %lu,address = %p",(NSUInteger)hashValue,(NSUInteger)self,self);
    return hashValue;
}
@end

调用代码

//调用重写hash后的方法
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    self.mutSet = [NSMutableSet set];
    Person *person1 = [[Person alloc] initWithID:1 name:@"nihao"];
    Person *person2 = [[Person alloc] initWithID:2 name:@"nihao2"];
    NSLog(@"begin add %@",person1);
    [self.mutSet addObject:person1];
    NSLog(@"after add %@",person1);
    
    NSLog(@"begin add %@",person2);
    [self.mutSet addObject:person2];
    NSLog(@"after add %@",person2);
    
    NSLog(@"count = %d",self.mutSet.count);
    
    Person *person3 = [[Person alloc] initWithID:1 name:@"nihao"];
    NSLog(@"begin add %@",person3);
    [self.mutSet addObject:person3];
    NSLog(@"after add %@",person3);
    
    NSLog(@"count = %d",self.mutSet.count);
}

在这里无论运行多少次,最终结果都是一样(不考虑内存地址及比较顺序),这就是我们想要的。

begin add 0x60000003b000(1,nihao)
hash = 1,addressValue = 105553116508160,address = 0x60000003b000
hash = 1,addressValue = 105553116508160,address = 0x60000003b000
after add 0x60000003b000(1,nihao)
begin add 0x60000003b100(2,nihao2)
hash = 2,addressValue = 105553116508416,address = 0x60000003b100
after add 0x60000003b100(2,nihao2)
count = 2
begin add 0x60000003b0e0(1,nihao)
hash = 1,addressValue = 105553116508384,address = 0x60000003b0e0
0x60000003b000(1,nihao) compare with 0x60000003b0e0(1,nihao) result = Equal
after add 0x60000003b0e0(1,nihao)
count = 2

继续梳理log我们可以得出以下结论:

  • 结论1:当把一个实例假设为personA添加到NSMutableSet/NSSet中的时候一定会调用- (NSUInteger)hash
  • 结论2:当把一个实例假设为personA添加到NSMutableSet/NSSet中的时候,如果NSMutableSet/NSSet中存在>=1个元素,那么personA调用- (NSUInteger)hash方法后,会根据其返回值,判断是否需要继续调用- (BOOL)isEqual:
  • 结论3:当把一个实例假设为personA添加到NSMutableSet/NSSet中的时候,如果集合中存在某个成员假设为personB的- (NSUInteger)hash返回值和personA的- (NSUInteger)hash返回值相等,则personA会继续调用- (BOOL)isEqual:,以personB为参数。否则不等, 继续下一个元素判断。
  • 结论4:详细判断规则如下:
  • Step1: 集成成员的某个元素假设为personB的- (NSUInteger)hash返回值是否和personA的- (NSUInteger)hash返回值相等, 如果不相等则进入step2;否则进入Step3。
  • Step2: NSMutableSet/NSSet是否存在下一个没有比较过得元素,如果有继续Step1;否则personA会被添加到NSMutableSet/NSSet集合中,执行结束。
  • Step3: 调用personA的- (BOOL)isEqual: 以personB为参数,如果返回结果为NO则执行Step2;如果返回结果为Yes则NSMutableSet/NSSet中存在和personA相同元素,personA不会被添加到集合中,执行结束。

这里就不给大家普及 isEqual与hash的的深层理论东西。具体感兴趣请看下面文档。本博客只是讲解实际应用。点击可下载测试代码

参考文档如下:
参考文档1iOS开发 之 不要告诉我你真的懂isEqual与hash!
参考文档2Equality
参考文档3best-practices-for-overriding-isequal-and-hash

3 写在最后

在2.2的测试中遇到一个问题无法解答,知道的请留言,不甚感激!!!

因为如果我实现hash方法只是调用系统默认实现[super hash]或者返回self地址值,如下:

- (NSUInteger)hash{
    reutrn [super hash];
    //reutrn self;
}

通过2.2的log输出,我们可以看到即使hash值不相等即(内存地址不相等),那么后面一样会调用isEqual:方法比较。这个是为什么呢???

日记本
Web note ad 1