NS_ENUM和NS_OPTIONS
碰到问题
最近在看一本书,书名叫《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》,其中谈及了NS_ENUM
和NS_OPTIONS
的区别,不是很能理解,直接上手来探究一番。
探究问题
使用场景
NS_ENUM
通常用在单一、不可组合的状态,例如状态栏的样式
typedef NS_ENUM(NSInteger, UIStatusBarStyle) {
UIStatusBarStyleDefault = 0, // Dark content, for use on light backgrounds
UIStatusBarStyleLightContent NS_ENUM_AVAILABLE_IOS(7_0) = 1, // Light content, for use on dark backgrounds
UIStatusBarStyleBlackTranslucent NS_ENUM_DEPRECATED_IOS(2_0, 7_0, "Use UIStatusBarStyleLightContent") = 1,
UIStatusBarStyleBlackOpaque NS_ENUM_DEPRECATED_IOS(2_0, 7_0, "Use UIStatusBarStyleLightContent") = 2,
} __TVOS_PROHIBITED;
而 NS_OPTIONS
则用在非单一、可组合的状态,例如屏幕方向
typedef NS_OPTIONS(NSUInteger, UIInterfaceOrientationMask) {
UIInterfaceOrientationMaskPortrait = (1 << UIInterfaceOrientationPortrait),
UIInterfaceOrientationMaskLandscapeLeft = (1 << UIInterfaceOrientationLandscapeLeft),
UIInterfaceOrientationMaskLandscapeRight = (1 << UIInterfaceOrientationLandscapeRight),
UIInterfaceOrientationMaskPortraitUpsideDown = (1 << UIInterfaceOrientationPortraitUpsideDown),
UIInterfaceOrientationMaskLandscape = (UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight),
UIInterfaceOrientationMaskAll = (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight | UIInterfaceOrientationMaskPortraitUpsideDown),
UIInterfaceOrientationMaskAllButUpsideDown = (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight),
} __TVOS_PROHIBITED;
宏定义
在此认识的基础上,来看一下NS_ENUM
和NS_OPTIONS
这两个宏的定义分别是什么,
#define NS_ENUM(...) CF_ENUM(__VA_ARGS__)
#define NS_OPTIONS(_type, _name) CF_OPTIONS(_type, _name)
再看看CF_ENUM
和CF_OPTIONS
分别是什么,CF_ENUM
中间还多了一层,先来看一下CF_ENUM
的定义,
#define CF_ENUM(...) __CF_ENUM_GET_MACRO(__VA_ARGS__, __CF_NAMED_ENUM, __CF_ANON_ENUM, )(__VA_ARGS__)
结合__CF_ENUM_GET_MACRO
的定义来看,
#define __CF_ENUM_GET_MACRO(_1, _2, NAME, ...) NAME
不难理解,当CF_ENUM
只有一个入参的时候,等同于__CF_ANON_ENUM(__VA_ARGS__)
,有两个入参的时候,则等同于__CF_NAMED_ENUM(__VA_ARGS__)
,很快就能找这两个宏的定义,
#if (__cplusplus && __cplusplus >= 201103L && (__has_extension(cxx_strong_enums) || __has_feature(objc_fixed_enum))) || (!__cplusplus && __has_feature(objc_fixed_enum))
#define __CF_NAMED_ENUM(_type, _name) enum __CF_ENUM_ATTRIBUTES _name : _type _name; enum _name : _type
#define __CF_ANON_ENUM(_type) enum __CF_ENUM_ATTRIBUTES : _type
#if (__cplusplus)
#define CF_OPTIONS(_type, _name) _type _name; enum __CF_OPTIONS_ATTRIBUTES : _type
#else
#define CF_OPTIONS(_type, _name) enum __CF_OPTIONS_ATTRIBUTES _name : _type _name; enum _name : _type
#endif
#else
#define __CF_NAMED_ENUM(_type, _name) _type _name; enum
#define __CF_ANON_ENUM(_type) enum
#define CF_OPTIONS(_type, _name) _type _name; enum
#endif
发现CF_OPTIONS
的定义也在这里,其中第一个#if的内容并未深究,按书中所说,是用来判断编译器是否支持新的枚举定义方式。
可以发现,CF_OPTIONS
和__CF_NAMED_ENUM
两者在__cplusplus
为NO
的时候定义是一样的,为YES
,定义才不一样,按书上所说原因是:
在用或运算操作两个枚举值时,C++认为运算结果的数据类型应该是枚举的底层数据类型,也就是NSUInteger。而且C++不允许将这个底层类型“隐式转换”为枚举类型本身。
验证一下
在.m文件中添加如下代码,
typedef NS_ENUM(NSUInteger, MyType) {
MyType1,
MyType2,
MyType3,
};
typedef NS_OPTIONS(NSUInteger, MyState){
MyState1 = 1 << 0,
MyState2 = 1 << 1,
MyState3 = 1 << 2,
};
任意一处添加如下代码,并编译,
MyType type;
MyState state;
type = 1; //no warning
type = MyType1 | MyType2; //no warning
state = 1; //no warning
state = MyState2 | MyState3; //no warning
将.m文件改成.mm文件,再次编译,
MyType type;
MyState state;
type = 1; //warning
type = MyType1 | MyType2; //warning
state = 1; //no warning
state = MyState2 | MyState3; //no warning
由此可见,用NS_ENUM
定义的枚举,在C++模式下,在碰到需要数据类型进行隐式转换的时候,就会报错,通过Product
--Perform Action
--Preprocess
来看一下两者在C++编译模式下的区别,
typedef enum __attribute__((enum_extensibility(open))) MyType : NSUInteger MyType; enum MyType : NSUInteger {
MyType1,
MyType2,
MyType3,
};
typedef NSUInteger MyState; enum __attribute__((flag_enum,enum_extensibility(open))) : NSUInteger{
MyState1 = 1 << 0,
MyState2 = 1 << 1,
MyState3 = 1 << 2,
};
可以看出,MyState
是由一个NSUInteger
数据类型typedef
来的,而不是一个枚举类型,以这种方式,避免了“C++不允许数据类型隐式转换”的问题。
enum_extensibility
在preprocess出来的代码中,发现了__attribute__((enum_extensibility(open)))
,这个是干嘛用的,于是我查了一下资料,关于__attribute__
的黑魔法,可以阅读一下文章结尾给出的一些链接,此处针对enum_extensibility
做一些研究。
Xcode默认的编译器是Clang,在Clang的文档中,我查到了enum_extensibility
的解释。
Attribute
enum_extensibility
is used to distinguish between enum definitions that are extensible and those that are not. The attribute can take eitherclosed
oropen
as an argument.closed
indicates a variable of the enum type takes a value that corresponds to one of the enumerators listed in the enum definition or, when the enum is annotated withflag_enum
, a value that can be constructed using values corresponding to the enumerators.open
indicates a variable of the enum type can take any values allowed by the standard and instructs clang to be more lenient when issuing warnings.
简而言之,就是open
的时候允许类型隐式转换,closed
的时候不允许类型隐式转换,并给出了例子,如下,
enum __attribute__((enum_extensibility(closed))) ClosedEnum {
A0, A1
};
enum __attribute__((enum_extensibility(open))) OpenEnum {
B0, B1
};
enum __attribute__((enum_extensibility(closed),flag_enum)) ClosedFlagEnum {
C0 = 1 << 0, C1 = 1 << 1
};
enum __attribute__((enum_extensibility(open),flag_enum)) OpenFlagEnum {
D0 = 1 << 0, D1 = 1 << 1
};
void foo1() {
enum ClosedEnum ce;
enum OpenEnum oe;
enum ClosedFlagEnum cfe;
enum OpenFlagEnum ofe;
ce = A1; // no warnings
ce = 100; // warning issued
oe = B1; // no warnings
oe = 100; // no warnings
cfe = C0 | C1; // no warnings
cfe = C0 | C1 | 4; // warning issued
ofe = D0 | D1; // no warnings
ofe = D0 | D1 | 4; // no warnings
}
代码拷贝进.m文件,编译,都不报错;
将.m改成.mm,编译,除了ce = A1;
和oe = B1;
,其他都报错;
跟例子里说的情况都不吻合,不知道是我理解错误,还是哪里不对,希望有看到的大神可以教教我。
总结
当需要定义单一、不可组合的枚举类型时,使用NS_ENUM
,当需要定义非单一、可组合的枚举行勒时,使用NS_OPTIONS
,这样使用,在任何编译模式下应该都不会出错。
对碰到的其他问题一知半解,主要原因是对各方面了解的比较少,还需要继续努力学习。