当前很多种编程语言都有"异常"(exception)机制,Objective-C也不例外。写过Java代码的程序员应该很习惯于用异常来处理错误。如果你也是这么使用异常的,那现在就把它忘了吧,我们得从头学起。
首先要注意的是,"自动引用计数"(Automatic Reference Counting, ARC,参见第30条)在默认情况下不是"异常安全的"(exception safe)。具体来说,这意味着: 如果抛出异常,那么本应在作用域末尾释放的对象现在却不会自动释放了。如果想生成"异常安全"的代码,可以通过设置编译器的标志来实现,不过这将引入一些额外代码,在不抛出异常时,也照样要执行这部分代码。需要打开的编译器标志叫做-fobjc-arc-exception。
即使不用ARC,也很难写出在抛出异常时不会导致内存泄漏的代码。比方说,设有段代码先创建好了某个资源,使用完之后再将其释放。可是,在释放资源之前如果抛出异常了,那么该资源就不会被释放了:
id someResource = …;
if ( /* check for error */ ) {
@throw [NSException exceptionWithName:@"ExceptionName"
reason:@"There was an error"
userInfo:nil];
}
[someResource doSomething];
[someResource release];
在抛出异常之前先释放someResource,这样做当然能解决此问题,不过要是待释放的资源有很多,而且代码的执行路径更为复杂的话,那么释放资源的代码就容易写得很乱。此外,代码中加入了新的资源之后,开发者经常会忘记在抛出异常前先把它释放掉。
Objective-C语言现在所采用的办法是: 只在极其罕见的情况下抛出异常,异常抛出之后,无须考虑恢复问题,而且应用程序此时也应该退出。这就是说,不用再编写复杂的"异常安全"代码了。
异常只应该用于极其严重的错误,比如说,你编写了某个抽象基类,它的正确用法是先从中继承一个子类,然后使用这个子类。在这种情况下,如果有人直接使用了这个抽象基类,那么可以考虑抛出异常。与其他语言不同,Objective-C中没办法将某个类标识为"抽象类"。要想达成类似效果,最好的办法是在那些子类必须覆写的超类方法里抛出异常。这样的话,只要有人直接创建抽象基类的实例并使用它,即会抛出异常:
- (void)mustOverrideMethod {
@throw [NSException
exceptionWithName:NSInternalInconsistencyException
reason:[NSString stringWithFormat:@"%@ must be overridden", _cmd]
userInfo:nil];
}
既然异常只用于处理严重错误(fatal error, 致命错误),那么对其他错误怎么办呢?在出现"不那么严重的错误"(nonfatal error, 非致命错误)时,Objective-C语言所用的编程范式为: 令方法返回nil/0,或是使用NSError,以表明其中有错误发生。比方说,如果初始化方法无法根据传入的参数来初始化当前实例,那么就可以令其返回nil/0:
- (id)initWithValue:(id)value {
if ((self = [super init])) {
if ( /* Value means instance can’t be created */ ) {
self = nil;
} else {
// Initialise instance
}
}
return self;
}
在这种情况下,如果if语句发现无法用传入的参数值来初始化当前实例(比如这个方法要求传入的value参数必须是non-nil的),那么就把self设置成nil,这样的话,整个方法的返回值也就是nil了。调用者发现初始化方法并没有把实例创建好,于是便可确定其中发生了错误。
NSError的用法更加灵活,因为经由此对象,我们可以把导致错误的原因回报给调用者。NSError对象里封装了三条信息:
- Error domain(错误范围,其类型为字符串)
错误发生的范围。也就是产生错误的根源,通常用一个特有的全局变量来定义。比方说,"处理URL的子系统"(URL-handling subsystem)在从URL中解析或取得数据时如果出错了,那么就会使用NSURLErrorDomain来表示错误范围。 - Error code(错误码,其类型为整数)
独有的错误代码,用以指明在某个范围内具体发生了何种错误。某个特定范围内可能会发生一系列相关错误,这些错误情况通常采用enum来定义。例如,当HTTP请求出错时,可能会把HTTP状态码设为错误码。 - User info(用户信息,其类型为字典)
有关此错误的额外信息,其中或许包含一段"本地化的描述"(localized description),或许还含有导致该错误发生的另外一个错误,经由此种信息,可将相关错误串成一条"错误链"(chain of errors)。
在设计API时,NSError的第一种常见用法是通过委托协议来传递此错误。有错误发生时,当前对象会把错误信息经由协议中的某个方法传给其委托对象(delegate)。例如,NSURLConnection在其委托协议NSURLConnectionDelegate之中就定义了如下方法:
- (void)connection:(NSURLConnection *)connection didFail WithError:(NSError *)error
当NSURLConnection出错之后(比如与远程服务器的连接操作超时了),就会调用此方法以处理相关错误。这个委托方法未必非得实现不可:是不是必须处理此错误,可交由NSURLConnection类的用户来判断。这比抛出异常要好,因为调用者至少可以自己决定NSURLConnection是否回报此错误。
NSError的另外一种常见用法是:经由方法的"输出参数"返回给调用者。比如像这样:
- (BOOL)doSomethingError:(NSError**)error
传递给方法的参数是个指针,而该指针本身又指向另外一个指针,那个指针指向NSError对象。或者也可以把它当成一个直接指向NSError对象的指针。这样一来,此方法不仅能有普通的返回值,而且还能经由"输出参数"把NSError对象回传给调用者。其用法如下:
NSError *error = nil;
BOOL ret = [object doSomethingError:&error];
if (error) {
// There was an error
}
像这样的方法一般都会返回Boolean值,用以表示该操作是成功了还是失败了。如果调用者不关注具体的错误信息,那么直接判断这个Boolean值就好;若是关注具体错误,那就检查经由"输出参数"所返回的那个错误对象。在不想知道具体错误的时候,可以给error参数传入nil。比方说,可以如下使用此方法:
BOOL ret = [object doSomethingError:nil];
if (ret) {
// There was an error
}
实际上,在使用ARC时,编译器会把方法签名中的NSError转换成NSError__autoreleasing,也就是说,指针所指的对象会在方法执行完毕后自动释放。这个对象必须自动释放,因为"doSomething:"方法不能保证其调用者可以把此方法中创建的NSError释放掉,所以必须加入autorelease。这就与大部分方法(以new、alloc、copy、mutableCopy开头的方法当然不在此列)的返回值所具备的语义相同了。
该方法通过下列代码把NSError对象传递到"输出参数"中:
- (BOOL)doSomethingError:(NSError**)error {
// Do something that may cause an error
if (/* there was an error */) {
if (error) {
*error = [NSError errorWithDomain:domain
code:code
userInfo:userInfo];
}
return NO;///< Indicate failure
} else {
return YES; ///< Indicate success
}
}
这段代码以*error语法为error参数"解引用"(dereference),也就是说,error所指的那个指针现在要指向一个新的NSError对象了。在解引用之前,必须先保证error参数不是nil,因为空指针解引用会导致"段错误"(segmentation fault)并使应用程序崩溃。调用者在不关心具体错误时,会给error参数传入nil,所以必须判断这种情况。
NSError对象里的"错误范围"(domain)、"错误码"(code)、"用户信息"(user information)等部分应该按照具体的错误情况填入适当内容。这样的话,调用者就可以根据错误类型分别处理各种错误了。错误范围应该定义成NSString型的全局常量,而错误码则定义成枚举类型为佳。例如,可以把这些值定义成下面这样:
// EOCErrors.h
extern NSString *const EOCErrorDomain;
typedef NS_ENUM(NSUInteger, EOCError) {
EOCErrorUnknown = −1,
EOCErrorInternalInconsistency = 100,
EOCErrorGeneralFault = 105,
EOCErrorBadInput = 500,
};
// EOCErrors.m
NSString *const EOCErrorDomain = @"EOCErrorDomain";
最好能为你自己的程序库中所发生的错误指定一个专用的"错误范围"字符串,使用此字符串创建NSError对象,并将其返回给库的使用者,这样的话,他们就能确定:该错误肯定是由你的程序库所汇报的。用枚举类型来表示错误码也是明智之举,因为这些枚举不仅解释了错误码的含义,而且还给它们起了个有意义的名字。此外,也可以在定义这些枚举的头文件里对每个错误类型详加说明。
要点
- 只有发生了可使整个应用程序崩溃的严重错误时,才应使用异常
- 在错误不那么严重的情况下,可以指派"委托方法"(delegate method)来处理错误,也可以把错误信息放在NSError对象里,经由"输出参数"返回给调用者。