第三章 接口與API設(shè)計—第21條:理解Objective-C錯誤模型

當(dāng)前很多種編程語言都有"異常"(exception)機制级及,Objective-C也不例外。寫過Java代碼的程序員應(yīng)該很習(xí)慣于用異常來處理錯誤硕并。如果你也是這么使用異常的观蓄,那現(xiàn)在就把它忘了吧,我們得從頭學(xué)起该镣。
首先要注意的是冻璃,"自動引用計數(shù)"(Automatic Reference Counting, ARC,參見第30條)在默認(rèn)情況下不是"異常安全的"(exception safe)损合。具體來說省艳,這意味著: 如果拋出異常,那么本應(yīng)在作用域末尾釋放的對象現(xiàn)在卻不會自動釋放了嫁审。如果想生成"異常安全"的代碼跋炕,可以通過設(shè)置編譯器的標(biāo)志來實現(xiàn),不過這將引入一些額外代碼律适,在不拋出異常時辐烂,也照樣要執(zhí)行這部分代碼。需要打開的編譯器標(biāo)志叫做-fobjc-arc-exception捂贿。
即使不用ARC纠修,也很難寫出在拋出異常時不會導(dǎo)致內(nèi)存泄漏的代碼。比方說厂僧,設(shè)有段代碼先創(chuàng)建好了某個資源扣草,使用完之后再將其釋放⊙胀溃可是德召,在釋放資源之前如果拋出異常了,那么該資源就不會被釋放了:

id someResource = …;
if ( /* check for error */ ) {
    @throw [NSException exceptionWithName:@"ExceptionName"
                                   reason:@"There was an error"
                                 userInfo:nil];
}
[someResource doSomething];
[someResource release];

在拋出異常之前先釋放someResource汽纤,這樣做當(dāng)然能解決此問題上岗,不過要是待釋放的資源有很多,而且代碼的執(zhí)行路徑更為復(fù)雜的話蕴坪,那么釋放資源的代碼就容易寫得很亂肴掷。此外,代碼中加入了新的資源之后背传,開發(fā)者經(jīng)常會忘記在拋出異常前先把它釋放掉呆瞻。
Objective-C語言現(xiàn)在所采用的辦法是: 只在極其罕見的情況下拋出異常,異常拋出之后径玖,無須考慮恢復(fù)問題痴脾,而且應(yīng)用程序此時也應(yīng)該退出。這就是說梳星,不用再編寫復(fù)雜的"異常安全"代碼了赞赖。
異常只應(yīng)該用于極其嚴(yán)重的錯誤滚朵,比如說,你編寫了某個抽象基類前域,它的正確用法是先從中繼承一個子類辕近,然后使用這個子類。在這種情況下匿垄,如果有人直接使用了這個抽象基類移宅,那么可以考慮拋出異常。與其他語言不同椿疗,Objective-C中沒辦法將某個類標(biāo)識為"抽象類"漏峰。要想達成類似效果,最好的辦法是在那些子類必須覆寫的超類方法里拋出異常届榄。這樣的話浅乔,只要有人直接創(chuàng)建抽象基類的實例并使用它,即會拋出異常:

- (void)mustOverrideMethod {
    @throw [NSException 
        exceptionWithName:NSInternalInconsistencyException
        reason:[NSString stringWithFormat:@"%@ must be overridden", _cmd]
        userInfo:nil];
}

既然異常只用于處理嚴(yán)重錯誤(fatal error, 致命錯誤)痒蓬,那么對其他錯誤怎么辦呢?在出現(xiàn)"不那么嚴(yán)重的錯誤"(nonfatal error, 非致命錯誤)時滴劲,Objective-C語言所用的編程范式為: 令方法返回nil/0攻晒,或是使用NSError,以表明其中有錯誤發(fā)生班挖。比方說鲁捏,如果初始化方法無法根據(jù)傳入的參數(shù)來初始化當(dāng)前實例,那么就可以令其返回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語句發(fā)現(xiàn)無法用傳入的參數(shù)值來初始化當(dāng)前實例(比如這個方法要求傳入的value參數(shù)必須是non-nil的)给梅,那么就把self設(shè)置成nil,這樣的話双揪,整個方法的返回值也就是nil了动羽。調(diào)用者發(fā)現(xiàn)初始化方法并沒有把實例創(chuàng)建好,于是便可確定其中發(fā)生了錯誤渔期。
NSError的用法更加靈活运吓,因為經(jīng)由此對象,我們可以把導(dǎo)致錯誤的原因回報給調(diào)用者疯趟。NSError對象里封裝了三條信息:

  • Error domain(錯誤范圍拘哨,其類型為字符串)
    錯誤發(fā)生的范圍。也就是產(chǎn)生錯誤的根源信峻,通常用一個特有的全局變量來定義倦青。比方說,"處理URL的子系統(tǒng)"(URL-handling subsystem)在從URL中解析或取得數(shù)據(jù)時如果出錯了盹舞,那么就會使用NSURLErrorDomain來表示錯誤范圍产镐。
  • Error code(錯誤碼隘庄,其類型為整數(shù))
    獨有的錯誤代碼,用以指明在某個范圍內(nèi)具體發(fā)生了何種錯誤磷账。某個特定范圍內(nèi)可能會發(fā)生一系列相關(guān)錯誤峭沦,這些錯誤情況通常采用enum來定義。例如逃糟,當(dāng)HTTP請求出錯時吼鱼,可能會把HTTP狀態(tài)碼設(shè)為錯誤碼。
  • User info(用戶信息绰咽,其類型為字典)
    有關(guān)此錯誤的額外信息菇肃,其中或許包含一段"本地化的描述"(localized description),或許還含有導(dǎo)致該錯誤發(fā)生的另外一個錯誤取募,經(jīng)由此種信息琐谤,可將相關(guān)錯誤串成一條"錯誤鏈"(chain of errors)。
    在設(shè)計API時玩敏,NSError的第一種常見用法是通過委托協(xié)議來傳遞此錯誤斗忌。有錯誤發(fā)生時,當(dāng)前對象會把錯誤信息經(jīng)由協(xié)議中的某個方法傳給其委托對象(delegate)旺聚。例如织阳,NSURLConnection在其委托協(xié)議NSURLConnectionDelegate之中就定義了如下方法:
- (void)connection:(NSURLConnection *)connection  didFail WithError:(NSError *)error

當(dāng)NSURLConnection出錯之后(比如與遠程服務(wù)器的連接操作超時了),就會調(diào)用此方法以處理相關(guān)錯誤砰粹。這個委托方法未必非得實現(xiàn)不可:是不是必須處理此錯誤唧躲,可交由NSURLConnection類的用戶來判斷。這比拋出異常要好碱璃,因為調(diào)用者至少可以自己決定NSURLConnection是否回報此錯誤弄痹。
NSError的另外一種常見用法是:經(jīng)由方法的"輸出參數(shù)"返回給調(diào)用者。比如像這樣:

- (BOOL)doSomethingError:(NSError**)error

傳遞給方法的參數(shù)是個指針嵌器,而該指針本身又指向另外一個指針肛真,那個指針指向NSError對象∷剑或者也可以把它當(dāng)成一個直接指向NSError對象的指針毁欣。這樣一來,此方法不僅能有普通的返回值岳掐,而且還能經(jīng)由"輸出參數(shù)"把NSError對象回傳給調(diào)用者凭疮。其用法如下:

NSError *error = nil;
BOOL ret = [object doSomethingError:&error];
if (error) {
    // There was an error
}

像這樣的方法一般都會返回Boolean值,用以表示該操作是成功了還是失敗了串述。如果調(diào)用者不關(guān)注具體的錯誤信息执解,那么直接判斷這個Boolean值就好;若是關(guān)注具體錯誤,那就檢查經(jīng)由"輸出參數(shù)"所返回的那個錯誤對象衰腌。在不想知道具體錯誤的時候新蟆,可以給error參數(shù)傳入nil。比方說右蕊,可以如下使用此方法:

BOOL ret = [object doSomethingError:nil];
if (ret) {
    // There was an error
}

實際上琼稻,在使用ARC時,編譯器會把方法簽名中的NSError轉(zhuǎn)換成NSError__autoreleasing饶囚,也就是說帕翻,指針?biāo)傅膶ο髸诜椒▓?zhí)行完畢后自動釋放。這個對象必須自動釋放萝风,因為"doSomething:"方法不能保證其調(diào)用者可以把此方法中創(chuàng)建的NSError釋放掉嘀掸,所以必須加入autorelease。這就與大部分方法(以new规惰、alloc睬塌、copy、mutableCopy開頭的方法當(dāng)然不在此列)的返回值所具備的語義相同了歇万。
該方法通過下列代碼把NSError對象傳遞到"輸出參數(shù)"中:

- (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參數(shù)"解引用"(dereference)揩晴,也就是說,error所指的那個指針現(xiàn)在要指向一個新的NSError對象了贪磺。在解引用之前硫兰,必須先保證error參數(shù)不是nil,因為空指針解引用會導(dǎo)致"段錯誤"(segmentation fault)并使應(yīng)用程序崩潰缘挽。調(diào)用者在不關(guān)心具體錯誤時瞄崇,會給error參數(shù)傳入nil呻粹,所以必須判斷這種情況壕曼。
NSError對象里的"錯誤范圍"(domain)、"錯誤碼"(code)等浊、"用戶信息"(user information)等部分應(yīng)該按照具體的錯誤情況填入適當(dāng)內(nèi)容腮郊。這樣的話,調(diào)用者就可以根據(jù)錯誤類型分別處理各種錯誤了筹燕。錯誤范圍應(yīng)該定義成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";

最好能為你自己的程序庫中所發(fā)生的錯誤指定一個專用的"錯誤范圍"字符串过咬,使用此字符串創(chuàng)建NSError對象,并將其返回給庫的使用者制妄,這樣的話掸绞,他們就能確定:該錯誤肯定是由你的程序庫所匯報的。用枚舉類型來表示錯誤碼也是明智之舉耕捞,因為這些枚舉不僅解釋了錯誤碼的含義衔掸,而且還給它們起了個有意義的名字烫幕。此外,也可以在定義這些枚舉的頭文件里對每個錯誤類型詳加說明敞映。
要點

  • 只有發(fā)生了可使整個應(yīng)用程序崩潰的嚴(yán)重錯誤時较曼,才應(yīng)使用異常
  • 在錯誤不那么嚴(yán)重的情況下,可以指派"委托方法"(delegate method)來處理錯誤振愿,也可以把錯誤信息放在NSError對象里捷犹,經(jīng)由"輸出參數(shù)"返回給調(diào)用者。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末埃疫,一起剝皮案震驚了整個濱河市伏恐,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌栓霜,老刑警劉巖翠桦,帶你破解...
    沈念sama閱讀 216,843評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異胳蛮,居然都是意外死亡销凑,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,538評論 3 392
  • 文/潘曉璐 我一進店門仅炊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來斗幼,“玉大人,你說我怎么就攤上這事抚垄⊥闪” “怎么了?”我有些...
    開封第一講書人閱讀 163,187評論 0 353
  • 文/不壞的土叔 我叫張陵呆馁,是天一觀的道長桐经。 經(jīng)常有香客問我,道長浙滤,這世上最難降的妖魔是什么阴挣? 我笑而不...
    開封第一講書人閱讀 58,264評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮纺腊,結(jié)果婚禮上畔咧,老公的妹妹穿的比我還像新娘。我一直安慰自己揖膜,他們只是感情好誓沸,可當(dāng)我...
    茶點故事閱讀 67,289評論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著壹粟,像睡著了一般拜隧。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,231評論 1 299
  • 那天虹蓄,我揣著相機與錄音犀呼,去河邊找鬼。 笑死薇组,一個胖子當(dāng)著我的面吹牛外臂,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播律胀,決...
    沈念sama閱讀 40,116評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼宋光,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了炭菌?” 一聲冷哼從身側(cè)響起罪佳,我...
    開封第一講書人閱讀 38,945評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎黑低,沒想到半個月后赘艳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,367評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡克握,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,581評論 2 333
  • 正文 我和宋清朗相戀三年蕾管,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片菩暗。...
    茶點故事閱讀 39,754評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡掰曾,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出停团,到底是詐尸還是另有隱情旷坦,我是刑警寧澤,帶...
    沈念sama閱讀 35,458評論 5 344
  • 正文 年R本政府宣布佑稠,位于F島的核電站秒梅,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏讶坯。R本人自食惡果不足惜番电,卻給世界環(huán)境...
    茶點故事閱讀 41,068評論 3 327
  • 文/蒙蒙 一岗屏、第九天 我趴在偏房一處隱蔽的房頂上張望辆琅。 院中可真熱鬧,春花似錦这刷、人聲如沸婉烟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,692評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽似袁。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間昙衅,已是汗流浹背扬霜。 一陣腳步聲響...
    開封第一講書人閱讀 32,842評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留而涉,地道東北人著瓶。 一個月前我還...
    沈念sama閱讀 47,797評論 2 369
  • 正文 我出身青樓,卻偏偏與公主長得像啼县,于是被迫代替她去往敵國和親材原。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,654評論 2 354

推薦閱讀更多精彩內(nèi)容