Skip to content

Latest commit

 

History

History
363 lines (270 loc) · 12.4 KB

一篇文章学会typedef enum、NS_ENUM、NS_OPTIONS和移位1 << 0.md

File metadata and controls

363 lines (270 loc) · 12.4 KB

如果你是一个游戏开发者,你会经常需要描述一个属性的不同变量。无论是攻击类型(近战、冰块、火焰、毒气等),还是敌人状态(空闲、惊动、追逐、攻击、休息等)都需要使用很多变量。最基础的实现方法如下:

    static NSInteger NONE = 0;
    static NSInteger MELEE = 1;
    static NSInteger FIRE = 2;
    static NSInteger ICE = 3;
    static NSInteger POISON = 4;
    NSInteger attackType = NONE;

这种方法的缺点是你不能控制赋给attackType值,其可以是任何整数值,你也可能使用++attackType这种愚蠢的方法。

Objective-C和Swift中的枚举(enumeration,缩写为enum)可以很好的解决这个问题:

    typedef NS_ENUM (NSInteger, AttackType) {
        None,
        Melee,
        Fire,
        Ice,
        Posion
    };

这一篇文章我们首先介绍enumtypedef,然后介绍Apple目前推荐的NS_ENUMNS_OPTIONS枚举方式。

1. 定义枚举

常见的枚举语句如下:

enum IssueStateTypes {
  IssueStateOpen,
  IssueStateClosed,
  IssueStateNotDetermined
};

上面的语句定义了IssueStateTypes枚举类型数据,IssueStateTypes类型对象只能赋值为IssueStateOpenIssueStateClosedIssueStateNotDetermined。如果赋其他值,编译器会发出警告。

声明enum IssueStateTypes类型变量时,需要用到关键字enum,如下所示:

enum IssueStateTypes IssueState;
// 赋值
IssueState = IssueStateOpen;

定义枚举类型时,必须确保枚举标志符与相同作用域内变量名和其他标志符不同。

2. typedef语句

通过使用typedef语句,可以为数据类型另外指派一个名称。

// 定义名称Counter和int等效。
typedef int Counter;

现在声明Counter类型的变量:

Counter m, n;
// 与下面声明相同。
int m, n;

typedef也可以定义枚举类型、结构类型。下面语句定义的enum IssueStateTypesIssueStateTypes1等效。

    // 1. 定义IssueStateTypes1和enum IssueStateTypes等效。
    typedef enum IssueStateTypes IssueStateTypes1;
    IssueStateTypes1 IssueState1;
    
    // 2. 与1等效。
    enum IssueStateTypes IssueState2;
    
    // 3. 赋值
    IssueState1 = IssueStateOpen;
    IssueState2 = IssueStateClosed;

使用typedef可以增加变量的可读性,清晰看出这些变量的使用目的。

另外,定义枚举类型、变量可以合并在一起,如下:

typedef enum IssueStateTypes {
    IssueStateOpen,
    IssueStateClosed,
    IssueStateNotDetermined
} IssueStateTypes;

// 和下面效果一致。
enum IssueStateTypes {
    IssueStateOpen,
    IssueStateClosed,
    IssueStateNotDetermined
};
typedef IssueStateTypes IssueStateTypes;

3. 定义匿名枚举

如果你不需要使用enum IssueState,可以省略枚举类型名称,即声明为匿名(anonymous)枚举类型。

typedef enum {
    IssueStateOpen,
    IssueStateClosed,
    IssueStateNotDetermined
} IssueStateTypes;

// 赋值
IssueStateTypes IssueState = IssueStateClosed;

这里声明IssueStateTypes为匿名枚举类型的名称,其声明的变量只能设置为枚举中的值。

4. 声明枚举值的类型

定义枚举类型时,可以将整数类型和枚举名称对应起来。这样,在赋值、使用switch枚举常量时,可以进行类型检查。

typedef enum IssueStateTypes : NSUInteger {
    IssueStateOpen,
    IssueStateClosed,
    IssueStateNotDetermined
} IssueStateTypes;

// 枚举类型名称可以省略。
typedef enum : NSUInteger {
    IssueStateOpen,
    IssueStateClosed,
    IssueStateNotDetermined
} IssueStateTypes;

事实上,enum的标志符均是整数。第一个标志符被设置为0,第二个被设置为1,其后依次递增。另外,也可以显式的为标志符指定值,其后标志符在指定值的基础上依次递增。

    enum direction {    // 值分别为
        up,             // 0
        down,           // 1
        left = 10,      // 10
        right,          // 11
    };

另外,枚举标志符可以共享相同的值:

enum UIStatusBarStyle {
    UIStatusBarStyleDefault = 0,
    UIStatusBarStyleLightContent = 1,
    UIStatusBarStyleBlackTranslucent = 1,
    UIStatusBarStyleBlackOpaque = 2
};

UIStatusBarStyleLightContentUIStatusBarStyleBlackTranslucent值相同,使用时效果一致。

通过使用枚举,可以把整数值和有象征意义的名称对应起来。所以,在使用枚举类型时,尽量不要把枚举值当作整数这个事实。如果需要修改整数值,只能在定义枚举的地方修改。

绝大部分情况下,使用上面的enum就可以了,但enum还有更高级用法。

5. 按位掩码bitmask及按位运算符bitwise operator

上面的标准枚举方法中,每个标志符只能包含一个值。但如果我们在近战(melee)过程中使用火焰(fire)呢?为了解决这个问题,可以使用按位掩码(bitmask)对整型值进行编码。按位掩码运算符有按位(bitmise)与(&)、按位或(bitmise |)、一次求返(~)等。

    typedef enum : NSUInteger {
        None    = 0,
        Melee   = 1,
        Fire    = 2,
        Ice     = 4,
        Posion  = 8
    } AttackType;
    AttackType attackType = Melee | Fire;

在上面的例子中,attackType包含MeleeFire两个值,在查看如何提取attackType值前,我们先看下其工作原理。

Enum内类型都是整型,其值与二进制数如下:

    typedef enum : NSUInteger {
        //     十进制        // 二进制
        None    = 0,        // 0000 0000
        Melee   = 1,        // 0000 0001
        Fire    = 2,        // 0000 0010
        Ice     = 4,        // 0000 0100
        Posion  = 8         // 0000 1000
    } AttackType;
    AttackType attackType = Melee | Fire;

可以看到,在二进制中每个值只有一个数字1,而且都处于不同的位。这样就可以把attackType看作一系列位(bit),每个位表示是否包括某个属性。如果第一位(从右侧开始)是1,为Melee,如果第二位是1,为Fire,如果第三位是1,表示Ice,以此类推。需要注意的是,每个值的二进制必须只包括一个数字1,稍后会有简便方法实现这一需求。

如果设备是32位的,枚举类型不能超过32个。如果设备是64位的,枚举类型不能超过64个。

Bitmask本质上是一个整数值,其中,二进制属性(1是/0否)独立存在于位中。要想对其进行操作,我们需要了解位运算符(bitwise operator)。

5.1 按位或OR(符号为|

在对两个值执行按位或运算时,会逐位比较两个值的二进制数,只要有一位值是1,结果对应位即为1。

按位或用来设置位值,可以增加新技能。

    AttackType attackType = Melee | Fire;
    //                  二进制   十进制
    // Melee            000001   1
    // Fire             000010   2
    // Melee | Fire     000011   3
5.2 按位与AND(符号为&

执行与运算时,会逐位比较两个值的二进制数,只有在对应位上都为1时,结果对应位上才是1。

按位或用来设置位值,按位与用来解除之前保存位的值。可以用来解除该技能外所有技能。

    //                      二进制   十进制
    // Ice:                 000100 = 4
    // MeleeAndFire:        000011 = 3
    // MeleeAndFire & Ice:  000000 = 0
    
    // Fire:                000010 = 2
    // MeleeAndFire:        000011 = 3
    // MeleeAndFire & Fire: 000010 = 2

与运算符&优先级高于或运算符|

5.3 一次求返bitwise not(符号为

一次求返运算符是一元运算符,其他运算符均为两元运算符。其将位中的1翻转为0,位中的0翻转为1。

这个会非常实用,如将近战火焰改为近战冰块。

    AttackType attackType;
    attackType = Melee | Fire;
    
    // 1
    attackType &= ~Fire;
    // 2
    attackType |= Ice;

上述代码分步说明如下:

  1. 通过对Fire一次求返后,除Fire位(第二位)为0,其他位均为1。与attackType按位与运算后会取消Fire,但其它位不变。
  2. 添加冰块攻击。
5.4 按位异或运算符XOR(符号为^

逐位比较两个运算数的二进制,只有在一个位是1,另一个数对应位不是1时,结果的对应位才是1。

XOR可以用来关闭值。

    AttackType attackType;
    attackType = Melee | Fire | Posion;
    
    attackType ^= Fire;    // 关闭Fire
    attackType ^= Posion;  // 关闭Posion
5.5 向左移位运算符(符号为<<

对值进行向左移动,超出数据项的高位将丢失,低位移入的值总为0,该操作需要说明位移动数目。

通过该运算,可以简洁的创建bitmask所需的数值。

    typedef enum AttackType : NSUInteger {
        //                         二进制     十进制
        None    = 0,            // 000000      0
        Melee   = 1 << 0,       // 000001      1
        Fire    = 1 << 1,       // 000010      2
        Ice     = 1 << 2,       // 000100      4
        Posion  = 1 << 3        // 001000      8
    } AttackType;

还有向右移位>>运算符,从值的低位移出的值将丢失。无符号的值向右移位总是在高位(左侧)移入0,对于有符号值而言,左侧移入1还时0取决于被移动数字的符号,还取决于该操作在系统中的实现方式。

如果符号位是0(即值为正数),都移入0;如果符号位是1,则有些计算机将移入1,有些计算机将移入0。前者称为算数右移,后者称为逻辑右移。

对于系统会使用算数右移还是逻辑右移,我们不能进行任何猜测,否则运行时会出错。

6. NS_ENUM NS_OPTIONS宏

NS_ENUMNS_OPTIONS宏使用了C语言的新特性,提供了一种简洁的方法定义enumerations和options,可以显式指定enumerations和options中类型。NS_ENUM的枚举可以被自动转换为Swift类型。

使用NS_ENUM宏定义:

    typedef NS_ENUM (NSInteger, IssueStateTypes) {
        IssueStateOpen,
        IssueStateClosed,
        IssueStateNotDetermined
    };
    // 赋值
    IssueStateTypes IssueState = IssueStateNotDetermined;

使用NS_OPTIONS宏定义:

    typedef NS_OPTIONS (NSInteger, AttackType) {
        None    = 0,
        Melee   = 1 << 0,
        Fire    = 1 << 1,
        Ice     = 1 << 2,
        Posion  = 1 << 3
    };
    // 赋值
    AttackType attackType = Posion;

NS_ENUMNS_OPTIONS是一种更现代的枚举方式,文档中也在使用。

typedef NS_ENUM(NSInteger, UIViewAnimationTransition) {
    UIViewAnimationTransitionNone,
    UIViewAnimationTransitionFlipFromLeft,
    UIViewAnimationTransitionFlipFromRight,
    UIViewAnimationTransitionCurlUp,
    UIViewAnimationTransitionCurlDown,
};

typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
    UIViewAutoresizingNone                 = 0,
    UIViewAutoresizingFlexibleLeftMargin   = 1 << 0,
    UIViewAutoresizingFlexibleWidth        = 1 << 1,
    UIViewAutoresizingFlexibleRightMargin  = 1 << 2,
    UIViewAutoresizingFlexibleTopMargin    = 1 << 3,
    UIViewAutoresizingFlexibleHeight       = 1 << 4,
    UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};

7. 转换枚举

如果你的代码中已经使用enum枚举,可以通过Xcode工具将其转换为更现代的枚举。

点击Xcode中的Edit > Convert > To Modern Objective-C Syntax…,可以将代码中的enum转换为NS_ENUMNS_OPTIONS类型。另外,也可以将id转换为instancetype

Enum

NS_ENUMNS_OPTIONS都是Objective-C和Swift开发中提升开发体验的新特性,也再次展示了这门语言在对象化和过程化之间的辩证关系。

参考资料:

  1. What is a typedef enum in Objective-C?
  2. Enum, Flags and bitwise operators
  3. Enumeration Macros
  4. Declaring and checking/comparing (bitmask-)enums in Objective-C