除了極少數(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ù)!除非您的自定義宏依賴(lài)于 Xcode 預(yù)處理器宏(如__LINE__),否則請(qǐng)將其重寫(xiě)為一個(gè)獨(dú)立函數(shù)。(即便依賴(lài)于 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ù)字面值是值得稱(chēng)贊的。而使用 #define 創(chuàng)建常量則不值得稱(chēng)贊。
Smell
#define kTimeoutInterval 90.0
如果一個(gè)常量只在單個(gè)文件中使用,則應(yīng)將其設(shè)置為靜態(tài)常量。我們賦予常量一個(gè)明確的類(lèi)型,增加了它的語(yǔ)義。如果你愿意,數(shù)字字面的表達(dá)也可以更簡(jiǎn)單,因?yàn)轱@式類(lèi)型明確了可接受的值域。下面就是我們得到的結(jié)果:
static const NSTimeInterval kTimeoutInterval = 90;
如果一個(gè)常量是跨文件共享的,那么就像處理其他文件一樣:在頭文件中創(chuàng)建一個(gè)聲明,在一個(gè)實(shí)現(xiàn)文件中創(chuàng)建一個(gè)定義。(當(dāng)然,你要遵循蘋(píng)果公司的編碼指南,在名稱(chē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è)單元格。......這就是枚舉類(lèi)型的作用。
enum {
firstNameRow,
lastNameRow,
address1Row,
cityRow,
// etc.
};
枚舉類(lèi)型可以方便地重新排列順序或添加新值。一般來(lái)說(shuō),人們使用 #define 是因?yàn)闃?gòu)造一個(gè)危險(xiǎn)的宏比構(gòu)造一個(gè)安全的常量更容易。但在這里,語(yǔ)言所提供的不僅更安全,而且更簡(jiǎn)單。
枚舉類(lèi)型不必命名。但如果將這些值作為參數(shù)傳遞,就需要定義一個(gè)類(lèi)型名,以增加編譯器檢查和語(yǔ)義。與其在所有需要使用 Address 枚舉類(lèi)型的地方都寫(xiě) enum Address,不如創(chuàng)建一個(gè)這樣的類(lèi)型定義:
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)包含了注釋?zhuān)@種方法就不起作用了。怎么辦呢?當(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)性編碼?;蛘吣阆肟焖僭趦煞N方法之間來(lái)回切換。這很好。
但在某些時(shí)候,我們會(huì)做出決定。實(shí)驗(yàn)方法得到驗(yàn)證,你就可以準(zhǔn)備發(fā)貨了。自行清理之后!除非有重要的歷史原因需要將被拒絕的代碼作為注釋保留,否則請(qǐng)將其刪除。如果您選擇保留,請(qǐng)刪除 Xcode 預(yù)處理器宏。將它變成真正的注釋?zhuān)⒏缴辖忉專(zhuān)粌H僅是代碼。
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è)類(lèi),然后通過(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)目特定代碼的方法改寫(xiě)為模板方法(Template Methods),由項(xiàng)目特定的子類(lèi)提供項(xiàng)目特定的操作。
步驟
- 為每個(gè)項(xiàng)目變量創(chuàng)建一個(gè)子類(lèi)。
- 在每個(gè)項(xiàng)目中,為該項(xiàng)目添加子類(lèi)。
- 編譯每個(gè)項(xiàng)目。
- 創(chuàng)建一個(gè)工廠方法,使用
#if創(chuàng)建正確的子類(lèi)。(我們引入預(yù)處理器的一種用法,這樣就可以消除其他用法)。 - 找到每個(gè)實(shí)例化原始類(lèi)的地方。讓它調(diào)用工廠方法。
- 編譯和測(cè)試每個(gè)項(xiàng)目。
- 對(duì)于每個(gè)有條件編譯的部分:
- 執(zhí)行提取方法,確定所需的簽名。
- 將主體的每個(gè)平臺(tái)特定部分向下移動(dòng)到平臺(tái)特定子類(lèi),直到基類(lèi)的方法為空。
- 編譯和測(cè)試每個(gè)項(xiàng)目。
- 查找每個(gè)子類(lèi)內(nèi)部以及子類(lèi)之間的重復(fù)代碼。
如果你的代碼中存在多個(gè)特定于平臺(tái)的子類(lè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
侵刪