Objc 相等性判断

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:方法

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,724评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,104评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,142评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,086评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,076评论 5 370
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,914评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,220评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,871评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,318评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,834评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,951评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,574评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,162评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,162评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,383评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,349评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,652评论 2 343

推荐阅读更多精彩内容

  • 前言 对数据的等同性判断包括对基本数据类型等同性的判断和对象等同性的判断。对基本数据类型等同性的判断是非常简单的,...
    VV木公子阅读 1,448评论 0 8
  • 闲话少说,先说本编博客的核心 iOS系统API给我们提供一个自动过滤重复元素的容器 NSMutableSet...
    upworld阅读 2,821评论 8 21
  • 禅与 Objective-C 编程艺术 (Zen and the Art of the Objective-C C...
    GrayLand阅读 1,601评论 1 10
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,579评论 18 399
  • 不管你是由于什么原因想成为一名文案,我都要恭喜你!因为这意味着你即将切换到一个丰富多彩、活力四射的生活状态! 为什...
    天价棒棒糖阅读 1,962评论 4 22