在Java中把一个自定义的类生成的对象作为HashTable的key是天生可行的,但是在Objective C中,NSDictionary的key使用的是copy方法,所以自定义的类必须要实现copyWithZone才可以。
假设现在有一个CustomClass:
@interface CustomClass : NSObject <NSCopying> @property (strong) NSString *name; @end
这个时候,如果不写copyWithZone,那么把它当作NSDictionary key时,就会报错。于是,需要实现copyWithZone方法:
- (id)copyWithZone:(NSZone *)zone { id aCopy = [[[self class] alloc]init]; if (aCopy) { [aCopy setName:[self.name copyWithZone:zone]]; } return aCopy; }
这样做之后,就可以当作key传到NSDictionary中去了。
但是这还没完,只实现了copyWithZone,只能保证放的进去,却没办法取出来。需要在CustomClass里面另外实现isEqual方法和hash方法才能保证取出来。
比如说我这里定义如果两个obj的name一样就算equal了:
//这是一个错误的设计 - (BOOL)isEqual:(id)object { if ([object isKindOfClass:self.class] && [((CustomClass *)object).name isEqualToString:self.name]) { return YES; } return NO; }
如果这个时候我不重写hash方法,那么用这样的Class生成的object作为key的时候就会出逻辑上的问题:
CustomClass *a = [[CustomClass alloc]init]; CustomClass *b = [[CustomClass alloc]init]; a.name = @"a"; b.name = @"a"; NSMutableDictionary *dict = [[NSMutableDictionary alloc]init]; [dict setObject:@"aaa" forKey:a]; [dict setObject:@"bbb" forKey:b]; NSLog(@"%@",[dict objectForKey:b]); NSLog(@"dict count: %ld",dict.count);
当两个对象的name是一样时,这个打印出来会是aaa,而且count是1。这是因为NSDictionary在找value的时候,是通过hash值和isEqual共同计算出来的。默认的hash算法会使得a和b返回的hash是一样,而isEqual又是一样的,那么a和b实际上就是相同的key了。
虽然不知道apple的NSDictionary具体是怎么工作的,但是通过GNUStep的源码可以知道,它的内部工作原理。它使用了如下的数据结构作为Map
/* This is the map C - array of the buckets * +---------------+ +---------------+ * | _GSIMapTable | /----->| nodeCount | * |---------------| / | firstNode ----+--\ * | buckets ---+----/ | .......... | | * | bucketCount =| size of --> | nodeCount | | * | nodeChunks ---+--\ | firstNode | | * | chunkCount =-+\ | | . | | * | .... || | | . | | * +---------------+| | | nodeCount | | * | | | fistNode | | * / | +---------------+ | * ---------- v v * / +----------+ +---------------------------+ * | | * ------+----->| Node1 | Node2 | Node3 ... | a chunk * chunkCount | * ------+--\ +---------------------------+ * is size of = | . | \ +-------------------------------+ * | . | ->| Node n | Node n + 1 | ... | another * +----------+ +-------------------------------+ * array pointing * to the chunks */
key的hash方法返回的值被用来计算bucket所在的位置(通过mod计算),计算出bucket之后,就从node1开始遍历,直到[nodeX->key isEqual: key]为止,再取出nodeX->value。
基于上述要求,我想到一个比较山寨的解决办法:
加入一个私有的@property (unsafe_unretained) NSUInteger myHash;
当init时,将myHash的值改为(NSUInteger)self;
在copyWithZone时,将拷贝出来的对象的myHash改成原先的myHash。
代码:
@interface CustomClass() @property (unsafe_unretained) NSUInteger myHash; @end @implementation CustomClass - (id)copyWithZone:(NSZone *)zone { id aCopy = [[[self class] alloc]init]; if (aCopy) { [aCopy setName:[self.name copyWithZone:zone]]; [aCopy setMyHash:self.myHash]; } return aCopy; } - (id)init { if (self = [super init]) { _myHash = (NSUInteger)self; } return self; } - (NSUInteger)hash { return _myHash; } - (BOOL)isEqual:(id)object { return self.myHash == ((CustomClass *)object).myHash; } @end