除了極少數(shù)例外权谁,使用 Xcode 預(yù)處理器宏是一種代碼氣味贞谓。C++ 程序員們已經(jīng)深有體會(huì):"不要使用預(yù)處理器來(lái)做語(yǔ)言本身提供的事情"铲汪。不幸的是斤富,還有很多的 Objective-C 程序員尚未領(lǐng)悟到這一點(diǎn)膏潮。
本文是Objective-C 中的代碼氣味系列文章中的一篇。
這是一個(gè)可以在終端運(yùn)行的便捷命令满力。它可以檢查并顯示當(dāng)前目錄下的源文件焕参,預(yù)處理器宏的使用情況轻纪,你應(yīng)該仔細(xì)檢查。
find . \( \( -name "*.[chm]" -o -name "*.mm" \) -o -name "*.cpp" \) -print0 | xargs -0 egrep -n '^\w*\#' | egrep -v '(import|pragma|else|endif)'
該命令包含一些例外情況叠纷。例如刻帚,#import
指令至關(guān)重要。......但我想對(duì)幾乎所有其他內(nèi)容提出質(zhì)疑涩嚣!這有什么關(guān)系呢崇众?因?yàn)槊看问褂妙A(yù)處理器時(shí),你看到的并不是你編譯的內(nèi)容航厚。對(duì)于作為常量使用的 #define
宏校摩,我們需要避免一些陷阱——其實(shí)我們完全可以避免這些陷阱。
以下是一些常見(jiàn)的 Xcode 預(yù)處理器宏阶淘,以及如何替換它們:
1、#include
讓我們從傳統(tǒng) C 中的一個(gè)簡(jiǎn)單例子開(kāi)始:
Smell
#include "foo.h"
除非您提供的是平臺(tái)無(wú)關(guān)的 C 或 C++ 代碼互妓,否則沒(méi)有理由使用 #include
以及與之一起的 include guards溪窒。使用 #import
可以省去那些 include guards的 #ifndef
。
2冯勉、Macros - 宏
Smell
#define WIDTH(view) view.frame.size.width
使用 Objective-C 并不意味著不能使用普通的 C 語(yǔ)言函數(shù)澈蚌!除非您的自定義宏依賴于 Xcode 預(yù)處理器宏(如__LINE__
),否則請(qǐng)將其重寫為一個(gè)獨(dú)立函數(shù)灼狰。(即便依賴于 Xcode 預(yù)處理宏宛瞄,也要讓您的宏調(diào)用另一個(gè)函數(shù),并盡可能多地轉(zhuǎn)移到該函數(shù)中)交胚。
C 語(yǔ)言和 C++ 的有一些相似的地方份汗。其中之一就是內(nèi)聯(lián)函數(shù)的能力:
static inline CGFloat width(UIView *view) { return view.frame.size.width; }
3、常量:數(shù)字常量
現(xiàn)在蝴簇,我們開(kāi)始使用一組圍繞常量的 Xcode 預(yù)處理器宏杯活。使用常量而不是重復(fù)字面值是值得稱贊的。而使用 #define
創(chuàng)建常量則不值得稱贊熬词。
Smell
#define kTimeoutInterval 90.0
如果一個(gè)常量只在單個(gè)文件中使用旁钧,則應(yīng)將其設(shè)置為靜態(tài)常量。我們賦予常量一個(gè)明確的類型互拾,增加了它的語(yǔ)義歪今。如果你愿意,數(shù)字字面的表達(dá)也可以更簡(jiǎn)單颜矿,因?yàn)轱@式類型明確了可接受的值域寄猩。下面就是我們得到的結(jié)果:
static const NSTimeInterval kTimeoutInterval = 90;
如果一個(gè)常量是跨文件共享的,那么就像處理其他文件一樣:在頭文件中創(chuàng)建一個(gè)聲明或衡,在一個(gè)實(shí)現(xiàn)文件中創(chuàng)建一個(gè)定義焦影。(當(dāng)然车遂,你要遵循蘋果公司的編碼指南,在名稱上使用前綴斯辰,對(duì)嗎舶担?)因此,.h 文件中將包含如下聲明:
extern const NSTimeInterval JMRTimeoutInterval;
.m文件中有定義:
const NSTimeInterval JMRTimeoutInterval = 90;
4彬呻、常量: 升序整數(shù)常量
Smell
#define firstNameRow 0
#define lastNameRow 1
#define address1Row 2
#define cityRow 3
// etc.
升序整數(shù)常量在編碼表格視圖時(shí)非常方便衣陶,可以確定哪些信息屬于哪個(gè)單元格。......這就是枚舉類型的作用闸氮。
enum {
firstNameRow,
lastNameRow,
address1Row,
cityRow,
// etc.
};
枚舉類型可以方便地重新排列順序或添加新值剪况。一般來(lái)說(shuō),人們使用 #define
是因?yàn)闃?gòu)造一個(gè)危險(xiǎn)的宏比構(gòu)造一個(gè)安全的常量更容易蒲跨。但在這里译断,語(yǔ)言所提供的不僅更安全,而且更簡(jiǎn)單或悲。
枚舉類型不必命名孙咪。但如果將這些值作為參數(shù)傳遞,就需要定義一個(gè)類型名巡语,以增加編譯器檢查和語(yǔ)義翎蹈。與其在所有需要使用 Address
枚舉類型的地方都寫 enum Address
,不如創(chuàng)建一個(gè)這樣的類型定義:
typedef enum {
firstNameRow,
lastNameRow,
address1Row,
cityRow,
// etc.
} AddressRow;
5男公、常量:字符串常量
Smell
#define JMRResponseSuccess @"Success"
與數(shù)字常量一樣荤堪,使用語(yǔ)言來(lái)定義常量。只不過(guò)枢赔,這次我們定義的是一個(gè)常量字符串澄阳,它實(shí)際上是一個(gè)對(duì)象,在 Objective-C 中表示為指針踏拜。因此寇荧,我們要定義一個(gè)常量指針。
常量字符串通常在多個(gè)文件中共享执隧,因此這里介紹如何在 .h 文件中聲明常量:
extern NSString *const JMRResponseSuccess;
因此揩抡,.m 文件中的定義是
NSString *const JMRResponseSuccess = @"Success";
6、條件編譯:注釋代碼
各種形式的條件編譯(#if
镀琉、#ifdef
等)是一種選擇性啟用或禁用代碼塊的方法峦嗤。它用于不同的目的,但始終是一種屋摔。
Smell
#if 0
…
#endif
在以前的 C 語(yǔ)言中烁设,唯一的注釋形式是 /* ... */。要注釋一段代碼,可以在前面加上 /*装黑,在后面加上 */副瀑。后來(lái)有人發(fā)現(xiàn),如果代碼中已經(jīng)包含了注釋恋谭,這種方法就不起作用了糠睡。怎么辦呢?當(dāng)時(shí)的答案是使用預(yù)處理器:用 #if 0
封裝代碼就可以了疚颊。
但那是很久以前的事了狈孔,那時(shí)還沒(méi)有現(xiàn)代集成開(kāi)發(fā)環(huán)境和彩色編碼方式。顏色編碼可以幫助我們更直觀地解析代碼......但在這種情況下并不適用材义。盡管在這種情況下有一個(gè) 0均抽,但一般來(lái)說(shuō),集成開(kāi)發(fā)環(huán)境無(wú)法知道是否要顯示條件編譯刪除了源文件中的某段代碼其掂。因此油挥,沒(méi)有任何可視化指示器顯示代碼被注釋掉了!它看起來(lái)就像其他代碼一樣款熬。
C 和 Xcode 快速發(fā)展到今天喘漏。C 語(yǔ)言不斷發(fā)展,并采用了 C++ 的 //
注釋風(fēng)格华烟。Xcode 充分利用了這一點(diǎn),并在菜單中提供了 "注釋選擇 "命令持灰。只需按?/
即可注釋出代碼的一部分:Xcode 會(huì)在每一行的開(kāi)頭添加 //
并用顏色標(biāo)記為注釋盔夜。再次按下 ?/
,過(guò)程就會(huì)逆轉(zhuǎn)堤魁,代碼就會(huì)恢復(fù)原狀喂链。
因此,Xcode 可以輕松啟用和禁用代碼妥泉。但還有一個(gè)問(wèn)題椭微,我們將在下一節(jié)中討論:如果注釋掉的代碼是臨時(shí)性的,并且您計(jì)劃很快將其清理干凈盲链,那么注釋掉代碼是沒(méi)有問(wèn)題的蝇率。但通常情況下,這些代碼會(huì)被丟在那里任其腐爛......
7刽沾、條件編譯:在實(shí)驗(yàn)之間切換
Smell
#if EXPERIMENT
…
#else
…
#endif
有時(shí)本慕,您需要進(jìn)行實(shí)驗(yàn)性編碼〔嗬欤或者你想快速在兩種方法之間來(lái)回切換锅尘。這很好。
但在某些時(shí)候布蔗,我們會(huì)做出決定藤违。實(shí)驗(yàn)方法得到驗(yàn)證浪腐,你就可以準(zhǔn)備發(fā)貨了。自行清理之后顿乒!除非有重要的歷史原因需要將被拒絕的代碼作為注釋保留议街,否則請(qǐng)將其刪除。如果您選擇保留淆游,請(qǐng)刪除 Xcode 預(yù)處理器宏傍睹。將它變成真正的注釋,并附上解釋犹菱,而不僅僅是代碼拾稳。
8、 條件編譯: 在暫存和生產(chǎn) URL 之間切換
Smell
#if STAGING
static NSString *const fooURLString = @"https://dev.foo.com/services/fooservice";
#else
static NSString *const fooURLString = @"https://foo.com/services/fooservice;
#endif
當(dāng)你開(kāi)發(fā)基于服務(wù)的應(yīng)用程序時(shí)腊脱,你希望能夠指定是與真正的生產(chǎn)服務(wù)對(duì)話访得,還是與暫存服務(wù)對(duì)話。
對(duì)于只有少量 URL 的簡(jiǎn)單應(yīng)用程序陕凹,我會(huì)為 URL 創(chuàng)建一個(gè)類悍抑,然后通過(guò)方法訪問(wèn)它們:
- (NSString *)fooURLString
{
DebugSettings *debugSettings = [self debugSettings];
if ([debugSettings usingStaging])
return @"https://dev.foo.com/services/fooservice";
else
return @"https://dev.foo.com/services/fooservice";
}
對(duì)于與許多服務(wù)對(duì)話的復(fù)雜應(yīng)用程序,可以考慮將 URL 放入 plist 中杜耙。有關(guān) plist 的示例搜骡,請(qǐng)參閱《我如何在暫存和生產(chǎn) URL 之間切換(How I Switch between Staging and Production URLs)》。
9佑女、條件編譯:支持多個(gè)項(xiàng)目或平臺(tái)
Smell
#if PROJECT_A
…
#else
…
#endif
在多個(gè)項(xiàng)目(或多個(gè)平臺(tái))中共享代碼時(shí)记靡,很容易在共享源文件中偷偷加入特定于項(xiàng)目的擴(kuò)展。這樣做看似方便团驱,但會(huì)污染源代碼摸吠,并掩蓋統(tǒng)一代碼的機(jī)會(huì)。
我們使用的是面向?qū)ο蟮恼Z(yǔ)言嚎花,所以讓我們使用 OO 模式寸痢,好嗎?基本策略是將包含項(xiàng)目特定代碼的方法改寫為模板方法(Template Methods)紊选,由項(xiàng)目特定的子類提供項(xiàng)目特定的操作啼止。
步驟
- 為每個(gè)項(xiàng)目變量創(chuàng)建一個(gè)子類。
- 在每個(gè)項(xiàng)目中兵罢,為該項(xiàng)目添加子類族壳。
- 編譯每個(gè)項(xiàng)目。
- 創(chuàng)建一個(gè)工廠方法趣些,使用
#if
創(chuàng)建正確的子類仿荆。(我們引入預(yù)處理器的一種用法,這樣就可以消除其他用法)。 - 找到每個(gè)實(shí)例化原始類的地方拢操。讓它調(diào)用工廠方法锦亦。
- 編譯和測(cè)試每個(gè)項(xiàng)目。
- 對(duì)于每個(gè)有條件編譯的部分:
- 執(zhí)行提取方法令境,確定所需的簽名杠园。
- 將主體的每個(gè)平臺(tái)特定部分向下移動(dòng)到平臺(tái)特定子類,直到基類的方法為空舔庶。
- 編譯和測(cè)試每個(gè)項(xiàng)目抛蚁。
- 查找每個(gè)子類內(nèi)部以及子類之間的重復(fù)代碼。
如果你的代碼中存在多個(gè)特定于平臺(tái)的子類層次結(jié)構(gòu)惕橙,你可能會(huì)發(fā)現(xiàn)使用橋接模式的機(jī)會(huì)瞧甩。
避免使用 Xcode 預(yù)處理器宏!
請(qǐng)?jiān)俅卧诮K端中執(zhí)行此命令弥鹦,以查找代碼中可能違規(guī)的 Xcode 預(yù)處理器宏肚逸。您找到了多少?能否減少它們彬坏?剩余的宏是否合理朦促?
請(qǐng)記住不要使用 Xcode 預(yù)處理器宏來(lái)做語(yǔ)言本身提供的事情!
譯自 Jon Reid 的 9 Ways You Can Avoid ObjC Xcode Preprocessor Macros
侵刪