做者:Mike Ash,原文连接,原文日期:2015-03-20 译者:俊东;校对:numbbbbb,Nemocdz;定稿:Pancfhtml
前几天我遇到了一个有趣的问题:如何编写一个 C 语言预处理器的宏,删除包围实参的括号?git
今天的文章,将为你们分享个人解决方案。github
C 语言预处理器是一个至关盲目的文本替换引擎,它并不理解 C 代码,更不用说 Objective-C 了。它的工做原理还算不错,能够应付大部分状况,但偶尔也会出现判断失误。swift
这里举个典型的例子:数组
objc
XCTAssertEqualObjects(someArray, @[ @"one", @"two" ], @"Array is not as expected");
复制代码
这会没法编译,而且会出现很是古怪的错误提示。预处理器查找分隔宏参数的逗号时,没能将数组结构 @ [...]
中的东西理解为一个单一的元素。结果代码尝试比较 someArray
和 @[@"one"
。断言失败消息 @"two"]
和 @"Array is not as expected"
是另外的实参。这些半成品部分用于 XCTAssertEqualObjects
的宏扩展中,生成的代码固然错得离谱。spa
要解决这个问题也很容易:添加括号就行。预编译器不能识别 []
,但它确实知道 ()
而且可以理解应该忽略里面的逗号。下面的代码就能正常运行:翻译
objc
XCTAssertEqualObjects(someArray, (@[ @"one", @"two" ]), @"Array is not as expected");
复制代码
在 C 语言的许多场景下,你添加多余的括号也不会有任何区别。宏扩展开以后,生成的代码虽然在数组文字周围有括号,但没有异常。你能够写搞笑的多层括号表达式,编译器会愉快地帮你解析到最里面一层:code
objc
NSLog(@"%d",((((((((((42)))))))))));
复制代码
甚至将 NSLog
这样处理也行:htm
objc
((((((((((NSLog))))))))))(@"%d",42);
复制代码
在 C 中有一个地方你不能随意添加括号:类型(types)。例如:blog
objc
int f(void); // 合法
(int) f(void); // 不合法
复制代码
何时会发生这种状况呢?这种状况并不常见,但若是你有一个使用类型的宏,而且类型包含的逗号不在括号内,则会出现这种状况。宏能够作不少事情,当一个类型遵循多个协议时,在 Objective-C 中可能出现一些类型带有未加括号的逗号;当使用带有多个模板参数的模板化类型时,在 C++ 中也可能出现。举个例子,这有一个简单的宏,建立从字典中提供静态类型值的 getter
:
objc
#define GETTER(type,name) \
- (type)name { \
return [_dictionary objectForKey: @#name]; \
}
复制代码
你能这样使用它:
objc
@implementation SomeClass {
NSDictionary *_dictionary;
}
GETTER(NSView *,view)
GETTER(NSString *,name)
GETTER(id<NSCopying>,someCopyableThing)
复制代码
到目前为止没问题。如今假设咱们想要建立一个遵循两个协议的类型:
objc
GETTER(id<NSCopying,NSCoding>,someCopyableAndCodeableThing)
复制代码
哎呀!宏不起做用了。并且添加括号也无济于事:
objc
GETTER((id<NSCopying,NSCoding>),someCopyableAndCodeableThing)
复制代码
这会产生非法代码。这时咱们须要一个删除可选括号的 UNPAREN 宏。将 GETTER
宏重写:
#define GETTER(type,name) \
- (UNPAREN(type))name { \
return [_dictionary objectForKey: @#name]; \
}
复制代码
咱们该怎么作呢?
删除括号很容易:
objc
#define UNPAREN(...) __VA_ARGS__
#define GETTER(type,name) \
- (UNPAREN type)name { \
return [_dictionary objectForKey: @#name]; \
}
复制代码
虽然看上去很扯,但这的确能运行。预编译器将 type
扩展为 (id <NSCopying,NSCoding>)
,生成 UNPAREN (id<NSCopying, NSCoding>)
。而后它会将 UNPAREN
宏扩展为 id <NSCopying,NSCoding>
。括号,消失!
可是,以前使用的 GETTER
失败了。例如,GETTER(NSView *,view)
在宏扩展中生成 UNPAREN NSView *
。不会进一步扩展就直接提供给编译器。结果天然会报编译器错误,由于 UNPAREN NSView *
是没法编译的。这虽然能够经过编写 GETTER((NSView *),view)
来解决,可是被迫添加这些括号很烦人。这样的结果可不是咱们想要的。
我马上想到了如何摆脱剩余的 UNPAREN
。当你想要一个标识符消失时,你可使用一个空的 #define
,以下所示:
objc
#define UNPAREN
复制代码
有了这个,a UNPAREN b
的序列变为 a b
。完美解决问题!可是,若是已经存在带参数的另外一个定义,则预处理器会拒绝此操做。即便预处理器可能选择其中一个,它也不会同时存在两种形式。若是可行的话,这能有效解决咱们的问题,但惋惜的是并不容许:
objc
#define UNPAREN(...) __VA_ARGS__
#define UNPAREN
#define GETTER(type,name) \
- (UNPAREN type)name { \
return [_dictionary objectForKey: @#name]; \
}
复制代码
这没法经过预处理器,它会因为 UNPAREN
的重复 #define
而报错。不过,它引导咱们走上了成功的道路。如今的瓶颈是怎么找出一种方法来实现相同的效果,而不会使两个宏具备相同的名称。
最终目标是让 UNPAREN(x)
和 UNPAREN((x))
结果都是 x
。朝着这个目标迈出的第一步是制做一些宏,其中传递 x
和 (x)
产生相同的输出,即便它并不肯定 x
是什么。这能够经过将宏名称放在宏扩展中来实现,以下所示:
objc
#define EXTRACT(...) EXTRACT __VA_ARGS__
复制代码
如今若是你写 EXTRACT(x)
,结果是 EXTRACT x
。固然,若是你写 EXTRACT x
,结果也是 EXTRACT x
,就像没有宏扩展的状况。这仍然给咱们留下一个 EXTRACT
。虽然不能用 #define
直接解决,但这已经进步了。
预处理器有一个操做符 ##
,它将两个标识符粘合在一块儿。例如,a ## b
变为 ab
。这能够用于从片断构造标识符,但也能够用于调用宏。例如:
objc
#define AA 1
#define AB 2
#define A(x) A ## x
复制代码
从这里能够看到,A(A)
产生 1
,A(B)
产生 2
。
让咱们将这个运算符与上面的 EXTRACT
宏结合起来,尝试生成一个 UNPAREN
宏。因为 EXTRACT(...)
使用前缀 EXTRACT
生成实参,所以咱们可使用标识符粘合来生成以 EXTRACT
结尾的其余标记。若是咱们 #define
那个新标记为空,那就搞定了。
这是一个以 EXTRACT
结尾的宏,它不会产生任何结果:
objc
#define NOTHING_EXTRACT
复制代码
这是对 UNPAREN
宏的尝试,它将全部内容放在一块儿:
objc
#define UNPAREN(x) NOTHING_ ## EXTRACT x
复制代码
不幸的是,这并不能实现咱们的目标。问题在操做顺序上。若是咱们写 UNPAREN((int))
,咱们将会获得:
objc
UNPAREN((int))
NOTHING_ ## EXTRACT (int)
NOTHING_EXTRACT (int)
(int)
复制代码
标示符粘合太早起做用,EXTRACT
宏永远不会有机会扩展开。
可使用间接的方式强制预处理器用不一样的顺序判断事件。咱们能够制做一个 PASTE
宏,而不是直接使用 ##
:
objc
#define PASTE(x,...) x ## __VA_ARGS__
复制代码
而后咱们将根据它编写 UNPAREN
:
objc
#define UNPAREN(x) PASTE(NOTHING_,EXTRACT x)
复制代码
这仍然不起做用。状况以下:
objc
UNPAREN((int))
PASTE(NOTHING_,EXTRACT (int))
NOTHING_ ## EXTRACT (int)
NOTHING_EXTRACT (int)
(int)
复制代码
但更接近咱们的目标了。序列 EXTRACT(int)
显然没有触发标示符粘合操做符。咱们必须让预处理器在它看到 ##
以前解析它。能够经过另外一种方式间接强制解析它。让咱们定义一个只包装 PASTE
的 EVALUATING_PASTE
宏:
objc
#define EVALUATING_PASTE(x,...) PASTE(x,__VA_ARGS__)
复制代码
如今让咱们用它写 UNPAREN
:
objc
#define UNPAREN(x) EVALUATING_PASTE(NOTHING_,EXTRACT x)
复制代码
这是展开以后:
objc
UNPAREN((int))
EVALUATING_PASTE(NOTHING_,EXTRACT (int))
PASTE(NOTHING_,EXTRACT int)
NOTHING_ ## EXTRACT int
NOTHING_EXTRACT int
int
复制代码
即便没有额外加括号也能正常运行,由于额外的赋值并无影响:
objc
UNPAREN(int)
EVALUATING_PASTE(NOTHING_,EXTRACT int)
PASTE(NOTHING_,EXTRACT int)
NOTHING_ ## EXTRACT int
NOTHING_EXTRACT int
int
复制代码
成功了!咱们如今编写 GETTER
时能够不须要围绕类型的括号了:
objc
#define GETTER(type,name) \
- (UNPAREN(type))name { \
return [_dictionary objectForKey: @#name]; \
}
复制代码
在选择一些宏来证实这个结构时,我构建了一个很好的 dispatch_once
宏来制做延迟初始化的常量。实现以下:
objc
#define ONCE(type,name,...) \
UNPAREN(type) name() { \
static UNPAREN(type) static_ ## name; \
static dispatch_once_t predicate; \
dispatch_once(&predicate,^{ \
static_ ## name = ({ __VA_ARGS__; }); \
}); \
return static_ ## name; \
}
复制代码
使用案例:
objc
ONCE(NSSet *,AllowedFileTypes,[NSSet setWithArray:@[ @"mp3",@"m4a",@"aiff" ]])
复制代码
而后,你能够调用 AllowedFileTypes()
来获取集合,并根据须要高效建立集合。若是类型不巧包括括号,添加括号就能运行。
仅仅写这个宏,我就发现了不少艰涩的知识。我但愿接触这些知识也不会影响你的思惟。请谨慎使用这些知识。
今天就这样。之后还会有更多使人兴奋的探索,可能比这还要再难以想象。在此以前,若是你对此主题有任何建议,请发送给 咱们!
本文由 SwiftGG 翻译组翻译,已经得到做者翻译受权,最新文章请访问 swift.gg。