条目5:使用枚举描述状态,选项,或者状态码
OC是基于C的,因此支持C的所有特性。其中的一种是枚举类型。它被广泛适用于系统框架中,但是通常被程序员给忽略了。它对定义命名的常量非常有用,例如错误码,选项。由于C++11标准的扩展,最近的系统框架版本包含了一种定义这样的枚举类型。是的,OC从C++11标准中受益。
枚举只不过是一种定义命名常量的方法而已。简单的枚举可能是用来定义程序运行的状态。例如,socket连接可能使用如下的枚举:
enum EOCConnectionState {
EOCConnectionStateDisconnected,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
使用枚举意味着代码可读性强,因为每一种状态被一种易读的值代替。编译器给枚举的每一个成员一个独一无二的值,从0开始,加1递增。存储这样的枚举所用的类型是编译器相关的,但是必须有足够的bit位来代表全部的枚举类型。在前面的枚举类型例子中,它们通常只是一个char类型, 因为枚举的最大的value是2.
但是上述这种枚举类型并不是特别常用,而且需要下面的语法:
enum EOCConnectionState state =EOCConnectionStateDisconnected;
如果你可以不必每次都书写enum,而是使用EOCConnectionState,那就显得更容易了。为了实现这种,你需要添加typedef到枚举的定义中:
enum EOCConnectionState {
EOCConnectionStateDisconnected,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
typedef enum EOCConnectionState EOCConnectionState;
这意味着你可以使用EOCConnectionState而不是使用全部的enum EOCConnectionState:
EOCConnectionState state =EOCConnectionStateDisconnected;
C++11标准的发布给枚举带来了一些改变。其中一个改变是可以使用基础类型来存储枚举类型变量。通过使用基础类型,你可以进行前向声明枚举类型。如果枚举不具备人为确定其基础类型,那么它是不可能被前向声明的,因为编译器并不知道枚举类型最终需要使用的存储大小。因此,当这种类型被使用的时候,编译器并不知道给枚举变量分配多大的存储空间。
你可以使用以下语法定义枚举的基础类型:
enum EOCConnectionStateConnectionState: NSInteger { /* ... */ };
这意味着枚举类型的值会确保存储在NSInteger中,如果你需要的话,它可以如下进行前向声明:
enumEOCConnectionStateConnectionState : NSInteger;
你还可以选择不使用编译器默认的枚举值,而自己定义一个确定的枚举值。语法如下:
enum EOCConnectionStateConnectionState{
EOCConnectionStateDisconnected = 1,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
这表明EOCConnectionStateDisconnected将会使用值1而不是0,它下面的其他值将会进行递增1。因此EOCConnectionStateConnected将会使用值3。
另一个使用枚举的原因是定义选项options,特别是当选项之间可以进行组合使用。如果枚举被正确的定义了,那么选项之间的组合可以使用位操作OR实现。例如,请看下面的苹果框架定义的枚举类型,取自于确定view哪个维度可以被修改大小:
enum UIViewAutoresizing {
UIViewAutoresizingNone = 0,
UIViewAutoresizingFlexibleLeftMargin= 1 << 0,
UIViewAutoresizingFlexibleWidth = 1<< 1,
UIViewAutoresizingFlexibleRightMargin= 1 << 2,
UIViewAutoresizingFlexibleTopMargin= 1 << 3,
UIViewAutoresizingFlexibleHeight = 1<< 4,
UIViewAutoresizingFlexibleBottomMargin= 1 << 5,
}
使用上面的语法可以使每一个选项都具有开或者关的操作,因为每一个选项使用一个bit位来代表。多个选项可以通过位或进行组合。例如,UIViewAutoResizingFlexibleWidth|
UIViewAutoresizingFlexibleHeigh展示了两种进行组合的枚举类型。
enum UIVewAutoresizing resizing =
UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleHeight;
然后可以通过位and进行判断是否有一个选项被设置:
if (resizing &UIViewAutoresizingFlexibleWidth) {
// UIViewAutoresizingFlexibleWidthis set
}
在系统框架中,这种方式被大量使用。另一个例子来自UIKit,在iOS UI框架中,通过使用枚举告诉系统你的界面可以支持的设备旋转方向。它通过一种叫UIInterfaceOrientationMask的枚举类型,你通过实现supportedInterfaceOrientations方法来表明应用程序支持的设备方向:
- (NSUInteger)supportedInterfaceOrientations{
return UIInterfaceOrientationMaskPortrait|
UIInterfaceOrientationMaskLandscapeLeft;
}
系统框架中定义了一些辅助方法来帮助你确定枚举类型所使用的具体类型。而且这些方法是向后兼容的,这意味着当你的编译器支持新标准的时候,它会使用新语法,如果不支持,它会使用就的语法。这些辅助方法以预编译宏#define的方式提供。其中一个提供了基本的枚举类型,例如EOCConnectionState。另外的一种是为像UIViewAutoresizing例子中的选项options服务的:你可以如下使用它们:
typedef NS_ENUM(NSUInteger,EOCConnectionState) {
EOCConnectionStateDisconnected,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
typedef NS_OPTIONS(NSUInteger,EOCPermittedDirection) {
EOCPermittedDirectionUp = 1 <<0,
EOCPermittedDirectionDown = 1<< 1,
EOCPermittedDirectionLeft = 1<< 2,
EOCPermittedDirectionRight = 1<< 3,
};
这是宏定义的样子:
#if (__cplusplus &&__cplusplus >= 201103L &&
(__has_extension(cxx_strong_enums)||
__has_feature(objc_fixed_enum))
) ||
(!__cplusplus &&__has_feature(objc_fixed_enum))
#define NS_ENUM(_type, _name)
enum _name : _type _name; enum _name: _type
#if (__cplusplus)
#define NS_OPTIONS(_type, _name)
_type _name; enum : _type
#else
#define NS_OPTIONS(_type, _name)
enum _name : _type _name; enum _name: _type
#endif
#else
#define NS_ENUM(_type, _name) _type_name; enum
#define NS_OPTIONS(_type, _name)_type _name; enum
#endif
由于有不同的场景,所以进行了不同方式的宏检测。第一种情况是检查编译器是否支持枚举的新语法。它通过相当复杂的Boolean逻辑实现,但是它们都是为了检查是否具有此种特性。如果没有这种特性,那么它通过旧的方式定义枚举。
如果这种特性可用,NS_ENUM类型将会进行如下的扩展:
typedef enum EOCConnectionState :NSUInteger EOCConnectionState;
enum EOCConnectionState : NSUInteger{
EOCConnectionStateDisconnected,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
NS_OPTIONS宏在使用或者不适用C++的编译的时候被定义了不同的方式。如果不是C++,它如同NS_ENUM一样进行扩展。可是,当使用C++,它会有一些不同。为什么?因为当枚举值进行位或的时候,C++编译器的行为会有一些不同。当两个值进行位或的时候,C++将结果类型认为是它们所代表的类型:NSUInteger类型。它同样不允许隐式的类型转换。为了证明这,考虑使用NS_ENUM的方式对EOCPermittedDirection扩展:
typedef enum EOCPermittedDirection :int EOCPermittedDirection;
enum EOCPermittedDirection : int {
EOCPermittedDirectionUp = 1 <<0,
EOCPermittedDirectionDown = 1<< 1,
EOCPermittedDirectionLeft = 1<< 2,
EOCPermittedDirectionRight = 1<< 3,
};
考虑下面的情况:
EOCPermittedDirectionpermittedDirections =
EOCPermittedDirectionLeft |EOCPermittedDirectionUp;
如果使用C++编译器或者OC++,可能会导致下面的错误:
error: cannot initialize a variableof type
'EOCPermittedDirection' with anrvalue of type 'int'
你可能需要一个显式的类型转换。因此NS_OPTIONS在C++中以另一种方式被定义以避免这种转换。基于这个原因,如果你使用枚举值的或操作,你应该总是使用NS_OPTIONS。如果不适用,你应该使用NS_ENUM。
枚举可以被使用在很多场景。Options和states已经在前面被讲述了。可是还有很多其他的场景存在。错误状态也是一种情况。不使用预编译宏或者const常量,而使用枚举类型将相似的状态码有逻辑组织在一起。另外一种使用枚举的情况是styles。例如,如果你需要创建一个多钟styles的UI元素,枚举类型将会是非常适合的。
关于枚举,最后需要注意的是使用switch语句时。有时,你会这样做:
typedef NS_ENUM(NSUInteger,EOCConnectionState) {
EOCConnectionStateDisconnected,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
switch (_currentState) {
EOCConnectionStateDisconnected:
// Handle disconnected state
break;
EOCConnectionStateConnecting:
// Handle connecting state
break;
EOCConnectionStateConnected:
// Handle connected state
break;
}
在switch语句中,有一个默认的入口很常见。可是,当在枚举中使用switch最好不要定义这样的一个默认入口。这是因为如果你后来给枚举添加一个状态,编译器将会自动的警告你新添加的状态并没有在switch中得到处理。如果你添加了默认入口,默认的代码会处理这种新的状态,因此编译器并不会发出警告。这同样适用于使用NS_ENUM宏定义的枚举类型。例如,如果你使用它定义UI元素的风格,你通常会确保switch可以处理所有的风格。
需要记住的
l 使用枚举使代码更易读,它应用在状态机中的状态,方法中的选项或者错误码中。
l 如果一种枚举可以以组合的方式使用,那么将它们的值以乘以2的方式定义。这样可以使用OR操作符进行组合使用。
l 使用枚举类型时候,不要在switch语句中加入默认的case。当你添加新的枚举时候,编译器会警告你switch中并没有处理所有的value值,这通常会很有帮助。