objective-c 自定义 NSDictionary 键类的注意事项

做 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

//
//  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

//
//  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

//
//  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

//
//  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

//
//  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;
}
工程文件的下载地址:

上传中,稍后奉上......

http://download.csdn.net/detail/yang3wei/4465769

csdn 上传资源的功能模块有所进步,以前传资源的时候不等半个小时,新上传的资源根本就瞧不见~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值