Swift 枚举

说说 Swift 枚举

# Swift 枚举的能力

相对于 OC 来说,Swift 枚举提供了更多的能力。从本质上来说 OC 的枚举实际上是整型的别名,只能存储整数。而 Swift 中枚举是独立的类型。

官方文档对 Swift 中枚举的描述是

在 Swift 中,枚举类型是一等(first-class)类型。它们采用了很多在传统上只被类(class)所支持的特性,例如计算属性(computed properties),用于提供枚举值的附加信息,实例方法(instance methods),用于提供和枚举值相关联的功能。枚举也可以定义构造函数(initializers)来提供一个初始值;可以在原始实现的基础上扩展它们的功能;还可以遵循协议(protocols)来提供标准的功能。

# 定义方法和计算属性

enum Direction {
	case left
	case right
	//方法定义
	func getValue() -> Int {
	    switch self {
	    case .left: return 1
	    case .right: return 2
	    }
	}
	//计算属性定义
	var value:Int {
	    switch self {
	    case .left: return 1
	    case .right: return 2
	    }
	}
}

# 枚举原始值支持更多类型

原始值可以是字符串、字符,或者任意整型值或浮点型值。每个原始值在枚举声明中必须是唯一的。相对的 OC 中的枚举值只能是整型。

enum Direction:String {
    case left = "left"
    case right = "right"
}

# 支持关联值

官方的例子其实很好表明了关联值的使用方式

enum Barcode {
    case upc(Int, Int, Int, Int)
    case qrCode(String)
}
let code = Barcode.upc(5, 5, 5, 5)
switch code {
//case .upc(let int, let int2, let int3, let int4): 这种写法比较复杂,将let前置更简单
case let .upc(int, int2, int3, int4):
    if int == 5 {
        print("5")
    }
case .qrCode(let string):
    print(string)
}

注意原始值和关联值是不同的。 原始值是在定义枚举时被预先填充的值。对于一个特定的枚举成员,它的原始值始终不变。 关联值是创建一个基于枚举成员的常量或变量时才设置的值,枚举成员的关联值可以变化。

# 作为一个类型,可以实现协议,定义扩展

实现简单例子如下:

protocol HelloProtocol { func printHello(); }
enum Direction:String,HelloProtocol {
    case left = "left"
    case right = "right"
    func printHello() {
        print("direction hello")
    }
}
extension Direction {
    mutating func makeLeft() {
        self = Direction.left
    }
}

# 实现递归枚举

这个例子大家直接看官方文档 (opens new window)就好了,我没有想到这种 CASE 的实际的使用场景…

# Swift 枚举内存布局

# 普通枚举

下面这个例子中,我们尝试计算得到枚举占用的内存空间。

enum Direction {
    case left
}
MemoryLayout<Direction>.size      //0
MemoryLayout<Direction>.stride    //1
MemoryLayout<Direction>.alignment //1

这里例子中获取到的 Directionsize 为 0,这个让我感到很疑惑,似乎是 Swift 对这种只有一个case的情况进行了编译优化。如果再增加一个 case 的话,size 就会变为 1。

如下,我们增加一个 case,可以看到 size 就会变 1。如果尝试增加更多的case 的话,其实 size 还是 1,只要不超过 255 个 case,都是占用 1 个字节。

enum Direction {
    case left
	case right
}
MemoryLayout<Direction>.size      //1
MemoryLayout<Direction>.stride    //1
MemoryLayout<Direction>.alignment //1

所以我们猜想底层的存储可能是一个枚举 case 对应0x00~0xff中的一个值,看看内存就知道了

enum Direction {
    case left
    case right
    case up
    case down
}
var direction1 = Direction.left   //0x0000000100008018
var direction2 = Direction.right
var direction3 = Direction.up
var direction4 = Direction.down

通过查看 0x0000000100008018 内存布局发现确实上是这样的,对应底层的value是不断递增的,分别对应 direction2,direction3,direction4 这三个变量的内存。每个变量都是占用 1 个字节。

(lldb) x 0x0000000100008018
0x100008018: 00 01 02 03 00 00 00 00

# 带原始值的枚举

带原始值的枚举定义如下

enum Direction:String {
    case left = "left"
    case right = "right"
    case up = "up"
    case down = "down"
}

本质上带原始值的枚举,编译后会生成类似下面这种代码,即使用 rawValue 的本质就是在底层调用 get 方法,从Mach-O对应地址中取出字符串并返回的操作。每个枚举值占用的内存还是 1 个字节。

enum Direction {
    case left
    case right
	case up
	case down
    var rawValue: String {
        switch self {
        case .left: return "left"
        case .right: return "right"
        case .up: return "up"
        case .down: return "down"
        }
    }
}

所以本质上并不会改变枚举的存储,通过 MemoryLayout<Direction>.size 这种 API 拿到的还是枚举占用的内存空间还是 1 个字节。

具体分析可以参考这里 (opens new window)这里 (opens new window)

# 带关联值的枚举

通过下面这个官方的例子来分析一下

enum Barcode {
    case upc(Int, Int, Int, Int)
    case qrCode(String)
}
MemoryLayout<Barcode>.size        //33
MemoryLayout<Barcode>.stride      //40
MemoryLayout<Barcode>.alignment   //8
--
var barcode = Barcode.upc(1, 2, 3, 4) //0x0000000100008008
barcode = .qrCode("hello")

还是直接看 barcode 的内存地址 0x0000000100008008

barcode 为 UPC 格式数字的时候,读取内存如下,单个的 Int 类型占用 4 个字节,正好对应 1,2,3,4。再多一个字节存储枚举CASE 本身占用的空间一个字节,总共需要占用 33 个字节。

(lldb) x --size 8 --format x 0x0000000100008008
0x100008008: 0x0000000000000001 0x0000000000000002
0x100008018: 0x0000000000000003 0x0000000000000004
0x100008028: 0x0000000000000000 0x0000000000000000

barcode 为 qrcode 的时候,读取内存如下,32 个字节之外,还有一个字节存储枚举值。

(lldb) x --size 8 --format x 0x0000000100008008
0x100008008: 0x0000006f6c6c6568 0xe500000000000000 //hello的编码
0x100008018: 0x0000000000000000 0x0000000000000000
0x100008028: 0x0000000000000001 0x0000000000000000

尽管当 barcode 为 qrcode 时候并不需要占用那么多的内存空间,但为了保持了枚举内存空间的统一,还是保留了 32 个字节来存储字符串相关的变量信息,所以内存空间分配规则是,编译器按占用多的内存空间的关联类型去计算最终的枚举内存空间大小。

可以看到当枚举有关联值的时候,底层已经不再是只占用一个字节的那种存储方式,反而是关联类型的内存布局也占据了枚举的内存布局的一部分,当然枚举本身占用的一个字节也没有消失。

突然感觉有点类似联合体的存储方式一样,两种类型共同占用同一块儿内存区域。

综上,我们基本上就简单分析了下 Swift 枚举的基本使用和 Swift 枚举在不同场景下的内存布局是什么样子的。

参考地址:

  1. 淘宝技术-OC 中枚举的问题 (opens new window)
  2. Swift-进阶 08:枚举enum (opens new window)
  3. StackOverflow-Enum Memory Usage (opens new window)
  4. StackOverflow-Using MemoryLayout on a struct gives the incorrect size (opens new window)