深入研究RAC的@keypath

在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"

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容