在RAC中有一个「keypath」宏定义,我们一般使用它来将对象的某个属性转换成字符串,它的亮点在于带上了编译检查的功能,避免直接使用字符串容易导致的拼写错误。
下面我们来分析一下这个宏是怎么实现的。
#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))」的实现。
「metamacro_if_eq」的实现比较简单,点进去看它的定义:
#define metamacro_if_eq(A, B) \
metamacro_concat(metamacro_if_eq, A)(B)
「metamacro_concat」它的作用是将参数A和B拼接起来,因此「metamacro_if_eq(1, metamacro_argcount(VA_ARGS))」会转换成:
接下来我们分析一下「metamacro_argcount」的实现:
#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」宏,经过转换,「metamacro_argcount(VA_ARGS)」转换成:
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」接收的参数个数。
由此我们可知,「metamacro_argcount」用于计算传入「keypath」的参数个数。
接下来,我们回去分析「metamacro_if_eq1」的实现:
#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」的实现,它也使用了「metamacro_at」,经过转换「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为前面计算的参数个数
「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」系列的宏的参数个数随着「VALUE」值的增加而增多,很明显,「_0」、「_1」这些参数是用来占位的,余下的参数会被传入「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)」就被转换为:
metamacro_head(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19)
而「metamacro_head」只是将参数直接传给「metamacro_head_」,「metamacro_head_」的作用也比较明显:取出第一个参数。因此,上面的宏被转换成:「0」。
由此可知,「metamacro_dec」的作用是让参数自减1.
PS:「metamacro_head」还有一个参数「0」,它是为了保证「metamacro_dec」转换的结果>=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」直接转换成「metamacro_if_eq0_VALUE」系列宏(只列出了前两个)。
这个宏是一个条件判断,如果传入「keypath」的参数个数为1,则执行「keypath1」,如果大于1,则执行「keypath2」。它是怎么做到的呢?我们来分析一下:
经过上面一系列的转换以后,我们得到下面这个宏:
metamacro_if_eq0_VALUE(keypath1(__VA_ARGS__))(keypath2(__VA_ARGS__))
当「VALUE」的值为0时,上面的宏转换成:
keypath1(__VA_ARGS__) metamacro_consume_(keypath2(__VA_ARGS__)) -> keypath1(__VA_ARGS__) # 「metamacro_consume_」宏没有作为替换的字符串,相当于直接去掉
当「VALUE」的值为1时,上面的宏就转换成了:
metamacro_expand_(keypath2(__VA_ARGS__)) -> keypath2(__VA_ARGS__)
到这里我们就剩下最后两个宏了,继续分析!
#define keypath1(PATH) \
(((void)(NO && ((void)PATH, NO)), strchr(# PATH, '.') + 1))
1)这里运用了逗号表达式,从左往右逐个执行表达式,最后一个表达式作为整个表达式的返回值。最后一个表达式的strchr函数用于查找参数一字符串中首次出现参数二字符的位置,返回值为指向该位置的指针。「#」可将后面的参数转换成字符串,作为strchr函数的第一个参数,因此「strchr(# PATH, '.') + 1」的返回值是指向「PATH」字符串中第一次出现 '.' 字符的后一个字符的指针。如:「PATH」为'self.object',则返回的指针指向 'o' ,指针打印出来为:「"object"」。
2)strchr函数的返回值是一个字符指针,而我们要的是一个NSString对象,它是怎么将一个字符指针转换成NSString对象的呢?答案在于最外层的括号,简化一下「@keypath1(PATH)」的转换结果:@(strchr(# PATH, '.') + 1),起转换作用的其实就是「@()」了,它能将一个字符指针转换成NSString对象。以前也不知道能这么干,涨姿势了...
3)只需要「strchr(# PATH, '.') + 1」就可以将PATH转换成目标字符串了,那为什么还需要前面这个表达式「((void)(NO && ((void)PATH, NO))」呢?
其实前面这个表达式的作用是给「@keypath」添加编译检查和代码提示功能,它将「PATH」作为表达式的一部分,Xcode会为表达式自动提示。简化一下相当于:(PATH,strchr(# PATH, '.') + 1)),前面的PATH虽然对整个表达式的返回结果没有影响,但Xcode会为它提供编译检查和代码提示功能。
4)「void」的作用是为了防止逗号表达式的警告。如:
int a = 0, b = 1;
int c = (a, b); # 由于变量a没有被使用,所以会有unused的警告
# 给它加个void就不会有警告了
int c = ((void)a, b);
5)加「NO &&」是为了条件短路,预编译的时候遇到它会直接跳过「&&」后面的表达式。
#define keypath2(OBJ, PATH) \
(((void)(NO && ((void)OBJ.PATH, NO)), # PATH))
这个宏跟「keypath1」比较类似,下面分析一下不同的地方:
1)它接收两个参数,「# PATH」把PATH参数直接转换成字符串,作为整个表达式的返回值。简化一下「@keypath2(OBJ, PATH)」的转换结果:@("PATH")。
2)当你输入第二个参数的时候,你会发现只能输入OBJ的属性,这是因为「OBJ.PATH」里的「.」,给「PATH」提供了编译检查。
到这里我们就分析完了,通过以上的分析我们也可以得出「@keypath」的作用:
参数为一个的时候,@keypath(self.object) → @"object"
参数为两个的时候,@keypath(self, object) → @"object"