做 ios 開發,NSDictionary、NSMutableDictionary,NSMutableArray、NSArray 都是很常用的容器類
Array 就不多做討論了,今天的文章主要討論 NSDictionary 和 NSMutableDictionary~
以往我用 cocoa 的 Dictionary 的時候,都是選擇用 NSString 來作為鍵對象的類型。
一直都沒有出什麼大的問題,用起來很順手~
不巧昨天工作室中的另外一個成員說用 NSString 做鍵類型很業余,拼接啊、解析啊什麼的,業余!!
其實我一直都覺得用字符串拼接解析這種模式還不錯的,不過後來聽到對方的舉證,也被說服了。
對方的主要觀念就用自定義的對象類型來作為字典的鍵類型,
我被說服的原因很簡單,自定義的對象類型確實更接近面向對象的思考模式~
好,我承認我被說服了,不過因為我手頭上也有 一些事情,所以並沒有在這個問題上面做編碼試驗~
我認為cocoa 的字典應該像支持 NSString 那樣,對用戶自定義的鍵對象類型提供良好的支持。
但事實就是這麼打擊人,當然,最先被打擊的不是我,是那個提出自定義鍵對象類型的伙計。
他遇到的第一個問題就是,從 NSObject 擴展出來的一個類,只包含兩個int 型的屬性
然後,用這個類作為字典的鍵的時候,竟然在 setObject: forKey:方法調用的時候,
報出 unrecognized selector "copyWithZone:" 的錯誤~
思忖良久外加google一番,終於發現原來是要在擴展類中將 copyWithZone 這個方法顯式的重寫一下~
(copyWithZone 這個方法在 NSObject 裡面有所定義,報錯就是因為該類型如果要作為字典的鍵就必須要實現這個方法)~
當然,問題如果這麼輕易就被干掉了,那我也就沒必要寫博文記載下來了~
字典的鍵最基本的要求就是不能重復。。
根據我做的編碼測試,copyWithZone 是將 鍵對象參數克隆一份,放置在字典對象裡面~
但事先要做一些檢查,檢查新加入的鍵和已存在的鍵們是否重復,如果重復的話,就覆蓋所設置的值或者忽略這一次添加~
不過怎麼樣,就是字典不允許存在兩個一模一樣的鍵,否則檢索值的時候會發生矛盾~
檢查是否重復主要集中在 NSObject 基礎類的兩個方法上面:
-(NSUInteger) hash;
-(BOOL) isEqual:(id)object;
hash相當於對一團數據做 md5 摘要,得出他的數字簽名,大意就是兩團數據只要有一丁點兒的不一樣
返回的那個無符號整數的值都不會相同(那是完美的哈希算法,現實生活中只要保證很大幾率下面不同的數據返回不盡相同的返回值就行了)
據我推測,檢查新添加的鍵和已存在的鍵是否重復包含兩步:
第一步是對比hash,如果hash都不相同的話,判定為兩個鍵不同,將第二步省略掉;
第二部是在 hash 相等的基礎上才會執行的,hash相等後,再用 isEqual 方法來判定兩個鍵是否相等~
(此相等不是說對象的內存地址相等,而是對象中所包含的數據是否相等)~
綜上,如果要自定義字典鍵對象的類型,要重寫下面的幾個方法:
1。copyWithZone:這個是必須重寫的,否則直接報找不到方法的錯誤
2。hash:這個你可以不重寫,但是你盡可以試一下,蛋疼死你~
3。isEqual: 這個方法必須要重寫一下,你不重寫的話,默認的實現就是對比兩個對象的內存地址
只有在兩個對象是同一個對象的時候,才會返回 YES。
當然這個不是我們所需要的,我們需要的就是直接構造一個新的鍵對象,只要這個新構造的鍵對象中所包含的數據
與字典中的鍵相一致了,就取出字典中那個鍵對象所對應著的值對象~
下面上代碼(建一個 mac command Line Project,將下面的幾個類文件拽進去),
給出一個典型的能夠直接拿來作為字典鍵對象的類型定義:
KeyObject.h
[cpp] //
// KeyObject.h
// DictionaryKeyObject
//
// Created by BruceYang on 12-7-29.
// Copyright (c) 2012年 EricGameStudio. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface KeyObject : NSObject {
int _x;
int _y;
}
@property int x;
@property int y;
+(id) kObjectWithX:(int)x y:(int)y;
-(id) initWithX:(int)x y:(int)y;
-(id) copyWithZone:(NSZone*)zone;
-(NSString*) description;
-(NSUInteger) hash;
-(BOOL) isEqual:(id)object;
@end
//
// KeyObject.h
// DictionaryKeyObject
//
// Created by BruceYang on 12-7-29.
// Copyright (c) 2012年 EricGameStudio. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface KeyObject : NSObject {
int _x;
int _y;
}
@property int x;
@property int y;
+(id) kObjectWithX:(int)x y:(int)y;
-(id) initWithX:(int)x y:(int)y;
-(id) copyWithZone:(NSZone*)zone;
-(NSString*) description;
-(NSUInteger) hash;
-(BOOL) isEqual:(id)object;
@end
KeyObject.m
[cpp]
//
// KeyObject.m
// DictionaryKeyObject
//
// Created by BruceYang on 12-7-29.
// Copyright (c) 2012年 EricGameStudio. All rights reserved.
//
#import "KeyObject.h"
@implementation KeyObject
@synthesize x = _x;
@synthesize y = _y;
+(id) kObjectWithX:(int)x y:(int)y {
return [[[self alloc] initWithX:x y:y] autorelease];
}
-(id) initWithX:(int)x y:(int)y {
if((self = [super init])){
_x = x;
_y = y;
}
return self;
}
/**
* 注意:
* 不能 autorelease!setObject:forKey: 的時候,key 對象的 retainCount 並不會加 1~
* autorelease 的話導致一脫離所在的域,鍵對象就失效,引發 EXEC_BAD_ACCESS 錯誤~
* 用 Instruments 做 allocation 測試,可以發現鍵對象一直是處於存活狀態的,這才是正常的情況~
*
* copyWithZone 是在 [NSMutableDictionary setObject:@"test" forKey:KeyObject對象] 方法調用的時候調用的~
* 大意就是將作為字典鍵的對象深拷貝一份放在字典對象裡面。
* 如果 KeyObject 作為字典的鍵對象來使用,使用完畢後需將鍵對象 release 掉,因為字典並不會使用這個對象,
* 而是會克隆出一個包含相同數據的對象來~
*
* 以上也是 setObject forKey 前後,為什麼鍵對象的 retainCount 依然為 1 的原因~
* (注:setObject forKey 前後, 值對象的 retainCount 會加 1~)
*/
-(id) copyWithZone:(NSZone*)zone {
NSLog(@"KeyObject.copyWithZone() 方法被調用~");
// 正確的寫法~
return [[KeyObject alloc] initWithX:_x y:_y];
// 錯誤的寫法~
// return [KeyObject kObjectWithX:_x y:_y];
}
-(NSString*) description {
return [NSString stringWithFormat:@"_x=%d, _y=%d", _x, _y];
}
-(NSUInteger) hash {
// NSLog(@"KeyObject.hash() 方法被調用~");
return 0;
}
-(BOOL) isEqual:(id)object {
// NSLog(@"KeyObject.isEqual() 方法被調用~");
if(![object isKindOfClass:[KeyObject class]]) {
// NSLog(@"Object passed in isn't a KeyObject instance!");
return NO;
}
KeyObject* tmp = (KeyObject*)object;
if(tmp.x == _x && tmp.y == _y) {
// NSLog(@"## Equals —— tmp.x=%d, tmp.y=%d, _x=%d, _y=%d", tmp.x, tmp.y, _x, _y);
return YES;
} else {
// NSLog(@"## Not Equals —— tmp.x=%d, tmp.y=%d, _x=%d, _y=%d", tmp.x, tmp.y, _x, _y);
return NO;
}
}
-(void) dealloc {
NSLog(@"%@%@", [self description], @" dealloc!");
[super dealloc];
}
@end
//
// KeyObject.m
// DictionaryKeyObject
//
// Created by BruceYang on 12-7-29.
// Copyright (c) 2012年 EricGameStudio. All rights reserved.
//
#import "KeyObject.h"
@implementation KeyObject
@synthesize x = _x;
@synthesize y = _y;
+(id) kObjectWithX:(int)x y:(int)y {
return [[[self alloc] initWithX:x y:y] autorelease];
}
-(id) initWithX:(int)x y:(int)y {
if((self = [super init])){
_x = x;
_y = y;
}
return self;
}
/**
* 注意:
* 不能 autorelease!setObject:forKey: 的時候,key 對象的 retainCount 並不會加 1~
* autorelease 的話導致一脫離所在的域,鍵對象就失效,引發 EXEC_BAD_ACCESS 錯誤~
* 用 Instruments 做 allocation 測試,可以發現鍵對象一直是處於存活狀態的,這才是正常的情況~
*
* copyWithZone 是在 [NSMutableDictionary setObject:@"test" forKey:KeyObject對象] 方法調用的時候調用的~
* 大意就是將作為字典鍵的對象深拷貝一份放在字典對象裡面。
* 如果 KeyObject 作為字典的鍵對象來使用,使用完畢後需將鍵對象 release 掉,因為字典並不會使用這個對象,
* 而是會克隆出一個包含相同數據的對象來~
*
* 以上也是 setObject forKey 前後,為什麼鍵對象的 retainCount 依然為 1 的原因~
* (注:setObject forKey 前後, 值對象的 retainCount 會加 1~)
*/
-(id) copyWithZone:(NSZone*)zone {
NSLog(@"KeyObject.copyWithZone() 方法被調用~");
// 正確的寫法~
return [[KeyObject alloc] initWithX:_x y:_y];
// 錯誤的寫法~
// return [KeyObject kObjectWithX:_x y:_y];
}
-(NSString*) description {
return [NSString stringWithFormat:@"_x=%d, _y=%d", _x, _y];
}
-(NSUInteger) hash {
// NSLog(@"KeyObject.hash() 方法被調用~");
return 0;
}
-(BOOL) isEqual:(id)object {
// NSLog(@"KeyObject.isEqual() 方法被調用~");
if(![object isKindOfClass:[KeyObject class]]) {
// NSLog(@"Object passed in isn't a KeyObject instance!");
return NO;
}
KeyObject* tmp = (KeyObject*)object;
if(tmp.x == _x && tmp.y == _y) {
// NSLog(@"## Equals —— tmp.x=%d, tmp.y=%d, _x=%d, _y=%d", tmp.x, tmp.y, _x, _y);
return YES;
} else {
// NSLog(@"## Not Equals —— tmp.x=%d, tmp.y=%d, _x=%d, _y=%d", tmp.x, tmp.y, _x, _y);
return NO;
}
}
-(void) dealloc {
NSLog(@"%@%@", [self description], @" dealloc!");
[super dealloc];
}
@end
下面是測試類
KeyTest.h
[cpp]
//
// KeyTest.h
// DictionaryKeyObject
//
// Created by BruceYang on 12-7-31.
// Copyright (c) 2012年 EricGameStudio. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "KeyObject.h"
@interface KeyTest : NSObject {
NSMutableDictionary* _dic;
}
-(id) init;
-(void) test;
-(void) add;
-(void) add1;
-(void) echo;
-(void) echo1;
-(void) search;
-(void) dealloc;
@end
//
// KeyTest.h
// DictionaryKeyObject
//
// Created by BruceYang on 12-7-31.
// Copyright (c) 2012年 EricGameStudio. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "KeyObject.h"
@interface KeyTest : NSObject {
NSMutableDictionary* _dic;
}
-(id) init;
-(void) test;
-(void) add;
-(void) add1;
-(void) echo;
-(void) echo1;
-(void) search;
-(void) dealloc;
@end
KeyTest.mm
[cpp]
//
// KeyTest.mm
// DictionaryKeyObject
//
// Created by BruceYang on 12-7-31.
// Copyright (c) 2012年 EricGameStudio. All rights reserved.
//
#import "KeyTest.h"
@implementation KeyTest
-(id) init {
if((self = [super init])) {
_dic = [[NSMutableDictionary alloc] init];
}
return self;
}
-(void) test {
NSMutableDictionary* dic = [[NSMutableDictionary alloc] init];
for(int i = 0; i < 5; ++ i) {
KeyObject* keyObject = [[KeyObject alloc] initWithX:i*5 y:i*3];
NSString* valueStr = [NSString stringWithFormat:@"Test%d", i];
/**
* 在調用 setObject:forKey 方法以後,
* key 對象的引用計數依然為 1,value 對象的引用計數增加為 2~
*
*/
NSLog(@"1.kRefCount = %lu, vRefCount = %lu", [keyObject retainCount], [valueStr retainCount]); // 1, 1~
/**
* 作為參數傳進來的 keyObject 不是 autorelease 的對象,所以要手動的釋放掉~
* 之所以要去手動釋放掉是因為字典對象已經將作為參數傳進來的 keyObject 鍵對象深拷貝了一份~
* 這也是之前提到的在調用字典對象的 setObject:forKey 方法後 keyObject 引用計數為什麼依然還是 1 的原因~
* 因為字典對象裡面的鍵對象並不是使用的作為參數傳進來的那個 keyObject ~
*/
[dic setObject:valueStr forKey:keyObject];
[keyObject release];
NSLog(@"2.KRefCount = %lu, vRefCount = %lu", [keyObject retainCount], [valueStr retainCount]); // 1, 2~
}
NSLog(@"\n---------------------- 分割線 -------------------------\n");
for(KeyObject* keyObject in dic) {
NSString* valueStr = [dic objectForKey:keyObject];
NSLog(@"ValueStrReferenceCount = %lu", [valueStr retainCount]);
NSLog(@"keyObject.x = %d, keyObject.y = %d, valueStr = %@", keyObject.x, keyObject.y, valueStr);
}
/**
* 引用計數為 1 的字典對象 release 一下之後引用計數依然為 1,何解?
* 比較合理的一種解釋:
* dealloc 之後的對象去取 retainCount 會返回 1 而非 0~
*/
NSLog(@"1.dicRefCount = %lu", [dic retainCount]); // 1~
[dic release];
NSLog(@"2.dicRefCount = %lu", [dic retainCount]); // 1~
}
-(void) add {
NSLog(@"add");
for(int i = 0; i < 5; ++ i) {
/**
* 因為是 autorelease 的鍵對象,所以使用完畢後不用再去手動釋放掉~
*/
KeyObject* keyObject = [KeyObject kObjectWithX:i*5 y:i*3];
NSString* valueStr = [NSString stringWithFormat:@"Test%d", i];
[_dic setObject:valueStr forKey:keyObject];
}
NSLog(@"_dic.count = %lu", [_dic count]);
}
-(void) add1 {
NSLog(@"add");
for(int i = 0; i < 5; ++ i) {
/**
* 非 autorelease 的鍵對象,使用完畢後要手動去 release 掉,效率較高(人為控制的銷毀時機是最佳的)~
*/
KeyObject* keyObject = [[KeyObject alloc] initWithX:i*5 y:i*3];
NSString* valueStr = [NSString stringWithFormat:@"Test%d", i];
[_dic setObject:valueStr forKey:keyObject];
[keyObject release];
}
NSLog(@"_dic.count = %lu", [_dic count]);
}
/**
* objective-c 字典常規的遍歷方式,先快速遍歷字典的鍵,然後用鍵找出對應的值~
*/
-(void) echo {
NSLog(@"echo");
NSLog(@"_dic.count = %lu", [_dic count]);
for(KeyObject* keyObject in _dic) {
NSString* valueStr = [_dic objectForKey:keyObject];
NSLog(@"keyObject.x = %d, keyObject.y = %d, valueStr = %@", keyObject.x, keyObject.y, valueStr);
}
}
/**
* 朋友問我怎麼打印出 NSMutableDictionary 中的內容,我不假思索地便回答:遍歷+打印啊~
* 然後,對方投來鄙視的一眼,“你就不知道重寫值對象的 -(NSString*) description;方法?”
* 頓時我就傻了~在 java 裡面沒少這麼干,在 objective-c 裡面竟然不知道舉一反三,實乃罪過~
*/
-(void) echo1 {
NSLog(@"echo1");
NSLog(@"_dic.count = %lu", [_dic count]);
NSLog(@"%@", _dic);
}
-(void) search {
NSLog(@" -------- search begin! --------");
KeyObject* k0 = [KeyObject kObjectWithX:5 y:3];
NSString* result0 = [_dic objectForKey:k0];
NSLog(@"結果為:%@", result0);
KeyObject* k1 = [KeyObject kObjectWithX:20 y:12];
NSString* result1 = [_dic objectForKey:k1];
NSLog(@"結果為:%@", result1);
NSLog(@" -------- search finish! --------");
}
-(void) dealloc {
[_dic release];
[super dealloc];
}
@end
//
// KeyTest.mm
// DictionaryKeyObject
//
// Created by BruceYang on 12-7-31.
// Copyright (c) 2012年 EricGameStudio. All rights reserved.
//
#import "KeyTest.h"
@implementation KeyTest
-(id) init {
if((self = [super init])) {
_dic = [[NSMutableDictionary alloc] init];
}
return self;
}
-(void) test {
NSMutableDictionary* dic = [[NSMutableDictionary alloc] init];
for(int i = 0; i < 5; ++ i) {
KeyObject* keyObject = [[KeyObject alloc] initWithX:i*5 y:i*3];
NSString* valueStr = [NSString stringWithFormat:@"Test%d", i];
/**
* 在調用 setObject:forKey 方法以後,
* key 對象的引用計數依然為 1,value 對象的引用計數增加為 2~
*
*/
NSLog(@"1.kRefCount = %lu, vRefCount = %lu", [keyObject retainCount], [valueStr retainCount]); // 1, 1~
/**
* 作為參數傳進來的 keyObject 不是 autorelease 的對象,所以要手動的釋放掉~
* 之所以要去手動釋放掉是因為字典對象已經將作為參數傳進來的 keyObject 鍵對象深拷貝了一份~
* 這也是之前提到的在調用字典對象的 setObject:forKey 方法後 keyObject 引用計數為什麼依然還是 1 的原因~
* 因為字典對象裡面的鍵對象並不是使用的作為參數傳進來的那個 keyObject ~
*/
[dic setObject:valueStr forKey:keyObject];
[keyObject release];
NSLog(@"2.KRefCount = %lu, vRefCount = %lu", [keyObject retainCount], [valueStr retainCount]); // 1, 2~
}
NSLog(@"\n---------------------- 分割線 -------------------------\n");
for(KeyObject* keyObject in dic) {
NSString* valueStr = [dic objectForKey:keyObject];
NSLog(@"ValueStrReferenceCount = %lu", [valueStr retainCount]);
NSLog(@"keyObject.x = %d, keyObject.y = %d, valueStr = %@", keyObject.x, keyObject.y, valueStr);
}
/**
* 引用計數為 1 的字典對象 release 一下之後引用計數依然為 1,何解?
* 比較合理的一種解釋:
* dealloc 之後的對象去取 retainCount 會返回 1 而非 0~
*/
NSLog(@"1.dicRefCount = %lu", [dic retainCount]); // 1~
[dic release];
NSLog(@"2.dicRefCount = %lu", [dic retainCount]); // 1~
}
-(void) add {
NSLog(@"add");
for(int i = 0; i < 5; ++ i) {
/**
* 因為是 autorelease 的鍵對象,所以使用完畢後不用再去手動釋放掉~
*/
KeyObject* keyObject = [KeyObject kObjectWithX:i*5 y:i*3];
NSString* valueStr = [NSString stringWithFormat:@"Test%d", i];
[_dic setObject:valueStr forKey:keyObject];
}
NSLog(@"_dic.count = %lu", [_dic count]);
}
-(void) add1 {
NSLog(@"add");
for(int i = 0; i < 5; ++ i) {
/**
* 非 autorelease 的鍵對象,使用完畢後要手動去 release 掉,效率較高(人為控制的銷毀時機是最佳的)~
*/
KeyObject* keyObject = [[KeyObject alloc] initWithX:i*5 y:i*3];
NSString* valueStr = [NSString stringWithFormat:@"Test%d", i];
[_dic setObject:valueStr forKey:keyObject];
[keyObject release];
}
NSLog(@"_dic.count = %lu", [_dic count]);
}
/**
* objective-c 字典常規的遍歷方式,先快速遍歷字典的鍵,然後用鍵找出對應的值~
*/
-(void) echo {
NSLog(@"echo");
NSLog(@"_dic.count = %lu", [_dic count]);
for(KeyObject* keyObject in _dic) {
NSString* valueStr = [_dic objectForKey:keyObject];
NSLog(@"keyObject.x = %d, keyObject.y = %d, valueStr = %@", keyObject.x, keyObject.y, valueStr);
}
}
/**
* 朋友問我怎麼打印出 NSMutableDictionary 中的內容,我不假思索地便回答:遍歷+打印啊~
* 然後,對方投來鄙視的一眼,“你就不知道重寫值對象的 -(NSString*) description;方法?”
* 頓時我就傻了~在 java 裡面沒少這麼干,在 objective-c 裡面竟然不知道舉一反三,實乃罪過~
*/
-(void) echo1 {
NSLog(@"echo1");
NSLog(@"_dic.count = %lu", [_dic count]);
NSLog(@"%@", _dic);
}
-(void) search {
NSLog(@" -------- search begin! --------");
KeyObject* k0 = [KeyObject kObjectWithX:5 y:3];
NSString* result0 = [_dic objectForKey:k0];
NSLog(@"結果為:%@", result0);
KeyObject* k1 = [KeyObject kObjectWithX:20 y:12];
NSString* result1 = [_dic objectForKey:k1];
NSLog(@"結果為:%@", result1);
NSLog(@" -------- search finish! --------");
}
-(void) dealloc {
[_dic release];
[super dealloc];
}
@end
最後是 main.m
[cpp]
//
// main.m
// DictionaryKeyObject
//
// Created by BruceYang on 12-7-31.
// Copyright (c) 2012年 EricGameStudio. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "KeyTest.h"
int main (int argc, const char * argv[]) {
// 自動釋放池~
@autoreleasepool {
KeyTest* kt = [[KeyTest alloc] init];
// [kt test];
[kt add1];
[kt echo1];
[kt echo];
[kt search];
}
return 0;
}