iOS笔记:进一步认识 ==、isEqual、hash

最近在新接手的项目中进行对象比较,对同一个对象调用isEqual来比较,结果居然是NO。猜测是对象重写了isEqual方法。查看代码以下:算法

果真重写了 isEqual方法,虽然方法不太严谨,没有首先判断==,代码看起来也什么大问题,可是同一个对象比较也不该该返回NO啊。看了下面一堆&&判断,难道要一个个po吗?忽然想到了 二分查找算法(算法基础仍是有用的)的优势,而后快速定位到放回NO的条件。

而后继续根据二分查找,最终找到self.releasetime比较的时候返回NO。原来releasetime字段为nil,使用isEqualToString比较两个为nil的对象的时候返回NO。

(lldb) po (BOOL)([self.releasetime isEqualToString:info.releasetime])
NO
(lldb) 
复制代码

解决办法:编程

场景一:针对当前的场景,只须要在的最顶部添加下面判断就能够了if (self == object) return YES;数组

场景二:可是一些其余场景,咱们确实会遇到比较两个地址不同,可是数据同样(而且对象有的属性确实为nil)的场景。这个时候咱们能够为NSString等基本数据类型添加category,而后重写isEqualToString:方法,若是要比较的两个对象都是nil则返回YES。可是若是要比较的两个对象都不是nil并且length也相同,难道我还要重写一个方法进行遍历比较吗?能不能直接用原来的isEqualToString:方法,可是category中不能调用super方法,再说分类也不是子类。此时咱们就须要利用runtime遍历方法列表找到原来的方法,而后调用一下返回结果就能够了。后来发现,想多了,给nil发送消息返回的始终是NO。最后在分类中实现一个类方法判断一下就能够了。bash

#import "NSString+isEqual.h"

@implementation NSString (isEqual)

+(void)load {
    NSLog(@"结果1 = %@", [nil isEqualToString:nil] ? @"YES" : @"NO");
    NSLog(@"结果2 = %@", [NSString isString:nil EqualToString:nil] ? @"YES" : @"NO");
}

+(BOOL)isString:(NSString *)aString EqualToString:(NSString *)bString {
    if (aString == nil && bString == nil ) {
        return YES;
    }
    return [aString isEqualToString:bString];
}

@end
复制代码

思考一:==与isEqual的区别?

  • 对于基本类型, ==运算符比较的是值;
  • 对于对象类型, ==运算符比较的是对象的地址
  • isEqual方法就是用来判断两个对象是否相等(自定义对象须要重写isEqual)

思考二:isEqual的默认实现

isEqual方法是NSObject中声明的,默认实现就是简单的比较对象地址。数据结构

@implementation NSObject (Approximate)
- (BOOL)isEqual:(id)object {
  return self == object;
}
@end
复制代码

思考三:自定义继承NSObject的子类须要重写的几个方法

  • 重写-(BOOL)isEqual:(id)object
  • 重写-(NSUInteger)hash
  • (非必须)实现NSCoping协议-(id)copyWithZone:(NSZone *)zone

注意: 使用OC自定义的类实例做为NSDictionary的key的话,须要实现NSCoping协议,若是不实现的话,向dictionary中添加数据时会报警告:Sending 'Coder *__strong to parameter of incompatible type 'id _Nonnull',在运行的时候,在setObject forKey函数这里直接崩溃函数

思考四:Foundation中NSObject的子类已经定义的本身的isEqual

  • NSAttributedString -isEqualToAttributedString:
  • NSData -isEqualToData:
  • NSDate -isEqualToDate:
  • NSDictionary -isEqualToDictionary:
  • NSHashTable -isEqualToHashTable:
  • NSIndexSet -isEqualToIndexSet:
  • NSNumber -isEqualToNumber:
  • NSOrderedSet -isEqualToOrderedSet:
  • NSSet -isEqualToSet:
  • NSString -isEqualToString:
  • NSTimeZone -isEqualToTimeZone:
  • NSValue -isEqualToValue:

思考五:重写的isEqual何时调用

  • 一、NSArray的containsObject:removeObject:方法都是使用了isEqual来判断成员是否相等的
  • 二、当hash方法设计不是很完美的时候,两个对象返回同样的hash值,就会调用isEqual继续判断是否为两个相同对象

思考六:为何须要hash

目的:为了提升查找的速度优化

  • 一、在数组未排序的状况下, 查找的时间复杂度是O(n)。
  • 二、当成员被加入到Hash Table中时, 会给它分配一个hash值, 以标识该成员在集合中的位置,经过这个位置标识能够将查找的时间复杂度优化到O(1), 固然若是多个成员都是同一个位置标识, 那么查找就不能达到O(1)了。
  • 三、分配的这个hash值(即用于查找集合中成员的位置标识), 就是经过hash方法计算得来的, 且hash方法返回的hash值最好惟一

和数组相比, 基于hash值索引的Hash Table查找某个成员的过程就是ui

  • Step 1: 经过hash值直接找到查找目标的位置
  • Step 2: 若是目标位置上有多个相同hash值得成员, 此时再按照数组方式进行查找

思考七:hash何时调用

HashTable是一种基本数据结构,NSSet和NSDictionary都是使用HashTable存储数据的,所以能够能够确保他们查询成员的速度为O(1)。而NSArray使用了顺序表存储数据,查询数据的时间复杂度为O(n)。atom

  • 一、hash方法会在对象被添加到NSSet中的时候调用
Coder* coder1 = [Coder initWith:@"C++" level:@"11"];
Coder* coder2 = [Coder initWith:@"C++" level:@"11"];
Coder* coder3 = [Coder initWith:@"C++" level:@"17"];
NSSet* coderSet = [NSSet setWithObjects:coder1, coder2, coder3, nil];
NSLog(@"coderSet.count = %ld", coderSet.count);
复制代码
  • 二、hash方法会在对象被用做NSDictionary的key的时候调用
Coder* coder1 = [Coder initWith:@"C++" level:@"11"];
Coder* coder2 = [Coder initWith:@"C++" level:@"11"];
Coder* coder3 = [Coder initWith:@"C++" level:@"17"];
NSMutableDictionary* coderDic2 = [NSMutableDictionary dictionary];
[coderDic2 setObject:@"1" forKey:coder1];
[coderDic2 setObject:@"2" forKey:coder2];
[coderDic2 setObject:@"3" forKey:coder3];
NSLog(@"coderDic2.count = %ld", coderDic2.count);
复制代码

思考八:hash和isEqual的关系

  • 一、若是两个对象相等,那么他们hash值必定相等
  • 二、若是两个对象hash值相等(hash算法不完美致使),他们不必定相等,还要继续经过isEqual进行判断是否真的相等

思考九:重写hash的原则

  • hash 方法不能返回一个常量。由于使用了这个值做为 hash 表的 key,会致使 hash 表 100%的碰撞。
  • 一个对象实例的 hash 计算结果应该是肯定的。hash方法设计的好坏直接影响查找的效率。

贴代码

.h文件spa

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Coder : NSObject<NSCopying>
@property (nonatomic, strong) NSString *language;
@property (nonatomic, strong) NSString *level;
+(instancetype)initWith:(NSString*)language level:(NSString*)level;
@end

NS_ASSUME_NONNULL_END
复制代码

.m文件

#import "Coder.h"

@implementation Coder
+(instancetype)initWith:(NSString*)language level:(NSString*)level {
    Coder* coder = [[Coder alloc] initWith:language level:level];
    return coder;
}
-(instancetype)initWith:(NSString*)language level:(NSString*)level {
    self.language = language;
    self.level = level;
    return self;
}
// 对象用做NSSDictionary的key必须实现
-(id)copyWithZone:(NSZone *)zone {
    Coder* coder = [[Coder allocWithZone:zone] initWith:self.language level:self.level];
    return coder;
}
-(BOOL)isEqual:(id)object {
    NSLog(@"func = %s", __func__);
    if (self == object) {
        return YES;
    }
    if (![object isKindOfClass:[self class]]) {
        // object == nil 在此返回NO
        return NO;
    }
    return [self isEqualToCoder:object];
}
-(BOOL)isEqualToCoder:(Coder*)object {
    // isEqualToString 须要使用分类重写一下,不然 [nil isEqualToString:nil]会返回NO
    if (![self.language isEqualToString:object.language]) {
        return NO;
    }
    if (![self.level isEqualToString:object.level]) {
        return NO;
    }
    return YES;
}
-(NSUInteger)hash {
    BOOL isCareAddress = YES;
    NSUInteger hashValue = 0;
    if (isCareAddress) {
        // 若是指望对地址不一样、可是内容相同的对象作区分
        hashValue = [super hash];
        // 结果:两个地址不一样,可是内容相同的对象添加到NSMutableSet中,NSMutableSet的个数返回的是2
    }
    else {
        // 不关心地址是否相同,只对内容进行区分(对关键属性的hash值进行按位异或运算做为hash值)
        hashValue = [self.language hash] ^ [self.level hash];
        // 结果:两个地址不一样,可是内容相同的对象添加到NSMutableSet中,NSMutableSet的个数返回的是1
    }
    NSLog(@"func = %s, hashValue = %ld", __func__, hashValue);
    return hashValue;
}
@end
复制代码

调用

#import "HashViewController.h"
#import "Coder.h"

@interface HashViewController ()
@end

@implementation HashViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Coder* coder1 = [Coder initWith:@"C++" level:@"11"];
    Coder* coder2 = [Coder initWith:@"C++" level:@"11"];
    Coder* coder3 = [Coder initWith:@"C++" level:@"17"];

    NSArray* coderList = @[coder1, coder2, coder3];
    NSLog(@"array-containsObject-coder1-start");
    [coderList containsObject:coder1];
    NSLog(@"array-containsObject-coder1-end");
    NSLog(@"array-containsObject-coder2-start");
    [coderList containsObject:coder2];
    NSLog(@"array-containsObject-coder2-end");
    NSLog(@"array-containsObject-coder3-start");
    [coderList containsObject:coder3];
    NSLog(@"array-containsObject-coder3-end");
    NSLog(@"coderList.count = %ld", coderList.count);

    NSSet* coderSet = [NSSet setWithObjects:coder1, coder2, coder3, nil];
    NSLog(@"coderSet.count = %ld", coderSet.count);

    NSDictionary* coderDic = @{@"1":coder1, @"2":coder2, @"3":coder3};
    NSLog(@"coderDic.count = %ld", coderDic.count);
    
    NSMutableDictionary* coderDic2 = [NSMutableDictionary dictionary];
    [coderDic2 setObject:@"1" forKey:coder1];
    [coderDic2 setObject:@"2" forKey:coder2];
    [coderDic2 setObject:@"3" forKey:coder3];
    NSLog(@"coderDic2.count = %ld", coderDic2.count);
}

@end
复制代码

参考博客:

Objective-C -- isEqual与hash

禅与 Objective-C 编程艺术

相关文章
相关标签/搜索