iOS开发

NS_ENUM和NS_OPTIONS

2018-06-03  本文已影响31人  YummyDog

碰到问题

最近在看一本书,书名叫《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 黑魔法小记

上一篇 下一篇

猜你喜欢

热点阅读