第三章 接口与API设计—第21条:理解Objective-C错误模型

当前很多种编程语言都有"异常"(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对象里,经由"输出参数"返回给调用者。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容