iOS_理解“对象等同性”这一概念(==、isEqual、hash)

一、==isEqualhash

​ 首先需要了解==符号:比较的是两个对象的指针本身,而不是其所指向的对象。因此我们需要用到NSObject协议中声明的isEqual方法来判断两个对象的等同性。然而其默认的实现却是跟==一样的。因此需要子类们覆写该方法,实现自身内容的比较。

NSObject协议中有两个用于判断等同性的关键方法:

// 基类的默认实现:isEqual 等同 ==
- (BOOL)isEqual:(id)object {
  return self == object; // 比较两`指针对象`的地址
}
- (NSUInteger)hash; // 哈希值

NSObject类对这两个方法的默认实现是:当且仅当其指针值pointer value)完全相等时,这两个对象才相等。若想在自定义的对象中正确覆写这些方法,就必须先理解其约定(contract)。如果isEqual:方法判断两个对象相等,那么其hash方法也必须返回同一个值。相反 如果两个对象的hash方法返回同一个值,那么isEuqal:方法未必会认为两者相等。


二、重写isEqual:hash

  • isEqual::先对比对象地址,然后对比类型,再调用高层比较方法
  • hash:因为collection(如:NSSetNSDictionary等)都使用了HashTable存储数据,添加和查找时都是使用对象的哈希值做索引的。如:set会根据哈希值把对象分装到不同的数组中。在向set中添加新对象时,要根据其哈希值找到与之相关的那个数组,一次检查其中各个元素,看数组中已有对象与之相等。如果相等则说明要添加的对象已经在set里了。(由此可见,如果多个对象返回相同的哈希值时,那么在set中已经有100w个对象的情况下,继续加时则需要将这100w个对象都扫描一遍)所以我们需要尽量降低哈希值的碰撞率。(对散列表HashTable不太熟的同学,自行去补补课)

​ 举例实现如下:

- (BOOL)isEqual:(id)object {
  if (self == object) return YES; // 指针是否相等
  if ([self class] != [object class]) return NO; // 类是否相等
  
  MOPerson *otherPerson = (MOPerson *)object;
  // 其他属性判断,相等返回YES,否则返回NO(看项目需求)
}
- (NSUInteger)hash {
  NSUInteger identify = [identify hash];
  NSUInteger age = _age;
  return identify ^ age;
  // 保持高效,限制位数,降低碰撞率
}

三、isEuqalTo__ClassName__

​ 一般来说,两个类型不同的对象总是不想等的。某些对象提供了特殊的等同性判定方法,如果已经知道两个对象都属于同一个类,就可以使用这种方法。该类方法传递的对象必须跟当前对象一致,因此比调用isEqual:方法快,后者还要执行额外的步骤(因为它不知道受测对象的类型)。Foundation中,很多NSObject的子类已经定义好了自己类的等同性方法:

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

​ 举例实现如下:

- (BOOL)isEqualToPerson:(MOPerson *)otherPerson {
  if (self == otherPerson) return YES;
  
  // 其他属性判断,相等返回YES,否则返回NO(看项目需求)
}
// 也应覆写isEqual方法,实现如下:
- (BOOL)isEqual:(id)object {
  if ([self class] == [object class]) {
    // 是该类则调用自己的判定方法
    return [self isEqualToPerson:(MOPerson *)object];
  } else {
    // 否则交由超类来判定
    return [super isEqual:object];
  }
}

四、等同性判定的执行深度

​ 创建等同性判定方法时,需要决定是根据整个对象来判断等同性,还是仅根据其中几个字段来判断。NSArray的判定方式:先看两数组的count是否相同;若相同,再在每个对应位置的两个对象上调用其isEqual:方法。如果均相等,那么这两个数组相等,这叫做深度等同性判定(deep equality)。

​ 不过有些时候无须将所有数据逐个比较,只根据其中部分数据即可判定二者是否等同。(如:若EOCPerson类的实例是根据数据库里的数据创建而来的,那么其中就可能会含有一个属性是唯一标识符(unique indentifier)),在数据库中用作主键(primary key):@property NSUInteger identifier;,尤其是此属性声明为readonly时,那么只判断标识符就可。因为这两对象是由同一个数据源所创建的,据此断定其余数据也相同。

​ 当然具体情况还是得具体分析,根据你项目的特点和需求来做判断。


五、容器中可变类的等同性

​ 注意当把一个对象放入容器(collection)之后,就不应该改变其哈希值了。如上所说的collection会把各个对象按照其哈希值分装到不同的“箱子数组”中。如果某对象放入“箱子”之后哈希值又变了,那么其现在所处的箱子对它来说就是“错误”的。要想解决这个问题,需要确保哈希值不是根据对象的“可变部分”计算出来的,或是保证放入collection之后就不再改变对象内容了。

​ 看下面一个现象:(尽量将对象做成“不可变的”(immutable))

NSMutableSet *set = [NSMutableSet new];
[set addObject:@[@1, @2]];
[set addObject:@[@1, @2]];
NSLog(@"set: %@", set); // set: {((1, 2))} 集合里不能存两个相等的数组

NSMutableArray *arr = [@[@1] mutableCopy];
[set addObject:arr];
NSLog(@"set: %@", set); // set: {((1), (1, 2))}
[arr addObject:@2];
NSLog(@"set: %@", set); // set: {((1, 2), (1, 2))} 集合里却存了两个相等的数组
// (根据set的语义是不允许这样的,现在却无法保证这一点了,因为我们修改了set中已有的对象)
NSSet *setB = [set copy]; // 如果拷贝此set,那就更糟糕了
NSLog(@"setB: %@", setB); // setB: {((1, 2))} 又只剩一个对象了

​ 对于这种现象大家都有争议,可能符合你的需求,也可能不符合。因此得出结论:如果把某对象放入set之后又修改其内容,那么后面的行为将很难预料。

参考:

《Effective Objective-C 2.0》

Equality

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

莫小言mo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值