异常
异常(exception)就是指必须中断程序的正常执行来进行处理的特殊状态。
编码时采取将异常发生时的处理和程序原本应该执行的处理分开进行的结构,就是异常处理机制(exception handling mechanism)。当异常状况产生时,程序会自动脱离出普通的函数和方法的调用,转而执行异常处理过程。
Objective-C中的异常处理
利用异常的程序或通过引发异常来控制动作的编程类型并不被推荐。也就是说,因设计错误或程序错误而产生的执行时错误,在开发时就应该被去除。
使用OC异常处理机制的目的:
对调试起到辅助作用
防止部分异常波及整个应用
异常处理机制概述
异常句柄和异常处理域
异常发生时,为执行异常处理而准备的流程称为异常句柄(exception handler)。该异常句柄处理的对象的代码部分称为异常处理域(exception handling domain)。
异常表示类NSException
通过生成NSException实例并发送raise消息,就可以启动异常处理。这也称为“产生异常”或“抛出异常”。NSException实例称为异常对象。
- (void)raise
//向异常对象抛出异常
当向NSException类实例发送raise消息时该状况已经发生了。但是,在异常处理机制方面来看,只要没有开始异常处理,就不是异常。也就是说,向NSException实例发送消息raise这一时刻和异常发生被同等看待了。而且有时也会将指向NSException的实例称为异常。
异常对象包含的信息:
信息
解释
名称
为了识别异常而使用的短字符串。判断是否为应该使用异常句柄处理的对象时使用。使用下面的方法取出:
- (NSString *)name
原因用文字描述异常原因的长字符串。用如下方法取出:
- (NSString *)reason
用户字典传递与异常相关的各种信息时使用的NSDictionary字典。字典中的信息因异常而异。用如下方法取出:
- (NSDictionary *)userInfo
异常的名字为全局字符串变量,被预先定义在各种类的头文件中。调查发生的异常时,将捕捉的异常对象名和这些异常名比较。而且,像什么样的情况下发生什么异常的信息,在各类的参考文档中都有详述。
异常处理机制采用如下语法
@try{
/*异常处理域*/
/*在此记述正常处理*/
...
}
@catch(NSException *exception) {
/*异常句柄*/
/*记述异常处理过程。例如显示错误消息等*/
...
}
@finally{
/*后处理*/
/*在此记述无论异常发送与否最后都会执行的代码*/
...
}
@try必须写在@catch和@fianlly的前面。@catch块可以写多个,也可以不写,但此时必须有@finally块。
变量名exception只在@catch块内有效。
@finally块不需要时也可以不写。但只要写了,该块内的代码一定会执行。
简单的异常处理的示例程序:
#import
intmain(intargc,char*argv[]) {
@autoreleasepool{
idarray = [NSMutableArrayarray];
@try{
if(argc ==1)
array = [arrayobjectAtIndex:1];//NSRangeException
else
[arrayaddObject:nil];//NSInvalidArgumentException
NSLog(@"success\n");
}
@catch(NSException *ex) {
NSString*name = [exname];
NSLog(@"Name: %@\n", name);
NSLog(@"Reason: %@\n", [exreason]);
if([nameisEqualToString:NSRangeException])
NSLog(@"Exception was caught successfully.\n");
else
[exraise];//再次抛出异常
}
NSLog(@"main exit");//行尾没有换行符也没有关系。
}
return0;
}
该程序根据命令行中是否存在参数而产生不同的异常。生成空数组,取出第一个不存在的元素时将产生异常NSRangeException,追加新的元素nil时将产生异常NSInvalidArgumentException。因为@catch块中捕获的异常可以访问ex变量,所以首先显示异常的名字和原因。接着,当发生的异常为NSRangeException时就显示能够捕获这一消息,而当其他异常发生时,则向ex发送raise消息来再次抛出异常。这样以来,NSRangeException以外的异常就会由运行时系统来处理。
[ex raise]被执行时,会显示和普通的运行时错误发生时几乎相同的消息,然后程序异常就终止了。这是因为运行时系统的未捕获异常句柄(uncaught exception handler)流程捕获并处理了异常。异常处理域外部发生异常时,或程序异常句柄中异常处理不能结束时,未捕获的异常句柄就会向终端,控制台或日志文件中打印消息然后终止程序。
但是,Cocoa应用的主线程内发生异常时,NSApplication(UIApplication)实例将捕获未处理的异常,所以未捕获异常句柄并不会直接终止应用。然而,即使应用能够捕捉到,异常的产生也仍可能会给应用带来若干影响,导致应用之后的处理不能正确执行。
异常的发生和传播
异常的传播
伴随着程序的执行,某个异常处理域中可能还会有其他异常处理在执行,异常句柄中也可能再次发生其他异常。
只使用@catch块不能结束某个异常处理时,为了委托外部异常句柄来处理,有时就需要在@catch块内故意抛出异常。像这样,为了保证异常的正常处理,被调用方向调用方有序有序传递异常的过程,称为异常传播(propagation)。任何异常句柄,当不能被合适处理或在异常处理域外部发生异常时,最终都会由未捕获异常句柄来处理。
自己触发异常
如前所述,向异常对象发送raise消息就可以抛出异常,但是,实际上使用下面的类NSException的方法会更加容易。
+ (void)raise:(NSString *)name formar:(NSString *)format,...
//将异常名及原因保存成信息并创建临时实例,直接产生异常。
也可以自定义异常名并产生新型的异常,此时,必须起一个可以和其他异常相区别的名字。
用@throw语法产生异常
将@throw方法应用于NSException实例,同发送raise消息是一样的。
@throwobject;
即便@catch块内没有参数也可以使用@throw。此时,@catch块捕捉的异常对象就被视为参数,同传入参数的效果一样。
将NSException以外的普通对象指定为@throw参数也可以产生异常,@catch语法中必须要指明该对象的类型。如果对象类型不明确,那么使用id类型后,结果就将捕捉包含NSException在内的所有异常。
@catch块可以被书写多个,但是必须要注意它们的顺序。这是因为,根据书写顺序能够得知用@throw产生异常时使用的对象和哪个@catch块参数一致。
@catch的特殊语法
@catch参数除了上面介绍的书写方法外,也可以采用下面的方式书写。即括号内不省略,而是写三个点号。
@catch(...) {
/*异常句柄*/
}
在需要捕捉异常而不需要知道异常内容的情况下,经常会使用该写法。如果存在其他的@catch块,则必须将其书写为最后的块。块内@throw不指定参数时,就能传播异常。
异常传播和@finally
@catch块内发生异常时(或传播),或者不存在可以捕捉发生的异常的@catch块时,通常就会将处理移交给外部的异常句柄来执行。但如果有@finally块,那么在向外侧异常句柄移交处理前,@finally块的内容先被执行。在没发生异常时,@finally会在@try块之后执行,而当异常不向外侧传播时,@finally块必须紧挨着@catch块执行。所以,这里就要书写实例释放或文件关闭等必须要执行的后处理。如下图。如果不捕捉异常而只想执行后处理,也可以不写@catch。
异常处理程序的注意点
递归调用(函数调用)中发生异常时,处理有时就会跳过内侧的方法而转移到外侧的异常句柄中进行。如图。如果处理被中断时务必进行后处理。
引用计数管理和异常。不能保证对象被正确释放。因此,在OC中,写代码时不能以使用异常处理机制为前提。
关于C语言中的全局跳转函数。C语言有两个函数setjmp()和longjmp(),它们都可以从函数调用的深层递归中返回。但是,异常处理机制在实现时使用了这些功能,所以异常处理机制和这些跳转函数不可以同时使用。
断言
断言是什么
在程序中书写程序必须满足的条件,当条件被破坏时就触发异常的结构称为断言(assertion)。
断言使用宏来书写,定义在NSException.h中。
断言宏的参数中写着必须为真的条件。
NASSert(x > y, *@"illegal values x(%d) & y(%d)", x, y);
条件为假时,异常NSInternalInconsistencyException就会被触发。
编译时定义了NS_BLOCK_ASSERTIONS宏时,断言不会被写入代码,而因为没有写入,就会被当成空字符串处理。于是在编译完成后的程序时,就要在编译器的选项中加入-DNS_BLOCK_ASSERTIONS。选项-D的作用是可以从命令行定义宏,就不需要特意在文件中书写#define了。
断言宏
断言宏可以在OC方法内以及C函数内使用。断言条件为假时,如果是在函数内,那么就用函数名来表示;而如果是在方法内,那么就用方法名来表示。
首先,只在条件为真时使用的宏有两个:
NSParameterAssert(condition)….在方法内使用
NSCParameterAssert(condition)…在函数内使用
此外还有宏可以在条件为假时传递触发的异常,以及生成用来说明异常的字符串。它们和printf()一样,取得格式字符串和对应的参数。使用目前为止的C语言规范尚不可以定义参数个数不确定的宏,而在新的规范中是可以的。
在方法内使用
NSAssert(condition,NSString *description [,arg,…])
在函数内使用
NSCAssert(condition,Nsstring *description[,arg,…])
错误处理
错误处理结构的目的
在Cocoa环境下,为了能够统一表示错误的种类和消息,可以使用NSError。简单来说,NSError持有错误说明和错误的处理方法的相关信息。
表示错误的类NSError的使用方法
信息
解释
域
导致错误的技术因素。例如,显示Cocoa,Carbon,Unix等中的谁的API相关的字符串。用下面方法取出:
- (NSString *) domain
代码表示错误类型的整数值。需要注意的是代码的意义可根据域而发生变化。用下面的方法取出:
- (NSInteger)code
用户字典为得到有关错误的各种信息而使用的字典对象。字典中保存的信息随错误而不同。用下面的方法取出:
- (NSDictionary *)userInfo
如下所示,准备好保存的实例变量,并使用&运算符指向该地址即可。发生指定的文件不存在等错误时,代入错误对象给变量err。如果没有发生错误则不带入对象。因此为了明确表示错误没有发生,应该养成预先为变量err代入nil的操作习惯。然而使用ARC管理内存时,方法内的自动变量地址必须将(NSError**)类型的参数指定为nil。
NSString*path, *contents;
NSError*err;
...
err =nil;
contents = [NSString stringWithContentsOfFile:path encoding:NSShiftJISStringEncoding error:&err];
获取错误对象的信息
为了获得表示错误内容的消息字符串,可向NSError实例发送下面的消息。没有必要直接访问用户字典或域。
- (NSString *)localizedDescription
//返回说明错误的字符串。字符串被本地化,不会返回nil。用户字典中如果存在NSLocalizedDescriptionKey键的对象,则返回该对象。
- (NSString *)localizedFailureReason
//显示错误产生的原因,返回本地化的短字符串。有时返回nil。用户字典中如果存在NSLocalizedFailureReasonErrorKey键的对象,则返回该对象。
- (NSString *)localizedRecoverySuggestion
//显示错误的处理方法或建议,返回本地化的短字符串。有时返回nil。用户字典中如果存在NSLocalizedRecoverySuggestionErrorKey键的对象,则返回该对象。
生成自定义错误对象
与异常相同,自己也可以生成NSError实例。可以使用下面的简易常数类。
+ (id)errorWithDomain:(NSString *)domain code:(NSInteger)code userInfo:(nullableNSDictionary *)dict
//指定域名,错误代码和用户字典后创建错误对象。用户字典为nil也没有关系,域名必须要指定。典型的错误域名如下所示。
域名
解释
头文件
NSCocoaErrorDomain与Cocoa环境相关的错误Foundation/FoundationErrors.h
AppKit/AppKitErrors.h
CoreData/CoreDataErrors.h
NSPOSIXErrorDomain
与Unix环境相关的错误sys/errno.h
NSMachErrorDomain
与Mach核相关的错误mach/kern_return.h
返回新创建的错误对象时,如果错误原因和上述任一个域相同,就可以直接使用域和错误代码。在Mac OS环境下,用户字典即使为nil,只要指定了域名和错误代码,多数情况下警告面板中也会显示日语的错误消息。
错误反应链
通过将NSError结合应用框架来使用,就可以在GUI环境中灵活的进行错误处理。核心模块是错误反应链(error-responder chain)。
错误反应链的结构
错误反应链与反应链同样,是位于底层的组件指向上层组件的对象(反应器)的连接。
当与某个组件相关的错误发生时,会将错误对象作为参数将消息presentError:发送给该组件。于是错误对象就会从该组件开始被转发给上层的组件。最终由窗体发送给NSApplication实例,错误提示和恢复被执行。
错误对象的更改和恢复
因发生错误传递的错误对象以及用户字典是常数对象,所以willPresentError:必须要返回一个新创建的错误对象。而且原错误对象使用新错误对象用户字典中NSUnderlyingErrorKey键并保存它。
包含多个字符串时,第一个字符串在面板的最右侧(默认),然后是右边第二个,按照此顺序排列。也可能返回nil,此时警告面板中只显示“OK”键。
接着,在取得按键的选择结果后,指定对象来执行恢复处理。该对象就称为恢复尝试对象(recovery attempter)。
恢复尝试对象可以是任意类的实例,但为了进行恢复,需要实现NSErrorRecoveryAttempting非正式协议的其中任意一个方法。该非正式协议在Foundation/NSError.h中声明。