NSNull之研究
为什么会有NSNull?
Objective-C是C的一个超集,主要引入了OO的设计理念。所以,Objective-C不可避免地使用指针以及指针变量来描述一个对象的内存地址。那么,既然存在指针这种东西,当然就允许存在NULL指针,也就是空指针。
另外,Objective-C主要定义了两种容器:NSArray和NSDictionary,并且规定了这两种容器中都不能放置nil指针,只能存放NSObject对象。那么就存在有些场景确实需要存nil指针,可能是出于保证count的正确性的目的。所以,在需要存放nil指针的位置就存放了NSNull对象,用来表示空对象,除此之外无任何实际意义。这也就是为什么Cocoa需要设计NSNull的原因。
NSNull带来的问题
NSNull虽然可以保证容器count的正确性,但是对于我们的业务来说却是一种麻烦。虽然NSArray和NSDictionary里皆可存放不同类型的对象,通常使用时存放的都为同一类型的对象,这样我们便可以直接使用foreach遍历容器。但是,NSNull给我们带来了问题,我们不确定容器里是否包含NSNull对象,使用每个容器时都需要过滤一下里边包含的NSNull对象,如果不加处理地假定容器中未包含NSNull,则很可能带来crash。
sqlite3中定义的NULL类型,以及从服务端传来的json数据,都会生成一个对应的NSNull对象。甚至kvo的使用,导致NSNull对象遍布系统中。
一种替代NSNull的技术
如果每次使用容器时都做过滤NSNull的操作,可能会带来一下几个问题:
- 额外的计算量。
- 冗余的代码。
- 代码略显奇怪。
回想一下,最常使用的也就两种数据:数字和字符串,对应Objective-C中的NSNumber和NSString。并且Objective-C作为一种动态绑定的语言,也就是我们常说的Runtime Binding。在Objective-C中每一个method都包含一个implementation和selector,Runtime根据每个对象接收的selector通过Objective-C类对象模型找到对应的implementation。Objective-C的Runtime提供一种Forwarding技术,可以将该对象无法处理的消息转发给某个可以响应这个消息的对象,如果最终无法找到一个可以响应该消息的对象,那么将抛出一个NSInvalidArgumentException。利用这种机制,我们便可根据接收的消息返回一个可以响应该消息的默认对象。比如,接收charValue消息便可返回一个@0对象,而接收intValue消息即可返回一个@“”对象。code 如下:
- (id)forwardingTargetForSelector:(SEL)aSelector {
if ([NSString instancesRespondToSelector:aSelector]) {
return @"";
}else if ([NSNumber instancesRespondToSelector:aSelector]) {
return @0;
}else {
return nil;
}
}
根据这种技术,设计了GNGeneralNullValue类,它可以自动地替换系统定义的NSNull,所有发往NSNull类生产的对象的消息都将发送给GNGeneralNullValue类所生产的对象,然后根据接收的消息返回一个可以响应的具有默认值的对象。同时,GNGeneralNullValue还支持添加用户自定义的类,除了基本的数据类型。根据所写的单元测试代码,可以一窥其使用方式。
- (void)test_NSNumber_charValue_shouldResponse {
NSNumber *number = (NSNumber *)[GNGeneralNullValue
generalNullValue];
XCTAssertEqual(0, [number charValue]);
}
- (void)test_NSNumber_floatValue_shouldResponse {
NSNumber *number = (NSNumber *)[GNGeneralNullValue
generalNullValue];
XCTAssertEqual(0.0f, [number floatValue]);
}
其具体的实现和源码地址为GNGeneralNullValue,欢迎反馈问题和交流。其使用非常方便方便,只需包含源码,无需其他操作即可享受远离NSNull的生活。