你应该知道的Hash和isEqual的深层次解析(一)—— 两个方法剖析以及他们之间的关系

版本记录

版本号 时间
V1.0 2017.09.15

前言

+ (NSUInteger)hash;- (BOOL)isEqual:(id)object;都是NSObject类中的方法,也就是说所有的类都可以调用这两个方法来实现相应的功能,他们两个的功能和深层次原理是什么呢?下面我们开始一起研究。

isEqual方法

1. isEqual方法的基本作用

这个大概大家都知道的作用就是用来比较是否相等,但是具体比较什么相等呢?还有就是和==有什么区别呢?这里有几点需要注意:

  • ==对于基本数据类型,比较的是值是否相等;而对于对象类型,==运算符比较的是对象的地址,也就是说判断是否是一个对象。
  • - (BOOL)isEqual:(id)object;方法比较的是对象是否相等,不是指地址。并且非对象不能使用此方法。

下面我们看一个简单的例子。

- (void)demoEqual
{
    UIColor *color1 = [UIColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:1.0];
    UIColor *color2 = [UIColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:1.0];
    
    NSInteger number1 = 20;
    NSInteger number2 = 20;
    
    if (color1 == color2) {
        NSLog(@"== YES");
    }
    else {
        NSLog(@"== NO");
    }
    
    if (number1 == number2) {
        NSLog(@"== YES");
    }
    else {
        NSLog(@"== NO");
    }
    
    if ([color1 isEqual:color2]) {
        NSLog(@"isEqual YES");
    }
    else {
       NSLog(@"isEqual NO");
    }

}

从上面可以看出来:

  • 由于color1和color2不是一个对象,地址不一样。
  • ==判断的是是否是同一个地址,所以color1 == color2会输出NO,而[color1 isEqual:color2]判断的是对象是否相等,这里显然是相等的。

2. 几种常见的isEqual方法

  • NSString isEqualToString
  • NSDate isEqualToDate
  • NSArray isEqualToArray
  • NSDictionary isEqualToDictionary
  • NSSet isEqualToSet
  • NSAttributedString -isEqualToAttributedString:
  • NSData -isEqualToData:
  • NSHashTable -isEqualToHashTable:
  • NSIndexSet -isEqualToIndexSet:
  • NSNumber -isEqualToNumber:
  • NSOrderedSet -isEqualToOrderedSet:
  • NSTimeZone -isEqualToTimeZone:
  • NSValue -isEqualToValue:

这里需要注意:

  • 当比较上面任意类型的两个对象时,建议尽量使用上面的方法,而不建议使用isEqual方法。

3. NSArray的isEqual方法的内部实现

下面我们就是猜测一下NSArray对象调用isEqual方法的内部可能实现,实际上可能会更复杂,还是直接看代码。

@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

4. 自定义对象的isEqual内部实现

这个简单的例子是借鉴别人的,我这里只给出源码,就不自己去验证了。感兴趣的可以自己进行验证。

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSDate *birthday;

@end
- (BOOL)isEqual:(id)object 
{
    if (self == object) {
        return YES;
    }

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

    return [self isEqualToPerson:(Person *)object];
}

- (BOOL)isEqualToPerson:(Person *)person 
{
    if (!person) {
        return NO;
    }

    BOOL haveEqualNames = (!self.name && !person.name) || [self.name isEqualToString:person.name];
    BOOL haveEqualBirthdays = (!self.birthday && !person.birthday) || [self.birthday isEqualToDate:person.birthday];

    return haveEqualNames && haveEqualBirthdays;
}

上面是一个简单的例子,猜测的内部实现,实际上可能会比这复杂很多。

5. 其实你可以定义任何对象是否相等

这里说明的是,如果你重写isEqual方法的时候,在哪个类中重写,就用那个类调用isEqual方法,就可以随意实现是否相等。

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

#pragma mark -  Override Base Function

- (void)viewDidLoad
{
    [super viewDidLoad];

    [self demoObject];
}

- (BOOL)isEqual:(id)object
{
    return YES;
}

#pragma mark -  Object Private Function

- (void)demoObject
{
    UIFont *font = [UIFont systemFontOfSize:30.0];
    
    if ([self isEqual:font]) {
        NSLog(@"isEqual YES");
    }
    else {
        NSLog(@"isEqual NO");
    }
}

@end

下面看输出结果

2017-09-15 12:05:34.119 qswwqes[1996:713751] isEqual YES

这里我重写了isEqual方法,所以控制器VC类调用这个方法,就可以和UIFont对象相等了,还是很好玩的,当然我们比并不建议这么做,这里只是玩玩而已,目的是加深大家对这个方法的理解和使用。


Hash

该方法仍然是NSObject中的方法,是一个类方法,也就是说任何类都可以调用这个方法,返回的结果是一个NSInteger值。

1. Hash Table

Hash表其实是一种数据结构,我们平时在数组中查找成员的时候,就是遍历,这是一个解决方案,但是,如果数组的规模如果很大的时候,那么查找起来将会很浪费时间,有没有更好的方法呢?有,那就是借助哈希表。

哈希表的工作原理就是当成员加入到哈希表中时,会给他分配一个哈希值,标识改成员在集合中的位置,通过位置标识可以将查找的时间复杂度降低到很低的一个水平。分配的Hash值,就是用+ (NSUInteger)hash这个类方法计算得到的,并且改方法返回的Hash值最好是唯一的。

Hash Table不是顺序地存储元素(0,1,...,n-1),哈希表在存储器中分配n个位置,并且使用函数来计算该范围内的位置。 哈希函数是确定性的,并且良好的散列函数以相对均匀的分布生成值,而不会太计算昂贵。 当两个不同的对象计算相同的哈希值时,发生哈希冲突。 当这种情况发生时,散列表将从碰撞点寻找,并将新对象放置在第一个可用的位置。 随着哈希表变得更加拥挤,碰撞的可能性增加,这导致花费更多的时间来寻找可用空间(这就是具有均匀分布的哈希函数的好处)。

关于实现自定义哈希函数的最常见的误解之一就是确认结果,认为散列值必须是不同的。

下面看一下基于哈希表查找某个成员的过程。

  • 通过hash值直接找到查找目标的位置。
  • 如果目标位置上有多个相同hash值得成员,在按照数组模式进行查找。

2. hash方法的调用时机

我们可以重写hash方法。

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

通过重写该方法进行验证,可以发现只有NSSetNSDictionary添加新的对象的时候会调用。NSSetNSDictionary根据hash值查找成员来提高查找效率。

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];
}

3. hash方法和isEqual的关系

hash方法和isEqual到底是什么关系呢?简单来说就是hash值是对象判等的必要非充分条件。扩展来说,如果两个对象相等,那么他们的hash值一定相等,但是,如果两个对象的哈希值相等是不能一定推出来这两个对象是相等的。

为了优化NSSetNSDictionary判断成员相等的效率,一般会如下:

  • 先判断集合成员的hash值是否和目标的hash值是否相等,如果不等直接就判断不相等,如果相等进入下面步骤。
  • hash值相同的情况下,再对对象进行判等,作为最终的判断结果。

参考文章

[1. Equality Written by Mattt Thompson — August 26th, 2013]
2. iOS开发 之 不要告诉我你真的懂isEqual与hash!

后记

未完,待续~~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容