Foundation中的断言处理
经常在看一些第三方库的代码时,或者自己在写一些基础类时,都会用到断言。所以在此总结一下Objective-C中关于断言的一些问题。
Foundation中定义了两组断言相关的宏,分别是:
- NSAssert / NSCAssert
- NSParameterAssert / NSCParameterAssert
这两组宏主要在功能和语义上有所差别,这些区别主要有以下两点:
如果我们需要确保方法或函数的输入参数的正确性,则应该在方法(函数)的顶部使用NSParameterAssert / NSCParameterAssert;而在其它情况下,使用NSAssert / NSCAssert。
另一个不同是介于C和Objective-C之间。NSAssert / NSParameterAssert应该用于Objective-C的上下文(方法)中,而NSCAssert / NSCParameterAssert应该用于C的上下文(函数)中。
当断言失败时,通常是会抛出一个如下所示的异常:
- *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'true is not equal to false'
Foundation为了处理断言,专门定义了一个NSAssertionHandler来处理断言的失败情况。NSAssertionHandler对象是自动创建的,用于处理失败的断言。当断言失败时,会传递一个字符串给NSAssertionHandler对象来描述失败的原因。每个线程都有自己的NSAssertionHandler对象。当调用时,一个断言处理器会打印包含方法和类(或函数)的错误消息,并引发一个NSInternalInconsistencyException异常。就像上面所看到的一样。
我们很少直接去调用NSAssertionHandler的断言处理方法,通常都是自动调用的。
NSAssertionHandler提供的方法并不多,就三个,如下所示:
- // 返回与当前线程的NSAssertionHandler对象。
- // 如果当前线程没有相关的断言处理器,则该方法会创建一个并指定给当前线程
- + (NSAssertionHandler *)currentHandler
- // 当NSCAssert或NSCParameterAssert断言失败时,会调用这个方法
- - (void)handleFailureInFunction:(NSString *)functionName file:(NSString *)object lineNumber:(NSInteger)fileName description:(NSString *)line, format,...
- // 当NSAssert或NSParameterAssert断言失败时,会调用这个方法
- - (void)handleFailureInMethod:(SEL)selector object:(id)object file:(NSString *)fileName lineNumber:(NSInteger)line description:(NSString *)format, ...
另外,还定义了一个常量字符串,
- NSString * const NSAssertionHandlerKey;
主要是用于在线程的threadDictionary字典中获取或设置断言处理器。
关于断言,还需要注意的一点是在Xcode 4.2以后,在release版本中断言是默认关闭的,这是由宏NS_BLOCK_ASSERTIONS来处理的。也就是说,当编译release版本时,所有的断言调用都是无效的。
我们可以自定义一个继承自NSAssertionHandler的断言处理类,来实现一些我们自己的需求。如Mattt Thompson的NSAssertionHandler实例一样:
- @interface LoggingAssertionHandler : NSAssertionHandler
- @end
- @implementation LoggingAssertionHandler
- - (void)handleFailureInMethod:(SEL)selector
- object:(id)object
- file:(NSString *)fileName
- lineNumber:(NSInteger)line
- description:(NSString *)format, ...
- {
- NSLog(@"NSAssert Failure: Method %@ for object %@ in %@#%i", NSStringFromSelector(selector), object, fileName, line);
- }
- - (void)handleFailureInFunction:(NSString *)functionName
- file:(NSString *)fileName
- lineNumber:(NSInteger)line
- description:(NSString *)format, ...
- {
- NSLog(@"NSCAssert Failure: Function (%@) in %@#%i", functionName, fileName, line);
- }
- @end
上面说过,每个线程都有自己的断言处理器。我们可以通过为线程的threadDictionary字典中的NSAssertionHandlerKey指定一个新值,来改变线程的断言处理器。
如下代码所示:
- - (BOOL)application:(UIApplication *)application
- didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
- {
- NSAssertionHandler *assertionHandler = [[LoggingAssertionHandler alloc] init];
- [[[NSThread currentThread] threadDictionary] setValue:assertionHandler
- forKey:NSAssertionHandlerKey];
- // ...
- return YES;
- }
而什么时候应该使用断言呢?通常我们期望程序按照我们的预期去运行时,如调用的参数为空时流程就无法继续下去时,可以使用断言。但另一方面,我们也需要考虑,在这加断言确实是需要的么?我们是否可以通过更多的容错处理来使程序正常运行呢?
Mattt Thompson在NSAssertionHandler中的倒数第二段说得挺有意思,在此摘抄一下:
But if we look deeper into NSAssertionHandler—and indeed, into our own hearts, there are lessons to be learned about our capacity for kindness and compassion; about our ability to forgive others, and to recover from our own missteps. We can't be right all of the time. We all make mistakes. By accepting limitations in ourselves and others, only then are we able to grow as individuals.
参考
NSAssertionHandler
NSAssertionHandler Class Reference