1,nil的简单定义
你可能还不知道nil在Objective-C中是什么意思。为了避免混乱,先简单定义一下nil:
nil表示一个对象指针不指向任何对象时的值。
如果你熟悉其它源自C语言的语言,你可能会问:NULL和nil是一样的吗?
答案是基本一样。NULL可以用在C语言的各种指针上,而nil用在Objective-C的对象指针上。如果你在xcode上右击nil,查看定义,你会发现:nil、NULL和__DARWIN_NULL全都定义成一个值,那就是(void *)0。
Objective-C还定义了一个常量Nil。注意这个N是大写的。它的值和nil是一样的。它用于类指针。但在实际使用的时候,都使用nil就可以了。
2,初始化时,任何东西都是0
在Objective-C语言中,新alloc出来的对象的实例变量(除了isa)会被设置成0。这与C语言的calloc比较像。
这意味着一个对象中所有指向其它对象的指针已经被设置成nil了。所以可以不设特意将实例变量初始化为nil,false,0什么的。
3,可以向nil发送消息
在Objective-C中,你可以向nil发送任何消息。这一点和C/C++不太一样,如果向NULL发送消息的话,程序就会崩溃了。向nil发送消息什么也不会做,返回值会是0。因此下面的代码:
if(myObject==nil|| ![myObjectisEqualTo:anotherObject]){
...
}
可以简写成:
if(![myObject isEqualTo:anotherObject]){
...
}
向nil发送消息有一些缺点:
1,当期待的返回值类型的大小,小于或等于一个pointer的大小时,是不会有问题。或者返回类型是long long、double、或者是struct,也没有问题。对long long、double、和struct的支持是从mac os 10.5开始的。
2,有时候nil在代码中可能表示一个特殊的情况。
3,nil有时会让程序debug变得困难。本来一个变量不应该是nil。这样的错误很难被发现。
/##########################################################################/
补充:
向nil发送消息 在Objective-C中向nil发送消息是完全有效的——只是在运行时不会有任何作用。Cocoa中的几种模式就利用到了这一点。发向nil的消息的返回值也可以是有效的:
• 如果一个方法返回值是一个对象,那么发送给nil的消息将返回0(nil)。例如:Person * motherInlaw = [ aPerson spouse] mother]; 如果spouse对象为nil,那么发送给nil的消息mother也将返回nil。
• 如果方法返回值为指针类型,其指针大小为小于或者等于sizeof(void*),float,double,long double 或者long long的整型标量,发送给nil的消息将返回0。
• 如果方法返回值为结构体,正如在《Mac OS X ABI 函数调用指南》,发送给nil的消息将返回0。结构体中各个字段的值将都是0。其他的结构体数据类型将不是用0填充的。
• 如果方法的返回值不是上述提到的几种情况,那么发送给nil的消息的返回值将是未定义的。
既然给空指针发送消息不会崩溃,那么我们是否还有必要在发消息前判断一下指针是否为空?
在知乎上有人问到说,斯坦福课程里推荐不用判断 nil。但是有时候不判断 nil 又会导致程序崩溃,例如往 NSArray 里插入一个 nil 的情况。
以下是我的回答:
不管斯坦福怎么说,我的建议是如果这个指针可能为空,那么用之前都做一下判断。
为什么这么建议呢?
首先,最实际的理由是,给空指针发消息是非常慢的,而做一下 if 判断是否为空是非常快的操作。
这个速度上的差别是百倍这个单位的。
为什么呢?因为给 nil 发消息这件事情是在运行时判断的,而系统也并非简单的丢掉这个消息,系统还需要判断这个消息是否需要返回值,如果需要,还要判断返回值的类型:如果返回指针、数字、真伪等,则返回 0,如果返回的是 OS 定义过的数据结构,那么会返回一个被填充上 0 的结构回来,如果返回的是其它类型,则系统会返回一个未定义的类型回来。而一个 if,则能够直接跳过这些运行时的处理。
我做过一个实验:
分别是两个100万次的循环,其中一个给 nil 对象发送了 100万次消息,另外一个循环在发消息前,会判断一下该对象是否为空。
实验的结果是两个循环的执行时间相差了200多倍。这还是消息中没有传任何参数的情况。
这就好像我们不建议用 try cache 来做本可以用 if else 判断掉的错误处理一样,因为开销太大。
另外,“给空指针发消息不会崩溃”是语言特性,是为了防止人为失误导致程序崩溃的“保险”机制。我们写代码时应该尽量避免依赖语言特性的写法,因为这不利于代码(逻辑)移植,语言特性本身也是会变化的(哪怕变化的可能性为零)。
最后,这能养成一个好习惯,培养出一个好的思维方式。上述的这种遇到“错误”也让程序继续跑下去的语言特性本身就是有争议的,虽然给 nil 发消息不会崩溃,但也正是由于程序不会崩溃,当你的代码写错时你也无法察觉到,你的程序有可能会进入一个你完全没有预期到的状态,看似一切正常,但返回的结果却是完全错误的。而写代码时花1秒钟加一个简单的 if 判断,就能够清楚地看出哪个指针是可能为空的,这些信息在分析代码问题的时候是非常宝贵的。
/##########################################################################/
4,NSNull
NSNull对象是nil的代替者。主要用在使用一些容器类的时候使用,比如NSArray,NSSet,NSDictionary。因为这些容器类是不能包含nil的。
你可以试着将一个NSNull对象插入到自己的容器对象中。但是,它的真正用途是cocoa自动为你添加时,看看下面的例子:
Objective-C
1 |
NSArray *namesOfObjects=[arrayOfObjects valueForKey:@"name"]; |
valueForKey:方法使用KVC将arrayOfObjects 这个数组中每个对象的”name”取出,生成一个NSArray。如果所有的name属性都是nil,就会返回一个NSNull的对象。
注意:最好测试一下是不是返回NSNull。NSSet使用KVC的时候就不会创建NSNull对象。
5,设置NSMutableDictionary的值为nil
NSArray *namesOfObjects=[arrayOfObjectsvalueForKey:@"name"];
上面的代码会抛出异常。如果你要将一些对象插入到NSMutableDictionary当中的话,又不知道这些对象中有没有nil。所以代码要像下面这样来写:
NSMutableDictionary *newDictionary=[NSMutableDictionary dictionary];
id firstObject=[source getFirstObject];
if(firstObject)
{
[newDictionary setObject:firstObject forKey:@"firstObjectKey"];
}
// ... and so on for all other objects
如果key是String的,可以使用NSMutableDictionary的KVC方法,这样写就可以了。
NSMutableDictionary *newDictionary=[NSMutableDictionary dictionary];
[newDictionary setValue:[source getFirstObject] forKey:@"firstObjectKey"];
// ... and so on for all other objects
注意用setValue:forKey:代替setObject:forKey:方法,setValue:forKey:方法首先先测试value,看是不是nil。如果是nil,就会像调用了removeObjectForKey:了一样。如果dictionary里没有对象,那就什么也不做。
KVC需要string的键,所以如果一个dictionary的键不是string的,上面的方法就不行了。