在RAC中有一個「keypath」宏定義,我們一般使用它來將對象的某個屬性轉(zhuǎn)換成字符串,它的亮點在于帶上了編譯檢查的功能,避免直接使用字符串容易導(dǎo)致的拼寫錯誤。
下面我們來分析一下這個宏是怎么實現(xiàn)的。
#define keypath(...) \
metamacro_if_eq(1, metamacro_argcount(__VA_ARGS__))(keypath1(__VA_ARGS__))(keypath2(__VA_ARGS__))
這是「keypath」的定義,先看前半部分「metamacro_if_eq(1, metamacro_argcount(VA_ARGS))」的實現(xiàn)。
「metamacro_if_eq」的實現(xiàn)比較簡單,點進(jìn)去看它的定義:
#define metamacro_if_eq(A, B) \
metamacro_concat(metamacro_if_eq, A)(B)
「metamacro_concat」它的作用是將參數(shù)A和B拼接起來,因此「metamacro_if_eq(1, metamacro_argcount(VA_ARGS))」會轉(zhuǎn)換成:
接下來我們分析一下「metamacro_argcount」的實現(xiàn):
#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__)
「metamacro_at」里也使用了「metamacro_concat」宏,經(jīng)過轉(zhuǎn)換,「metamacro_argcount(VA_ARGS)」轉(zhuǎn)換成:
metamacro_at20(__VA_ARGS__, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1)
metamacro_at20」的作用是計算「VA_ARGS」接收的參數(shù)個數(shù)。
由此我們可知,「metamacro_argcount」用于計算傳入「keypath」的參數(shù)個數(shù)。
接下來,我們回去分析「metamacro_if_eq1」的實現(xiàn):
#define metamacro_if_eq1(VALUE) metamacro_if_eq0(metamacro_dec(VALUE))
#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)
先看「metamacro_dec」的實現(xiàn),它也使用了「metamacro_at」,經(jīng)過轉(zhuǎn)換「metamacro_dec(VALUE)」變成:
metamacro_atVALUE(-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19) # VALUE為前面計算的參數(shù)個數(shù)
「metamacro_atVALUE」系列的定義為:(比較多,只列出了前三個,順便貼上了「metamacro_head」和「metamacro_head_」的定義)
#define metamacro_at0(...) metamacro_head(__VA_ARGS__)
#define metamacro_at1(_0, ...) metamacro_head(__VA_ARGS__)
#define metamacro_at2(_0, _1, ...) metamacro_head(__VA_ARGS__)
#define metamacro_head(...) \
metamacro_head_(__VA_ARGS__, 0)
#define metamacro_head_(FIRST, ...) FIRST
「metamacro_atVALUE」系列的宏的參數(shù)個數(shù)隨著「VALUE」值的增加而增多,很明顯,「_0」、「_1」這些參數(shù)是用來占位的,余下的參數(shù)會被傳入「metamacro_head」中。
比如VALUE的值為1,那么「metamacro_at1(-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19)」就被轉(zhuǎn)換為:
metamacro_head(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19)
而「metamacro_head」只是將參數(shù)直接傳給「metamacro_head_」,「metamacro_head_」的作用也比較明顯:取出第一個參數(shù)。因此,上面的宏被轉(zhuǎn)換成:「0」。
由此可知,「metamacro_dec」的作用是讓參數(shù)自減1.
PS:「metamacro_head」還有一個參數(shù)「0」,它是為了保證「metamacro_dec」轉(zhuǎn)換的結(jié)果>=0。
回到代碼1的分析,我們接下來要分析的是「metamacro_if_eq0」:
#define metamacro_if_eq0(VALUE) \
metamacro_concat(metamacro_if_eq0_, VALUE)
#define metamacro_if_eq0_0(...) __VA_ARGS__ metamacro_consume_
#define metamacro_if_eq0_1(...) metamacro_expand_
#define metamacro_consume_(...)
#define metamacro_expand_(...) __VA_ARGS__
「metamacro_if_eq0」直接轉(zhuǎn)換成「metamacro_if_eq0_VALUE」系列宏(只列出了前兩個)。
這個宏是一個條件判斷,如果傳入「keypath」的參數(shù)個數(shù)為1,則執(zhí)行「keypath1」,如果大于1,則執(zhí)行「keypath2」。它是怎么做到的呢?我們來分析一下:
經(jīng)過上面一系列的轉(zhuǎn)換以后,我們得到下面這個宏:
metamacro_if_eq0_VALUE(keypath1(__VA_ARGS__))(keypath2(__VA_ARGS__))
當(dāng)「VALUE」的值為0時,上面的宏轉(zhuǎn)換成:
keypath1(__VA_ARGS__) metamacro_consume_(keypath2(__VA_ARGS__)) -> keypath1(__VA_ARGS__) # 「metamacro_consume_」宏沒有作為替換的字符串,相當(dāng)于直接去掉
當(dāng)「VALUE」的值為1時,上面的宏就轉(zhuǎn)換成了:
metamacro_expand_(keypath2(__VA_ARGS__)) -> keypath2(__VA_ARGS__)
到這里我們就剩下最后兩個宏了,繼續(xù)分析!
#define keypath1(PATH) \
(((void)(NO && ((void)PATH, NO)), strchr(# PATH, '.') + 1))
1)這里運用了逗號表達(dá)式,從左往右逐個執(zhí)行表達(dá)式,最后一個表達(dá)式作為整個表達(dá)式的返回值。最后一個表達(dá)式的strchr函數(shù)用于查找參數(shù)一字符串中首次出現(xiàn)參數(shù)二字符的位置,返回值為指向該位置的指針?!?」可將后面的參數(shù)轉(zhuǎn)換成字符串,作為strchr函數(shù)的第一個參數(shù),因此「strchr(# PATH, '.') + 1」的返回值是指向「PATH」字符串中第一次出現(xiàn) '.' 字符的后一個字符的指針。如:「PATH」為'self.object',則返回的指針指向 'o' ,指針打印出來為:「"object"」。
2)strchr函數(shù)的返回值是一個字符指針,而我們要的是一個NSString對象,它是怎么將一個字符指針轉(zhuǎn)換成NSString對象的呢?答案在于最外層的括號,簡化一下「@keypath1(PATH)」的轉(zhuǎn)換結(jié)果:@(strchr(# PATH, '.') + 1),起轉(zhuǎn)換作用的其實就是「@()」了,它能將一個字符指針轉(zhuǎn)換成NSString對象。以前也不知道能這么干,漲姿勢了...
3)只需要「strchr(# PATH, '.') + 1」就可以將PATH轉(zhuǎn)換成目標(biāo)字符串了,那為什么還需要前面這個表達(dá)式「((void)(NO && ((void)PATH, NO))」呢?
其實前面這個表達(dá)式的作用是給「@keypath」添加編譯檢查和代碼提示功能,它將「PATH」作為表達(dá)式的一部分,Xcode會為表達(dá)式自動提示。簡化一下相當(dāng)于:(PATH,strchr(# PATH, '.') + 1)),前面的PATH雖然對整個表達(dá)式的返回結(jié)果沒有影響,但Xcode會為它提供編譯檢查和代碼提示功能。
4)「void」的作用是為了防止逗號表達(dá)式的警告。如:
int a = 0, b = 1;
int c = (a, b); # 由于變量a沒有被使用,所以會有unused的警告
# 給它加個void就不會有警告了
int c = ((void)a, b);
5)加「NO &&」是為了條件短路,預(yù)編譯的時候遇到它會直接跳過「&&」后面的表達(dá)式。
#define keypath2(OBJ, PATH) \
(((void)(NO && ((void)OBJ.PATH, NO)), # PATH))
這個宏跟「keypath1」比較類似,下面分析一下不同的地方:
1)它接收兩個參數(shù),「# PATH」把PATH參數(shù)直接轉(zhuǎn)換成字符串,作為整個表達(dá)式的返回值。簡化一下「@keypath2(OBJ, PATH)」的轉(zhuǎn)換結(jié)果:@("PATH")。
2)當(dāng)你輸入第二個參數(shù)的時候,你會發(fā)現(xiàn)只能輸入OBJ的屬性,這是因為「OBJ.PATH」里的「.」,給「PATH」提供了編譯檢查。
到這里我們就分析完了,通過以上的分析我們也可以得出「@keypath」的作用:
參數(shù)為一個的時候,@keypath(self.object) → @"object"
參數(shù)為兩個的時候,@keypath(self, object) → @"object"