前言
Objective-C作为一门面向对象编程的语言,我们在使用它开发的时候,接触的最多的就是类与对象了。
我们在使用对象的时候,往往需要判断两个对象是否相等,这种相等包含两种含义:1.在程序上是否是同一块内存地址。2.在语义上,是否能指代同一对象。
这就是对象的等同性
关于如何判断两个对象是否相同,我们由浅至深通过代码逐渐分析:
目录
正文
首先,为了能够对对象进行方便操作,我们定义一个 Person 的类,其中含有 name 和 gender 两个属性,当两个 person 的 name 和 gender 同时一致时,我们认为这两个对象指代同一个人。
一. == 操作
第一步,既然要进行相等判断,就可以使用 == 来进行
Person *person1 = [[Person alloc] init];
Person *person2 = nil;
person2 = person1;
NSLog(@"person1 == person2: %d", person1 == person2);
此时的输出结果为:
person1 == person2: 1
这样就能表示两个 Person 是相等的吗? 并不是,看下面代码:
Person *person1 = [[Person alloc] init];
person1.name = @"张三";
person1.gender = PersonGenderMan;
Person *person2 = [[Person alloc] init];
person2.name = @"张三";
person2.gender = PersonGenderMan;
NSLog(@"person1 == person2: %d", person1 == person2);
此时的输出结果为:
person1 == person2: 0
很显然,这并不能满足我们对于 Person 类相等的设定。同时也可以得出结论: == 操作只有分辨两个对象是否是同一内存地址的功能。
二. - isEqual: 与 hash
在Objective-C中,基本所有的对象都是继承自 NSObject 基类的,在 NSObject 基类的文档中,我们可以看到, NSObject 类实现了 NSObject 协议,而 NSObject 这个协议中,有 - (BOOL)isEqual:(id)object; 一个方法与 @property (readonly) NSUInteger hash; 这样一个属性需要类去实现,我们看一下官方文档对这两个东西的描述:
- isEqual:
Returns a Boolean value that indicates whether the receiver and a given object are equal.
Required.
hash
Returns an integer that can be used as a table address in a hash table structure.
Required.
大意就是说 - (BOOL)isEqual:(id)object; 这个方法是用来判断两个对象是否相等的。而 hash 是用在哈希表表结构的表地址。
由文档我们可知, - isEqual: 方法返回 YES ,那么 hash 必然相等; 如果 hash 相等,那么 - isEqual: 返回的不一定是 YES 。
三. - isEqual: 的系统实现
系统是如何实现 - isEqual: 的呢,看下面代码:
Person *person1 = [[Person alloc] init];
person1.name = @"张三";
person1.gender = PersonGenderMan;
Person *person2 = person1;
Person *person3 = [[Person alloc] init];
person1.name = @"张三";
person1.gender = PersonGenderMan;
NSLog(@"person1 == person2: %d", [person1 isEqual:person2]);
NSLog(@"person1 == person3: %d", [person1 isEqual:person3]);
输出结果为:
person1 == person2: 1
person1 == person3: 0
由此可见,系统默认 NSObject 中 - isEqual: 的实现仅仅是对两个对象地址的判断。
四. - isEqual: 与 hash 的自定义实现
要自定义 - isEqual: 方法,我们可以这样考虑:
如果此时两个对象的内存地址一致,那么这两个对象的各个属性包括自身都是相等的,此时 - isEqual: 方法应该是这样的:
- (BOOL)isEqual:(id)object {
if (self == object) {
return YES;
}
return NO;
}
然后,如果两个对象表示的不是一种实物,那么它们就没必要相同了,此时 - isEqual: 方法应该是这样的:
- (BOOL)isEqual:(id)object {
if (self == object) {
return YES;
} else if ([self class] != [object class]) {
return NO;
}
return NO;
}
接下来,有一种情况,如果 Person 有一个子类 Man ,如果是一个 Person 对象和一个 Man 对象进行对比,那么上段代码无疑是不相等的,所以在这个地方,我们要根据实际逻辑进行调整。现在假如 Person 与 Man 类无多余差别,那么修改之后 - isEqual: 应该是:
- (BOOL)isEqual:(id)object {
if (self == object) {
return YES;
} else if (![object isKindOfClass:[self class]]) {
return NO;
}
return NO;
}
最后,我们根据 Person 类的只要 name 和 gender 相同,那么两个对象就相等的设定继续改进 - isEqual: 方法,修改之后应该是:
- (BOOL)isEqual:(id)object {
if (self == object) {
return YES;
} else if (![object isKindOfClass:[self class]]) {
return NO;
}
Person *person = (Person *)object;
if ([self.name isEqualToString:person.name] &&
self.gender == person.gender) {
return YES;
}
return NO;
}
这样我们就完成了 - isEqual: 方法的自定义。接下来我们进行 hash 的自定义。
首先,既然 - isEqual: 方法返回YES时, hash 值相等, 而 hash 相等时, - isEqual: 不一定返回YES,那么 hash 可以这样实现:
- (NSUInteger)hash {
return 9999;
}
为了验证这种实现方式的可靠性,我们对 - isEqual: 和 hash 插入一些打印:
- (BOOL)isEqual:(id)object {
NSLog(@"%@->%@ call isEqual, param:%@", self.name, self, object);
if (self == object) {
return YES;
} else if (![object isKindOfClass:[self class]]) {
return NO;
}
Person *person = (Person *)object;
if ([self.name isEqualToString:person.name] &&
self.gender == person.gender) {
return YES;
}
return NO;
}
- (NSUInteger)hash {
NSLog(@"%@->%@ call hash", self.name, self);
return 9999;
}
然后执行下面的代码:
NSMutableSet *set = [NSMutableSet setWithCapacity:100];
for (int i = 0; i < 5; i++) {
Person *person = [[Person alloc] init];
person.name = [NSString stringWithFormat:@"张三%d", i];
person.gender = PersonGenderMan;
[set addObject:person];
NSLog(@"-----------------------------");
}
控制台输出如下:
张三0-><Person: 0x60000023bf40> call hash
-----------------------------
张三1-><Person: 0x60000023c160> call hash
张三0-><Person: 0x60000023bf40> call isEqual, param:<Person: 0x60000023c160>
-----------------------------
张三2-><Person: 0x60000023bf80> call hash
张三0-><Person: 0x60000023bf40> call isEqual, param:<Person: 0x60000023bf80>
张三1-><Person: 0x60000023c160> call isEqual, param:<Person: 0x60000023bf80>
-----------------------------
张三3-><Person: 0x604000036b60> call hash
张三0-><Person: 0x60000023bf40> call isEqual, param:<Person: 0x604000036b60>
张三1-><Person: 0x60000023c160> call isEqual, param:<Person: 0x604000036b60>
张三2-><Person: 0x60000023bf80> call isEqual, param:<Person: 0x604000036b60>
-----------------------------
张三4-><Person: 0x6040000379c0> call hash
张三0-><Person: 0x60000023bf40> call isEqual, param:<Person: 0x6040000379c0>
张三1-><Person: 0x60000023c160> call isEqual, param:<Person: 0x6040000379c0>
张三2-><Person: 0x60000023bf80> call isEqual, param:<Person: 0x6040000379c0>
张三3-><Person: 0x604000036b60> call isEqual, param:<Person: 0x6040000379c0>
-----------------------------
Set的实现原理为:将相同 hash 值的对象放在同一个数组,然后依次对比数组中每个对象,如果 - isEqual:返回 YES ,说明对象已经存在;如果 - isEqual: 全部返回 NO ,则证明对象不存在。
根据Set的实现原理,我们可知,如果 hash 值返回同一个值,那么向存在多个相同类对象的Set中添加新的对象,要进行多次的 - isEqual: 的调用,这样做是不值得的。
hash 值的目的是尽最大可能返回一个标识,但并不是一定要是唯一的。根据 Person 的设定,能标识 Person 的是 name 和 gender 属性,所以我们可以根据这两个属性来进行 hash 值的生成。
- (NSUInteger)hash {
return [self.name hash] ^ self.gender;
}
五. 保证 hash 的不可变
在我们将对象放入容器中后,就要保证对象 hash 的不可变,如果此时将对象的 hash 进行改变,那么容器就可能会有错误数据的出现,例如:
Person *person1 = [[Person alloc] init];
person1.name = @"张三";
person1.gender = PersonGenderMan;
Person *person2 = [[Person alloc] init];
person2.name = @"李四";
person2.gender = PersonGenderMan;
NSMutableSet *set = [NSMutableSet setWithObjects:person1, person2, nil];
NSLog(@"before set: %@", set);
此时输出为:
before set: {(
<Person: 0x60400023d280>,
<Person: 0x604000035800>
)}
接下来我们将其中一个对象改为与另一个对象一致:
person2.name = @"张三";
NSLog(@"after set: %@", set);
NSLog(@"person1 hash: %zd", [person1 hash]);
NSLog(@"person2 hash: %zd", [person2 hash]);
NSLog(@"person1 isEqual person2: %d", [person1 isEqual:person2]);
此时的输出为:
after set: {(
<Person: 0x60400023d280>,
<Person: 0x604000035800>
)}
person1 hash: 32052695
person2 hash: 32052695
person1 isEqual person2: 1
此时根据两个对象的 hash 值以及 - isEqual: 方法的判断,两个对象是相等的,那么此时 Set 中的数据便存在错误。
我们对此时的 Set 进行一份拷贝:
NSSet *newSet = [set copy];
NSLog(@"new set:%@", newSet);
结果为:
new set:{(
<Person: 0x60400023d280>
)}
新 Set 中,相等的对象以及被剔除了。
所以我们在容器中使用对象时,要尽量避免对象 hash 值的不可变,或者说在将对象放入容器后不再改变对象的内容,这样才能避免容器中出现错误的数据。
参考
Effective+Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法