理解"对象等同性"概念

根据"等同性"来比较对象是一个非常有用的功能,不过,按照"=="操作符比较出来的结果未必就是我们想要的.因为该操作是比较的两个指针本身,而不是其所指向的对象,应该使用NSObject协议中国声明的"isEqual":方法来判断两个对象的等同性.一般来说,两个类型不同的对象总是不相等的.

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

- (BOOL)isEqual:(id)object;
- (NSUInteger)hash;

NSObject类对这两个方法的默认实现是:当且仅当其"指针值(内存地址)"完全相等时,这两个对象才相等.若想在自定义的对象中正确覆写这些方法,就必须先理解其约定.

如果"isEqual:"方法断定两个对象相等,那么其hash方法也必须返回同一个值,但是,如果两个对象的hash方法返回同一个值,那么"isEqual:"方法未必会认为两者相等.

比如有下面这个类:

@interface CWGPerson : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, assign) NSUInteger age;
@end

我们认为:如果两个CWGPerson的所有字段全部相等, 那么这两个对象就相等.于是"isEqual:"方法可以写成:

- (BOOL)isEqual:(id)object {
  if (self == object) return YES;
  if ([self class] != [object class]) return NO;

  CWGPerson *otherPerson = (CWGPerson *)object;
  if (![_firstName isEqualToString:otherPerson.firstName]) return NO;
  if (![_lastName isEqualToString:otherPerson.lastName]) return NO;
  if (_age != otherPerson.age]) return NO;
  return YES;
}

先判断两个指针是否相等,接下来判断两个对象所属的类,最后检测每个属性是否相等。
接下来该实现hash方法了,下面这种写法完全可行:

- (NSUInteger)hash {
  return 1337;
 }

不过这样的话,在collection中使用这种对象将产生性能问题,因为collection在检索哈希表时,会用到对象的哈希码做索引。这样的话假如集合中有10000个对象,若是继续向其中添加对象,则需要将这10000个对象全部扫描一遍。
hash方法也可以这样来实现:

- (NSUInteger)hash {
  NSString *stringToHash = [NSString stringWithFormat:@"%@:%@:%i", _firstName, _lastName, _age];
  return [stringToHash hash];
}

这样做将NSString对象中的熟悉都塞入另一个字符串中,然后令hash方法返回该字符串的哈希码、这样做符合约定,因为两个相等的CWGPerson对象总是返回相同的哈希码。但是这样做有额外增加了创建字符串的开销。
再来看一种方法:

- (NSUInteger)hash {
  NSUInteger firstNameHash = [_firstName hash];
  NSUInteger lastNameHash = [_lastName hash];
  NSUInteger ageHash = _age;
  return firstNameHash ^ lastNameHash ^ ageHash;
}

这种做法既能保持较高效率,又能使生成的哈希码至少位于一定范围之内,而不会过于频繁的重复。当然,此算法生成的哈希码还是会碰撞的,不过至少可以保证哈希码有多种可能的取值。编写hash方法时,应该用当前的对象做实验,以便在减少碰撞频度与降低运算复杂程度间取舍。

特定类所具有的等同性判定方法

如果经常需要判断等同性,那么可能会自己开创建等同性判定方法,因为无需检查参数类型,所以能大大提升检查速度。在编写判定方法时,也应该覆写“isEqual”方法,后者的常见实现方式为:如果受测的参数与接收该消息的对象都属于同一个类,那么就该调用自己编写的判定方法,否则就交由超类来判断。
例如,在CWGPerson类中可以竖线如下两个方法:

- (BOOL)isEqualToPerson:(CWGPerson *)otherPerson {
  if (self == object) return YES;
   if (![_firstName isEqualToString:otherPerson.firstName]) return NO;
  if (![_lastName isEqualToString:otherPerson.lastName]) return NO;
  if (_age != otherPerson.age]) return NO;
  return YES;
}

- (BOOL)isEqual:(id)object {
  if ([self class] == [object class]) {
    return [self isEqualToPerson:(CWGPerson *)object];
  } else {
    return [super isEqual:object];
  }
}
等同性判定的执行深度

创建等同性判定方法时,一定要根据整个对象来判断等同性,还是仅仅根据其中几个字段来判断。NSArray的检测方式为先看看两个数组所含对象个数是否相同,若是相同的,则在每个对应位子的两个对象身上调用“isEqual”方法,这叫“深度等同性判定”。不过有时没有必要这么做,比如说:我们假设CWGPerson类的实例是根据数据库中的数据创建出来的,那么其中就可能还有有个属性叫做“主键”。在这样的情况下,我们只要根据主键来判断就行了。

容器中可变类的等同性

在这里我们举个例子就能很好的理解这点:

NSMutableSet *set = [NSMutableSet new];

NSMutableArray *arrayA = [@[@1, @2] mutableCopy];
[set addObject: arrayA];
NSLog(@"set = %@", set);
// Output: set = {(1, 2)}

如果这时,再向set中加入一个数组, 此数组与前面的数组一模一样。那么:

NSMutableArray *arrayB = [@[@1, @2] mutableCopy];
[set addObject: arrayB];
NSLog(@"set = %@", set);
// Output: set = {(1, 2)}

此时set里仍然只有一个对象,因为刚才要加入的那个数组对象和set中也有的数组对象相等,所以set并没有改变。但是如果我们添加的是一个不一样的数组:

NSMutableArray *arrayC = [@[@1] mutableCopy];
[set addObject: arrayC];
NSLog(@"set = %@", set);
// Output: set = {((1), (1, 2))}

然而这时,我们进行如下操作:

[arrayC addObject:@2];
NSLog(@"set = %@", set);
// Output: set = {((1, 2), (1, 2))}

set中居然有2个相等的数组,根据set的语法规则,这时绝对不允许出现的。然而现在却无法保证这一点了,因为我们修改了set中已有的对象,若是拷贝此set,那就更可怕了:

NSSet *setB = [set copy];
NSLog(@"set = %@", set);
// Output: set = {(1, 2)}

复制之后又只剩下一个对象了,此set看上去好像是由一个空set开始,通过逐个向其中添加新对象而创建出来的。这可能符合你的要求,也可能不符合,有的开发者也许想要忽略set中的错误,“找原样”复制一个新的出来,还有的开发者则会认为这样做挺好的。其实这两种拷贝算法都说得通,于是就进一步印证了刚才说的那个问题:如果把某个对象放入set之后又修改其内容,那么后面的行为就很难预料。
举这个例子是为了提醒大家,把某个对象放入collection之后改变其内容将会造成什么后果。笔者并不是说绝对不能这么做,而是要提醒你这样做的隐患,用相印的代码处理可能发生的问题。

总结:

  • 若是检测对象的等同性,请提供“isEqual:”和“hash”方法
  • 相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同。
  • 不要盲目的这个检测每条属性,二手应该依照具体需求来制定检测方案。
  • 编写hash方法时,应该使用计算速度快而且哈希码碰撞几率低的算法。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容