iOS代碼里邏輯分支的處理
我們大致上可以將代碼按執(zhí)行方式分解為三類:Sequence,Selection,Iteration。
Sequence
Sequence即為按前后順序依次執(zhí)行,從第一行按序一直執(zhí)行到第 n 行。比如:
- NSString *name = @"default"; //definition
- name = @"peak"; //assignment
- NSLog(@"name is %@", name); //send message
3 行代碼包含 Definition,Assignment,Send Message 不同類型的指令,但他們被運(yùn)行的時(shí)候作為一個(gè)整體是依照 Sequence 模式依次執(zhí)行。
Selection
Selection即為條件模式,說的簡單一點(diǎn)就是平常我們寫代碼時(shí)所用的 if else,switch。這是我們代碼的邏輯產(chǎn)生分支的地方,也是這篇文章的主題。記得之前讀到過一句話,大意說是當(dāng)我們想要重構(gòu)代碼的時(shí)候,if else 總會(huì)是個(gè)好的著手點(diǎn),或者說 if else 是我們代碼最容易出錯(cuò)的地方。
按我個(gè)人理解,邏輯分支之所以容易出錯(cuò)在于兩點(diǎn)。
其一是所依賴的條件不確定,或者不穩(wěn)定。比如:
- if ([users objectAtIndex:0] == currentUser) {
- ...
- }
看似簡單的條件代碼 [users objectAtIndex:0] == currentUser 會(huì)在各種情況下出錯(cuò),比如 users 當(dāng)中沒有任何元素會(huì)發(fā)生越界,比如 users 已被釋放導(dǎo)致內(nèi)存訪問異常,同樣的情況也會(huì)發(fā)生在 currentUser 身上,一個(gè)條件語句所包含的狀態(tài)越多,出錯(cuò)的可能性也就越大。
其二是遺漏某個(gè)條件分支。比如:
- typedef enum : NSUInteger {
- EUserLoginStatusLoggedIn,
- EUserLoginStatusLoggedOut,
- EUserLoginStatusKickedOut,
- } EUserLoginStatus;
- EUserLoginStatus userStatus;
- ...
- if (userStatus == EUserLoginStatusLoggedIn) {
- ...
- } else if (userStatus == EUserLoginStatusLoggedOut) {
- ...
- }
比如上面代碼忘記處理 EUserLoginStatusKickedOut, 當(dāng)然如果代碼是同一個(gè)人所寫,一般不會(huì)遺漏。但如果代碼交由后面的人維護(hù),EUserLoginStatus 新增了 status,而 if else 的處理有散落的工程的各個(gè)角落,忘記處理新的分支就很容易發(fā)生了。
Iteration
Iteration發(fā)生在我們需要循環(huán)或多次處理某些數(shù)據(jù)的時(shí)候,比如我們常見的 while,for 循環(huán)。iteration 有時(shí)也會(huì)依賴某些數(shù)據(jù)或者某些條件語句,在處理的時(shí)候也會(huì)存在 Selection 語句容易遇到的狀態(tài)不穩(wěn)定問題。
Sequence,Selection,Iteration 可以概括我們所寫的全部代碼。其中 Selection 是最容易出錯(cuò)的地方,也是我個(gè)人平時(shí) review 代碼的重點(diǎn)。
Selection 第一個(gè)所依賴狀態(tài)不穩(wěn)定的問題,多注意數(shù)據(jù)或者對象的生命周期,不可變性,多線程安全即可。
分支遺留
第二個(gè)分支遺漏的問題,出現(xiàn)的概率比大多數(shù)人想象的要高,尤其是隨著項(xiàng)目代碼的膨脹,工程師的更替。所以從代碼層面做一些限制可以有效的避免這一問題出現(xiàn)。
一種常見的做法是針對多分支的邏輯處理,盡量使用 switch 而非 if else,比如工程師 A 先寫了如下代碼:
- // File A
- typedef enum : NSUInteger {
- EUserLoginStatusLoggedIn,
- EUserLoginStatusLoggedOut,
- } EUserLoginStatus;
- // File B
- EUserLoginStatus userStatus;
- ...
- switch (userStatus) {
- case EUserLoginStatusLoggedIn:
- {
- }
- break;
- case EUserLoginStatusLoggedOut:
- {
- }
- break;
- }
之后工程師 B 在 File A 中又加了一種 enum 值 EUserLoginStatusKickedOut,那么此時(shí)編譯器會(huì)以警告的方式,幫助我們檢查遺漏的類型,這里的關(guān)鍵在于寫 switch 時(shí)不要寫 default case,否則編譯器會(huì)認(rèn)為新增的 enum 值有默認(rèn)的處理邏輯了。
如果沒寫 default case,Xcode 會(huì)給出如下警告:
這幾乎可以看做是 iOS 下處理邏輯分支的 best practice 了。
Match
除此之外,我們還有另一種更“激進(jìn)”的方式來避免這類問題,match pattern。過去一年看到越來越多的代碼采用這種方式。使用 match pattern 代碼如下:
- // File A
- typedef enum : NSUInteger {
- EUserLoginStatusLoggedIn,
- EUserLoginStatusLoggedOut,
- } EUserLoginStatus;
- // File B
- typedef void (^UserLoggedInBlock)(void);
- typedef void (^UserLoggedoutBlock)(void);
- - (void)someMatchUserStatusLogic
- {
- [self matchUserStatusLoggedIn:^{
- //...
- } loggedOut:^{
- //...
- }];
- }
- - (void)matchUserStatusLoggedIn:(UserLoggedInBlock)loggedInBlock loggedOut:(UserLoggedoutBlock)loggedoutBlock
- {
- EUserLoginStatus userStatus = EUserLoginStatusLoggedIn;
- switch (userStatus) {
- case EUserLoginStatusLoggedIn:
- {
- loggedInBlock();
- }
- break;
- case EUserLoginStatusLoggedOut:
- {
- loggedoutBlock();
- }
- break;
- }
- }
這種方式在 switch 的基礎(chǔ)之上再封裝了一層函數(shù)調(diào)用,將分支的處理寫進(jìn)函數(shù)簽名里面,好處很明顯,當(dāng)你新增 EUserLoginStatusKickedOut case 的時(shí)候,只要更改 matchUserStatusLoggedIn 函數(shù),新增一個(gè)參數(shù):
- // File B
- typedef void (^UserLoggedInBlock)(void);
- typedef void (^UserLoggedoutBlock)(void);
- typedef void (^UserKickedoutBlock)(void);
- - (void)matchUserStatusLoggedIn:(UserLoggedInBlock)loggedInBlock loggedOut:(UserLoggedoutBlock)loggedoutBlock kickedOut:(UserKickedoutBlock)kickedoutBlock;
那么所有被影響的代碼只要一編譯都會(huì)報(bào)錯(cuò),改起來相當(dāng)方便,相比較于 warning,compile error 顯然更能借助編譯器來避免我們代碼上的分支遺漏。即使代碼被第二個(gè)人接手,改動(dòng)起來也一目了然。
這種寫法如果不明白目的所在,第一眼看上去顯得笨重且多余。我個(gè)人感覺,有時(shí)候如果多寫的代碼模式固定且簡單容易理解,同時(shí)這種多出來的代碼可以讓邏輯更健壯,那么這些多余的代碼就并不多余。尤其是當(dāng)項(xiàng)目代碼量過于龐大且參與人數(shù)眾多的情況下,優(yōu)質(zhì)的代碼書寫避免代碼產(chǎn)生意料之外的降級。