NS_ENUM和NS_OPTIONS

碰到问题

最近在看一本书,书名叫《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》,其中谈及了NS_ENUMNS_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_ENUMNS_OPTIONS这两个宏的定义分别是什么,

#define NS_ENUM(...) CF_ENUM(__VA_ARGS__)
#define NS_OPTIONS(_type, _name) CF_OPTIONS(_type, _name)

再看看CF_ENUMCF_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两者在__cplusplusNO的时候定义是一样的,为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 either closed or open 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 with flag_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,这样使用,在任何编译模式下应该都不会出错。

对碰到的其他问题一知半解,主要原因是对各方面了解的比较少,还需要继续努力学习。

相关文章

黑魔法attribute((cleanup))
attribute 总结
Clang Attributes 黑魔法小记

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容