1. 思路
当我们想要判断两个对象相等时,我们到底是在比较什么?我觉得可以通过以下三个方面的比较来确定两个对象相等
- 指针是否相等
- 两个对象所属的类是否一样
- 两个对象中的各个字段是否相等
2. 实现
在NSObject协议中有两个用于判断等同性的关键方法
- (BOOL)isEqual:(id)object;
- (NSUInteger)hash;
NSObject类对这两个方法的默认实现是:当且仅当其"指针值"完全相等时,这两个对象才相等.这与我们之前的思路并不一样,只对指针做了判断,并不能满足我们的需求,所以我们需要在自定义的类中覆写这两个方法以满足我们的需求.
比如我们有下面这个类:
@interface EOCPerson : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, assign) NSUInteger age;
@end
覆写isEqual方法
- (BOOL)isEqual:(id)object {
//判断指针
if (self == object) {
return YES;
}
//判断所属类
if ([self class] != [object class]) {
return NO;
}
//判断各字段
EOCPerson *otherPerson = (EOCPerson *)object;
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 == object) {
return YES;
}
//判断所属类
if (![object isKindOfClass:[self class]]) {
return NO;
}
//判断各字段
EOCPerson *otherPerson = (EOCPerson *)object;
if (![_firstName isEqualToString:otherPerson.firstName]) {
return NO;
}
if (![_lastName isEqualToString:otherPerson.lastName]) {
return NO;
}
if (_age != otherPerson.age) {
return NO;
}
return YES;
}
覆写hash方法
首先需要理解一下hash方法是做什么用的,我理解的hash算法是将对象放入hash表中时,系统会根据hash方法的返回值将数据分组,举个例子:
OC中的NSSet表示集合,集合有一个特性是其中的对象不能重复,当我们把一个对象放入集合中,系统会根据hash算法的返回值去集合中对应的分组中进行比对,这样做缩小了比对范围,增加了比对速度
根据定义:若两个对象相等,则其哈希码也相等,但是两个哈希码相同的对象却未必相等,我们可以把hash方法写成下面这样
- (NSUInteger)hash {
return 1337;
}
当两个对象相等时,hash值都是1337是相等的,反过来这些对象的hash值都是相等的,但是其中某些字段不同,这样就满足了对象相等hash值也相等,hash值相等对象不一定相等的定义
但是这样写会产生性能问题,因为collection在检索哈希表时,会用对象的哈希码做索引.假如某个collection是用set实现的,那么set可能会根据哈希码把对象封装到不同的bin中.在向set中添加新对象时,要根据其哈希码找到与之相关的那个bin,依次检查其中各个元素,看数组中已有的对象是否和将要添加的新对象相等.如果相等,那就说明要添加的对象已经在set里面了.由此可知,如果按照上面的写法,全部返回1337,那么set中就根据hash码将对象封装的bin就只有一个,那么在set中已有1000000个对象的情况下,bin中就有1000000个对象,若是继续向set中添加对象,则需要将这1000000个对象全部扫描一遍.
- (NSUInteger)hash {
NSString *stringToHash = [NSString stringWithFormat:@"%@%@%lu",_firstName,_lastName,(unsigned long)_age];
return [stringToHash hash];
}
上面这段代码用的方法是将Person对象中的属性都塞入另一个字符串中,然后令hash方法返回该字符串的哈希码.这样做是满足要求的,但是这样做需要先创建字符串,比返回单一值要慢.而且把这种对象添加到collection中时,也会产生性能问题,因为要想添加,必须先计算其哈希码.
- (NSUInteger)hash {
NSUInteger firstNameHash = [_firstName hash];
NSUInteger lastNameHash = [_lastName hash];
NSUInteger ageHash = _age;
return firstNameHash ^ lastNameHash ^ ageHash;
}
最终采用这种方法,先返回各字段的hash值,再将这些hash值做异或运算,这种做法既能保持较高效率,又能使生成的hash码至少位于一定范围内,而不会过于频繁地重复.
特定类所具有的等同性判定方法
- (BOOL)isEqualToPerson:(EOCPerson *)otherPerson {
if (self == otherPerson) {
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:(EOCPerson *)object];
} else {
return [super isEqual:object];
}
}
若果要经常判断等同性,我们就可以自己创建等同性判定方法,这样就不用检测参数类型,能够大大提升检测速度.这样做也能使代码更美观,更易读.
注意:这里在覆写"isEqual"时,如果受测的参数与接收该消息的对象都属于同一个类,那么就调用自己编写的判定方法,否则就交由超类判断
等同性判定的执行深度
NSArray的检测方式为先看两个数组所含对象个数是否相同,若相同,则在每个对应位置的两个对象身上调用其"isEqual:"方法.如果对应位置上的对象均相等,那么这两个数组就相等,这叫做"深度等同性判定".不过有时候无须将所有数据逐个比较,只根据其中部分数据即可判明二者是否等同.
比方说,我们假设EOCPerson类的实例是根据数据库里的数据创建而来,那么其中就可能会含有另外一个属性,此属性是"唯一标识符",在数据库中用作"主键",在这种情况下,我们也许只会根据标识符来判断等同性,尤其是在此属性声明为readonly时更应该如此.
容器中可变类的等同性
把某个对象放入collection之后,就不应再改变其哈希码了.因为,collection会把各个对象按照其哈希码封装到不同的"箱子数组"中.如果某对象在放入"箱子"之后哈希码又变了,那么其所处的这个箱子对它来说就是"错误"的.要想解决这个问题,需要确保哈希码不是根据对象的"可变部分"计算出来的,或是保证放入collection之后就不再改变对象内容了.
如图所示,我们修改了arrayC从而导致set中出现了两个相同的对象.这个例子并不是说这样做就是错的,这种结果在某些情况下可能正是我们需要的结果,这里只是说明这么做会造成什么样的后果
参考文献:[1]Matt Galloway.Effective Objective-C 2.0[M].北京:机械工业出版社, 2015: 30-35