Objc 相等性判断
今天做任务时遇到一个问题,情况是这样的:
我新建一个类,然后创建一个这个类的对象,然后将这个类对象放到一个数组中,在此后使用的时候就从这个数组中把这个类对象取出来,如果这个数组中没有这个类,就重新创建一个类,放入到这个数组,也就是说这个数组中始终有一个这个类的对象。但是有一个问题,就是我判断这个类对象是否在这个数组中的时候,使用了NSArray的函数- (BOOL)containsObject:(ObjectType)anObject,然后问题就出现了,这个函数始终返回NO。用代码描述:
Person *personA = [[Person alloc] init];
Person *personB = [[Person alloc] init];
NSArray *personArrA = @[personA];
if ([personArrA containsObject:personB]) {
NSLog(@"personArrA 中包含 personB");
} else {
NSLog(@"personArrA 中不包含 personB");
}
如果这样子判断的话,始终会打印<strong>personArrA 中不包含 personB</strong>
现在就把我的理解记录一下,出现这个问题的原因是:
NAArray的这个方法- (BOOL)containsObject:(ObjectType)anObject是用来判断这个数组的对象元素是否包含指定对象元素,但是这个函数会去调用对象元素的- (BOOL)isEqual:(id)object方法去判断元素对象的相等与否,如果这个方法返回为YES则说明这两个对象元素相等,即数组中存在;否则不相等,即数组中不存在。如果对象元素没有实现这个判断相等的方法,则默认调用基类中的- (BOOL)isEqual:(id)object方法,而NSObject中这个方法的默认实现是比较两个对象的内存地址,而我的Person类没有实现- (BOOL)isEqual:(id)object方法,所以它会去比较personA和personB的内存地址,这两个对象的地址肯定不相等,所以这样子就出现问题了。。
Object中相等性判断
在Object中,基类NSObject使用isEqual:这个方法来测试和其他对象的相等性,其实现的本质,就是如果两个对象指向了同一个内存地址,那么就认为是相同的,相等的;可能的实现为:
- (BOOL)isEqual:(id)object {
return self == object;
}
讲到这里我们顺便说一下<strong>==</strong>这个操作符,这个操作符实际上就是直接比较两个对象的内存地址,所以在Objc中我们很少使用==这个操作符来判断两个对象是否相等。
如果需要比较两个对象是否相等,我们应该在自己的对象中实现NSObject协议中声明的用于判断等同性的两个关键方法:
- (BOOL)isEqual:(id)object;
- (NSUInteger)hash;
在Foundation框架中,许多NSObject的子类都有自己的相等性检查,如下:
NSAttributedString -isEqualToAttributedString:
NSData -isEqualToData:
NSDate -isEqualToDate:
NSDictionary -isEqualToDictionary:
NSHashTable -isEqualToHashTable:
NSIndexSet -isEqualToIndexSet:
NSNumber -isEqualToNumber:
NSOrderedSet -isEqualToOrderedSet:
NSSet -isEqualToSet:
NSString -isEqualToString:
NSTimeZone -isEqualToTimeZone:
NSValue -isEqualToValue:
所以我们使用框架提供给我们的类对象时直接使用框架提供的等同性判断方法即可。比如,NSSting类实现了一个自己独有的等同性判断方法,名叫:isEqualToSting:。
NSLog(@"----------字符串相等比较-------------");
NSString *strA = @"ljt";
NSString *strB = [NSString stringWithFormat:@"%@",@"ljt"];
NSLog(@"内存地址:strA = %p,strB = %p",strA,strB);
NSLog(@"----------直接==比较---------------");
if (strA == strB) {
NSLog(@"strA == strB");
} else {
NSLog(@"strA != strB");
}
NSLog(@"----------isEqualToString比较---------------");
if ([strA isEqualToString:strB]) {
NSLog(@"strA == strB");
} else {
NSLog(@"strA != strB");
}
日志:
2016-12-07 20:16:06.937 TestString[5852:226853] ----------字符串相等比较-------------
2016-12-07 20:16:06.938 TestString[5852:226853] 内存地址:strA = 0x10d50a0a8,strB = 0xa00000000746a6c3
2016-12-07 20:16:06.938 TestString[5852:226853] ----------直接==比较---------------
2016-12-07 20:16:06.938 TestString[5852:226853] strA != strB
2016-12-07 20:16:06.938 TestString[5852:226853] ----------isEqualToString比较---------------
2016-12-07 20:16:06.938 TestString[5852:226853] strA == strB
从代码中就可以很明想的看出这个方法和==的区别。
在这里插入一个小插曲
有时候我们会看到使用 == 来判断字符串是否相等,比如
NSString *a = @"ljt";
NSString *b = @"ljt";
NSLog(@"内存地址:a = %p,b = %p",a,b);
NSLog(@"----------直接==比较---------------");
if (a == b) {
NSLog(@"a == b");
} else {
NSLog(@"a != b");
}
日志:
2016-12-07 20:47:58.731 TestString[5916:236643] 内存地址:a = 0x10eb8b088,b = 0x10eb8b088
2016-12-07 20:47:58.732 TestString[5916:236643] ----------直接==比较---------------
2016-12-07 20:47:58.732 TestString[5916:236643] a == b
2016-12-07 20:47:58.732 TestString[5916:236643] ----------isEqualToString比较---------------
2016-12-07 20:47:58.732 TestString[5916:236643] a == b
首先要明确一点,比较NSString对象正确的方法是:-isEqualToString:。任何情况下都不要直接使用==来对NSString进行比较。但是现在看来结果貌似是正确的,所有这些行为,都来源于一种称为<strong>字符串驻留</strong>的优化技术,它把一个不可变字符串对象的值拷贝给各个不同的指针。NSString *a和*b都指向同样一个驻留字符串值@"ljt"。注意所有这些针对的都是静态定义的不可变字符串。
1、非静态定义的字符串呢?
NSString *str = @"ljt";
NSString *a = [str stringByAppendingString:@"ljt"];
NSString *b = [str stringByAppendingString:@"ljt"];
NSLog(@"内存地址:a = %p,b = %p",a,b);
NSLog(@"----------直接==比较---------------");
if (a == b) {
NSLog(@"a == b");
} else {
NSLog(@"a != b");
}
NSLog(@"----------isEqualToString比较---------------");
if ([a isEqualToString:b]) {
NSLog(@"a == b");
} else {
NSLog(@"a != b");
}
日志:
2016-12-07 20:55:27.216 TestString[5957:240029] 内存地址:a = 0x7fda58e6f370,b = 0x7fda58e093b0
2016-12-07 20:55:27.217 TestString[5957:240029] ----------直接==比较---------------
2016-12-07 20:55:27.217 TestString[5957:240029] a != b
2016-12-07 20:55:27.217 TestString[5957:240029] ----------isEqualToString比较---------------
2016-12-07 20:55:27.218 TestString[5957:240029] a == b
2、如果比较的是NSArray和NSDictionary呢?
NSArray *arr1 = @[@"ljt",@"hdu",@"ths"];
NSArray *arr2 = @[@"ljt",@"hdu",@"ths"];
NSLog(@"内存地址:arr1 = %p,arr2 = %p",arr1,arr2);
NSLog(@"----------直接==比较---------------");
if (arr1 == arr2) {
NSLog(@"arr1 == arr2");
} else {
NSLog(@"arr1 != arr2");
}
日志:
2016-12-07 20:58:31.704 TestString[5973:241680] 内存地址:arr1 = 0x7fff3b523010,arr2 = 0x7fff3b506200
2016-12-07 20:58:31.705 TestString[5973:241680] ----------直接==比较---------------
2016-12-07 20:58:31.705 TestString[5973:241680] arr1 != arr2
综上:字符串使用使用==比较相等结果貌似是正确的现象,只是一种特殊情况。
自建对象判断相等性
我们这里创建一个Person的类继承与NSObject,暂时只实现一个初始方法:
###Person.h
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *sex;
@property (nonatomic, copy) NSString *age;
- (BOOL)isEqualToPerson:(Person *)person;
@end
###Person.m
#import "Person.h"
@implementation Person
- (instancetype)init {
if (self == [super init]) {
self.name = @"ljt";
self.sex = @"man";
self.age = @"25";
}
return self;
}
接下来我们创建两个类判断一下相等看看会有什么结果:
Person *personA = [[Person alloc] init];
Person *personB = [[Person alloc] init];
NSLog(@"内存地址:personA = %p,personB = %p",personA,personB);
NSLog(@"----------直接==比较---------------");
if (personA == personB) {
NSLog(@"personA == personB");
} else {
NSLog(@"personA != personB");
}
日志输出:
2016-12-07 21:12:00.348 TestString[5998:246004] 内存地址:personA = 0x7fb7b2c28570,personB = 0x7fb7b2ca1e40
2016-12-07 21:12:00.348 TestString[5998:246004] ----------直接==比较---------------
2016-12-07 21:12:00.348 TestString[5998:246004] personA != personB
由以上可知,两个不可能相等。
然后我们实现一个自定义的判断相等的方法:- (BOOL)isEqualToPerson:(Person *)person;
- (BOOL)isEqualToPerson:(Person *)person {
if (self == person) {
return YES;
}
if (![self.name isEqualToString:person.name]) {
return NO;
}
if (![self.sex isEqualToString:person.sex]) {
return NO;
}
if (![self.age isEqualToString:person.age]) {
return NO;
}
return YES;
}
####################################
if ([personA isEqualToPerson:personB]) {
NSLog(@"personA == personB");
} else {
NSLog(@"personA != personB");
}
日志:
2016-12-07 21:15:21.489 TestString[5998:246004] personA == personB
到这里我们就实现了两个对象的相等性判断,但是这样是不行的,这样子还是没有解决我们最初的问题。我们还需要实现NSObject协议中声明的用于判断等同性的两个关键方法:- (BOOL)isEqual:(id)object和- (NSUInteger)hash。我们先看一下- (BOOL)isEqual:(id)object方法,- (NSUInteger)hash这个方法稍后再说。
- (BOOL)isEqual:(id)object
自建类编写isEqual方法的一般方式是实现一个自定义的判断相等性的方法,如果所比较的参数对象和接受消息的对象都同属一个类,那么句调用自己编写的自定义方法,否则就交与父类来判断,所以Person类的isEqual方法可以这样实现:
- (BOOL)isEqual:(id)object {
if ([self class] == [object class]) {
return [self isEqualToPerson:(Person *)object];
}
return [super isEqual:object];
}
这样实现以后我们最初的问题就可以解决:
Person *personA = [[Person alloc] init];
Person *personB = [[Person alloc] init];
NSArray *personArrA = @[personA];
if ([personArrA containsObject:personB]) {
NSLog(@"personArrA 中包含 personB");
} else {
NSLog(@"personArrA 中不包含 personB");
}
日志:
2016-12-07 21:24:13.841 TestString[5998:246004] personArrA 中包含 personB
当执行[personArrA containsObject:personB]时,遍历数组中的元素对象和PersonB比较,自动去调用Person类中的isEqual方法,然后发现两者是同属一个类,所以调用isEqualToPerson去判断两者的相等性,发现相等,返回YES。
</br>
还有一个函数我们也是需要实现的- (NSUInteger)hash,
- (NSUInteger)hash
对于面向对象编程来说,对象相等性检查的主要用例,就是确定一个对象是不是集合的成员。为了加快这个进程,子类当中需要实现hash方法,这两个方法的关系:
1、如果isEqual方法判断两个对象相等,那么其hash方法也必须返回同一个值
2、如果两个对象的hash方法返回同一个值,那么isEqual方法未必会认为两者相等。
这种现象在Set容器中表现的最为明显,我们先将Person中的hash方法实现设置为父类的默认实现:
- (NSUInteger)hash {
NSUInteger hash = [super hash];
NSLog(@"hash值:%lu",hash);
return hash;
}
然后创建两个Person对象放入到Set容器中
Person *personA = [[Person alloc] init];
Person *personB = [[Person alloc] init];
NSMutableSet *set = [[NSMutableSet alloc] initWithCapacity:1];
[set addObject:personA];
[set addObject:personB];
NSLog(@"%@",[set allObjects]);
日志:
2016-12-07 21:39:10.567 TestString[6080:258148] 内存地址:personA = 0x7f7ffbc09a10,personB = 0x7f7ffbca0f80
2016-12-07 21:39:10.568 TestString[6080:258148] hash值:140187661277712
2016-12-07 21:39:10.568 TestString[6080:258148] hash值:140187661897600
2016-12-07 21:39:10.568 TestString[6080:258148] (
"<Person: 0x7f7ffbc09a10>",
"<Person: 0x7f7ffbca0f80>"
)
可以发现:当想Set容器中添加元素时,先调用对象的hash函数,比较两个对象的hash值,如果hash值不想等,则认为这两个元素对象不相等,则添加到Set容器中。
下面,我们修改一下hash函数,时期返回相同的值:
- (NSUInteger)hash {
NSUInteger nameHash = [self.name hash];
NSUInteger sexHash = [self.sex hash];
NSUInteger ageHash = [self.age hash];
NSUInteger hash = nameHash ^ sexHash ^ ageHash;
// NSUInteger hash = [super hash];
NSLog(@"hash值:%lu",hash);
return hash;
}
从新执行一下:
日志:
2016-12-07 21:42:25.393 TestString[6094:259733] 内存地址:personA = 0x7fbec2f37600,personB = 0x7fbec2f368e0
2016-12-07 21:42:25.394 TestString[6094:259733] hash值:1233295
2016-12-07 21:42:25.394 TestString[6094:259733] hash值:1233295
2016-12-07 21:42:25.394 TestString[6094:259733] (
"<Person: 0x7fbec2f37600>"
)
这样就会只添加一个元素,因为这两个元素是相等的。断点跟踪一下,它的执行流程为:
1、先调用hash函数获取对象的hash值,如果两者的hash值不等,则直接判断两个对象不想等。
2、如果hash值相等,则调用isEqual方法作进一步比较,如果isEqual返回NO,则认为两对象不相等。
3、如果isEqual方法返回YES,则任务两个对象相等。
还有一种情况也可以加深一下对这两个函数的理解:让对象当做字典的键。
首先作为字典的键需要实现NSCopying协议,我们对Person类做一下修改:
@interface Person : NSObject<NSCopying> //声明NSCopying
·
·
·
@end
#import "Person.h"
@implementation Person
- (id)copyWithZone:(NSZone *)zone {
Person *person = [[[self class] allocWithZone:zone] init];
person.name = self.name;
person.sex = self.sex;
person.age = self.age;
return person;
}
·
·
·
@end
然后我们这样操作:
Person *personA = [[Person alloc] init];
Person *personB = [[Person alloc] init];
NSMutableDictionary *dic = [[NSMutableDictionary alloc] initWithCapacity:1];
[dic setObject:@"ljt" forKey:personA];
NSLog(@"%@",[dic objectForKey:personB]);
日志:
2016-12-07 21:52:33.235 TestString[6139:264284] 内存地址:personA = 0x7fbe73d13a30,personB = 0x7fbe73d1f790
2016-12-07 21:52:57.146 TestString[6139:264284] hash值:1233295
2016-12-07 21:53:06.623 TestString[6139:264284] hash值:1233295
2016-12-07 21:53:20.339 TestString[6139:264284] ljt
以personA为键设的值,尽然通过personB取出来了!断点跟踪我们可以发现,字典设值和取值的时候,先获取对象的hash值,如果hash值相等,则通过isEqual去比较,如果比较结果一致,则认为是同一个对象。设值的时候如果字典里面没有,则加入字典中,如果字典中已经存在了相同的键,则重新赋值。
NOTE:字典在设置Key的时候需要复制这个Key对象的(这个key值对象需要实现NSCopying协议),也就是说,我们的personA在作为key值得时候,已经被字典复制一份了,因为字典需要确保这个key值不可变,否则,作为key值得对象通过修改内部属性而导致这个对象的hash值发生变化,会出现找不到对象的情况。
至此:我们可以对Objc中对象的相等比较,有了一个大致的了解。
参考文献
1、NShipster Equality
2、使用NSArray containObject:方法比较对象
3、重载hash与isEqual:方法