MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Objective-C枚举类型声明:传统enum与NS_ENUM对比

2022-07-253.0k 阅读

传统enum枚举类型声明

在Objective - C中,传统的enum用于定义一组相关的命名整数值。其基本语法如下:

enum SomeEnum {
    EnumValue1,
    EnumValue2,
    EnumValue3
};

这里,SomeEnum是枚举类型的名称,EnumValue1EnumValue2EnumValue3是该枚举类型的成员。默认情况下,EnumValue1的值为0,EnumValue2的值为1,EnumValue3的值为2,依次递增。

我们可以使用typedef来为这个枚举类型定义一个更方便使用的别名,例如:

typedef enum SomeEnum {
    EnumValue1,
    EnumValue2,
    EnumValue3
} SomeEnumType;

这样,我们就可以像使用其他数据类型一样使用SomeEnumType

SomeEnumType myValue = EnumValue2;

如果我们想要显式指定枚举成员的值,可以这样做:

typedef enum SomeEnum {
    EnumValue1 = 5,
    EnumValue2,
    EnumValue3 = 10
} SomeEnumType;

在这个例子中,EnumValue1的值为5,EnumValue2的值为6(因为它会在EnumValue1的基础上递增1),EnumValue3的值为10。

传统enum的内存布局

传统的enum类型在内存中占用的空间取决于其成员值的范围。如果所有成员值都可以用一个字节表示(即0到255),那么enum类型的变量可能只占用一个字节。但是,如果成员值的范围较大,例如需要用一个32位整数来表示,那么enum类型的变量将占用4个字节。

例如:

typedef enum LargeRangeEnum {
    SmallValue = 0,
    BigValue = 1000000
} LargeRangeEnumType;

在这种情况下,LargeRangeEnumType类型的变量很可能会占用4个字节,因为1000000无法用一个字节表示。

传统enum的优缺点

优点

  1. 简单直观:语法简单,易于理解和使用,适用于定义一组简单的相关整数值。
  2. 轻量级:在成员值范围较小时,占用内存空间较小。

缺点

  1. 类型安全性较弱enum类型的变量可以被赋值为任何整数值,即使这个值不属于枚举定义的范围。例如:
typedef enum SomeEnum {
    EnumValue1,
    EnumValue2,
    EnumValue3
} SomeEnumType;

SomeEnumType myValue = (SomeEnumType)4; // 虽然4不在定义的范围内,但编译器不会报错
  1. 可扩展性有限:当需要添加新的枚举成员时,可能需要手动调整其他成员的值,以避免冲突。

NS_ENUM枚举类型声明

NS_ENUM是苹果在Objective - C中引入的一种更现代的枚举声明方式。它的语法如下:

typedef NS_ENUM(NSInteger, SomeNS_ENUM) {
    NS_ENUMValue1,
    NS_ENUMValue2,
    NS_ENUMValue3
};

这里,NSInteger指定了枚举类型的底层存储类型,SomeNS_ENUM是枚举类型的名称,NS_ENUMValue1NS_ENUMValue2NS_ENUMValue3是枚举成员。

NS_ENUM的底层存储类型

NS_ENUM的第一个参数指定了其底层存储类型。常见的选择有NSIntegerNSUIntegerNSInteger会根据目标平台自动选择合适的整数类型(在32位平台上是32位整数,在64位平台上是64位整数),而NSUInteger是无符号版本。

例如,在32位平台上,NSInteger等同于int,在64位平台上,NSInteger等同于long

选择合适的底层存储类型很重要,因为它会影响枚举类型的取值范围和内存占用。如果我们知道枚举成员的值不会超过8位无符号整数的范围(0到255),可以选择NSUInteger,这样可以节省内存空间。

NS_ENUM的优点

  1. 类型安全性增强NS_ENUM类型的变量只能被赋值为枚举定义中明确列出的值或者nil(在某些情况下)。如果尝试赋值一个未定义的值,编译器会发出警告。例如:
typedef NS_ENUM(NSInteger, SomeNS_ENUM) {
    NS_ENUMValue1,
    NS_ENUMValue2,
    NS_ENUMValue3
};

SomeNS_ENUM myNS_ENUMValue = (SomeNS_ENUM)4; // 编译器会发出警告
  1. 更好的可扩展性:当添加新的枚举成员时,不需要手动调整其他成员的值,因为它们会按照默认规则自动递增。
  2. 与现代Objective - C特性更好的集成NS_ENUM与ARC(自动引用计数)、Core Foundation等特性有更好的兼容性,在使用一些框架时,使用NS_ENUM定义的枚举类型可以更方便地与框架进行交互。

传统enum与NS_ENUM对比示例

示例1:表示方向的枚举

传统enum方式

typedef enum Direction {
    DirectionNorth,
    DirectionSouth,
    DirectionEast,
    DirectionWest
} DirectionType;

void moveInDirection(DirectionType direction) {
    switch (direction) {
        case DirectionNorth:
            NSLog(@"Moving north");
            break;
        case DirectionSouth:
            NSLog(@"Moving south");
            break;
        case DirectionEast:
            NSLog(@"Moving east");
            break;
        case DirectionWest:
            NSLog(@"Moving west");
            break;
        default:
            NSLog(@"Unknown direction");
            break;
    }
}

在这个例子中,如果调用moveInDirection函数时传入一个未定义的值,编译器不会报错,这可能导致运行时出现意外行为。

NS_ENUM方式

typedef NS_ENUM(NSInteger, Direction) {
    DirectionNorth,
    DirectionSouth,
    DirectionEast,
    DirectionWest
};

void moveInDirectionNS_ENUM(Direction direction) {
    switch (direction) {
        case DirectionNorth:
            NSLog(@"Moving north");
            break;
        case DirectionSouth:
            NSLog(@"Moving south");
            break;
        case DirectionEast:
            NSLog(@"Moving east");
            break;
        case DirectionWest:
            NSLog(@"Moving west");
            break;
        default:
            NSLog(@"Unknown direction");
            break;
    }
}

如果尝试将一个未定义的值传递给moveInDirectionNS_ENUM函数,编译器会发出警告,提醒我们可能存在的问题。

示例2:表示文件权限的枚举

传统enum方式

typedef enum FilePermissions {
    ReadPermission = 1,
    WritePermission = 2,
    ExecutePermission = 4
} FilePermissionsType;

BOOL hasPermission(FilePermissionsType permissions, FilePermissionsType requiredPermission) {
    return (permissions & requiredPermission) == requiredPermission;
}

在这种情况下,如果需要添加新的权限,可能需要小心地选择新的值,以避免与现有值冲突。

NS_ENUM方式

typedef NS_ENUM(NSUInteger, FilePermissions) {
    ReadPermission = 1,
    WritePermission = 2,
    ExecutePermission = 4
};

BOOL hasPermissionNS_ENUM(FilePermissions permissions, FilePermissions requiredPermission) {
    return (permissions & requiredPermission) == requiredPermission;
}

使用NS_ENUM,当添加新的权限时,不需要担心值冲突问题,因为编译器会自动处理。

NS_ENUM在实际项目中的应用场景

  1. UIKit框架中的应用:在UIKit框架中,NS_ENUM被广泛使用。例如,UIViewAutoresizing枚举用于指定视图的自动调整行为:
typedef NS_ENUM(NSUInteger, UIViewAutoresizing) {
    UIViewAutoresizingNone                 = 0,
    UIViewAutoresizingFlexibleLeftMargin   = 1 << 0,
    UIViewAutoresizingFlexibleWidth        = 1 << 1,
    UIViewAutoresizingFlexibleRightMargin  = 1 << 2,
    UIViewAutoresizingFlexibleTopMargin    = 1 << 3,
    UIViewAutoresizingFlexibleHeight       = 1 << 4,
    UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};

这种方式使得在设置视图的自动调整属性时,代码更加清晰和类型安全。

  1. Core Animation框架中的应用:在Core Animation框架中,CAMediaTimingFunctionName枚举用于指定动画的时间函数:
typedef NS_ENUM(NSInteger, CAMediaTimingFunctionName) {
    kCAMediaTimingFunctionLinear,
    kCAMediaTimingFunctionEaseIn,
    kCAMediaTimingFunctionEaseOut,
    kCAMediaTimingFunctionEaseInEaseOut,
    kCAMediaTimingFunctionDefault
};

通过使用NS_ENUM,可以确保在设置动画时间函数时传入的参数是正确的枚举值。

传统enum在某些遗留代码中的存在原因

  1. 历史遗留:在NS_ENUM引入之前,传统的enum是Objective - C中定义枚举类型的唯一方式。许多早期的项目使用传统enum编写,由于代码规模较大,全面替换为NS_ENUM可能会带来一定的风险和成本。
  2. 简单场景适用性:对于一些非常简单且不会进行扩展的枚举定义,传统enum的简单性和轻量级特性仍然具有吸引力。例如,在一些内部工具或者小型模块中,可能只是简单地定义几个相关的整数值,使用传统enum可以快速实现,而不需要引入NS_ENUM的额外语法。

如何在项目中选择使用传统enum还是NS_ENUM

  1. 新项目:对于新的Objective - C项目,强烈推荐使用NS_ENUM。它提供的类型安全性和更好的可扩展性可以减少潜在的错误,并且与现代Objective - C框架和特性更兼容。
  2. 维护旧项目:如果在维护旧项目,遇到传统enum时,在不影响现有功能的前提下,可以逐步将其转换为NS_ENUM。特别是对于那些可能会进行扩展或者需要更严格类型检查的枚举定义,转换为NS_ENUM是一个不错的选择。
  3. 性能敏感场景:在一些对性能非常敏感的场景中,如果确定枚举成员值的范围非常小,使用传统enum并手动指定合适的底层存储类型(如char)可能会比NS_ENUM更节省内存,因为NS_ENUM通常会使用NSInteger作为底层存储类型,在64位平台上可能会占用更多内存。但这种情况比较少见,并且需要对项目的性能进行详细的分析和测试。

NS_ENUM与其他相关概念的对比

  1. NS_OPTIONSNS_OPTIONS也是苹果引入的一种枚举声明方式,与NS_ENUM类似,但它主要用于定义可以进行按位操作的枚举值。例如:
typedef NS_OPTIONS(NSUInteger, UIViewAnimationOptions) {
    UIViewAnimationOptionLayoutSubviews             = 1 << 0,
    UIViewAnimationOptionAllowUserInteraction       = 1 << 1,
    UIViewAnimationOptionBeginFromCurrentState      = 1 << 2,
    // 更多选项...
};

NS_OPTIONS类型的枚举值可以通过按位或操作(|)组合在一起,而NS_ENUM通常用于表示互斥的取值。

  1. C++枚举:在C++中,枚举类型也有类似的概念。C++11引入了强类型枚举(enum class),它提供了更强的类型安全性,类似于Objective - C中的NS_ENUM。例如:
enum class Direction {
    North,
    South,
    East,
    West
};

void moveInDirection(Direction direction) {
    // 实现
}

在C++中,Direction类型的变量只能被赋值为定义中的枚举值,与Objective - C中NS_ENUM的类型安全性类似。

总结传统enum与NS_ENUM在实际使用中的差异

  1. 类型检查NS_ENUM提供了更强的类型检查,而传统enum类型安全性较弱。在实际开发中,这意味着使用NS_ENUM可以减少因错误赋值导致的运行时错误。
  2. 扩展性NS_ENUM在添加新成员时更方便,不需要手动调整其他成员的值。传统enum在扩展时可能需要更多的小心处理,以避免值冲突。
  3. 内存占用:传统enum在某些情况下(如成员值范围小)可以更精确地控制内存占用,而NS_ENUM通常使用NSInteger作为底层存储类型,在64位平台上可能占用更多内存。但在大多数情况下,这种内存差异并不显著。
  4. 与框架集成NS_ENUM与苹果的框架(如UIKit、Core Animation等)集成更好,在使用这些框架时,使用NS_ENUM定义的枚举类型可以更自然地与框架交互。

在Objective - C开发中,根据具体的需求和场景,合理选择传统enumNS_ENUM可以提高代码的质量、可维护性和可读性。对于新开发的项目,优先使用NS_ENUM是一个明智的选择,而对于维护旧项目,可以根据实际情况逐步将传统enum转换为NS_ENUM