断言

断言

断言 (assertion) 是指在开发期间使用的, 让程序在运行时进行自检的代码 (通常是一个子程序或宏. 若断言为真, 则表明程序运行正常; 而断言为假, 则意味着它已经在代码中发现了意料之外的错误. 断言对于大型的复杂程序或可靠性要求极高的程序来说尤其有用.

断言使用的指导性建议:

  1. 用错误处理代码来处理预期会发生的状况, 用断言来处理绝不应该发生的状况
  2. 避免把需要执行的代码放到断言中
  3. 用断言来注解并验证前条件和后条件
  4. 对于高健壮性的代码, 应该先使用断言再处理错误

NSAssertassert 都是断言, 主要的差别在于 assert 在断言失败的时候只是简单的终止程序, 而 NSAssert 会报告出错误信息并且打印出来, 因此应尽量使用 NSAssert.

iOS 中常用的是两对断言, NSAssert/NSCAssertNSParameterAssert/NSCparameterAssert.

NSAssertionHandler

Objective-C 中的断言处理使用的是 NSAssertionHandler: 每个线程拥有它自己的断言处理器, 它是 NSAssertionHandler 类的实例对象. 当被调用时, 一个断言处理器打印一条包含方法和类名(或者函数名)的错误信息. 然后抛出一个 NSInternalInconsistencyException 异常.

NSAssert 和 NSCAssert

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// NSAssert
#if !defined(_NSAssertBody)
#define NSAssert(condition, desc, ...) \
do { \
__PRAGMA_PUSH_NO_EXTRA_ARG_WARNINGS \
if (__builtin_expect(!(condition), 0)) { \
NSString *__assert_file__ = [NSString stringWithUTF8String:__FILE__]; \
__assert_file__ = __assert_file__ ? __assert_file__ : @"<Unknown File>"; \
[[NSAssertionHandler currentHandler] handleFailureInMethod:_cmd \
object:self file:__assert_file__ \
lineNumber:__LINE__ description:(desc), ##__VA_ARGS__]; \
} \
__PRAGMA_POP_NO_EXTRA_ARG_WARNINGS \
} while(0)
#endif

// NSCAssert
#if !defined(_NSCAssertBody)
#define NSCAssert(condition, desc, ...) \
do { \
__PRAGMA_PUSH_NO_EXTRA_ARG_WARNINGS \
if (__builtin_expect(!(condition), 0)) { \
NSString *__assert_fn__ = [NSString stringWithUTF8String:__PRETTY_FUNCTION__]; \
__assert_fn__ = __assert_fn__ ? __assert_fn__ : @"<Unknown Function>"; \
NSString *__assert_file__ = [NSString stringWithUTF8String:__FILE__]; \
__assert_file__ = __assert_file__ ? __assert_file__ : @"<Unknown File>"; \
[[NSAssertionHandler currentHandler] handleFailureInFunction:__assert_fn__ \
file:__assert_file__ \
lineNumber:__LINE__ description:(desc), ##__VA_ARGS__]; \
} \
__PRAGMA_POP_NO_EXTRA_ARG_WARNINGS \
} while(0)
#endif

NSParameterAssert 和 NSCParameterAssert

1
2
3
4
5
// NSParameterAssert
#define NSParameterAssert(condition) NSAssert((condition), @"Invalid parameter not satisfying: %@", @#condition)

// NSCParameterAssert
#define NSCParameterAssert(condition) NSCAssert((condition), @"Invalid parameter not satisfying: %@", @#condition)

根据定义, 第一组 (NSAssert / NSCAssert) 是针对条件的断言, 第二组 (NSParameterAssert / NSCparameterAssert) 是处理参数是否存在的断言; 其中, 每组前者是适用于 Object-C 的方法, _cmd 和 self 与运行时有关, 后者是适用于 C 的函数.

  • NSAssert 和 assert
    NSAssert 和 assert 都是断言, 主要差别为 assert 在断言失败的时候只是简单的终止程序, 而 NSAssert 会报告错误信息并且打印出来.

Xcode 编译

从 Xcode 4.2 开始,发布构建默认关闭了断言, 它是通过定义 NS_BLOCK_ASSERTIONS 宏实现的. 也就是说, 当编译 release 版本时, 任何调用 NSAssert 等的地方都被有效的移除了.

(在 Debug 情况下, 所有 NSAssert 都会被执行, 在 Release 下不希望 NSAssert 被执行, 通常在 Release 中将断言设置成禁用.
设置方法: 在 Build Settings 菜单, 搜索 Preprocessor Macros 项, 其下面有一个选择, 用于程序生成配置: Debug 版和 Release 版. 选择 Release 项, 添加一个规则: NS_BLOCK_ASSERTIONS, 即不进行断言检查.)

自定义 NSAssertionHandler

NSAssertionHandler 类中, 有两个需要在子类中实现的方法: - (void)handleFailureInMethod:... (当 NSAssert / NSParameterAssert 失败时调用) 和 - (void)handleFailureInFunction:... (当 NSCAssert / NSCParameterAssert 失败时调用).

每个线程都可以指定断言处理器.
如果 NSAssert 和 NSCAssert 条件评估为错误, 会向 NSAssertionHandler 实例发送一个表示错误的字符串. 如果想设置一个 NSAssertionHandler 的子类来处理失败的断言, 在线程的 threadDictionary 对象中设置 NSAssertionHandlerKey 字段.

这样我们就完成在当前线程中使用自定义断言处理器的配置, 那么接下来, 如果有和我们条件不同的情况都直接会回调对应着的那两个失败的方法.
(有时也可以在自定义 NSAssertionHandler 中重写上述两个失败的回调方法, 在这里执行我们想要抛出的错误 – 打印或者直接报错, 可以使得出现断言时, 控制台输出错误, 但是程序仍然继续运行, 不会强制退出程序):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#import "MyAssertHandler.h"

@implementation MyAssertHandler

// 处理 Objective-C 的断言
- (void)handleFailureInMethod:(SEL)selector object:(id)object file:(NSString *)fileName lineNumber:(NSInteger)line description:(NSString *)format,...
{
NSLog(@"NSAssert Failure: Method %@ for object %@ in %@#%li", NSStringFromSelector(selector), object, fileName, (long)line);
}

// 处理 C 的断言
- (void)handleFailureInFunction:(NSString *)functionName file:(NSString *)fileName lineNumber:(NSInteger)line description:(NSString *)format,...
{
NSLog(@"NSCAssert Failure: Function (%@) in %@#%li", functionName, fileName, (long)line);
}

@end
1
2
3
4
NSAssertionHandler *myAssertHandler = [[MyAssertHandler alloc] init];
//给当前的线程
[[[NSThread currentThread] threadDictionary] setValue:myAssertHandler
forKey:NSAssertionHandlerKey];

注意事项

仔细观察 NSAssert 宏定义, 会发现其中包含 self, 有 self 的地方一定要注意 block 循环引用问题.

那如果想在 block 中使用断言该怎么办呢? 可以用 NSCAssert 替换 NSAssert, 用 NSCParameterAssert 替换 NSParameterAssert.

参考

------ END ------
0%