EasyTuple 源代码分析

EasyTuple是由美团开源的一个第三方库,它给Objective-C 添加了元组的能力,可以将几个对象包裹在一个对象中,这样我们就可以从一个函数中返回多个值。它的使用非常简单,比如我们想创建一个由两个元素组成的元组,那么可以这样写:

如果使用 Xcode 辅助编辑器查看预编译后的代码,那么上面的例子在预编译后,会被展开为

可以看到原来的宏的写法会自动被转换成 Objective-C 中的类的创建语法了,那么这个转换过程是怎样发生的呢?下面让我们一步步地去分析这个转换的过程。

EZTuple

右边这个看起来像函数的EZTuple,其实是一个宏:

EZ_CONCAT

我们遇到的第一个宏就是 EZ_CONCAT,它的定义如下

作用是把 A 和 B 两个字符串连接到一起,比如
EZ_CONCAT(hello, world)的结果就是helloworld

EZ_ARG_COUNT

EZ_ARG_COUNT是我们遇到的第二个宏,它的定义有些复杂:

EZ_ARG_COUNT是对_EZ_ARG_COUNT的一个包装。它会被展开为

根据

上面这个宏就是EZ_CONCAT(EZ_ARG_AT, 20)(__VA_ARGS__),而EZ_CONCAT(EZ_ARG_AT, 20)也就是EZ_ARG_AT20,因此这个宏进而就等同于

也就是说我们在使用EZ_ARG_COUNT(...)这个宏的时候,它会被最终展开为

现在假设有这么一行代码EZ_ARG_COUNT(1,2,3),那么它就会展开为

我们注意到

1占据了_0的位置,2占据了_1的位置,3占据了_3的位置,20,19,18,…,4依次占据了
_4、_5、_6、… _19的位置,剩下的3,2,1,0被当做参数传入了 EZ_ARG_HEAD中,
因此EZ_ARG_COUNT(1,2,3)会被展开为 EZ_ARG_HEAD(3,2,1,0)

EZ_ARG_HEAD

EZ_ARG_HEAD的定义如下

它的作用是取出宏的第一个参数,因此

也就是

因此EZ_ARG_COUNT的作用就是获得输入的参数的个数

由以上三个宏的定义,EZTuple(@1, @"string")会被展开为EZTupleAs(EZTuple2, @1, @"string")

进而,根据EZTupleAs的定义

EZTupleAs(EZTuple2, @1, @"string")会被展开为

EZ_FOR_EACH

EZ_FOR_EACH的定义如下

对于EZTupleAs(EZTuple2, @1, @"string")展开的结果中的EZ_FOR_EACH(EZ_INIT_PARAM_CALL, ,@1, @"string")而言,MACROEZ_INIT_PARAM_CALLSEP为空,...@1, @"string",所以
它会被展开为

根据

那么对于EZ_FOR_EACH_CTX(EZ_FOR_EACH_ITER_, , EZ_INIT_PARAM_CALL, @1, @"string")MACROEZ_FOR_EACH_ITER_SEP为空,CTXEZ_INIT_PARAM_CALL,所以它会被展开为

再根据

MACROEZ_FOR_EACH_ITER_SEP为空,CTXEZ_INIT_PARAM_CALL,_0为@1,_1为@”string”,所以上式首先会被展开为

然后

中,MACROEZ_FOR_EACH_ITER_SEP为空,CTXEZ_INIT_PARAM_CALL,_0为@1,所以它会被展开成

所以

会被展开为

也就是说,最一开始的

被展开为

我们先关注EZ_FOR_EACH_ITER_(0, @1, EZ_INIT_PARAM_CALL)的展开情况。EZ_FOR_EACH_ITER_(1, @"string", EZ_INIT_PARAM_CALL)和它是类似的。
根据

那么EZ_FOR_EACH_ITER_(0, @1, EZ_INIT_PARAM_CALL)会被展开为EZ_INIT_PARAM_CALL(0, @1),进而被展开为

EZ_IF_EQ这个宏的定义如下

那么EZ_IF_EQ(0, 0)会被展开为EZ_IF_EQ0(0)EZ_IF_EQ0(0)会被展开为EZ_IF_EQ0_0,所以

会被展开为

_EZ_INIT_PARAM_CALL_FIRST(0, @1)会被展开为EZ_ORDINAL_CAP_AT(0):@1_EZ_INIT_PARAM_CALL(0, @1)会被展开为EZ_ORDINAL_AT(0):@1
EZ_ORDINAL_CAP_ATEZ_ORDINAL_AT的定义如下

EZ_ARG_AT(N, EZ_ORDINAL_NUMBERS)这个宏是取EZ_ORDINAL_NUMBERS中的第 N 个参数,因此EZ_ORDINAL_CAP_AT(0):@1也就是First:@1EZ_ORDINAL_AT(0):@1也就是first:@1
所以

也就是

可知,

会被展开为First:@1 EZ_CONSUME_(first:@1),进而展开为First:@1
同理

展开后得到Second:@"string"
那么,

最终被展开的结果就是First:@1 Second:@"string",所以

展开的结果就是initWithFirst:@1 Second:@"string"

因此EZTupleAs(EZTuple2, @1, @"string")就是

所以最开始的EZTuple(@1,@"string")就被转换为了 Objective-C 中的类的创建语法。

EZ_TUPLE_CLASSES_DEF

回到最开始的声明:

左边的EZTuple2是一个类的名字,但是如果通过Xcode 中的Go To Definition来查看这个类的定义的话,会发现 Xcode 将这个类的定义定位到了一个文件中,这个文件里除了头文件外只有一行宏定义:

EZ_TUPLE_CLASSES_DEF这个宏的定义如下

EZ_FOR的定义则是

对于EZ_FOR(20, EZ_TUPLE_DEF_FOREACH, ;)COUNT为20,MACROEZ_TUPLE_DEF_FOREACHSEP;,因此它会被展开为EZ_CONCAT(EZ_FOR, COUNT)(MARCO, SEP),即EZ_FOR20(EZ_TUPLE_DEF_FOREACH,;)。而EZ_FOR20(EZ_TUPLE_DEF_FOREACH,;)展开后得到

EZ_FOR19(EZ_TUPLE_DEF_FOREACH,;)展开后得到

这样一层层展开,最终的结果为

EZ_TUPLE_DEF_FOREACH(index)的定义如下

内层的EZ_INC(index)会对index进行加1操作,那么EZ_TUPLE_CLASSES_DEF就会展开为

接下来我们再看一下 EZ_TUPLE_DEF(i)的定义:

可以看出EZ_TUPLE_DEF(i)展开后是一个类的定义,并且这个类的定义明显地可以分为三部分:第一部分是拼接出来的类名,该类继承自EZTupleBase,第二部分是通过EZ_FOR_RECURSIVE生成的属性,第三部分是拼接出来的初始化方法。

EZ_FOR_COMMA

这个宏出现在类名中,

这个宏和上面提过的EZ_FOR非常类似,所以展开的结果也是类似的。那么对于EZ_FOR_COMMA(2, EZ_GENERIC_TYPE),展开的结果为

EZ_GENERIC_TYPE(index)

EZ_CHARS_AT(N)的作用是从EZ_CHARS取出第 N 位的字符,因此EZ_GENERIC_TYPE(0)会被展开为__covariant A: idEZ_GENERIC_TYPE(1)会被展开为__covariant B: id

所以

会被展开为

__covariant这个关键字表示协变性,即子类型可以强转到父类型,是Objective-C 新出现的一个用来表达泛型能力的关键字,与它一同出现的另一个关键字是____contravariant,表示逆变性,即父类型可以强转到子类型。对这两个关键字的更详细介绍,可以看一下 sunnyxx 老师的博文《2015 Objective-C 新特性》

回到类的接口定义,根据上面的分析,

会被展开为

EZ_FOR_RECURSIVE

这个宏被用于类定义的第二部分,即类的属性的生成中。它的定义和EZ_FOR也是类似的:

所以

就会被展开为

EZ_PROPERTY_DEF(index)

所以

首先被展开为

进而被展开为

EZ_FOR_SPACE

这个宏的名字和EZ_FOR_COMMA类似,定义也类似,因此它的作用也是类似的,这里就不浪费篇幅了。EZ_FOR_SPACE(2, EZ_INIT_PARAM)会被展开为

这里我们也只看一下EZ_INIT_PARAM(0)是如何展开的,EZ_INIT_PARAM(1)的展开和它是类似的。

EZ_INIT_PARAM(0)首先被展开为

_EZ_INIT_PARAM_FIRST(0)会被展开为

_EZ_INIT_PARAM(0)会被展开为

所以

也就是

EZ_IF_EQ(0,0)也就是EZ_IF_EQ0(0),也就是EZ_IF_EQ0_0
根据

EZ_IF_EQ(0,0)(First:(A)first)(first:(A)first)就是

最终就是First:(A)first。即EZ_INIT_PARAM(0)最终展开的结果就是First:(A)first。同理,EZ_INIT_PARAM(1)最终展开的结果就是Second:(B)second
因此,

展开后的结果就是

回到一开始的类定义

当 i = 2的时候,上面这段宏就会被展开为

我们在前面分析过,EZ_TUPLE_CLASSES_DEF会被展开为

所以在预编译时,这些宏就会被展开成EZTuple1EZTuple2……EZTuple20等类的定义。也就是说,EasyTuple通过这个宏为我们一口气定义了20个元祖类。这样的好处是显而易见的:如果不使用宏的话,那么为了创建这么多个类,我们就要手动去书写很多重复的代码;而使用宏的话,则能够通过宏的巧妙组合在预编译的时候自动生成代码。

小结

宏是一门非常非常强大的技术,但是之前我一直不知道它有这么多高级的玩法和用法,看了EasyTuple的源代码,真是让人大开眼界。其实EasyTuple中还有一些上文中没有提到的宏的用法,限于篇幅这里就不一一展开分析了。

参考资料:

  1. ReactiveCocoa 中奇妙无比的“宏”魔法
  2. Reactive Cocoa Tutorial [1] = 神奇的Macros

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据