最近新加入了项目组,看到了一个让我很费解的宏定义
#define ServiceTypeMake(_cls, _sel) (NO && ((void)[_cls _sel[Service new]], NO),\ [NSString stringWithFormat:@"%s.%s", #_cls, #_sel])
去掉一些敏感信息后的宏就是这个样子。看到这个宏之后,根据我之前的经验只晓得[NSString stringWithFormat:@"%s.%s", #_cls, #_sel]这块是字符串拼接,前半部分目测像写了个废话(NO &&结果明显是NO啊!!!)。
ServiceTypeMake(IDUrlService, sendUrlRequest:)
预编译展开后就是这样的:
(__objc_no && ((void)[IDUrlService sendUrlRequest:[Service new]], __objc_no), [NSString stringWithFormat:@"%s.%s", "IDUrlService", "sendUrlRequest:"])
&&短路与,前面判断为假后面就不执行了,最后相当于就是个字符串拼接,所以这个宏的写法我觉得不是很能理解。通过查阅一些资料和文章后,我对iOS中宏的定义和使用有了一些深入的了解,希望大家指正。
1.宏的入门
*宏的定义
任何中文的翻译都不够权威,这里摘抄一下GCC对宏的定义(定义链接):
A macro is a fragment of code which has been given a name.Whenever the name is used, it is replaced by the contents of the macro.There are two kinds of macros. They differ mostly in what they look like when they are used.Object-like macros resemble data objects when used,function-like macros resemble function calls.
You may define any valid identifier as a macro, even if it is a C keyword. The preprocessor does not know anything about keywords. Thiscan be useful if you wish to hide a keyword such as const from an older compiler that does not understand it. However, the preprocessor operator defined(see Defined) can never be defined as amacro, and C++’s named operators (see C++ Named Operators) cannot be macros when you are compiling C++.
从定义中可以知道宏分两种对象宏(Object-like macros)和方法宏(function-like macros),接下来一一介绍。
对象宏(Object-like macros)
这里先举一些:
#define BUFFER_SIZE 1024
#define NUMBERS 1, \
+ 2, \
+ 3
#define NUMBER_TOTAL NUMBERS
看上面这些例子可以总结出以下东西:
1.在定义宏的时候,宏的名字通常都会使用大写字母,目的是方便大家一眼就能看出这是个宏,引用的GCC原文是:
By convention, macro names are written in uppercase. Programs are
easier to read when it is possible to tell at a glance which names are
macros.
2.如果一个宏的定义中需要换行的话可以用反斜杠“\”结尾来断行
3.在调用宏时,预处理器在替换宏的内容时会继续检查宏的内容本身是否也是宏定义,如果是,会继续替换宏定义的内容,直到全部展开。
这里可能会有一点疑问,就是对于自引用宏的处理,先举个例子:
#define FOO (4 + FOO)
这要是在使用中要一直展开的话就会是一个无限递归的过程,GCC中有对这种情况进行处理,只会在使用FOO的地方替换成(4 + FOO),还有一种情况是:
#define x (4 + y)
#define y (2 * x)
这里会展开为如下形式:
x → (4 + y)
→ (4 + (2 * x))
y → (2 * x)
→ (2 * (4 + y))
4.宏定义以最后有效的定义为准,例如
#define FOO 4
#undef FOO
#define FOO 5
NSLog(@"%d", FOO); → 5
方法宏(function-like macros)
方法宏的特点是在定义宏的时候宏的名字后会接着一对括号,如:
#define MAX(a, b) ((a) > (b))
这里也有一些注意点:
1.重中之重,就是有参数的时候在表达式里尽量多加括号(防御式编程),避免一些操作符优先级问题造成结果错误:
#define MULTIP(a, b) a * b
NSLog(@"%d", MULTIP(1+2, 3+4)); →11
MULTIP(1+2, 3+4)
→1+2 * 3+4
→11
2.如果有字符串内容,即使与参数名称相同也不会被替换
#define func(x) x, @"x"
NSLog(@"%@,%@", func(@"abc")); // 输出结果为 abc,x
3.使用"#"预处理操作符来实现将宏中的参数转化为字符(串),这个操作会将参数中的所有字符都实现字符(串)话,包括引号。(有的文章里面说会把参数里面的多个空格替换为一个空格,亲测不会)
#define XSTR(s) STR(s)
#define STR(x) #x
#define FOO @" a b cc"
NSLog(@"%s", STR(FOO)); //输出FOO
NSLog(@"%s", XSTR(FOO)); //输出@" a b cc"
4.使用"##"实现宏中token的连接
#define VIEW(prefix/*前缀*/, suffix/*后缀*/) prefix##View##suffix
VIEW(UI,) // 等价于 UIView
VIEW(UI, Controller) // 等价于 UIViewController
示例所示表示把prefix和suffix对应的内容与View连接起来,当然连接后的字符(串)必须是有意义的,否则会出现错误或警告;但不能将 “##” 放在宏的最后,否则会出现错误。
预处理器会将注释转化成空格,因此在宏中间,参数中间加入注释都是可以的。只能加入/**/这种注释而不能加入//这种。
“##”的特殊用法:
#define NSLog(args, ...) NSLog(args, ##__VA_ARGS__);
NSLog(@"abc") // 输出结果为 abc
NSLog(@"123, "@"abc") // 输出结果为 123, abc
将 “##” 放在 "," 和参数之间,那么如果参数留空的话,那么 “##” 前面的 "," 就会删掉,从而防止编译错误。
上例中使用标识符 __VA_ARGS__ 来表示多个参数,在宏的定义中则使用 (...) 表示。
经过上面这些内容,大家对宏的使用已经有了基本的认识,但在实际使用过程中依然还有很多坑(特别是函数宏),只有多使用和学习,才能慢慢达到掌握的水准,我们要开始踩坑之旅了。
1.MIN
我们很快会得出我们的版本:
#define MIN_(a, b) ((a) < (b) ? (a) : (b))
测试一下:
初级版:
NSLog(@"%d",MIN_(3,5));
//=>NSLog(@"%d", ((3) < (5) ? (3) : (5)));
//=>3
高级版:
NSLog(@"%d",MIN_(2+6,3*4));
//=>NSLog(@"%d", ((2+6) < (3*4) ? (2+6) : (3*4)));
//=>8
测试通过,发布上线!!!
项目上线了,跑的没问题,直到有一天一个特殊的情况出现了:
NSInteger a = 5;
NSInteger b = 2;
NSInteger c = MIN_(a, b++);
NSLog(@"a = %d, b = %d, c = %d", a, b, c); //输出a = 5 , b = 4, c = 3;
一脸懵逼,这不是真的~
我们期望的结果是a = 5,b = 3, c = 2,a是5,b是2,b++先取b的值跟a比,比a小,所以c得值是2,b自增1,最后b的值是3,这才是想要的结果嘛。为啥会出现上面的结果呢,不妨展开一下
NSInteger c = MIN_(a, b++) = ((a) < (b++) ? (a) : (b++));
这里a先跟b比,5比2大,b会自增1,b为3,此时c取b得值为3,接着b会再自增1,为4。问题就出在比较完a,b的值后b进行了自增1的操作。这里小括号已经无法解决这个问题了,我们需要借助GNU C的赋值扩展({...}),使用这样的方法可以计算出一个对象,而且不会浪费变量名,可以在小范围的作用域内计算特殊的值
NSInteger x = ({
NSInteger y = 2;
NSInteger z = 3;
y + z;
});
NSInteger y = 4; //继续使用y,z当变量没问题
NSInteger z = 5;
有了这个扩展,我们就能解决MIN的写法问题了,GNU C中MIN的标准写法是
#define MIN(A,B) ({ __typeof__(A) __a = (A);\
__typeof__(B) __b = (B);\
__a < __b ? __a : __b; })
赋值扩展里包含3个语句,前两个定义了两个变量__a和__b其类型为输出参数的类型,再进行取小值得运算。用这个写法试了下完美解决问题,但依然会有坑。
NSInteger __a = 5;
NSInteger __b =2;
NSInteger __c =MIN_(__a, __b++);
NSLog(@"__a =%d, __b = %d, __c = %d", __a, __b, __c); //输出__a = 5, __b = 2, __c = 0
因为赋值函数内变量名与外部变量名重名而造成无法被初始化,造成宏的行为不可预知。Apple的工程师彻底解决了这个问题,官方的写法是
#define __NSX_PASTE__(A,B) A##B
#if !defined(MIN)
#define __NSMIN_IMPL__(A,B,L) ({ __typeof__(A) __NSX_PASTE__(__a,L) = (A); __typeof__(B) __NSX_PASTE__(__b,L) = (B); (__NSX_PASTE__(__a,L) < __NSX_PASTE__(__b,L)) ? __NSX_PASTE__(__a,L) : __NSX_PASTE__(__b,L); })
#define MIN(A,B) __NSMIN_IMPL__(A, B, __COUNTER__)
#endif
__NSX_PASTE__这个宏是用来做字符串拼接, __NSMIN_IMPL__这个宏除了字符串拼接之外跟之前的宏写法完全一样,比较费解的就是__NSMIN_IMPL__(A, B, __COUNTER__)这里第三个参数__COUNTER__,__COUNTER__是预定义的宏,在编译阶段开始时从0开始计数,每次被使用时加1,这样就大大避免了变量名重名的可能性。如果一定要任性的定义变量名为__a234这样的名字,后果只会是"no zuo, no die"。
2.NSLog
通过打印来调试是我们经常使用的一种方式,我们希望测试的时候多输出一些信息,比如所在行、方法名等,在release环境下不打印信息,通常我们的写法会是
#ifdef DEBUG
#defineNSLog(format, ...) do { \
fprintf(stderr,"<%s : %d> %s\n", \
[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], \
__LINE__, __func__); \
(NSLog)((format), ##__VA_ARGS__); \
fprintf(stderr,"-------\n"); \
} while (0)
#else
#define NSLog(...);
#endif
这个宏定义比较复杂了,首先判断是否定义了DEBUG,就是说是否在DEBUG模式下,我们按住command点击一下并找不到定义的位置,那DEBUG在哪里定义的呢? 在 "Target > Build Settings > Preprocessor Macros > Debug" 里有一个"DEBUG=1"。在没打包的情况下,就属于DEBUG模式。再看宏定义,先抛开do{}while(0),看内部。首先会打印文件路径的最后一个路径,还有行数和调用的方法。这里出现了__FILE__, __LINE__, __func__三个预定义宏,具体的作用大家可以参考预定义宏。接着是一个标准的NSLog调用,输出参数信息,最后在打印一排“------------”并换行。这里比较费解的就是外层的这个do{}while(0)的作用了,貌似没啥用,就是让代码执行一次,那我们可不可以去掉do()while(0)把NSLog写成这样
#defineNSLog(format, ...) \
fprintf(stderr,"<%s : %d> %s\n", \
[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], \
__LINE__, __func__); \
(NSLog)((format), ##__VA_ARGS__); \
fprintf(stderr,"-------\n"); \
NSLog(@"aaa");
这个例子里我们可以正常输出了,但如果我们碰到这样的呢
if(1)
NSLog(@"aaa");
这里首先出现了一种反面的写法,就是if条件语句判断后不带大括号。我们先看看程序预编译之后的结果
if (1)
fprintf(__stderrp,"<%s : %d> %s\n", [[[NSString stringWithUTF8String:"/Users/baidu/Desktop/test/TestMacro/TestMacro/ViewController.m"] lastPathComponent] UTF8String],98, __func__); (NSLog)((@"aaa")); fprintf(__stderrp,"-------\n");;
我们知道if判断不带大括号的写法是只会执行判断条件后面的一个语句,虽然这里执行完后好像对结果没什么影响,我们还是觉得是不是加个大括号更好一点,就有了一个高级点的版本
#defineNSLog(format, ...) { \
fprintf(stderr,"<%s : %d> %s\n", \
[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], \
__LINE__, __func__); \
(NSLog)((format), ##__VA_ARGS__); \
fprintf(stderr,"-------\n"); \
}
这样看着就没什么毛病了,还减少了do()while(0)的执行,减少了点点CPU的消耗,很开森!很明显,事情不会这么简单,总有童鞋会有“新奇”的写法
if(1)
NSLog(@"aaa");
else
NSLog(@"bbb");
这么写了一下,编译都会报错,wtf???看下预编译的结果
if (1)
{ fprintf(__stderrp,"<%s : %d> %s\n", [[[NSString stringWithUTF8String:"/Users/baidu/Desktop/test/TestMacro/TestMacro/ViewController.m"] lastPathComponent] UTF8String],98, __func__); (NSLog)((@"aaa")); fprintf(__stderrp,"-------\n"); };
else
{ fprintf(__stderrp,"<%s : %d> %s\n", [[[NSString stringWithUTF8String:"/Users/baidu/Desktop/test/TestMacro/TestMacro/ViewController.m"] lastPathComponent] UTF8String],100, __func__); (NSLog)((@"bbb")); fprintf(__stderrp,"-------\n"); };
细心的同学应该会发现if判断的大括号后居然多了一个“;”,这个“;”哪里来的呢,是NSLog(@"aaa");这里出来的,就报错了。在试试加了do{}while(0)的版本呢,编译一下看看区别
if (1)
do{ fprintf(__stderrp,"<%s : %d> %s\n", [[[NSString stringWithUTF8String:"/Users/baidu/Desktop/test/TestMacro/TestMacro/ViewController.m"] lastPathComponent] UTF8String],98, __func__); (NSLog)((@"aaa")); fprintf(__stderrp,"-------\n"); }while(0);
else
do{ fprintf(__stderrp,"<%s : %d> %s\n", [[[NSString stringWithUTF8String:"/Users/baidu/Desktop/test/TestMacro/TestMacro/ViewController.m"] lastPathComponent] UTF8String],100, __func__); (NSLog)((@"bbb")); fprintf(__stderrp,"-------\n"); }while(0);
咦,一下就不一样了,do{}while(0)完美的利用上了最后的分号。事实上,在编译器编译阶段,遇到do{}while(0)时会做一些优化,执行的时间并不会变多。从这里我们总结出来两点
(1)在宏展开有多个语句时,可以包裹上do{}while(0)来吃掉“;”
(2)不要出现if判断不带{}的写法,貌似简化了一点,事实上往往会害人害己,出现一些莫名其妙的异常。
在我们平时遇到的宏中还有很多更复杂、更有趣的宏。首先不要谎,可以先将其展开来慢慢分析,通过一点点琢磨,会理解其精髓的。最后提一下iOS中怎么进行预编译查看结果
3.选择下拉中出现的Preprocess选项,就会出现编译结果。
《宏的入门和理解》就到这里了,之后有时间还会在分享一些更复杂的宏。
参考文章:
1.GCC文档:https://gcc.gnu.org/onlinedocs/cpp/Operator-Precedence-Problems.html#Operator-Precedence-Problems
2.宏菜鸟起飞手册:https://blog.csdn.net/hopedark/article/details/20699723
3.京东宏分享:https://mp.weixin.qq.com/s/qTFLZFL2IAz1ScrV1u313Q