寫在前面
在開發(fā)過程中很多時候需要閱讀第三方源碼,但是里面有大量的宏聊替。沒有換行,沒有著色培廓,與平時寫的代碼完全不同惹悄,還會層層嵌套,總之各種看不懂肩钠。
因此對宏的了解欠缺已經(jīng)是阻礙自身能力提升的一大障礙泣港。于是拜讀了@onevcat這篇文章暂殖。
讀完文章后仍有部分存疑此處作為記錄
- 為什么會出現(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)化和方便央星,以及跨平臺能力,宏被大量使用惫东,可以說底層開發(fā)離開define將寸步難行莉给。而在更高層級進行開發(fā)時,我們會將更多的重心放在業(yè)務(wù)邏輯上廉沮,似乎對宏的使用和依賴并不多颓遏。但是使用宏定義的好處是不言自明的,在節(jié)省工作量的同時滞时,代碼可讀性大大增加叁幢。如果想成為一個能寫出漂亮優(yōu)雅代碼的開發(fā)者,宏定義絕對是必不可少的技能(雖然宏本身可能并不漂亮優(yōu)雅XD)坪稽。但是因為宏定義對于很多人來說曼玩,并不像業(yè)務(wù)邏輯那樣是每天會接觸的東西。即使是能偶爾使用到一些宏窒百,也更多的僅僅只停留在使用的層級黍判,卻并不會去探尋背后發(fā)生的事情。有一些開發(fā)者確實也有探尋的動力和意愿篙梢,但卻在點開一個定義之后發(fā)現(xiàn)還有宏定義中還有其他無數(shù)定義顷帖,再加上滿屏幕都是不同于平時的代碼,既看不懂又不變色渤滞,于是乎心生煩惱贬墩,怒而回退。本文希望通過循序漸進的方式妄呕,通過幾個例子來表述C系語言宏定義世界中的一些基本規(guī)則和技巧陶舞,從0開始,希望最后能讓大家至少能看懂和還原一些相對復(fù)雜的宏趴腋〉跛担考慮到我自己現(xiàn)在objc使用的比較多,這個站點的讀者應(yīng)該也大多是使用objc的优炬,所以有部分例子是選自objc颁井,但是本文的大部分內(nèi)容將是C系語言通用。
入門
如果您完全不知道宏是什么的話蠢护,可以先來熱個身雅宾。很多人在介紹宏的時候會說,宏嘛很簡單葵硕,就是簡單的查找替換嘛眉抬。嗯贯吓,只說對了的一半。C中的宏分為兩類蜀变,對象宏(object-like macro)和函數(shù)宏(function-like macro)悄谐。對于對象宏來說確實相對簡單,但卻也不是那么簡單的查找替換库北。對象宏一般用來定義一些常數(shù)爬舰,舉個例子:
//This defines PI
#define M_PI 3.14159265358979323846264338327950288
#define
關(guān)鍵字表明即將開始定義一個宏,緊接著的M_PI
是宏的名字寒瓦,空格之后的數(shù)字是內(nèi)容情屹。類似這樣的#define X A
的宏是比較簡單的,在編譯時編譯器會在語義分析認定是宏后杂腰,將X替換為A垃你,這個過程稱為宏的展開。比如對于上面的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ù)宏。從最簡單的例子開始林说,比如下面這個函數(shù)宏
//A simple function-like macro
#define SELF(x) x
NSString *name = @"Macro Rookie";
NSLog(@"Hello %@",SELF(name));
// => NSLog(@"Hello %@",name);
// => Hello Macro Rookie
這個宏做的事情是煎殷,在編譯時如果遇到SELF
,并且后面帶括號腿箩,并且括號中的參數(shù)個數(shù)與定義的相符豪直,那么就將括號中的參數(shù)換到定義的內(nèi)容里去,然后替換掉原來的內(nèi)容珠移。 具體到這段代碼中弓乙,SELF
接受了一個name,然后將整個SELF(name)用name替換掉钧惧。嗯..似乎很簡單很沒用暇韧,身經(jīng)百戰(zhàn)閱碼無數(shù)的你一定會認為這個宏是寫出來賣萌的。那么接受多個參數(shù)的宏肯定也不在話下了浓瞪,例如這樣的:
#define PLUS(x,y) x + y
printf("%d",PLUS(3,2));
// => printf("%d",3 + 2);
// => 5
相比對象宏來說懈玻,函數(shù)宏要復(fù)雜一些,但是看起來也相當簡單吧乾颁?嗯涂乌,那么現(xiàn)在熱身結(jié)束艺栈,讓我們正式開啟宏的大門吧。
宏的世界湾盒,小有乾坤
因為宏展開其實是編輯器的預(yù)處理湿右,因此它可以在更高層級上控制程序源碼本身和編譯流程。而正是這個特點罚勾,賦予了宏很強大的功能和靈活度毅人。但是凡事都有兩面性,在獲取靈活的背后荧库,是以需要大量時間投入以對各種邊界情況進行考慮來作為代價的堰塌。可能這么說并不是很能讓人理解分衫,但是大部分宏(特別是函數(shù)宏)背后都有一些自己的故事场刑,挖掘這些故事和設(shè)計的思想會是一件很有意思的事情。另外蚪战,我一直相信在實踐中學(xué)習(xí)才是真正掌握知識的唯一途徑牵现,雖然可能正在看這篇博文的您可能最初并不是打算親自動手寫一些宏,但是這我們不妨開始動手從實際的書寫和犯錯中進行學(xué)習(xí)和挖掘邀桑,因為只有肌肉記憶和大腦記憶協(xié)同起來瞎疼,才能說達到掌握的水準”诨可以說贼急,寫宏和用宏的過程,一定是在在犯錯中學(xué)習(xí)和深入思考的過程捏萍,我們接下來要做的太抓,就是重現(xiàn)這一系列過程從而提高進步。
第一個題目是令杈,讓我們一起來實現(xiàn)一個MIN
宏吧:實現(xiàn)一個函數(shù)宏走敌,給定兩個數(shù)字輸入,將其替換為較小的那個數(shù)逗噩。比如MIN(1,2)
出來的值是1掉丽。嗯哼,simple enough异雁?定義宏捶障,寫好名字,兩個輸入纲刀,然后換成比較取值残邀。比較取值嘛,任何一本入門級別的C程序設(shè)計上都會有講啊,于是我們可以很快寫出我們的第一個版本:
//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ā)布驱闷!
但是在實際使用中,我們很快就遇到了這樣的情況
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;
嘛空另,寫程序這個東西,bug出來了蹋砚,原因知道了扼菠,事后大家就都是諸葛亮了。因為小于和比較符號的優(yōu)先級是較低的坝咐,所以乘法先被運算了循榆,修正非常簡單嘛,加括號就好了墨坚。
//Version 2.0
#define MIN(A,B) (A < B ? A : B)
這次2 * MIN(3, 4)
這樣的式子就輕松愉快地拿下了秧饮。經(jīng)過了這次修改,我們對自己的宏信心大增了…直到泽篮,某一天一個怒氣沖沖的同事跑來摔鍵盤盗尸,然后給出了一個這樣的例子:
int a = MIN(3, 4 < 5 ? 4 : 5);
printf("%d",a);
// => 4
簡單的相比較三個數(shù)字并找到最小的一個而已,要怪就怪你沒有提供三個數(shù)字比大小的宏帽撑,可憐的同事只好自己實現(xiàn)4和5的比較泼各。在你開始著手解決這個問題的時候,你首先想到的也許是既然都是求最小值亏拉,那寫成MIN(3, MIN(4, 5))
是不是也可以扣蜻。于是你就隨手這樣一改,發(fā)現(xiàn)結(jié)果變成了3及塘,正是你想要的..接下來弱贼,開始懷疑之前自己是不是看錯結(jié)果了,改回原樣磷蛹,一個4赫然出現(xiàn)在屏幕上。你終于意識到事情并不是你想像中那樣簡單溪烤,于是還是回到最原始直接的手段味咳,展開宏。
int a = MIN(3, 4 < 5 ? 4 : 5);
// => int a = (3 < 4 < 5 ? 4 : 5 ? 3 : 4 < 5 ? 4 : 5); //希望你還記得運算符優(yōu)先級
// => int a = ((3 < (4 < 5 ? 4 : 5) ? 3 : 4) < 5 ? 4 : 5); //為了您不太糾結(jié)檬嘀,我給這個式子加上了括號
// => int a = ((3 < 4 ? 3 : 4) < 5 ? 4 : 5)
// => int a = (3 < 5 ? 4 : 5)
// => int a = 4
找到問題所在了槽驶,由于展開時連接符號和被展開式子中的運算符號優(yōu)先級相同,導(dǎo)致了計算順序發(fā)生了變化鸳兽,實質(zhì)上和我們的1.0版遇到的問題是差不多的掂铐,還是考慮不周。那么就再嚴格一點吧,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)在對這個簡單的宏充滿了疑惑枢舶。于是你跑了無數(shù)的測試用例而且它們都通過了,我們似乎徹底解決了括號問題替久,你也認為從此這個宏就妥妥兒的哦了凉泄。不過如果你真的這么想,那你就圖樣圖森破了蚯根。生活總是殘酷的后众,該來的bug也一定是會來的。不出意外地稼锅,在一個霧霾陰沉的下午吼具,我們又收到了一個出問題的例子。
float a = 1.0f;
float b = MIN(a++, 1.5f);
printf("a=%f, b=%f",a,b);
// => a=3.000000, b=2.000000
拿到這個出問題的例子你的第一反應(yīng)可能和我一樣矩距,這TM的誰這么二貨還在比較的時候搞++拗盒,這簡直亂套了!但是這樣的人就是會存在锥债,這樣的事就是會發(fā)生陡蝇,你也不能說人家邏輯有錯誤。a是1哮肚,a++表示先使用a的值進行計算登夫,然后再加1。那么其實這個式子想要計算的是取a和b的最小值允趟,然后a等于a加1:所以正確的輸出a為2恼策,b為1才對!嘛潮剪,滿眼都是淚涣楷,讓我們這些久經(jīng)摧殘的程序員淡定地展開這個式子,來看看這次又發(fā)生了些什么吧:
float a = 1.0f;
float b = MIN(a++, 1.5f);
// => float b = ((a++) < (1.5f) ? (a++) : (1.5f))
其實只要展開一步就很明白了抗碰,在比較a++和1.5f的時候狮斗,先取1和1.5比較,然后a自增1弧蝇。接下來條件比較得到真以后又觸發(fā)了一次a++碳褒,此時a已經(jīng)是2折砸,于是b得到2,最后a再次自增后值為3沙峻。出錯的根源就在于我們預(yù)想的是a++只執(zhí)行一次睦授,但是由于宏展開導(dǎo)致了a++被多執(zhí)行了,改變了預(yù)想的邏輯专酗。解決這個問題并不是一件很簡單的事情睹逃,使用的方式也很巧妙。我們需要用到一個GNU C的賦值擴展祷肯,即使用({...})
的形式沉填。這種形式的語句可以類似很多腳本語言,在順次執(zhí)行之后佑笋,會將最后一次的表達式的賦值作為返回翼闹。舉個簡單的例子,下面的代碼執(zhí)行完畢后a的值為3蒋纬,而且b和c只存在于大括號限定的代碼域中
int a = ({
int b = 1;
int c = 2;
b + c;
});
// => a is 3
有了這個擴展猎荠,我們就能做到之前很多做不到的事情了。比如徹底解決MIN
宏定義的問題蜀备,而也正是GNU C中MIN
的標準寫法
//GNUC MIN
#define MIN(A,B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __a : __b; })
這里定義了三個語句关摇,分別以輸入的類型申明了__a
和__b
,并使用輸入為其賦值碾阁,接下來做一個簡單的條件比較输虱,得到__a
和__b
中的較小值,并使用賦值擴展將結(jié)果作為返回脂凶。這樣的實現(xiàn)保證了不改變原來的邏輯宪睹,先進行一次賦值,也避免了括號優(yōu)先級的問題蚕钦,可以說是一個比較好的解決方案了亭病。如果編譯環(huán)境支持GNU C的這個擴展,那么毫無疑問我們應(yīng)該采用這種方式來書寫我們的MIN
宏嘶居,如果不支持這個環(huán)境擴展罪帖,那我們只有人為地規(guī)定參數(shù)不帶運算或者函數(shù)調(diào)用,以避免出錯邮屁。
關(guān)于MIN
我們討論已經(jīng)夠多了整袁,但是其實還存留一個懸疑的地方。如果在同一個scope內(nèi)已經(jīng)有__a
或者__b
的定義的話(雖然一般來說不會出現(xiàn)這種悲劇的命名樱报,不過誰知道呢),這個宏可能出現(xiàn)問題泞当。在申明后賦值將因為定義重復(fù)而無法被初始化迹蛤,導(dǎo)致宏的行為不可預(yù)知。如果您有興趣,不妨自己動手試試看結(jié)果會是什么盗飒。Apple在Clang中徹底解決了這個問題嚷量,我們把Xcode打開隨便建一個新工程,在代碼中輸入MIN(1,1)
逆趣,然后Cmd+點擊即可找到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); })
似乎有點長宣渗,看起來也很吃力抖所。我們先美化一下這宏,首先是最后那個__NSMIN_IMPL__
內(nèi)容實在是太長了痕囱。我們知道代碼的話是可以插入換行而不影響含義的田轧,宏是否也可以呢?答案是肯定的鞍恢,只不過我們不能使用一個單一的回車來完成傻粘,而必須在回車前加上一個反斜杠\
。改寫一下帮掉,為其加上換行好看些:
#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
一共由三個宏定義組合而成弦悉。第一個__NSX_PASTE__
里出現(xiàn)的兩個連著的井號##
在宏中是一個特殊符號,它表示將兩個參數(shù)連接起來這種運算蟆炊。注意函數(shù)宏必須是有意義的運算稽莉,因此你不能直接寫AB
來連接兩個參數(shù),而需要寫成例子中的A##B
盅称。宏中還有一切其他的自成一脈的運算符號肩祥,我們稍后還會介紹幾個。接下來是我們調(diào)用的兩個參數(shù)的MIN
,它做的事是調(diào)用了另一個三個參數(shù)的宏__NSMIN_IMPL__
尸变,其中前兩個參數(shù)就是我們的輸入桐腌,而第三個__COUNTER__
我們似乎不認識,也不知道其從何而來将饺。其實__COUNTER__
是一個預(yù)定義的宏,這個值在編譯過程中將從0開始計數(shù)痛黎,每次被調(diào)用時加1予弧。因為唯一性,所以很多時候被用來構(gòu)造獨立的變量名稱湖饱。有了上面的基礎(chǔ)掖蛤,再來看最后的實現(xiàn)宏就很簡單了。整體思路和前面的實現(xiàn)和之前的GNUC MIN是一樣的井厌,區(qū)別在于為變量名__a
和__b
添加了一個計數(shù)后綴蚓庭,這樣大大避免了變量名相同而導(dǎo)致問題的可能性(當然如果你執(zhí)拗地把變量叫做__a9527并且出問題了的話致讥,就只能說不作死就不會死了)。
花了好多功夫器赞,我們終于把一個簡單的MIN
宏徹底搞清楚了垢袱。宏就是這樣一類東西,簡單的表面之下隱藏了很多玄機港柜,可謂小有乾坤请契。作為練習(xí)大家可以自己嘗試一下實現(xiàn)一個SQUARE(A)
,給一個數(shù)字輸入夏醉,輸出它的平方的宏爽锥。雖然一般這個計算現(xiàn)在都是用inline來做了,但是通過和MIN
類似的思路我們是可以很好地實現(xiàn)它的授舟,動手試一試吧 :)
Log救恨,永恒的主題
Log人人愛,它為我們指明前進方向释树,它為我們抓蟲提供幫助肠槽。在objc中,我們最多使用的log方法就是NSLog
輸出信息到控制臺了奢啥,但是NSLog的標準輸出可謂殘廢秸仙,有用信息完全不夠,比如下面這段代碼:
NSArray *array = @[@"Hello", @"My", @"Macro"];
NSLog (@"The array is %@", array);
打印到控制臺里的結(jié)果是類似這樣的
2014-01-20 11:22:11.835 TestProject[23061:70b] The array is (
Hello,
My,
Macro
)
我們在輸出的時候關(guān)心什么桩盲?除了結(jié)果以外寂纪,很多情況下我們會對這行l(wèi)og的所在的文件位置方法什么的會比較關(guān)心。在每次NSLog里都手動加上方法名字和位置信息什么的無疑是個笨辦法赌结,而如果一個工程里已經(jīng)有很多NSLog
的調(diào)用了捞蛋,一個一個手動去改的話無疑也是噩夢。我們通過宏柬姚,可以很簡單地完成對NSLog
原生行為的改進拟杉,優(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)在為止見到的最長的一個宏了吧…沒關(guān)系,一點一點來分析就好撕捍。首先是定義部分拿穴,第2行的NSLog(format, ...)
。我們看到的是一個函數(shù)宏忧风,但是它的參數(shù)比較奇怪默色,第二個參數(shù)是...
,在宏定義(其實也包括函數(shù)定義)的時候狮腿,寫為...
的參數(shù)被叫做可變參數(shù)(variadic)腿宰〉苁矗可變參數(shù)的個數(shù)不做限定。在這個宏定義中酗失,除了第一個參數(shù)format
將被單獨處理外,接下來輸入的參數(shù)將作為整體一并看待昧绣」骐龋回想一下NSLog的用法,我們在使用NSLog時夜畴,往往是先給一個format字符串作為第一個參數(shù)拖刃,然后根據(jù)定義的格式在后面的參數(shù)里跟上寫要輸出的變量之類的。這里第一個格式化字符串即對應(yīng)宏里的format
贪绘,后面的變量全部映射為...
作為整體處理兑牡。
接下來宏的內(nèi)容部分。上來就是一個下馬威税灌,我們遇到了一個do while語句…想想看你上次使用do while是什么時候吧均函?也許是C程序設(shè)計課的大作業(yè)?或者是某次早已被遺忘的算法面試上菱涤?總之雖然大家都是明白這個語句的苞也,但是實際中可能用到它的機會少之又少。乍一看似乎這個do while什么都沒做粘秆,因為while是0如迟,所以do肯定只會被執(zhí)行一次。那么它存在的意義是什么呢攻走,我們是不是可以直接簡化一下這個宏殷勘,把它給去掉,變成這個樣子呢昔搂?
//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");
答案當然是否定的玲销,也許簡單的測試里你沒有遇到問題,但是在生產(chǎn)環(huán)境中這個宏顯然悲劇了巩趁⊙魍妫考慮下面的常見情況
if (errorHappend)
NSLog(@"Oops, error happened");
展開以后將會變成
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系語言可不是靠縮進來控制代碼塊和邏輯關(guān)系的。所以說如果使用這個宏的人沒有在條件判斷后加大括號的話议慰,你的宏就會一直調(diào)用真正的NSLog輸出東西蠢古,這顯然不是我們想要的邏輯。當然在這里還是需要重新批評一下認為if后的單條執(zhí)行語句不加大括號也沒問題的同學(xué)别凹,這是陋習(xí)草讶,無需理由,請改正炉菲。不論是不是一條語句堕战,也不論是if后還是else后坤溃,都加上大括號,是對別人和自己的一種尊重嘱丢。
好了知道我們的宏是如何失效的薪介,也就知道了修改的方法。作為宏的開發(fā)者越驻,應(yīng)該力求使用者在最大限度的情況下也不會出錯汁政,于是我們想到直接用一對大括號把宏內(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"); \
}
展開剛才的那個式子记劈,結(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í)行并巍,正確目木!因為用大括號標識代碼塊是不會嫌多的,所以這樣一來的話我們的宏在不論if后面有沒有大括號的情況下都能工作了懊渡!這么看來刽射,前面例子中的do while果然是多余的?于是我們又可以愉快地發(fā)布了剃执?如果你夠細心的話柄冲,可能已經(jīng)發(fā)現(xiàn)問題了,那就是上面最后的一個分號忠蝗。雖然編譯運行測試沒什么問題现横,但是始終稍微有些刺眼有木有?沒錯阁最,因為我們在寫NSLog本身的時候戒祠,是將其當作一條語句來處理的,后面跟了一個分號速种,在宏展開后姜盈,這個分號就如同噩夢一般的多出來了。什么配阵,你還沒看出哪兒有問題馏颂?試試看展開這個例子吧:
if (errorHappend)
NSLog(@"Oops, error happened");
else
//Yep, no error, I am happy~ :)
No! I am not haapy at all! 因為編譯錯誤了!實際上這個宏展開以后變成了這個樣子:
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~ :)
}
因為else前面多了一個分號棋傍,導(dǎo)致了編譯錯誤救拉,很惱火..要是寫代碼的人乖乖寫大括號不就啥事兒沒有了么?但是我們還是有巧妙的解決方法的瘫拣,那就是上面的do while亿絮。把宏的代碼塊添加到do中,然后之后while(0),在行為上沒有任何改變派昧,但是可以巧妙地吃掉那個悲劇的分號黔姜,使用do while的版本展開以后是這個樣子的
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~ :)
}
這個吃掉分號的方法被大量運用在代碼塊宏中,幾乎已經(jīng)成為了標準寫法蒂萎。而且while(0)的好處在于秆吵,在編譯的時候,編譯器基本都會為你做好優(yōu)化五慈,把這部分內(nèi)容去掉帮毁,最終編譯的結(jié)果不會因為這個do while而導(dǎo)致運行效率上的差異。在終于弄明白了這個奇怪的do while之后豺撑,我們終于可以繼續(xù)深入到這個宏里面了。宏本體內(nèi)容的第一行沒有什么值得多說的fprintf(stderr, "<%s : %d> %s\n",
黔牵,簡單的格式化輸出而已聪轿。注意我們使用了\
將這個宏分成了好幾行來寫,實際在最后展開時會被合并到同一行內(nèi)猾浦,我們在剛才MIN
最后也用到了反斜杠陆错,希望你還能記得。接下來一行我們填寫這個格式輸出中的三個token金赦,
[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __LINE__, __func__);
這里用到了三個預(yù)定義宏音瓷,和剛才的__COUNTER__
類似,預(yù)定義宏的行為是由編譯器指定的夹抗。__FILE__
返回當前文件的絕對路徑绳慎,__LINE__
返回展開該宏時在文件中的行數(shù),__func__
是改宏所在scope的函數(shù)名稱漠烧。我們在做Log輸出時如果帶上這這三個參數(shù)杏愤,便可以加快解讀Log,迅速定位已脓。關(guān)于編譯器預(yù)定義的Log以及它們的一些實現(xiàn)機制珊楼,感興趣的同學(xué)可以移步到gcc文檔的PreDefine頁面和clang的Builtin Macro進行查看。在這里我們將格式化輸出的三個參數(shù)分別設(shè)定為文件名的最后一個部分(因為絕對路徑太長很難看)度液,行數(shù)厕宗,以及方法名稱。
接下來是還原原始的NSLog堕担,(NSLog)((format), ##__VA_ARGS__);
中出現(xiàn)了另一個預(yù)定義的宏__VA_ARGS__
(我們似乎已經(jīng)找出規(guī)律了已慢,前后雙下杠的一般都是預(yù)定義)。__VA_ARGS__
表示的是宏定義中的...
中的所有剩余參數(shù)霹购。我們之前說過可變參數(shù)將被統(tǒng)一處理蛇受,在這里展開的時候編譯器會將__VA_ARGS__
直接替換為輸入中從第二個參數(shù)開始的剩余參數(shù)。另外一個懸疑點是在它前面出現(xiàn)了兩個井號##
。還記得我們上面在MIN
中的兩個井號么兢仰,在那里兩個井號的意思是將前后兩項合并乍丈,在這里做的事情比較類似,將前面的格式化字符串和后面的參數(shù)列表合并把将,這樣我們就得到了一個完整的NSLog方法了轻专。之后的幾行相信大家自己看懂也沒有問題了,最后輸出一下試試看察蹲,大概看起來會是這樣的请垛。
-------
<AppDelegate.m : 46> -[AppDelegate application:didFinishLaunchingWithOptions:]
2014-01-20 16:44:25.480 TestProject[30466:70b] The array is (
Hello,
My,
Macro
)
-------
帶有文件,行號和方法的輸出洽议,并且用橫杠隔開了(請原諒我沒有質(zhì)感的設(shè)計宗收,也許我應(yīng)該畫一只牛,比如這樣亚兄?)混稽,debug的時候也許會輕松一些吧 :)
這個Log有三個懸念點,首先是為什么我們要把format單獨寫出來审胚,然后吧其他參數(shù)作為可變參數(shù)傳遞呢匈勋?如果我們不要那個format,而直接寫成NSLog(...)
會不會有問題膳叨?對于我們這里這個例子來說的話是沒有變化的洽洁,但是我們需要記住的是...
是可變參數(shù)列表,它可以代表一個菲嘴、兩個饿自,或者是很多個參數(shù),但同時它也能代表零個參數(shù)龄坪。如果我們在申明這個宏的時候沒有指定format參數(shù)璃俗,而直接使用參數(shù)列表,那么在使用中不寫參數(shù)的NSLog()也將被匹配到這個宏中悉默,導(dǎo)致編譯無法通過城豁。如果你手邊有Xcode,也可以看看Cocoa中真正的NSLog方法的實現(xiàn)抄课,可以看到它也是接收一個格式參數(shù)和一個參數(shù)列表的形式唱星,我們在宏里這么定義,正是為了其傳入正確合適的參數(shù)跟磨,從而保證使用者可以按照原來的方式正確使用這個宏间聊。
第二點是既然我們的可變參數(shù)可以接受任意個輸入,那么在只有一個format輸入抵拘,而可變參數(shù)個數(shù)為零的時候會發(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", );
似乎是存在問題的尚蝌,你一定會有疑惑迎变,這種方式怎么可能編譯通過呢?飘言!原來大神們其實早已想到這個問題衣形,并且進行了一點特殊的處理。這里有個特殊的規(guī)則姿鸿,在逗號
和__VA_ARGS__
之間的雙井號谆吴,除了拼接前后文本之外,還有一個功能苛预,那就是如果后方文本為空句狼,那么它會將前面一個逗號吃掉。這個特性當且僅當上面說的條件成立時才會生效热某,因此可以說是特例腻菇。加上這條規(guī)則后,我們就可以將剛才的式子展開為正確的(NSLog)((@"Hello"));
了苫拍。
最后一個值得討論的地方是(NSLog)((format), ##__VA_ARGS__);
的括號使用。把看起來能去掉的括號去掉旺隙,寫成NSLog(format, ##__VA_ARGS__);
是否可以呢绒极?在這里的話應(yīng)該是沒有什么大問題的,首先format不會被調(diào)用多次也不太存在誤用的可能性(因為最后編譯器會檢查NSLog的輸入是否正確)蔬捷。另外你也不用擔心展開以后式子里的NSLog會再次被自己展開垄提,雖然展開式中NSLog也滿足了我們的宏定義,但是宏的展開非常聰明周拐,展開后會自身無限循環(huán)的情況铡俐,就不會再次被展開了。
作為一個您讀到了這里的小獎勵妥粟,附送三個debug輸出rect审丘,size和point的宏,希望您能用上(嗯..想想曾經(jīng)有多少次你需要打印這些結(jié)構(gòu)體的某個數(shù)字而被折磨致死勾给,讓它們玩兒蛋去吧滩报!當然請先加油看懂它們吧)
#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)
兩個實際應(yīng)用的例子
當然不是說上面介紹的宏實際中不能用。它們相對簡單播急,但是里面坑不少脓钾,所以顯得很有特點,非常適合作為入門用桩警。而實際上在日常中很多我們常用的宏并沒有那么多奇怪的問題可训,很多時候我們按照想法去實現(xiàn),再稍微注意一下上述介紹的可能存在的共通問題,一個高質(zhì)量的宏就可以誕生握截。如果能寫出一些有意義價值的宏飞崖,小了從對你的代碼的使用者來說,大了從整個社區(qū)整個世界和減少碳排放來說川蒙,你都做出了相當?shù)呢暙I蚜厉。我們通過幾個實際的例子來看看,宏是如何改變我們的生活畜眨,和寫代碼的習(xí)慣的吧昼牛。
先來看看這兩個宏
#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
是什么的話康聂,強烈建議補習(xí)一下測試驅(qū)動開發(fā)的相關(guān)知識贰健,我想應(yīng)該會對您之后的道路很有幫助。如果你已經(jīng)很熟悉這個命令了恬汁,那我們一起開始來看看幕后發(fā)生了什么伶椿。
有了上面的基礎(chǔ),相信您大體上應(yīng)該可以自行解讀這個宏了氓侧。({...})
的語法和##
都很熟悉了脊另,這里有三個值得注意的地方,在這個宏的一開始约巷,我們后面的的參數(shù)是format...
偎痛,這其實也是可變參數(shù)的一種寫法,和...
與__VA_ARGS__
配對類似独郎,{NAME}...
將于{NAME}
配對使用踩麦。也就是說,在這里宏內(nèi)容的format
指代的其實就是定義的先對expression
取了兩次反氓癌?我不是科班出身谓谦,但是我還能依稀記得這在大學(xué)程序課上講過,兩次取反的操作可以確保結(jié)果是BOOL值贪婉,這在objc中還是比較重要的(關(guān)于objc中BOOL的討論已經(jīng)有很多反粥,如果您還沒能分清BOOL, bool和Boolean,可以參看NSHisper的這篇文章)疲迂。然后就是@#expression
這個式子星压。我們接觸過雙井號##
,而這里我們看到的操作符是單井號#
鬼譬,注意井號前面的@
是objc的編譯符號娜膘,不屬于宏操作的對象。單個井號的作用是字符串化优质,簡單來說就是將替換后在兩頭加上”“竣贪,轉(zhuǎn)為一個C字符串军洼。這里使用@然后緊跟#expression,出來后就是一個內(nèi)容是expression的內(nèi)容的NSString演怎。然后這個NSString再作為參數(shù)傳遞給_XCTRegisterFailure
和_XCTFailureDescription
等匕争,繼續(xù)進行展開,這些是后話爷耀。簡單一瞥甘桑,我們大概就可以想象宏幫助我們省了多少事兒了,如果各位看官要是寫個斷言還要來個十多行的話歹叮,想象都會瘋掉的吧跑杭。
另外一個例子,找了人民群眾喜聞樂見的ReactiveCocoa(RAC)中的一個宏定義咆耿。對于RAC不熟悉或者沒聽過的朋友德谅,可以簡單地看看Limboy的一系列相關(guān)博文(搜索ReactiveCocoa),介紹的很棒萨螺。如果覺得“哇哦這個好酷我很想學(xué)”的話窄做,不妨可以跟隨raywenderlich上這個系列的教程做一些實踐,里面簡單地用到了RAC慰技,但是都已經(jīng)包含了RAC的基本用法了椭盏。RAC中有幾個很重要的宏,它們是保證RAC簡潔好用的基本吻商,可以說要是沒有這幾個宏的話掏颊,是不會有人喜歡RAC的。其中RACObserve
就是其中一個手报,它通過KVC來為對象的某個屬性創(chuàng)建一個信號返回(如果你看不懂這句話蚯舱,不要擔心改化,這對你理解這個宏的寫法和展開沒有任何影響)掩蛤。對于這個宏,我決定不再像上面那樣展開和講解陈肛,我會在最后把相關(guān)的宏都貼出來揍鸟,大家不妨拿它練練手,看看能不能將其展開到代碼的狀態(tài)句旱,并且明白其中都發(fā)生了些什么阳藻。如果你遇到什么問題或者在展開過程中有所心得,歡迎在評論里留言分享和交流 :)
好了谈撒,這篇文章已經(jīng)夠長了腥泥。希望在看過以后您在看到宏的時候不再發(fā)怵,而是可以很開心地說這個我會這個我會這個我也會啃匿。最終目標當然是寫出漂亮高效簡潔的宏蛔外,這不論對于提高生產(chǎn)力還是~~震懾你的同事~~提升自己實力都會很有幫助蛆楞。
另外,在這里一定要宣傳一下關(guān)注了很久的@hangcom 吳航前輩的新書《iOS應(yīng)用逆向工程》夹厌。很榮幸能夠在發(fā)布之前得到前輩的允許拜讀了整本書豹爹,可以說看的暢快淋漓。我之前并沒有越獄開發(fā)的任何基礎(chǔ)矛纹,也對相關(guān)領(lǐng)域知之甚少臂聋,在這樣的前提下跟隨書中的教程和例子進行探索的過程可以說是十分有趣。我也得以能夠用不同的眼光和高度來審視這幾年所從事的iOS開發(fā)行業(yè)或南,獲益良多孩等。可以說《iOS應(yīng)用逆向工程》是我近期所愉快閱讀到的很cool的一本好書∮祝現(xiàn)在這本書還在預(yù)售中瞎访,但是距離1月28日的正式發(fā)售已經(jīng)很近,有興趣的同學(xué)可以前往亞馬遜或者ChinaPub的相關(guān)頁面預(yù)定吁恍,相信這本書將會是iOS技術(shù)人員非常棒的春節(jié)讀物扒秸。
最后是我們說好的留給大家玩的練習(xí),我加了一點注釋幫助大家稍微理解每個宏是做什么的冀瓦,在文章后面留了一塊試驗田伴奥,大家可以隨便填寫玩弄∫砻觯總之拾徙,加油!
//調(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__))
//這個宏在取得keypath的同時在編譯期間判斷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是否相等尼啡,若相等則展開為后面的第一項,否則展開為后面的第二項
//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);