久久国产成人av_抖音国产毛片_a片网站免费观看_A片无码播放手机在线观看,色五月在线观看,亚洲精品m在线观看,女人自慰的免费网址,悠悠在线观看精品视频,一级日本片免费的,亚洲精品久,国产精品成人久久久久久久

分享

宏定義的黑魔法

 wintelsui 2014-02-11
轉(zhuǎn)自OneV's Den的博客
宏定義在C系開發(fā)中可以說占有舉足輕重的作用,。底層框架自不必說,,為了編譯優(yōu)化和方便,以及跨平臺(tái)能力,,宏被大量使用,,可以說底層開發(fā)離開define將寸步難行。而在更高層級(jí)進(jìn)行開發(fā)時(shí),,我們會(huì)將更多的重心放在業(yè)務(wù)邏輯上,,似乎對(duì)宏的使用和依賴并不多。但是使用宏定義的好處是不言自明的,,在節(jié)省工作量的同時(shí),,代碼可讀性大大增加。
如果想成為一個(gè)能寫出漂亮優(yōu)雅代碼的開發(fā)者,,宏定義絕對(duì)是必不可少的技能(雖然宏本身可能并不漂亮優(yōu)雅XD),。但是因?yàn)楹甓x對(duì)于很多人來說,并不像業(yè)務(wù)邏輯那樣是每天會(huì)接觸的東西,。即使是能偶爾使用到一些宏,,也更多的僅僅只停留在使用的層級(jí),卻并不會(huì)去探尋背后發(fā)生的事情,。
有一些開發(fā)者確實(shí)也有探尋的動(dòng)力和意愿,,但卻在點(diǎn)開一個(gè)定義之后發(fā)現(xiàn)還有宏定義中還有其他無數(shù)定義,,再加上滿屏幕都是不同于平時(shí)的代碼,既看不懂又不變色,,于是乎心生煩惱,,怒而回退。本文希望通過循序漸進(jìn)的方式,,通過幾個(gè)例子來表述C系語言宏定義世界中的一些基本規(guī)則和技巧,,從0開始,希望最后能讓大家至少能看懂和還原一些相對(duì)復(fù)雜的宏,??紤]到我自己現(xiàn)在objc使用的比較多,這個(gè)站點(diǎn)的讀者應(yīng)該也大多是使用objc的,,所以有部分例子是選自objc,,但是本文的大部分內(nèi)容將是C系語言通用。
入門
如果您完全不知道宏是什么的話,,可以先來熱個(gè)身,。很多人在介紹宏的時(shí)候會(huì)說,宏嘛很簡單,,就是簡單的查找替換嘛,。嗯,只說對(duì)了的一半,。C中的宏分為兩類,,對(duì)象宏(object-like macro)和函數(shù)宏(function-like macro)。對(duì)于對(duì)象宏來說確實(shí)相對(duì)簡單,,但卻也不是那么簡單的查找替換,。對(duì)象宏一般用來定義一些常數(shù),舉個(gè)例子:
  1. //This defines PI 
  2. #define M_PI        3.14159265358979323846264338327950288 
#define關(guān)鍵字表明即將開始定義一個(gè)宏,,緊接著的M_PI是宏的名字,,空格之后的數(shù)字是內(nèi)容。類似這樣的#define X A的宏是比較簡單的,,在編譯時(shí)編譯器會(huì)在語義分析認(rèn)定是宏后,,將X替換為A,這個(gè)過程稱為宏的展開,。比如對(duì)于上面的M_PI
  1. #define M_PI        3.14159265358979323846264338327950288 
  2.   
  3. double r = 10.0; 
  4. double circlePerimeter = 2 * M_PI * r; 
  5. // => double circlePerimeter = 2 * 3.14159265358979323846264338327950288 * r; 
  6.   
  7. printf("Pi is %0.7f",M_PI); 
  8. //Pi is 3.1415927 
那么讓我們開始看看另一類宏吧,。函數(shù)宏顧名思義,就是行為類似函數(shù),,可以接受參數(shù)的宏,。具體來說,在定義的時(shí)候,如果我們?cè)诤昝趾竺娓弦粚?duì)括號(hào)的話,,這個(gè)宏就變成了函數(shù)宏,。從最簡單的例子開始,比如下面這個(gè)函數(shù)宏
  1. //A simple function-like macro 
  2. #define SELF(x)      x 
  3. NSString *name = @"Macro Rookie"; 
  4. NSLog(@"Hello %@",SELF(name)); 
  5. // => NSLog(@"Hello %@",name); 
  6. //   => Hello Macro Rookie 
這個(gè)宏做的事情是,,在編譯時(shí)如果遇到SELF,,并且后面帶括號(hào),并且括號(hào)中的參數(shù)個(gè)數(shù)與定義的相符,,那么就將括號(hào)中的參數(shù)換到定義的內(nèi)容里去,,然后替換掉原來的內(nèi)容。 具體到這段代碼中,,SELF接受了一個(gè)name,,然后將整個(gè)SELF(name)用name替換掉。嗯..似乎很簡單很沒用,,身經(jīng)百戰(zhàn)閱碼無數(shù)的你一定會(huì)認(rèn)為這個(gè)宏是寫出來賣萌的。那么接受多個(gè)參數(shù)的宏肯定也不在話下了,,例如這樣的:
  1. #define PLUS(x,y) x + y 
  2. printf("%d",PLUS(3,2)); 
  3. // => printf("%d",3 + 2); 
  4. //  => 5 
相比對(duì)象宏來說,,函數(shù)宏要復(fù)雜一些,但是看起來也相當(dāng)簡單吧,?嗯,,那么現(xiàn)在熱身結(jié)束,讓我們正式開啟宏的大門吧,。
宏的世界,,小有乾坤
因?yàn)楹暾归_其實(shí)是編輯器的預(yù)處理,因此它可以在更高層級(jí)上控制程序源碼本身和編譯流程,。而正是這個(gè)特點(diǎn),,賦予了宏很強(qiáng)大的功能和靈活度。但是凡事都有兩面性,,在獲取靈活的背后,,是以需要大量時(shí)間投入以對(duì)各種邊界情況進(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)??梢哉f,,寫宏和用宏的過程,一定是在在犯錯(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è)輸入,,然后換成比較取值,。比較取值嘛,任何一本入門級(jí)別的C程序設(shè)計(jì)上都會(huì)有講啊,,于是我們可以很快寫出我們的第一個(gè)版本:
  1. //Version 1.0 
  2. #define MIN(A,B) A < B ? A : B 
Try一下
  1. int a = MIN(1,2); 
  2. // => int a = 1 < 2 ? 1 : 2; 
  3. printf("%d",a); 
  4. // => 1 
輸出正確,,打包發(fā)布!
瀟灑走一回
但是在實(shí)際使用中,,我們很快就遇到了這樣的情況
  1. int a = 2 * MIN(3, 4); 
  2. printf("%d",a); 
  3. // => 4 
看起來似乎不可思議,,但是我們將宏展開就知道發(fā)生什么了
  1. int a = 2 * MIN(3, 4); 
  2. // => int a = 2 * 3 < 4 ? 3 : 4; 
  3. // => int a = 6 < 4 ? 3 : 4; 
  4. // => int a = 4; 
嘛,寫程序這個(gè)東西,,bug出來了,,原因知道了,事后大家就都是諸葛亮了,。因?yàn)樾∮诤捅容^符號(hào)的優(yōu)先級(jí)是較低的,,所以乘法先被運(yùn)算了,修正非常簡單嘛,,加括號(hào)就好了,。
  1. //Version 2.0 
  2. #define MIN(A,B) (A < B ? A : B) 
這次2 * MIN(3, 4)這樣的式子就輕松愉快地拿下了。經(jīng)過了這次修改,,我們對(duì)自己的宏信心大增了…直到,,某一天一個(gè)怒氣沖沖的同事跑來摔鍵盤,然后給出了一個(gè)這樣的例子:
  1. int a = MIN(3, 4 < 5 ? 4 : 5); 
  2. printf("%d",a); 
  3. // => 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í)到事情并不是你想像中那樣簡單,于是還是回到最原始直接的手段,,展開宏,。
  1. int a = MIN(3, 4 < 5 ? 4 : 5); 
  2. // => int a = (3 < 4 < 5 ? 4 : 5 ? 3 : 4 < 5 ? 4 : 5);  //希望你還記得運(yùn)算符優(yōu)先級(jí) 
  3. //  => int a = ((3 < (4 < 5 ? 4 : 5) ? 3 : 4) < 5 ? 4 : 5);  //為了您不太糾結(jié),我給這個(gè)式子加上了括號(hào) 
  4. //   => int a = ((3 < 4 ? 3 : 4) < 5 ? 4 : 5) 
  5. //    => int a = (3 < 5 ? 4 : 5) 
  6. //     => int a = 4 
找到問題所在了,,由于展開時(shí)連接符號(hào)和被展開式子中的運(yùn)算符號(hào)優(yōu)先級(jí)相同,,導(dǎo)致了計(jì)算順序發(fā)生了變化,實(shí)質(zhì)上和我們的1.0版遇到的問題是差不多的,,還是考慮不周,。那么就再嚴(yán)格一點(diǎn)吧,3.0版,!
  1. //Version 3.0 
  2. #define MIN(A,B) ((A) < (B) ? (A) : (B)) 
至于為什么2.0版本中的MIN(3, MIN(4, 5))沒有出問題,可以正確使用,,這里作為練習(xí),,大家可以試著自己展開一下,來看看發(fā)生了什么,。
經(jīng)過兩次悲劇,,你現(xiàn)在對(duì)這個(gè)簡單的宏充滿了疑惑。于是你跑了無數(shù)的測(cè)試用例而且它們都通過了,,我們似乎徹底解決了括號(hào)問題,,你也認(rèn)為從此這個(gè)宏就妥妥兒的哦了。不過如果你真的這么想,,那你就圖樣圖森破了,。生活總是殘酷的,該來的bug也一定是會(huì)來的,。不出意外地,,在一個(gè)霧霾陰沉的下午,我們又收到了一個(gè)出問題的例子,。
  1. float a = 1.0f; 
  2. float b = MIN(a++, 1.5f); 
  3. printf("a=%f, b=%f",a,b); 
  4. // => 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才對(duì)!嘛,,滿眼都是淚,,讓我們這些久經(jīng)摧殘的程序員淡定地展開這個(gè)式子,來看看這次又發(fā)生了些什么吧:
  1. float a = 1.0f; 
  2. float b = MIN(a++, 1.5f); 
  3. // => 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只存在于大括號(hào)限定的代碼域中
  1. int a = ({ 
  2.     int b = 1; 
  3.     int c = 2; 
  4.     b + c; 
  5. }); 
  6. // => a is 3 
有了這個(gè)擴(kuò)展,我們就能做到之前很多做不到的事情了,。比如徹底解決MIN宏定義的問題,,而也正是GNU C中MIN的標(biāo)準(zhǔn)寫法
  1. //GNUC MIN 
  2. #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)保證了不改變?cè)瓉淼倪壿?,先進(jìn)行一次賦值,也避免了括號(hào)優(yōu)先級(jí)的問題,,可以說是一個(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或者_(dá)_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)的部分抄錄如下:
  1. //CLANG MIN 
  2. #define __NSX_PASTE__(A,B) A##B 
  3.   
  4. #define MIN(A,B) __NSMIN_IMPL__(A,B,__COUNTER__) 
  5.   
  6. #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è)反斜杠\,。改寫一下,,為其加上換行好看些:
  1. #define __NSX_PASTE__(A,B) A##B 
  2.   
  3. #define MIN(A,B) __NSMIN_IMPL__(A,B,__COUNTER__) 
  4.   
  5. #define __NSMIN_IMPL__(A,B,L) ({ __typeof__(A) __NSX_PASTE__(__a,L) = (A); \ 
  6.                                  __typeof__(B) __NSX_PASTE__(__b,L) = (B); \ 
  7.                                  (__NSX_PASTE__(__a,L) < __NSX_PASTE__(__b,L)) ? __NSX_PASTE__(__a,L) : __NSX_PASTE__(__b,L); \ 
  8.                               }) 
但可以看出MIN一共由三個(gè)宏定義組合而成。第一個(gè)__NSX_PASTE__里出現(xiàn)的兩個(gè)連著的井號(hào)##在宏中是一個(gè)特殊符號(hào),,它表示將兩個(gè)參數(shù)連接起來這種運(yùn)算,。注意函數(shù)宏必須是有意義的運(yùn)算,,因此你不能直接寫AB來連接兩個(gè)參數(shù),而需要寫成例子中的A##B,。宏中還有一切其他的自成一脈的運(yùn)算符號(hào),,我們稍后還會(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)槲ㄒ恍裕院芏鄷r(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)輸出可謂殘廢,有用信息完全不夠,,比如下面這段代碼:
  1. NSArray *array = @[@"Hello", @"My", @"Macro"]; 
  2. NSLog (@"The array is %@", array); 
打印到控制臺(tái)里的結(jié)果是類似這樣的
  1. 2014-01-20 11:22:11.835 TestProject[23061:70b] The array is ( 
  2.     Hello, 
  3.     My, 
  4.     Macro 
我們?cè)谳敵龅臅r(shí)候關(guān)心什么,?除了結(jié)果以外,,很多情況下我們會(huì)對(duì)這行l(wèi)og的所在的文件位置方法什么的會(huì)比較關(guān)心。在每次NSLog里都手動(dòng)加上方法名字和位置信息什么的無疑是個(gè)笨辦法,,而如果一個(gè)工程里已經(jīng)有很多NSLog的調(diào)用了,,一個(gè)一個(gè)手動(dòng)去改的話無疑也是噩夢(mèng)。我們通過宏,,可以很簡單地完成對(duì)NSLog原生行為的改進(jìn),,優(yōu)雅,高效,。只需要在預(yù)編譯的pch文件中加上
  1. //A better version of NSLog 
  2. #define NSLog(format, ...) do {                                                                          \ 
  3.                              fprintf(stderr, "<%s : %d> %s\n",                                           \ 
  4.                              [[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String],  \ 
  5.                              __LINE__, __func__);                                                        \ 
  6.                              (NSLog)((format), ##__VA_ARGS__);                                           \ 
  7.                              fprintf(stderr, "-------\n");                                               \ 
  8.                            } 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的用法,我們?cè)谑褂肗SLog時(shí),,往往是先給一個(gè)format字符串作為第一個(gè)參數(shù),,然后根據(jù)定義的格式在后面的參數(shù)里跟上寫要輸出的變量之類的。這里第一個(gè)格式化字符串即對(duì)應(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è)樣子呢,?
  1. //A wrong version of NSLog 
  2. #define NSLog(format, ...)   fprintf(stderr, "<%s : %d> %s\n",                                           \ 
  3.                              [[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String],  \ 
  4.                              __LINE__, __func__);                                                        \ 
  5.                              (NSLog)((format), ##__VA_ARGS__);                                           \ 
  6.                              fprintf(stderr, "-------\n");        
答案當(dāng)然是否定的,,也許簡單的測(cè)試?yán)锬銢]有遇到問題,但是在生產(chǎn)環(huán)境中這個(gè)宏顯然悲劇了,??紤]下面的常見情況
  1. if (errorHappend) 
  2.     NSLog(@"Oops, error happened"); 
展開以后將會(huì)變成
  1. if (errorHappend) 
  2.     fprintf((stderr, "<%s : %d> %s\n",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __LINE__, __func__); 
  3. (NSLog)((format), ##__VA_ARGS__); //I will expand this later 
  4. fprintf(stderr, "-------\n"); 
注意..C系語言可不是靠縮進(jìn)來控制代碼塊和邏輯關(guān)系的。所以說如果使用這個(gè)宏的人沒有在條件判斷后加大括號(hào)的話,,你的宏就會(huì)一直調(diào)用真正的NSLog輸出東西,,這顯然不是我們想要的邏輯。當(dāng)然在這里還是需要重新批評(píng)一下認(rèn)為if后的單條執(zhí)行語句不加大括號(hào)也沒問題的同學(xué),,這是陋習(xí),,無需理由,請(qǐng)改正,。不論是不是一條語句,,也不論是if后還是else后,都加上大括號(hào),,是對(duì)別人和自己的一種尊重,。
好了知道我們的宏是如何失效的,也就知道了修改的方法,。作為宏的開發(fā)者,,應(yīng)該力求使用者在最大限度的情況下也不會(huì)出錯(cuò),于是我們想到直接用一對(duì)大括號(hào)把宏內(nèi)容括起來,,大概就萬事大吉了,?像這樣:
  1. //Another wrong version of NSLog 
  2. #define NSLog(format, ...)   { 
  3.                                fprintf(stderr, "<%s : %d> %s\n",                                           \ 
  4.                                [[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String],  \ 
  5.                                __LINE__, __func__);                                                        \ 
  6.                                (NSLog)((format), ##__VA_ARGS__);                                           \ 
  7.                                fprintf(stderr, "-------\n");                                               \ 
  8.                              } 
展開剛才的那個(gè)式子,結(jié)果是:
  1. //I am sorry if you don't like { in the same like. But I am a fan of this style :P 
  2. if (errorHappend) { 
  3.     fprintf((stderr, "<%s : %d> %s\n",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __LINE__, __func__); 
  4.     (NSLog)((format), ##__VA_ARGS__); 
  5.     fprintf(stderr, "-------\n"); 
  6. }; 
編譯,,執(zhí)行,,正確!因?yàn)橛么罄ㄌ?hào)標(biāo)識(shí)代碼塊是不會(huì)嫌多的,,所以這樣一來的話我們的宏在不論if后面有沒有大括號(hào)的情況下都能工作了,!這么看來,前面例子中的do while果然是多余的,?于是我們又可以愉快地發(fā)布了,?如果你夠細(xì)心的話,可能已經(jīng)發(fā)現(xiàn)問題了,那就是上面最后的一個(gè)分號(hào),。雖然編譯運(yùn)行測(cè)試沒什么問題,,但是始終稍微有些刺眼有木有?沒錯(cuò),,因?yàn)槲覀冊(cè)趯慛SLog本身的時(shí)候,,是將其當(dāng)作一條語句來處理的,后面跟了一個(gè)分號(hào),,在宏展開后,,這個(gè)分號(hào)就如同噩夢(mèng)一般的多出來了。什么,,你還沒看出哪兒有問題,?試試看展開這個(gè)例子吧:
  1. if (errorHappend) 
  2.     NSLog(@"Oops, error happened"); 
  3. else 
  4.   //Yep, no error, I am happy~ :) 
No! I am not haapy at all! 因?yàn)榫幾g錯(cuò)誤了,!實(shí)際上這個(gè)宏展開以后變成了這個(gè)樣子:
  1. if (errorHappend) { 
  2.     fprintf((stderr, "<%s : %d> %s\n",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __LINE__, __func__); 
  3.     (NSLog)((format), ##__VA_ARGS__); 
  4.     fprintf(stderr, "-------\n"); 
  5. }; else { 
  6.     //Yep, no error, I am happy~ :) 
因?yàn)閑lse前面多了一個(gè)分號(hào),,導(dǎo)致了編譯錯(cuò)誤,,很惱火..要是寫代碼的人乖乖寫大括號(hào)不就啥事兒沒有了么?但是我們還是有巧妙的解決方法的,,那就是上面的do while,。把宏的代碼塊添加到do中,然后之后while(0),,在行為上沒有任何改變,,但是可以巧妙地吃掉那個(gè)悲劇的分號(hào),使用do while的版本展開以后是這個(gè)樣子的
  1. if (errorHappend) 
  2.   do { 
  3.         fprintf((stderr, "<%s : %d> %s\n",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __LINE__, __func__); 
  4.         (NSLog)((format), ##__VA_ARGS__); 
  5.         fprintf(stderr, "-------\n"); 
  6.     } while (0); 
  7. else { 
  8.     //Yep, no error, I am really happy~ :) 
這個(gè)吃掉分號(hào)的方法被大量運(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),,我們?cè)趧偛臡IN最后也用到了反斜杠,,希望你還能記得。接下來一行我們填寫這個(gè)格式輸出中的三個(gè)token,,
  1. [[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __LINE__, __func__); 
這里用到了三個(gè)預(yù)定義宏,,和剛才的__COUNTER__類似,,預(yù)定義宏的行為是由編譯器指定的。__FILE__返回當(dāng)前文件的絕對(duì)路徑,,__LINE__返回展開該宏時(shí)在文件中的行數(shù),__func__是改宏所在scope的函數(shù)名稱,。我們?cè)谧鯨og輸出時(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)榻^對(duì)路徑太長很難看),,行數(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è)井號(hào)##,。還記得我們上面在MIN中的兩個(gè)井號(hào)么,,在那里兩個(gè)井號(hào)的意思是將前后兩項(xiàng)合并,在這里做的事情比較類似,,將前面的格式化字符串和后面的參數(shù)列表合并,,這樣我們就得到了一個(gè)完整的NSLog方法了。之后的幾行相信大家自己看懂也沒有問題了,,最后輸出一下試試看,,大概看起來會(huì)是這樣的。
  1. ------- 
  2. <AppDelegate.m : 46> -[AppDelegate application:didFinishLaunchingWithOptions:] 
  3. 2014-01-20 16:44:25.480 TestProject[30466:70b] The array is ( 
  4.     Hello, 
  5.     My, 
  6.     Macro 
  7. ------- 
帶有文件,,行號(hào)和方法的輸出,,并且用橫杠隔開了(請(qǐng)?jiān)徫覜]有質(zhì)感的設(shè)計(jì),也許我應(yīng)該畫一只牛,,比如這樣,?),debug的時(shí)候也許會(huì)輕松一些吧 :)
hello cowsay
這個(gè)Log有三個(gè)懸念點(diǎn),,首先是為什么我們要把format單獨(dú)寫出來,,然后吧其他參數(shù)作為可變參數(shù)傳遞呢?如果我們不要那個(gè)format,,而直接寫成NSLog(...)會(huì)不會(huì)有問題,?對(duì)于我們這里這個(gè)例子來說的話是沒有變化的,但是我們需要記住的是...是可變參數(shù)列表,它可以代表一個(gè),、兩個(gè),,或者是很多個(gè)參數(shù),但同時(shí)它也能代表零個(gè)參數(shù),。如果我們?cè)谏昝鬟@個(gè)宏的時(shí)候沒有指定format參數(shù),,而直接使用參數(shù)列表,那么在使用中不寫參數(shù)的NSLog()也將被匹配到這個(gè)宏中,,導(dǎo)致編譯無法通過,。如果你手邊有Xcode,也可以看看Cocoa中真正的NSLog方法的實(shí)現(xiàn),,可以看到它也是接收一個(gè)格式參數(shù)和一個(gè)參數(shù)列表的形式,,我們?cè)诤昀镞@么定義,正是為了其傳入正確合適的參數(shù),,從而保證使用者可以按照原來的方式正確使用這個(gè)宏,。
第二點(diǎn)是既然我們的可變參數(shù)可以接受任意個(gè)輸入,那么在只有一個(gè)format輸入,,而可變參數(shù)個(gè)數(shù)為零的時(shí)候會(huì)發(fā)生什么呢,?不妨展開看一看,記住##的作用是拼接前后,,而現(xiàn)在##之后的可變參數(shù)是空:
  1. NSLog(@"Hello"); 
  2. => do { 
  3.        fprintf((stderr, "<%s : %d> %s\n",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __LINE__, __func__); 
  4.        (NSLog)((@"Hello"), ); 
  5.        fprintf(stderr, "-------\n"); 
  6.    } while (0); 
中間的一行(NSLog)(@"Hello", );似乎是存在問題的,,你一定會(huì)有疑惑,這種方式怎么可能編譯通過呢,?,!原來大神們其實(shí)早已想到這個(gè)問題,并且進(jìn)行了一點(diǎn)特殊的處理,。這里有個(gè)特殊的規(guī)則,,在逗號(hào)和__VA_ARGS__之間的雙井號(hào),除了拼接前后文本之外,,還有一個(gè)功能,,那就是如果后方文本為空,那么它會(huì)將前面一個(gè)逗號(hào)吃掉,。這個(gè)特性當(dāng)且僅當(dāng)上面說的條件成立時(shí)才會(huì)生效,,因此可以說是特例。加上這條規(guī)則后,,我們就可以將剛才的式子展開為正確的(NSLog)((@"Hello"));了,。
最后一個(gè)值得討論的地方是(NSLog)((format), ##__VA_ARGS__);的括號(hào)使用。把看起來能去掉的括號(hào)去掉,,寫成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)然請(qǐng)先加油看懂它們吧)
  1. #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) 
  2. #define NSLogSize(size) NSLog(@"%s w:%.4f, h:%.4f", #size, size.width, size.height) 
  3. #define NSLogPoint(point) NSLog(@"%s x:%.4f, y:%.4f", #point, point.x, point.y) 
兩個(gè)實(shí)際應(yīng)用的例子
當(dāng)然不是說上面介紹的宏實(shí)際中不能用。它們相對(duì)簡單,,但是里面坑不少,,所以顯得很有特點(diǎn),非常適合作為入門用,。而實(shí)際上在日常中很多我們常用的宏并沒有那么多奇怪的問題,,很多時(shí)候我們按照想法去實(shí)現(xiàn),再稍微注意一下上述介紹的可能存在的共通問題,,一個(gè)高質(zhì)量的宏就可以誕生,。如果能寫出一些有意義價(jià)值的宏,小了從對(duì)你的代碼的使用者來說,,大了從整個(gè)社區(qū)整個(gè)世界和減少碳排放來說,,你都做出了相當(dāng)?shù)呢暙I(xiàn)。我們通過幾個(gè)實(shí)際的例子來看看,,宏是如何改變我們的生活,,和寫代碼的習(xí)慣的吧。
先來看看這兩個(gè)宏
  1. #define XCTAssertTrue(expression, format...) \ 
  2.     _XCTPrimitiveAssertTrue(expression, ## format) 
  3.   
  4. #define _XCTPrimitiveAssertTrue(expression, format...) \ 
  5. ({ \ 
  6.     @try { \ 
  7.         BOOL _evaluatedExpression = !!(expression); \ 
  8.         if (!_evaluatedExpression) { \ 
  9.             _XCTRegisterFailure(_XCTFailureDescription(_XCTAssertion_True, 0, @#expression),format); \ 
  10.         } \ 
  11.     } \ 
  12.     @catch (id exception) { \ 
  13.         _XCTRegisterFailure(_XCTFailureDescription(_XCTAssertion_True, 1, @#expression, [exception reason]),format); \ 
  14.     }\ 
  15. }) 
如果您常年做蘋果開發(fā),,卻沒有見過或者完全不知道XCTAssertTrue是什么的話,,強(qiáng)烈建議補(bǔ)習(xí)一下測(cè)試驅(qū)動(dòng)開發(fā)的相關(guān)知識(shí),我想應(yīng)該會(huì)對(duì)您之后的道路很有幫助,。如果你已經(jīng)很熟悉這個(gè)命令了,,那我們一起開始來看看幕后發(fā)生了什么。
有了上面的基礎(chǔ),,相信您大體上應(yīng)該可以自行解讀這個(gè)宏了,。({...})的語法和##都很熟悉了,這里有三個(gè)值得注意的地方,,在這個(gè)宏的一開始,,我們后面的的參數(shù)是format...,,這其實(shí)也是可變參數(shù)的一種寫法,和...與__VA_ARGS__配對(duì)類似,,{NAME}...將于{NAME}配對(duì)使用,。也就是說,在這里宏內(nèi)容的format指代的其實(shí)就是定義的先對(duì)expression取了兩次反,?我不是科班出身,,但是我還能依稀記得這在大學(xué)程序課上講過,兩次取反的操作可以確保結(jié)果是BOOL值,,這在objc中還是比較重要的(關(guān)于objc中BOOL的討論已經(jīng)有很多,,如果您還沒能分清BOOL, bool和Boolean,可以參看NSHisper的這篇文章),。然后就是@#expression這個(gè)式子,。我們接觸過雙井號(hào)##,而這里我們看到的操作符是單井號(hào)#,,注意井號(hào)前面的@是objc的編譯符號(hào),,不屬于宏操作的對(duì)象。單個(gè)井號(hào)的作用是字符串化,,簡單來說就是將替換后在兩頭加上”“,,轉(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è)宏定義,。對(duì)于RAC不熟悉或者沒聽過的朋友,可以簡單地看看Limboy的一系列相關(guān)博文(搜索ReactiveCocoa),,介紹的很棒,。如果覺得“哇哦這個(gè)好酷我很想學(xué)”的話,不妨可以跟隨raywenderlich上這個(gè)系列的教程做一些實(shí)踐,,里面簡單地用到了RAC,,但是都已經(jīng)包含了RAC的基本用法了。RAC中有幾個(gè)很重要的宏,,它們是保證RAC簡潔好用的基本,,可以說要是沒有這幾個(gè)宏的話,,是不會(huì)有人喜歡RAC的。其中RACObserve就是其中一個(gè),,它通過KVC來為對(duì)象的某個(gè)屬性創(chuàng)建一個(gè)信號(hào)返回(如果你看不懂這句話,,不要擔(dān)心,這對(duì)你理解這個(gè)宏的寫法和展開沒有任何影響),。對(duì)于這個(gè)宏,,我決定不再像上面那樣展開和講解,我會(huì)在最后把相關(guān)的宏都貼出來,,大家不妨拿它練練手,,看看能不能將其展開到代碼的狀態(tài),并且明白其中都發(fā)生了些什么,。如果你遇到什么問題或者在展開過程中有所心得,,歡迎在評(píng)論里留言分享和交流 :)
好了,這篇文章已經(jīng)夠長了,。希望在看過以后您在看到宏的時(shí)候不再發(fā)怵,,而是可以很開心地說這個(gè)我會(huì)這個(gè)我會(huì)這個(gè)我也會(huì),。最終目標(biāo)當(dāng)然是寫出漂亮高效簡潔的宏,,這不論對(duì)于提高生產(chǎn)力還是震懾你的同事提升自己實(shí)力都會(huì)很有幫助。
另外,,在這里一定要宣傳一下關(guān)注了很久的@hangcom 吳航前輩的新書《iOS應(yīng)用逆向工程》,。很榮幸能夠在發(fā)布之前得到前輩的允許拜讀了整本書,可以說看的暢快淋漓,。我之前并沒有越獄開發(fā)的任何基礎(chǔ),,也對(duì)相關(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)田,大家可以隨便填寫玩弄,??傊?,加油!
  1. //調(diào)用 RACSignal是類的名字 
  2. RACSignal *signal = [RACObserve(self, currentLocation)]; 
  3.   
  4. //以下開始是宏定義 
  5. //rac_valuesForKeyPath:observer:是方法名 
  6. #define RACObserve(TARGET, KEYPATH) \ 
  7.     [(id)(TARGET) rac_valuesForKeyPath:@keypath(TARGET, KEYPATH) observer:self] 
  8.   
  9. #define keypath(...) \ 
  10.     metamacro_if_eq(1, metamacro_argcount(__VA_ARGS__))(keypath1(__VA_ARGS__))(keypath2(__VA_ARGS__)) 
  11.   
  12. //這個(gè)宏在取得keypath的同時(shí)在編譯期間判斷keypath是否存在,,避免誤寫 
  13. //您可以先不用介意這里面的巫術(shù).. 
  14. #define keypath1(PATH) \ 
  15.     (((void)(NO && ((void)PATH, NO)), strchr(# PATH, '.') + 1)) 
  16.   
  17. #define keypath2(OBJ, PATH) \ 
  18.     (((void)(NO && ((void)OBJ.PATH, NO)), # PATH)) 
  19.   
  20. //A和B是否相等,,若相等則展開為后面的第一項(xiàng),否則展開為后面的第二項(xiàng) 
  21. //eg. metamacro_if_eq(0, 0)(true)(false) => true 
  22. //    metamacro_if_eq(0, 1)(true)(false) => false 
  23. #define metamacro_if_eq(A, B) \ 
  24.         metamacro_concat(metamacro_if_eq, A)(B) 
  25.   
  26. #define metamacro_if_eq1(VALUE) metamacro_if_eq0(metamacro_dec(VALUE)) 
  27.   
  28. #define metamacro_if_eq0(VALUE) \ 
  29.     metamacro_concat(metamacro_if_eq0_, VALUE) 
  30.   
  31. #define metamacro_if_eq0_1(...) metamacro_expand_ 
  32.   
  33. #define metamacro_expand_(...) __VA_ARGS__ 
  34.   
  35. #define metamacro_argcount(...) \ 
  36.         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) 
  37.   
  38. #define metamacro_at(N, ...) \ 
  39.         metamacro_concat(metamacro_at, N)(__VA_ARGS__) 
  40.   
  41. #define metamacro_concat(A, B) \ 
  42.         metamacro_concat_(A, B) 
  43.   
  44. #define metamacro_concat_(A, B) A ## B 
  45.   
  46. #define metamacro_at2(_0, _1, ...) metamacro_head(__VA_ARGS__) 
  47.   
  48. #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__) 
  49.   
  50. #define metamacro_head(...) \ 
  51.         metamacro_head_(__VA_ARGS__, 0) 
  52.   
  53. #define metamacro_head_(FIRST, ...) FIRST 
  54.   
  55. #define metamacro_dec(VAL) \ 
  56.         metamacro_at(VAL, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19) 

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,,所有內(nèi)容均由用戶發(fā)布,,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式,、誘導(dǎo)購買等信息,,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,,請(qǐng)點(diǎn)擊一鍵舉報(bào),。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多