首先,OC中向nil发消息,程序是不会崩溃的。
因为OC的函数调用都是通过objc_msgSend进行消息发送来实现的,相对于C和C++来说,对于空指针的操作会引起Crash的问题,而objc_msgSend会通过判断self来决定是否发送消息,如果self为nil,那么selector也会为空,直接返回,所以不会出现问题。视方法返回值,向nil发消息可能会返回nil(返回值为对象)、0(返回值为一些基础数据类型)或0X0(返回值为id)等。但是对[NSNull
null]对象发送消息时,是会crash的,因为这个NSNull类只有一个null方法。
当然,如果一个对象已经被释放了(引用计数为0了),那么这个时候再去调用方法肯定是会Crash的,因为这个时候这个对象就是一个野指针(指向僵尸对象(对象的引用计数为0,指针指向的内存已经不可用)的指针)了,安全的做法是释放后将对象重新置为nil,使它成为一个空指针,大家可以在关闭ARC后手动release对象验证一下。
nil的定义是null pointer to object-c object,指的是一个OC对象指针为空,本质就是(id)0,是OC对象的字面0值
不过这里有必要提一点就是OC中给空指针发消息不会崩溃的语言特性,原因是OC的函数调用都是通过objc_msgSend进行消息发送来实现的,相对于C和C++来说,对于空指针的操作会引起Crash的问题,而objc_msgSend会通过判断self来决定是否发送消息,如果self为nil,那么selector也会为空,直接返回,所以不会出现问题。
这里补充一点,如果一个对象已经被释放了,那么这个时候再去调用方法肯定是会Crash的,因为这个时候这个对象就是一个野指针了,安全的做法是释放后将对象重新置为nil,使它成为一个空指针,大家可以在关闭ARC后手动release对象验证一下。
NSString *name = @"Allen";
if (name != nil && [name isEqualToString:@"Allen"]) {
NSLog(@"name: %@", name);
} else {
NSLog(@"name is nil");
}
//or
if ([name isEqualToString:@"Allen"]) {
NSLog(@"name: %@", name);
} else {
NSLog(@"name is nil");
}
上面的两种判断都是正确的,我们不必担心当name为nil时调用isEqualToString会出现Crash,但是我还是想说,在使用一个对象之前判断它是否为nil是一个很好的习惯,个人觉得有两个原因:
- 降低时间复杂度(感觉可以这么说吧),如果你增加了nil的判断,那么不需要对空指针发送消息了,发消息其实是件费时的操作。详情可以看这里
- 把判断为空养成习惯其实是好事,这样在你切换语言时也不容易出错。
Nil的定义是null pointer to object-c class,指的是一个类指针为空。本质就是(class)0,OC类的字面零值。
Class class = [NSString class];
if (class != Nil) {
NSLog(@"class name: %@", class);
}
NULL的定义是null pointer to primitive type or absence of data,指的是一般的基础数据类型为空,可以给任意的指针赋值。本质就是(void *)0,是C指针的字面0值。
NSInteger *pointerA = NULL;
NSInteger pointerB = 10;
pointerA = &pointerB;
NSLog(@”%ld”, *pointerA);
我们要尽量不去将NULL初始化OC对象,可能会产生一些异常的错误,要使用nil,NULL主要针对基础数据类型。
NSNull好像没有什么具体的定义(懵),它包含了唯一一个方法+(NSNull*)null,[NSNull null]是一个对象,用来表示零值的单独的对象。
NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init];
NSString *nameOne = @”Allen”;
NSString *nameTwo = [NSNull null]; // 不用使用 nil,nil在字典,数组中有特殊含义–元素结束标记
NSString *nameThree = @"Tom";
[dictionary setObject:nameOne forKey:@"nameOne"];
[dictionary setObject:nameTwo forKey:@"nameTwo"];
[dictionary setObject:nameThree forKey:@"nameThree"];
NSLog(@"names: %@", dictionary);
NSMutableArray *array = [[NSMutableArray alloc] init];
[array addObject:nameOne];
[array addObject:nameTwo];
[array addObject:nameThree];
NSLog(@"names : %@", array);
NSNull主要用在不能使用nil的场景下,比如NSMutableArray是以nil作为数组结尾判断的,所以如果想插入一个空的对象就不能使用nil,NSMutableDictionary也是类似,我们不能使用nil作为一个object,而要使用NSNull
以上转自:
文/北辰明(作者)
原文链接:http://www.jianshu.com/p/2ea9c3f737ea
向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的消息的返回值将是未定义的。
Objective-C 是以 C 语言为基础的,PC 上,在 C 语言中对空指针进行操作,程序会由于越界访问而出现保护错进而崩溃,但是 Objective-C 中为什么不会崩溃呢?
原因需要从源代码中寻找,
下面是 objc_msgSend 的 arm 版汇编代码片段:
在 arm 的函数调用过程中,一般用 r0-r4 传递参数,用 r0 传递返回值。对应 objc_msgSend,第一个参数为 self,返回值也是 self,都放在 r0(a1)中。
/********************************************************************
* idobjc_msgSend(idself, SELop, ...)
* On entry: a1 is the message receiver,
* a2 is the selector
* ********************************************************************/
ENTRY objc_msgSend
# check whether receiver is nil
teq a1, #0
moveq a2, #0
bxeq lr
teq 指令说明:
TEQ Rn, Operand2 The TEQ instruction performs a bitwise Exclusive OR
operation on the value in Rn and the value of Operand2.
测试 self 是否为空。
moveq 指令说明:
如果self为空,则将 selector 也设置为空。
bx 指令说明:
在 arm 中 bx lr 用来返回到调用子程序的地方(即:返回到调用者),此处是:如果 self 为空,就返回到调用objc_msgSend 的地方继续执行。
总之:
如果传递给 objc_msgSend 的 self 参数是 nil,该函数不会执行有意义的操作,直接返回。
理解一下nil,NULL和[NSNull null]的区别:
nil用来给对象赋值(Objective-C中的任何对象都属于id类型),NULL则给任何指针赋值,NULL和nil不能互换,nil用于类指针赋值(在Objective-C中类也是一个对象,是类的meta-class的实例,有关meta-class参见资料【译】Objective-C 中的 Meta-class 是什么?), 而NSNull则用于集合操作,用在数组或字典中要添加某个内容为空的情况。
虽然它们表示的都是空值,但使用的场合完全不同。所以在编码时严格按照变量类型来赋值,将正确的空值赋给正确的类型,使代码易于阅读和维护,也不易引起错误。
示例如下:
id object = nil;
// 判断对象不为空
if (object) {
}
// 判断对象为空
if (object == nil) {
}
// 数组初始化,空值结束
NSArray *array = [[NSArray alloc] initWithObjects:@"First", @"Second", nil];
// 判断数组元素是否为空
NSString *element = [array objectAtIndex:2];
if ((NSNull *)element == [NSNull null]) {
}
//做项目时遇到,要判断数组元素是否为空,以下写法,都无效
if(!element)
if([element length]>0)
if(element == NULL)
if(element == nil)
// 判断字典对象的元素是否为空
NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:
@"iPhone", @"First", @"iPad", @"Second", nil];
NSString *value = [dictionary objectForKey:@"First"];
if ((NSNull *)value == [NSNull null]) {
}
总结一下:
1. nil:一般赋值给空对象;
2. NULL:一般赋值给nil之外的其他空值。如SEL等,如:
[NSApp beginSheet:sheet modalForWindow:mainWindow
modalDelegate:nil // 使用nil指向一个空对象
didEndSelector:NULL // 指向空
contextInfo:NULL];
3. 当向nil发送消息时,不会有异常,程序将继续执行下去;
4. 而向NSNull的对象发送消息时会收到异常,因为NSNull只有一个方法:+ (NSNull *) null;
,会“没有这样的selector”的错误。
[NSNull null]是一个对象,他用在不能使用nil的场合。因为在NSArray和NSDictionary中nil有特殊的含义。但是在有些时候,确实需要用到这样的空值,比如在字典中,电话簿中”Jack”关键字下有电话号码、家庭住址、Email等等信息,但是现在只知道他的电话号码,这种不知道其他信息的情况下为了消除一些歧义,有必要将它们设置为空,所以Cocoa提供了NSNull类。
[dictionary setObject:[NSNull null], forKey:"Email"];
if(EmailAdress == [NSNull null])
{
//to do something...
}
因为Object-C的集合对象,如NSArray、NSDictionary、NSSet等,都有可能包含NSNull对象,所以,如果以下代码中的item为NSNull,则会引起程序崩溃:
NSString *item=[NSArray objectAtIndex:i];
if([item isEqualToString:@"TestNumber"]) {
// do someThing
}
// 以下代码是常见的错误,release对象没有设置为nil,从而引起程序崩溃。
id someObject=[[Object alloc] init];
//...
[someObject release];
//...
if(someObject) {
//crash here
}
上面会发生常见的“EXC_BAD_ACCESS”错误,也就是野指针错误。因为someObject指针指向的那块内存的引用计数已经为0了,所以那块内存已经不可以访问了,但是someObject指针并没有设为nil,所以会报野指针错误,那块内存地址中的僵尸对象已经无法使用。
一个可以研究一下的问题:
// 下面dealloc方法中,三句话的区别
-(void) dealloc {
self.test = nil;
[_test release];
test = nil;
}
最简单的:[_test release];,这句话就是将对象的引用计数减1,所谓引用计数就是看有多少个指针(强引用)指向某个内存实体。当release一次,对应的指针就减少一个,release到0时,表示这块内存真正归还给系统了。
其次,self.test = nil;,这句话牵扯到属性的getter和setter方法,在使用MRC(手动引用计数)的时候,方法如下:
// 属性的setter方法
- (void)setTest:(NSString *)newString {
// 如果新值跟旧值一样就不用赋值了
if (_test != newString) {
// 新值和旧值不一样时,由于现在对旧值有强引用,需要先引用计数减1
[_test release];
// 然后将新值赋给旧值,而且不能直接_test = newString,这种方式是浅拷贝,只是把指针地址赋值过去了,对应那块内存的引用计数并没有变化,这样就会导致两个指针指向了一块引用计数为1的内存空间,有引起野指针的潜在风险,那么为了避免这个问题,就需要进行retain,使引用计数加1
_test = [newString retain];
}
}
// 属性的get方法
- (NSSString *)test {
return _test;
}
所以self.test = nil;这句话就变成如下:
if (_test != nil) {
[_test release];
_test = [nil retain];
}
所做的事情就是:retain nil对象。在这之前已经先release了旧的对象,这个方法优点是成员变量连指向随机数据的机会都没有,而通过别的方式,就可能会出现指向随机数据的情况。当release了之后,万一有别的方法要用要存取它,如果它已经dealloc了,可能就会crash,而指向nil之后,就不会发生错误了。nil说白了就是计数器为0,这么说吧,当真正release一个对象的时候,NSLog是打印不了它指向的内存控件的,而当nil的时候,是可以打印出来指向的一个内存空间。
所以也不难解释test = nil;了, 单纯的这种用法可以说是自己给自己制造内存泄露,这里可以这么理解,就是相当于将指向对象的指针直接和对象一刀两断了。直接让test指向nil,而内存实体不会消失,也不会有系统回收。