当前很多种编程语言都有异常处理机制,OC也不例外。不过跟Java的异常处理机制有很大的区别。
NSException的问题
首先要说明,iOS中的内存管理中采用了“自动引用计数”(ARC)的方法,在默认情况下不是“异常安全的”。具体来说,这意味着:如果抛出异常,那么本应在作用域末尾释放的对象现在却不会自动释放了。
即使不用ARC,也很难写出在抛出异常时不会导致内存泄漏的代码。比方说,设有段代码先创建好了某个资源,使用完了之后再将其释放。可是,在释放资源之前如果抛出异常了,那么该资源就不会被释放了:
id someResource = /* …… */
if (/* 检查错误相关代码 */) {
//这里抛出异常,someResource就无法正常释放了
@throw [NSException exceptionWithName:@"ExceptionName" reason:@"错误原因" userInfo:nil];
}
[someResource doSomething];
[someResource release];
在抛出异常之前先释放someResource,这样做当然能解决此问题,不过要是待释放的资源有很多,而且代码的执行路径更为复杂的话,那么释放资源的代码就容易写的很乱。
OC语言现在采用的办法是:只在极其严重的情况下抛出异常,异常抛出之后,无需考虑恢复问题,而且应用程序也应该退出。这就是说,不用再编写复杂的“异常安全”的代码了。
异常(NSException)只应该用于极其严重的错误,比如,你写了某个抽象基类,它的正确用法是先从中继承一个子类,然后使用这个子类。在这种情况下,如果有人直接使用了这个抽象基类,那么可以考虑抛出异常。由于OC中没办法将某个类标为抽象类,所以只能通过这种方式“曲线救国”, 如下段代码。
- (void)mustOverideMethod {
NSString *reason = @"此方法必须要子类重载,不能直接调用";
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason: reason userInfo:nil];
}
既然异常只用于处理严重错误,那么对其他错误怎么办呢?在出现不那么严重的错误时,OC有两种方式,一种是另方法返回为nil/0, 第二种就是使用NSError,以表明有错误发生。
通过返回nil 判断错误
对于第一种方法,常见的就是Init方法,如果self创建有问题就会返回nil,而这时就可以通过条件判断及时发现。
- (instancetype)init
{
self = [super init];
if (self) {
//这里之所以做一个 self判断,就是为了能够区分出self创建出错 返回nil的情况
}
return self;
}
NSError
NSError的方法则更加灵活一些,OC中因此更多的错误处理都是通过NSError来实现的。
NSError对象封装了三条信息:
-
Error domain(错误范围,其类型为字符串):
错误发生的范围。也就是产生错误的根源,通常用一个特有的全局变量来定义。比方说,“处理URL的子系统”(URL-handling subsystem)在从URL中解析或去的数据时出错了,那么就会使用NSURLErrorDomain来表示错误范围。 -
Error code(错误码,类型为整数):
独有的错误代码,用以指明在某个范围内具体发生了何种错误。某个特定范围内可能会发生一系列相关错误,这些错误情况采用enum来定义。例如,当HTTP请求出错时,可能把HTTP状态码设为错误码。 -
User info(用户信息,类型为字典):
有关此错误的额外信息,其中或许包含一段localized description,或许还含有导致该错误发生的另外一个错误,经由此种信息,可将相关错误串成一条错误链。
在设计API时,NSError的第一种常见用法是通过委托协议来传递此错误。有错误发生时,当前对象会把错误信息经由协议中的某个方法传给其代理对象。例如,NSURLConnection在其委托协议NSURLConnectionDelegate之中就定义了如下方法:
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;
当NSURLConnection出错之后,就会调用此方法以处理相关错误。这个委托方法并非必须实现不可,是不是处理此错误,由使用者来进行判断。
NSError的另外一种常见用法是:经由方法的“输出参数”返回给调用者。比如像这样:
- (BOOL)doSomething:(NSError **)error
传递给此方法的参数是个指针,而该指针本身又指向另外一个指针,那个指针指向NSError对象。或者也可以把它当做一个直接指向NError对象的指针。这样一来,此方法不仅能有普通的返回值,而且还能经由“输出参数”把NSError对象回传给调用者。其用法如下:
NSError *error = nil;
BOOL ret = [object doSomething:&error];
if (error) {
//发生错误
}
实际上,在使用ARC时,编译器会把方法签名中额NSError** 转化成NSError* __autoreleasing*, 也就是说指针所指的对象会在方法执行完毕后自动释放。这个对象必须自动释放,因为“doSomething”方法不能保证其调用者可以把此方法中创建的NSError释放掉,所以必须加入autorelease。这就与大部分方法的返回值所具备的语义相同了。
该方法通过下列代码把NSError对象传递到“输出参数”中:
- (BOOL)doSomething:(NSError **)error {
if (/* 判断错误*/) {
if (error) {
//创建error
error = [NSError errorWithDomain:domain code:code userInfo:userInfo];
}
return NO;
} else {
return YES;
}
}
这段代码以*error语法为error参数“解引用”,也就是说,error所指的那个指针现在要指向一个新的NSError对象了。在解引用执勤,必须先保证error参数不是nil,因为空指针解引用会导致“段错误”(segmentation fault)并使应用程序崩溃。调用者不在关心具体错误时,会给error参数传入nil,所以必须判断这种情况。
NSError对象里的domain、code、userInfo等部分应该按照具体的错误情况填入适当内容。这样的话,调用者就可以根据错误类型分别处理各种错误了。错误范围应该定义成NSString型的全局常量,而错误码则鼎城枚举类型为佳。
最好能为你自己的程序库中所发生的错误指定一个专用的“错误范围”字符串,使用此字符串创建NSError对象,并将其返回给库的使用者,这样的话,他们就能确信:该错误肯定是由你的应用程序库所汇报的。用枚举类型来表示错误码也是明智之举,因为这些枚举不仅解释了错误码的含义,而且还给它们起了个有意义的名字。此外,也可以在定义这些枚举的头文件里对每个错误类型详加说明。
本文参考《Effective Objective-C 2.0》中内容所写。
网友评论