寫在前面
在開發(fā)過程中很多時(shí)候需要閱讀第三方源碼,但是里面有大量的宏。沒有換行,沒有著色,與平時(shí)寫的代碼完全不同,還會(huì)層層嵌套,總之各種看不懂。
因此對宏的了解欠缺已經(jīng)是阻礙自身能力提升的一大障礙。于是拜讀了@onevcat這篇文章。
讀完文章后仍有部分存疑此處作為記錄
- 為什么會(huì)出現(xiàn)變量重名的問題
- NSLog的重寫無效
-
__NSX_PASTE__(__a,L)中的__a是作物參數(shù)但并未聲明為何能直接使用
#define __NSX_PASTE__(A,B) A##B
#define MIN(A,B) __NSMIN_IMPL__(A,B,__COUNTER__)
#define __NSMIN_IMPL__(A,B,L) ({ __typeof__(A) __NSX_PASTE__(__a,L) = (A); \
__typeof__(B) __NSX_PASTE__(__b,L) = (B); \
(__NSX_PASTE__(__a,L) < __NSX_PASTE__(__b,L)) ? __NSX_PASTE__(__a,L) : __NSX_PASTE__(__b,L); \
})

宏定義在C系開發(fā)中可以說占有舉足輕重的作用。底層框架自不必說,為了編譯優(yōu)化和方便,以及跨平臺(tái)能力,宏被大量使用,可以說底層開發(fā)離開define將寸步難行。而在更高層級進(jìn)行開發(fā)時(shí),我們會(huì)將更多的重心放在業(yè)務(wù)邏輯上,似乎對宏的使用和依賴并不多。但是使用宏定義的好處是不言自明的,在節(jié)省工作量的同時(shí),代碼可讀性大大增加。如果想成為一個(gè)能寫出漂亮優(yōu)雅代碼的開發(fā)者,宏定義絕對是必不可少的技能(雖然宏本身可能并不漂亮優(yōu)雅XD)。但是因?yàn)楹甓x對于很多人來說,并不像業(yè)務(wù)邏輯那樣是每天會(huì)接觸的東西。即使是能偶爾使用到一些宏,也更多的僅僅只停留在使用的層級,卻并不會(huì)去探尋背后發(fā)生的事情。有一些開發(fā)者確實(shí)也有探尋的動(dòng)力和意愿,但卻在點(diǎn)開一個(gè)定義之后發(fā)現(xiàn)還有宏定義中還有其他無數(shù)定義,再加上滿屏幕都是不同于平時(shí)的代碼,既看不懂又不變色,于是乎心生煩惱,怒而回退。本文希望通過循序漸進(jìn)的方式,通過幾個(gè)例子來表述C系語言宏定義世界中的一些基本規(guī)則和技巧,從0開始,希望最后能讓大家至少能看懂和還原一些相對復(fù)雜的宏??紤]到我自己現(xiàn)在objc使用的比較多,這個(gè)站點(diǎn)的讀者應(yīng)該也大多是使用objc的,所以有部分例子是選自objc,但是本文的大部分內(nèi)容將是C系語言通用。
入門
如果您完全不知道宏是什么的話,可以先來熱個(gè)身。很多人在介紹宏的時(shí)候會(huì)說,宏嘛很簡單,就是簡單的查找替換嘛。嗯,只說對了的一半。C中的宏分為兩類,對象宏(object-like macro)和函數(shù)宏(function-like macro)。對于對象宏來說確實(shí)相對簡單,但卻也不是那么簡單的查找替換。對象宏一般用來定義一些常數(shù),舉個(gè)例子:
//This defines PI
#define M_PI 3.14159265358979323846264338327950288
#define關(guān)鍵字表明即將開始定義一個(gè)宏,緊接著的M_PI是宏的名字,空格之后的數(shù)字是內(nèi)容。類似這樣的#define X A的宏是比較簡單的,在編譯時(shí)編譯器會(huì)在語義分析認(rèn)定是宏后,將X替換為A,這個(gè)過程稱為宏的展開。比如對于上面的M_PI
#define M_PI 3.14159265358979323846264338327950288
double r = 10.0;
double circlePerimeter = 2 * M_PI * r;
// => double circlePerimeter = 2 * 3.14159265358979323846264338327950288 * r;
printf("Pi is %0.7f",M_PI);
//Pi is 3.1415927
那么讓我們開始看看另一類宏吧。函數(shù)宏顧名思義,就是行為類似函數(shù),可以接受參數(shù)的宏。具體來說,在定義的時(shí)候,如果我們在宏名字后面跟上一對括號的話,這個(gè)宏就變成了函數(shù)宏。從最簡單的例子開始,比如下面這個(gè)函數(shù)宏
//A simple function-like macro
#define SELF(x) x
NSString *name = @"Macro Rookie";
NSLog(@"Hello %@",SELF(name));
// => NSLog(@"Hello %@",name);
// => Hello Macro Rookie
這個(gè)宏做的事情是,在編譯時(shí)如果遇到SELF,并且后面帶括號,并且括號中的參數(shù)個(gè)數(shù)與定義的相符,那么就將括號中的參數(shù)換到定義的內(nèi)容里去,然后替換掉原來的內(nèi)容。 具體到這段代碼中,SELF接受了一個(gè)name,然后將整個(gè)SELF(name)用name替換掉。嗯..似乎很簡單很沒用,身經(jīng)百戰(zhàn)閱碼無數(shù)的你一定會(huì)認(rèn)為這個(gè)宏是寫出來賣萌的。那么接受多個(gè)參數(shù)的宏肯定也不在話下了,例如這樣的:
#define PLUS(x,y) x + y
printf("%d",PLUS(3,2));
// => printf("%d",3 + 2);
// => 5
相比對象宏來說,函數(shù)宏要復(fù)雜一些,但是看起來也相當(dāng)簡單吧?嗯,那么現(xiàn)在熱身結(jié)束,讓我們正式開啟宏的大門吧。
宏的世界,小有乾坤
因?yàn)楹暾归_其實(shí)是編輯器的預(yù)處理,因此它可以在更高層級上控制程序源碼本身和編譯流程。而正是這個(gè)特點(diǎn),賦予了宏很強(qiáng)大的功能和靈活度。但是凡事都有兩面性,在獲取靈活的背后,是以需要大量時(shí)間投入以對各種邊界情況進(jìn)行考慮來作為代價(jià)的。可能這么說并不是很能讓人理解,但是大部分宏(特別是函數(shù)宏)背后都有一些自己的故事,挖掘這些故事和設(shè)計(jì)的思想會(huì)是一件很有意思的事情。另外,我一直相信在實(shí)踐中學(xué)習(xí)才是真正掌握知識(shí)的唯一途徑,雖然可能正在看這篇博文的您可能最初并不是打算親自動(dòng)手寫一些宏,但是這我們不妨開始動(dòng)手從實(shí)際的書寫和犯錯(cuò)中進(jìn)行學(xué)習(xí)和挖掘,因?yàn)橹挥屑∪庥洃浐痛竽X記憶協(xié)同起來,才能說達(dá)到掌握的水準(zhǔn)。可以說,寫宏和用宏的過程,一定是在在犯錯(cuò)中學(xué)習(xí)和深入思考的過程,我們接下來要做的,就是重現(xiàn)這一系列過程從而提高進(jìn)步。
第一個(gè)題目是,讓我們一起來實(shí)現(xiàn)一個(gè)MIN宏吧:實(shí)現(xiàn)一個(gè)函數(shù)宏,給定兩個(gè)數(shù)字輸入,將其替換為較小的那個(gè)數(shù)。比如MIN(1,2)出來的值是1。嗯哼,simple enough?定義宏,寫好名字,兩個(gè)輸入,然后換成比較取值。比較取值嘛,任何一本入門級別的C程序設(shè)計(jì)上都會(huì)有講啊,于是我們可以很快寫出我們的第一個(gè)版本:
//Version 1.0
#define MIN(A,B) A < B ? A : B
Try一下
int a = MIN(1,2);
// => int a = 1 < 2 ? 1 : 2;
printf("%d",a);
// => 1
輸出正確,打包發(fā)布!

但是在實(shí)際使用中,我們很快就遇到了這樣的情況
int a = 2 * MIN(3, 4);
printf("%d",a);
// => 4
看起來似乎不可思議,但是我們將宏展開就知道發(fā)生什么了
int a = 2 * MIN(3, 4);
// => int a = 2 * 3 < 4 ? 3 : 4;
// => int a = 6 < 4 ? 3 : 4;
// => int a = 4;
嘛,寫程序這個(gè)東西,bug出來了,原因知道了,事后大家就都是諸葛亮了。因?yàn)樾∮诤捅容^符號的優(yōu)先級是較低的,所以乘法先被運(yùn)算了,修正非常簡單嘛,加括號就好了。
//Version 2.0
#define MIN(A,B) (A < B ? A : B)
這次2 * MIN(3, 4)這樣的式子就輕松愉快地拿下了。經(jīng)過了這次修改,我們對自己的宏信心大增了…直到,某一天一個(gè)怒氣沖沖的同事跑來摔鍵盤,然后給出了一個(gè)這樣的例子:
int a = MIN(3, 4 < 5 ? 4 : 5);
printf("%d",a);
// => 4
簡單的相比較三個(gè)數(shù)字并找到最小的一個(gè)而已,要怪就怪你沒有提供三個(gè)數(shù)字比大小的宏,可憐的同事只好自己實(shí)現(xiàn)4和5的比較。在你開始著手解決這個(gè)問題的時(shí)候,你首先想到的也許是既然都是求最小值,那寫成MIN(3, MIN(4, 5))是不是也可以。于是你就隨手這樣一改,發(fā)現(xiàn)結(jié)果變成了3,正是你想要的..接下來,開始懷疑之前自己是不是看錯(cuò)結(jié)果了,改回原樣,一個(gè)4赫然出現(xiàn)在屏幕上。你終于意識(shí)到事情并不是你想像中那樣簡單,于是還是回到最原始直接的手段,展開宏。
int a = MIN(3, 4 < 5 ? 4 : 5);
// => int a = (3 < 4 < 5 ? 4 : 5 ? 3 : 4 < 5 ? 4 : 5); //希望你還記得運(yùn)算符優(yōu)先級
// => int a = ((3 < (4 < 5 ? 4 : 5) ? 3 : 4) < 5 ? 4 : 5); //為了您不太糾結(jié),我給這個(gè)式子加上了括號
// => int a = ((3 < 4 ? 3 : 4) < 5 ? 4 : 5)
// => int a = (3 < 5 ? 4 : 5)
// => int a = 4
找到問題所在了,由于展開時(shí)連接符號和被展開式子中的運(yùn)算符號優(yōu)先級相同,導(dǎo)致了計(jì)算順序發(fā)生了變化,實(shí)質(zhì)上和我們的1.0版遇到的問題是差不多的,還是考慮不周。那么就再嚴(yán)格一點(diǎn)吧,3.0版!
//Version 3.0
#define MIN(A,B) ((A) < (B) ? (A) : (B))
至于為什么2.0版本中的MIN(3, MIN(4, 5))沒有出問題,可以正確使用,這里作為練習(xí),大家可以試著自己展開一下,來看看發(fā)生了什么。
經(jīng)過兩次悲劇,你現(xiàn)在對這個(gè)簡單的宏充滿了疑惑。于是你跑了無數(shù)的測試用例而且它們都通過了,我們似乎徹底解決了括號問題,你也認(rèn)為從此這個(gè)宏就妥妥兒的哦了。不過如果你真的這么想,那你就圖樣圖森破了。生活總是殘酷的,該來的bug也一定是會(huì)來的。不出意外地,在一個(gè)霧霾陰沉的下午,我們又收到了一個(gè)出問題的例子。
float a = 1.0f;
float b = MIN(a++, 1.5f);
printf("a=%f, b=%f",a,b);
// => a=3.000000, b=2.000000
拿到這個(gè)出問題的例子你的第一反應(yīng)可能和我一樣,這TM的誰這么二貨還在比較的時(shí)候搞++,這簡直亂套了!但是這樣的人就是會(huì)存在,這樣的事就是會(huì)發(fā)生,你也不能說人家邏輯有錯(cuò)誤。a是1,a++表示先使用a的值進(jìn)行計(jì)算,然后再加1。那么其實(shí)這個(gè)式子想要計(jì)算的是取a和b的最小值,然后a等于a加1:所以正確的輸出a為2,b為1才對!嘛,滿眼都是淚,讓我們這些久經(jīng)摧殘的程序員淡定地展開這個(gè)式子,來看看這次又發(fā)生了些什么吧:
float a = 1.0f;
float b = MIN(a++, 1.5f);
// => float b = ((a++) < (1.5f) ? (a++) : (1.5f))
其實(shí)只要展開一步就很明白了,在比較a++和1.5f的時(shí)候,先取1和1.5比較,然后a自增1。接下來?xiàng)l件比較得到真以后又觸發(fā)了一次a++,此時(shí)a已經(jīng)是2,于是b得到2,最后a再次自增后值為3。出錯(cuò)的根源就在于我們預(yù)想的是a++只執(zhí)行一次,但是由于宏展開導(dǎo)致了a++被多執(zhí)行了,改變了預(yù)想的邏輯。解決這個(gè)問題并不是一件很簡單的事情,使用的方式也很巧妙。我們需要用到一個(gè)GNU C的賦值擴(kuò)展,即使用({...})的形式。這種形式的語句可以類似很多腳本語言,在順次執(zhí)行之后,會(huì)將最后一次的表達(dá)式的賦值作為返回。舉個(gè)簡單的例子,下面的代碼執(zhí)行完畢后a的值為3,而且b和c只存在于大括號限定的代碼域中
int a = ({
int b = 1;
int c = 2;
b + c;
});
// => a is 3
有了這個(gè)擴(kuò)展,我們就能做到之前很多做不到的事情了。比如徹底解決MIN宏定義的問題,而也正是GNU C中MIN的標(biāo)準(zhǔn)寫法
//GNUC MIN
#define MIN(A,B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __a : __b; })
這里定義了三個(gè)語句,分別以輸入的類型申明了__a和__b,并使用輸入為其賦值,接下來做一個(gè)簡單的條件比較,得到__a和__b中的較小值,并使用賦值擴(kuò)展將結(jié)果作為返回。這樣的實(shí)現(xiàn)保證了不改變原來的邏輯,先進(jìn)行一次賦值,也避免了括號優(yōu)先級的問題,可以說是一個(gè)比較好的解決方案了。如果編譯環(huán)境支持GNU C的這個(gè)擴(kuò)展,那么毫無疑問我們應(yīng)該采用這種方式來書寫我們的MIN宏,如果不支持這個(gè)環(huán)境擴(kuò)展,那我們只有人為地規(guī)定參數(shù)不帶運(yùn)算或者函數(shù)調(diào)用,以避免出錯(cuò)。
關(guān)于MIN我們討論已經(jīng)夠多了,但是其實(shí)還存留一個(gè)懸疑的地方。如果在同一個(gè)scope內(nèi)已經(jīng)有__a或者__b的定義的話(雖然一般來說不會(huì)出現(xiàn)這種悲劇的命名,不過誰知道呢),這個(gè)宏可能出現(xiàn)問題。在申明后賦值將因?yàn)槎x重復(fù)而無法被初始化,導(dǎo)致宏的行為不可預(yù)知。如果您有興趣,不妨自己動(dòng)手試試看結(jié)果會(huì)是什么。Apple在Clang中徹底解決了這個(gè)問題,我們把Xcode打開隨便建一個(gè)新工程,在代碼中輸入MIN(1,1),然后Cmd+點(diǎn)擊即可找到clang中 MIN的寫法。為了方便說明,我直接把相關(guān)的部分抄錄如下:
//CLANG MIN
#define __NSX_PASTE__(A,B) A##B
#define MIN(A,B) __NSMIN_IMPL__(A,B,__COUNTER__)
#define __NSMIN_IMPL__(A,B,L) ({ __typeof__(A) __NSX_PASTE__(__a,L) = (A); __typeof__(B) __NSX_PASTE__(__b,L) = (B); (__NSX_PASTE__(__a,L) < __NSX_PASTE__(__b,L)) ? __NSX_PASTE__(__a,L) : __NSX_PASTE__(__b,L); })
似乎有點(diǎn)長,看起來也很吃力。我們先美化一下這宏,首先是最后那個(gè)__NSMIN_IMPL__內(nèi)容實(shí)在是太長了。我們知道代碼的話是可以插入換行而不影響含義的,宏是否也可以呢?答案是肯定的,只不過我們不能使用一個(gè)單一的回車來完成,而必須在回車前加上一個(gè)反斜杠\。改寫一下,為其加上換行好看些:
#define __NSX_PASTE__(A,B) A##B
#define MIN(A,B) __NSMIN_IMPL__(A,B,__COUNTER__)
#define __NSMIN_IMPL__(A,B,L) ({ __typeof__(A) __NSX_PASTE__(__a,L) = (A); \
__typeof__(B) __NSX_PASTE__(__b,L) = (B); \
(__NSX_PASTE__(__a,L) < __NSX_PASTE__(__b,L)) ? __NSX_PASTE__(__a,L) : __NSX_PASTE__(__b,L); \
})
但可以看出MIN一共由三個(gè)宏定義組合而成。第一個(gè)__NSX_PASTE__里出現(xiàn)的兩個(gè)連著的井號##在宏中是一個(gè)特殊符號,它表示將兩個(gè)參數(shù)連接起來這種運(yùn)算。注意函數(shù)宏必須是有意義的運(yùn)算,因此你不能直接寫AB來連接兩個(gè)參數(shù),而需要寫成例子中的A##B。宏中還有一切其他的自成一脈的運(yùn)算符號,我們稍后還會(huì)介紹幾個(gè)。接下來是我們調(diào)用的兩個(gè)參數(shù)的MIN,它做的事是調(diào)用了另一個(gè)三個(gè)參數(shù)的宏__NSMIN_IMPL__,其中前兩個(gè)參數(shù)就是我們的輸入,而第三個(gè)__COUNTER__我們似乎不認(rèn)識(shí),也不知道其從何而來。其實(shí)__COUNTER__是一個(gè)預(yù)定義的宏,這個(gè)值在編譯過程中將從0開始計(jì)數(shù),每次被調(diào)用時(shí)加1。因?yàn)槲ㄒ恍?,所以很多時(shí)候被用來構(gòu)造獨(dú)立的變量名稱。有了上面的基礎(chǔ),再來看最后的實(shí)現(xiàn)宏就很簡單了。整體思路和前面的實(shí)現(xiàn)和之前的GNUC MIN是一樣的,區(qū)別在于為變量名__a和__b添加了一個(gè)計(jì)數(shù)后綴,這樣大大避免了變量名相同而導(dǎo)致問題的可能性(當(dāng)然如果你執(zhí)拗地把變量叫做__a9527并且出問題了的話,就只能說不作死就不會(huì)死了)。
花了好多功夫,我們終于把一個(gè)簡單的MIN宏徹底搞清楚了。宏就是這樣一類東西,簡單的表面之下隱藏了很多玄機(jī),可謂小有乾坤。作為練習(xí)大家可以自己嘗試一下實(shí)現(xiàn)一個(gè)SQUARE(A),給一個(gè)數(shù)字輸入,輸出它的平方的宏。雖然一般這個(gè)計(jì)算現(xiàn)在都是用inline來做了,但是通過和MIN類似的思路我們是可以很好地實(shí)現(xiàn)它的,動(dòng)手試一試吧 :)
Log,永恒的主題
Log人人愛,它為我們指明前進(jìn)方向,它為我們抓蟲提供幫助。在objc中,我們最多使用的log方法就是NSLog輸出信息到控制臺(tái)了,但是NSLog的標(biāo)準(zhǔn)輸出可謂殘廢,有用信息完全不夠,比如下面這段代碼:
NSArray *array = @[@"Hello", @"My", @"Macro"];
NSLog (@"The array is %@", array);
打印到控制臺(tái)里的結(jié)果是類似這樣的
2014-01-20 11:22:11.835 TestProject[23061:70b] The array is (
Hello,
My,
Macro
)
我們在輸出的時(shí)候關(guān)心什么?除了結(jié)果以外,很多情況下我們會(huì)對這行l(wèi)og的所在的文件位置方法什么的會(huì)比較關(guān)心。在每次NSLog里都手動(dòng)加上方法名字和位置信息什么的無疑是個(gè)笨辦法,而如果一個(gè)工程里已經(jīng)有很多NSLog的調(diào)用了,一個(gè)一個(gè)手動(dòng)去改的話無疑也是噩夢。我們通過宏,可以很簡單地完成對NSLog原生行為的改進(jìn),優(yōu)雅,高效。只需要在預(yù)編譯的pch文件中加上
//A better version of NSLog
#define NSLog(format, ...) do { \
fprintf(stderr, "<%s : %d> %s\n", \
[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], \
__LINE__, __func__); \
(NSLog)((format), ##__VA_ARGS__); \
fprintf(stderr, "-------\n"); \
} while (0)
嘛,這是我們到現(xiàn)在為止見到的最長的一個(gè)宏了吧…沒關(guān)系,一點(diǎn)一點(diǎn)來分析就好。首先是定義部分,第2行的NSLog(format, ...)。我們看到的是一個(gè)函數(shù)宏,但是它的參數(shù)比較奇怪,第二個(gè)參數(shù)是...,在宏定義(其實(shí)也包括函數(shù)定義)的時(shí)候,寫為...的參數(shù)被叫做可變參數(shù)(variadic)。可變參數(shù)的個(gè)數(shù)不做限定。在這個(gè)宏定義中,除了第一個(gè)參數(shù)format將被單獨(dú)處理外,接下來輸入的參數(shù)將作為整體一并看待?;叵胍幌翹SLog的用法,我們在使用NSLog時(shí),往往是先給一個(gè)format字符串作為第一個(gè)參數(shù),然后根據(jù)定義的格式在后面的參數(shù)里跟上寫要輸出的變量之類的。這里第一個(gè)格式化字符串即對應(yīng)宏里的format,后面的變量全部映射為...作為整體處理。
接下來宏的內(nèi)容部分。上來就是一個(gè)下馬威,我們遇到了一個(gè)do while語句…想想看你上次使用do while是什么時(shí)候吧?也許是C程序設(shè)計(jì)課的大作業(yè)?或者是某次早已被遺忘的算法面試上?總之雖然大家都是明白這個(gè)語句的,但是實(shí)際中可能用到它的機(jī)會(huì)少之又少。乍一看似乎這個(gè)do while什么都沒做,因?yàn)閣hile是0,所以do肯定只會(huì)被執(zhí)行一次。那么它存在的意義是什么呢,我們是不是可以直接簡化一下這個(gè)宏,把它給去掉,變成這個(gè)樣子呢?
//A wrong version of NSLog
#define NSLog(format, ...) fprintf(stderr, "<%s : %d> %s\n", \
[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], \
__LINE__, __func__); \
(NSLog)((format), ##__VA_ARGS__); \
fprintf(stderr, "-------\n");
答案當(dāng)然是否定的,也許簡單的測試?yán)锬銢]有遇到問題,但是在生產(chǎn)環(huán)境中這個(gè)宏顯然悲劇了??紤]下面的常見情況
if (errorHappend)
NSLog(@"Oops, error happened");
展開以后將會(huì)變成
if (errorHappend)
fprintf((stderr, "<%s : %d> %s\n",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __LINE__, __func__);
(NSLog)((format), ##__VA_ARGS__); //I will expand this later
fprintf(stderr, "-------\n");
注意..C系語言可不是靠縮進(jìn)來控制代碼塊和邏輯關(guān)系的。所以說如果使用這個(gè)宏的人沒有在條件判斷后加大括號的話,你的宏就會(huì)一直調(diào)用真正的NSLog輸出東西,這顯然不是我們想要的邏輯。當(dāng)然在這里還是需要重新批評一下認(rèn)為if后的單條執(zhí)行語句不加大括號也沒問題的同學(xué),這是陋習(xí),無需理由,請改正。不論是不是一條語句,也不論是if后還是else后,都加上大括號,是對別人和自己的一種尊重。
好了知道我們的宏是如何失效的,也就知道了修改的方法。作為宏的開發(fā)者,應(yīng)該力求使用者在最大限度的情況下也不會(huì)出錯(cuò),于是我們想到直接用一對大括號把宏內(nèi)容括起來,大概就萬事大吉了?像這樣:
//Another wrong version of NSLog
#define NSLog(format, ...) {
fprintf(stderr, "<%s : %d> %s\n", \
[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], \
__LINE__, __func__); \
(NSLog)((format), ##__VA_ARGS__); \
fprintf(stderr, "-------\n"); \
}
展開剛才的那個(gè)式子,結(jié)果是
//I am sorry if you don't like { in the same like. But I am a fan of this style :P
if (errorHappend) {
fprintf((stderr, "<%s : %d> %s\n",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __LINE__, __func__);
(NSLog)((format), ##__VA_ARGS__);
fprintf(stderr, "-------\n");
};
編譯,執(zhí)行,正確!因?yàn)橛么罄ㄌ枠?biāo)識(shí)代碼塊是不會(huì)嫌多的,所以這樣一來的話我們的宏在不論if后面有沒有大括號的情況下都能工作了!這么看來,前面例子中的do while果然是多余的?于是我們又可以愉快地發(fā)布了?如果你夠細(xì)心的話,可能已經(jīng)發(fā)現(xiàn)問題了,那就是上面最后的一個(gè)分號。雖然編譯運(yùn)行測試沒什么問題,但是始終稍微有些刺眼有木有?沒錯(cuò),因?yàn)槲覀冊趯慛SLog本身的時(shí)候,是將其當(dāng)作一條語句來處理的,后面跟了一個(gè)分號,在宏展開后,這個(gè)分號就如同噩夢一般的多出來了。什么,你還沒看出哪兒有問題?試試看展開這個(gè)例子吧:
if (errorHappend)
NSLog(@"Oops, error happened");
else
//Yep, no error, I am happy~ :)
No! I am not haapy at all! 因?yàn)榫幾g錯(cuò)誤了!實(shí)際上這個(gè)宏展開以后變成了這個(gè)樣子:
if (errorHappend) {
fprintf((stderr, "<%s : %d> %s\n",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __LINE__, __func__);
(NSLog)((format), ##__VA_ARGS__);
fprintf(stderr, "-------\n");
}; else {
//Yep, no error, I am happy~ :)
}
因?yàn)閑lse前面多了一個(gè)分號,導(dǎo)致了編譯錯(cuò)誤,很惱火..要是寫代碼的人乖乖寫大括號不就啥事兒沒有了么?但是我們還是有巧妙的解決方法的,那就是上面的do while。把宏的代碼塊添加到do中,然后之后while(0),在行為上沒有任何改變,但是可以巧妙地吃掉那個(gè)悲劇的分號,使用do while的版本展開以后是這個(gè)樣子的
if (errorHappend)
do {
fprintf((stderr, "<%s : %d> %s\n",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __LINE__, __func__);
(NSLog)((format), ##__VA_ARGS__);
fprintf(stderr, "-------\n");
} while (0);
else {
//Yep, no error, I am really happy~ :)
}
這個(gè)吃掉分號的方法被大量運(yùn)用在代碼塊宏中,幾乎已經(jīng)成為了標(biāo)準(zhǔn)寫法。而且while(0)的好處在于,在編譯的時(shí)候,編譯器基本都會(huì)為你做好優(yōu)化,把這部分內(nèi)容去掉,最終編譯的結(jié)果不會(huì)因?yàn)檫@個(gè)do while而導(dǎo)致運(yùn)行效率上的差異。在終于弄明白了這個(gè)奇怪的do while之后,我們終于可以繼續(xù)深入到這個(gè)宏里面了。宏本體內(nèi)容的第一行沒有什么值得多說的fprintf(stderr, "<%s : %d> %s\n",,簡單的格式化輸出而已。注意我們使用了\將這個(gè)宏分成了好幾行來寫,實(shí)際在最后展開時(shí)會(huì)被合并到同一行內(nèi),我們在剛才MIN最后也用到了反斜杠,希望你還能記得。接下來一行我們填寫這個(gè)格式輸出中的三個(gè)token,
[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __LINE__, __func__);
這里用到了三個(gè)預(yù)定義宏,和剛才的__COUNTER__類似,預(yù)定義宏的行為是由編譯器指定的。__FILE__返回當(dāng)前文件的絕對路徑,__LINE__返回展開該宏時(shí)在文件中的行數(shù),__func__是改宏所在scope的函數(shù)名稱。我們在做Log輸出時(shí)如果帶上這這三個(gè)參數(shù),便可以加快解讀Log,迅速定位。關(guān)于編譯器預(yù)定義的Log以及它們的一些實(shí)現(xiàn)機(jī)制,感興趣的同學(xué)可以移步到gcc文檔的PreDefine頁面和clang的Builtin Macro進(jìn)行查看。在這里我們將格式化輸出的三個(gè)參數(shù)分別設(shè)定為文件名的最后一個(gè)部分(因?yàn)榻^對路徑太長很難看),行數(shù),以及方法名稱。
接下來是還原原始的NSLog,(NSLog)((format), ##__VA_ARGS__);中出現(xiàn)了另一個(gè)預(yù)定義的宏__VA_ARGS__(我們似乎已經(jīng)找出規(guī)律了,前后雙下杠的一般都是預(yù)定義)。__VA_ARGS__表示的是宏定義中的...中的所有剩余參數(shù)。我們之前說過可變參數(shù)將被統(tǒng)一處理,在這里展開的時(shí)候編譯器會(huì)將__VA_ARGS__直接替換為輸入中從第二個(gè)參數(shù)開始的剩余參數(shù)。另外一個(gè)懸疑點(diǎn)是在它前面出現(xiàn)了兩個(gè)井號##。還記得我們上面在MIN中的兩個(gè)井號么,在那里兩個(gè)井號的意思是將前后兩項(xiàng)合并,在這里做的事情比較類似,將前面的格式化字符串和后面的參數(shù)列表合并,這樣我們就得到了一個(gè)完整的NSLog方法了。之后的幾行相信大家自己看懂也沒有問題了,最后輸出一下試試看,大概看起來會(huì)是這樣的。
-------
<AppDelegate.m : 46> -[AppDelegate application:didFinishLaunchingWithOptions:]
2014-01-20 16:44:25.480 TestProject[30466:70b] The array is (
Hello,
My,
Macro
)
-------
帶有文件,行號和方法的輸出,并且用橫杠隔開了(請?jiān)徫覜]有質(zhì)感的設(shè)計(jì),也許我應(yīng)該畫一只牛,比如這樣?),debug的時(shí)候也許會(huì)輕松一些吧 :)

這個(gè)Log有三個(gè)懸念點(diǎn),首先是為什么我們要把format單獨(dú)寫出來,然后吧其他參數(shù)作為可變參數(shù)傳遞呢?如果我們不要那個(gè)format,而直接寫成NSLog(...)會(huì)不會(huì)有問題?對于我們這里這個(gè)例子來說的話是沒有變化的,但是我們需要記住的是...是可變參數(shù)列表,它可以代表一個(gè)、兩個(gè),或者是很多個(gè)參數(shù),但同時(shí)它也能代表零個(gè)參數(shù)。如果我們在申明這個(gè)宏的時(shí)候沒有指定format參數(shù),而直接使用參數(shù)列表,那么在使用中不寫參數(shù)的NSLog()也將被匹配到這個(gè)宏中,導(dǎo)致編譯無法通過。如果你手邊有Xcode,也可以看看Cocoa中真正的NSLog方法的實(shí)現(xiàn),可以看到它也是接收一個(gè)格式參數(shù)和一個(gè)參數(shù)列表的形式,我們在宏里這么定義,正是為了其傳入正確合適的參數(shù),從而保證使用者可以按照原來的方式正確使用這個(gè)宏。
第二點(diǎn)是既然我們的可變參數(shù)可以接受任意個(gè)輸入,那么在只有一個(gè)format輸入,而可變參數(shù)個(gè)數(shù)為零的時(shí)候會(huì)發(fā)生什么呢?不妨展開看一看,記住##的作用是拼接前后,而現(xiàn)在##之后的可變參數(shù)是空:
NSLog(@"Hello");
=> do {
fprintf((stderr, "<%s : %d> %s\n",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __LINE__, __func__);
(NSLog)((@"Hello"), );
fprintf(stderr, "-------\n");
} while (0);
中間的一行(NSLog)(@"Hello", );似乎是存在問題的,你一定會(huì)有疑惑,這種方式怎么可能編譯通過呢?!原來大神們其實(shí)早已想到這個(gè)問題,并且進(jìn)行了一點(diǎn)特殊的處理。這里有個(gè)特殊的規(guī)則,在逗號和__VA_ARGS__之間的雙井號,除了拼接前后文本之外,還有一個(gè)功能,那就是如果后方文本為空,那么它會(huì)將前面一個(gè)逗號吃掉。這個(gè)特性當(dāng)且僅當(dāng)上面說的條件成立時(shí)才會(huì)生效,因此可以說是特例。加上這條規(guī)則后,我們就可以將剛才的式子展開為正確的(NSLog)((@"Hello"));了。
最后一個(gè)值得討論的地方是(NSLog)((format), ##__VA_ARGS__);的括號使用。把看起來能去掉的括號去掉,寫成NSLog(format, ##__VA_ARGS__);是否可以呢?在這里的話應(yīng)該是沒有什么大問題的,首先format不會(huì)被調(diào)用多次也不太存在誤用的可能性(因?yàn)樽詈缶幾g器會(huì)檢查NSLog的輸入是否正確)。另外你也不用擔(dān)心展開以后式子里的NSLog會(huì)再次被自己展開,雖然展開式中NSLog也滿足了我們的宏定義,但是宏的展開非常聰明,展開后會(huì)自身無限循環(huán)的情況,就不會(huì)再次被展開了。
作為一個(gè)您讀到了這里的小獎(jiǎng)勵(lì),附送三個(gè)debug輸出rect,size和point的宏,希望您能用上(嗯..想想曾經(jīng)有多少次你需要打印這些結(jié)構(gòu)體的某個(gè)數(shù)字而被折磨致死,讓它們玩兒蛋去吧!當(dāng)然請先加油看懂它們吧)
#define NSLogRect(rect) NSLog(@"%s x:%.4f, y:%.4f, w:%.4f, h:%.4f", #rect, rect.origin.x, rect.origin.y, rect.size.width, rect.size.height)
#define NSLogSize(size) NSLog(@"%s w:%.4f, h:%.4f", #size, size.width, size.height)
#define NSLogPoint(point) NSLog(@"%s x:%.4f, y:%.4f", #point, point.x, point.y)
兩個(gè)實(shí)際應(yīng)用的例子
當(dāng)然不是說上面介紹的宏實(shí)際中不能用。它們相對簡單,但是里面坑不少,所以顯得很有特點(diǎn),非常適合作為入門用。而實(shí)際上在日常中很多我們常用的宏并沒有那么多奇怪的問題,很多時(shí)候我們按照想法去實(shí)現(xiàn),再稍微注意一下上述介紹的可能存在的共通問題,一個(gè)高質(zhì)量的宏就可以誕生。如果能寫出一些有意義價(jià)值的宏,小了從對你的代碼的使用者來說,大了從整個(gè)社區(qū)整個(gè)世界和減少碳排放來說,你都做出了相當(dāng)?shù)呢暙I(xiàn)。我們通過幾個(gè)實(shí)際的例子來看看,宏是如何改變我們的生活,和寫代碼的習(xí)慣的吧。
先來看看這兩個(gè)宏
#define XCTAssertTrue(expression, format...) \
_XCTPrimitiveAssertTrue(expression, ## format)
#define _XCTPrimitiveAssertTrue(expression, format...) \
({ \
@try { \
BOOL _evaluatedExpression = !!(expression); \
if (!_evaluatedExpression) { \
_XCTRegisterFailure(_XCTFailureDescription(_XCTAssertion_True, 0, @#expression),format); \
} \
} \
@catch (id exception) { \
_XCTRegisterFailure(_XCTFailureDescription(_XCTAssertion_True, 1, @#expression, [exception reason]),format); \
}\
})
如果您常年做蘋果開發(fā),卻沒有見過或者完全不知道XCTAssertTrue是什么的話,強(qiáng)烈建議補(bǔ)習(xí)一下測試驅(qū)動(dòng)開發(fā)的相關(guān)知識(shí),我想應(yīng)該會(huì)對您之后的道路很有幫助。如果你已經(jīng)很熟悉這個(gè)命令了,那我們一起開始來看看幕后發(fā)生了什么。
有了上面的基礎(chǔ),相信您大體上應(yīng)該可以自行解讀這個(gè)宏了。({...})的語法和##都很熟悉了,這里有三個(gè)值得注意的地方,在這個(gè)宏的一開始,我們后面的的參數(shù)是format...,這其實(shí)也是可變參數(shù)的一種寫法,和...與__VA_ARGS__配對類似,{NAME}...將于{NAME}配對使用。也就是說,在這里宏內(nèi)容的format指代的其實(shí)就是定義的先對expression取了兩次反?我不是科班出身,但是我還能依稀記得這在大學(xué)程序課上講過,兩次取反的操作可以確保結(jié)果是BOOL值,這在objc中還是比較重要的(關(guān)于objc中BOOL的討論已經(jīng)有很多,如果您還沒能分清BOOL, bool和Boolean,可以參看NSHisper的這篇文章)。然后就是@#expression這個(gè)式子。我們接觸過雙井號##,而這里我們看到的操作符是單井號#,注意井號前面的@是objc的編譯符號,不屬于宏操作的對象。單個(gè)井號的作用是字符串化,簡單來說就是將替換后在兩頭加上”“,轉(zhuǎn)為一個(gè)C字符串。這里使用@然后緊跟#expression,出來后就是一個(gè)內(nèi)容是expression的內(nèi)容的NSString。然后這個(gè)NSString再作為參數(shù)傳遞給_XCTRegisterFailure和_XCTFailureDescription等,繼續(xù)進(jìn)行展開,這些是后話。簡單一瞥,我們大概就可以想象宏幫助我們省了多少事兒了,如果各位看官要是寫個(gè)斷言還要來個(gè)十多行的話,想象都會(huì)瘋掉的吧。
另外一個(gè)例子,找了人民群眾喜聞樂見的ReactiveCocoa(RAC)中的一個(gè)宏定義。對于RAC不熟悉或者沒聽過的朋友,可以簡單地看看Limboy的一系列相關(guān)博文(搜索ReactiveCocoa),介紹的很棒。如果覺得“哇哦這個(gè)好酷我很想學(xué)”的話,不妨可以跟隨raywenderlich上這個(gè)系列的教程做一些實(shí)踐,里面簡單地用到了RAC,但是都已經(jīng)包含了RAC的基本用法了。RAC中有幾個(gè)很重要的宏,它們是保證RAC簡潔好用的基本,可以說要是沒有這幾個(gè)宏的話,是不會(huì)有人喜歡RAC的。其中RACObserve就是其中一個(gè),它通過KVC來為對象的某個(gè)屬性創(chuàng)建一個(gè)信號返回(如果你看不懂這句話,不要擔(dān)心,這對你理解這個(gè)宏的寫法和展開沒有任何影響)。對于這個(gè)宏,我決定不再像上面那樣展開和講解,我會(huì)在最后把相關(guān)的宏都貼出來,大家不妨拿它練練手,看看能不能將其展開到代碼的狀態(tài),并且明白其中都發(fā)生了些什么。如果你遇到什么問題或者在展開過程中有所心得,歡迎在評論里留言分享和交流 :)
好了,這篇文章已經(jīng)夠長了。希望在看過以后您在看到宏的時(shí)候不再發(fā)怵,而是可以很開心地說這個(gè)我會(huì)這個(gè)我會(huì)這個(gè)我也會(huì)。最終目標(biāo)當(dāng)然是寫出漂亮高效簡潔的宏,這不論對于提高生產(chǎn)力還是~~震懾你的同事~~提升自己實(shí)力都會(huì)很有幫助。
另外,在這里一定要宣傳一下關(guān)注了很久的@hangcom 吳航前輩的新書《iOS應(yīng)用逆向工程》。很榮幸能夠在發(fā)布之前得到前輩的允許拜讀了整本書,可以說看的暢快淋漓。我之前并沒有越獄開發(fā)的任何基礎(chǔ),也對相關(guān)領(lǐng)域知之甚少,在這樣的前提下跟隨書中的教程和例子進(jìn)行探索的過程可以說是十分有趣。我也得以能夠用不同的眼光和高度來審視這幾年所從事的iOS開發(fā)行業(yè),獲益良多??梢哉f《iOS應(yīng)用逆向工程》是我近期所愉快閱讀到的很cool的一本好書?,F(xiàn)在這本書還在預(yù)售中,但是距離1月28日的正式發(fā)售已經(jīng)很近,有興趣的同學(xué)可以前往亞馬遜或者ChinaPub的相關(guān)頁面預(yù)定,相信這本書將會(huì)是iOS技術(shù)人員非常棒的春節(jié)讀物。
最后是我們說好的留給大家玩的練習(xí),我加了一點(diǎn)注釋幫助大家稍微理解每個(gè)宏是做什么的,在文章后面留了一塊試驗(yàn)田,大家可以隨便填寫玩弄??傊?,加油!
//調(diào)用 RACSignal是類的名字
RACSignal *signal = RACObserve(self, currentLocation);
//以下開始是宏定義
//rac_valuesForKeyPath:observer:是方法名
#define RACObserve(TARGET, KEYPATH) \
[(id)(TARGET) rac_valuesForKeyPath:@keypath(TARGET, KEYPATH) observer:self]
#define keypath(...) \
metamacro_if_eq(1, metamacro_argcount(__VA_ARGS__))(keypath1(__VA_ARGS__))(keypath2(__VA_ARGS__))
//這個(gè)宏在取得keypath的同時(shí)在編譯期間判斷keypath是否存在,避免誤寫
//您可以先不用介意這里面的巫術(shù)..
#define keypath1(PATH) \
(((void)(NO && ((void)PATH, NO)), strchr(# PATH, '.') + 1))
#define keypath2(OBJ, PATH) \
(((void)(NO && ((void)OBJ.PATH, NO)), # PATH))
//A和B是否相等,若相等則展開為后面的第一項(xiàng),否則展開為后面的第二項(xiàng)
//eg. metamacro_if_eq(0, 0)(true)(false) => true
// metamacro_if_eq(0, 1)(true)(false) => false
#define metamacro_if_eq(A, B) \
metamacro_concat(metamacro_if_eq, A)(B)
#define metamacro_if_eq1(VALUE) metamacro_if_eq0(metamacro_dec(VALUE))
#define metamacro_if_eq0(VALUE) \
metamacro_concat(metamacro_if_eq0_, VALUE)
#define metamacro_if_eq0_1(...) metamacro_expand_
#define metamacro_expand_(...) __VA_ARGS__
#define metamacro_argcount(...) \
metamacro_at(20, __VA_ARGS__, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1)
#define metamacro_at(N, ...) \
metamacro_concat(metamacro_at, N)(__VA_ARGS__)
#define metamacro_concat(A, B) \
metamacro_concat_(A, B)
#define metamacro_concat_(A, B) A ## B
#define metamacro_at2(_0, _1, ...) metamacro_head(__VA_ARGS__)
#define metamacro_at20(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, ...) metamacro_head(__VA_ARGS__)
#define metamacro_head(...) \
metamacro_head_(__VA_ARGS__, 0)
#define metamacro_head_(FIRST, ...) FIRST
#define metamacro_dec(VAL) \
metamacro_at(VAL, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19)
//調(diào)用 RACSignal是類的名字 RACSignal *signal = RACObserve(self, currentLocation);