专注、坚持

Swift 拾遗 - enum

2020.08.01 by kingcos
Date Notes
2020-08-01 首次提交

Preface

《Swift 拾遗》是一个关于 Swift 的新文章专辑,这个系列的文章将不会涉及基本语法的层面,而是尝试从底层「拾」起之前所忽视的内容。今天我们将一起简单探究 Swift 中的枚举 enum

内存布局

内存布局(Memory Layout)指的是变量在内存中的占用情况,了解内存布局可以使我们对于数据类型的本质更加熟悉。

简单枚举

简单枚举这里指不带关联值与原始值的枚举类型。当 case 个数小于等于 256 个时,其占用 1 个字节(当然,超过 256 个时,将占用 2 个字节):

enum Foo {
    case first, second, third
}

print(MemoryLayout<Foo>.size)      // 1
print(MemoryLayout<Foo>.stride)    // 1
print(MemoryLayout<Foo>.alignment) // 1

var foo = Foo.third
withUnsafePointer(to: &foo) { print("\($0)") } // 0x00000001000040e0

// View Memory:
// 0x00000001000040e0 ->
// 02

View Memory

我们可以在 Xcode Menu - Debug - Debug Workflow - View Memory - Address 中输入通过 withUnsafePointer(to: &foo) 获得的内存地址,来查看内存占用情况;也可以在 LLDB 中输入 memory readx 加上内存地址,输出内存占用信息。

当 case 个数只有一个时,size0,即实际占用的内存大小为 0。这是因为此时枚举类型中有且仅有这一种可能,即使无需内存空间也可表示,而根据内存对齐最少 1 个字节,这样的类型最终在内存中被分配了 1 个字节:

enum Foo {
    case first
}

print(MemoryLayout<Foo>.size)      // 0
print(MemoryLayout<Foo>.stride)    // 1
print(MemoryLayout<Foo>.alignment) // 1

带有原始值

那么带有原始值的枚举类型变量在内存中是什么样呢?

enum Foo: Int {
    case first = 1000, second, third
}

print(MemoryLayout<Foo>.size)      // 1
print(MemoryLayout<Foo>.stride)    // 1
print(MemoryLayout<Foo>.alignment) // 1

var foo = Foo.third
print(foo.rawValue) // 1002

withUnsafePointer(to: &foo) { print("\($0)") } // 0x00000001000040e0

// View Memory:
// (lldb) memory read 0x00000001000040e0
// 0x1000040e0: 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
// 0x1000040f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

通过实际的输出我们可以发现,无论原始值的大小如何,枚举变量将总是占用 1 个字节。这是因为枚举的原始值并不存储在枚举变量中,而是作为整个枚举类型所共用。当然,枚举的原始值也无法再次改变。

带有关联值

不同于枚举的原始值,关联值并不在定义时确定,而每个枚举变量的关联值都可能不一样,因此关联值必须要有额外的内存空间来存储:

enum Foo {
    case first(Int, Int, Int, Int) // 4 个 Int 值,即 8 * 4 = 32 个字节
    case second, third
}

print(MemoryLayout<Foo>.size)      // 33,前 32 位存储关联值,第 33 位存储从 0 开始的次序值
print(MemoryLayout<Foo>.stride)    // 40
print(MemoryLayout<Foo>.alignment) // 8

var foo = Foo.first(Int.max, Int.max, Int.max, Int.max)
var next = Int.max

print(MemoryLayout.size(ofValue: foo)) // 33

withUnsafePointer(to: &foo) { print("\($0)") }  // 0x0000000100003098
withUnsafePointer(to: &next) { print("\($0)") } // 0x00000001000030c0

// View Memory:
// 0x0000000100003098 ->
// FF FF FF FF FF FF FF 7F
// FF FF FF FF FF FF FF 7F
// FF FF FF FF FF FF FF 7F
// FF FF FF FF FF FF FF 7F
// 00 00 00 00 00 00 00 00
// 0x00000001000030c0 ->
// FF FF FF FF FF FF FF 7F

foo = .second
print(MemoryLayout.size(ofValue: foo)) // 33,等同于 MemoryLayout<Foo>.size

// View Memory:
// 0x0000000100003098 ->
// 00 00 00 00 00 00 00 00
// 00 00 00 00 00 00 00 00
// 00 00 00 00 00 00 00 00
// 00 00 00 00 00 00 00 00
// 01 00 00 00 00 00 00 00
// 0x00000001000030c0 ->
// FF FF FF FF FF FF FF 7F

foo = .third
// View Memory:
// 0x0000000100003098 ->
// 01 00 00 00 00 00 00 00
// 00 00 00 00 00 00 00 00
// 00 00 00 00 00 00 00 00
// 00 00 00 00 00 00 00 00
// 01 00 00 00 00 00 00 00
// 0x00000001000030c0 ->
// FF FF FF FF FF FF FF 7F

当枚举类型中只有一个 case 且含有关联值时,枚举变量则无需额外的内存空间来充当标记位:

enum Foo {
    case first(Int)
}

print(MemoryLayout<Foo>.size)      // 8
print(MemoryLayout<Foo>.stride)    // 8
print(MemoryLayout<Foo>.alignment) // 8

当枚举类型中的 case 较多时,除去一个标记位,其它位则可以根据需要而共用:

enum Foo {
    case first(Int, Int, Int)
    case second(Int, Int)
    case third(Int)
    case fourth(Bool)
    case fifth
}

var foo = Foo.fifth

withUnsafePointer(to: &foo) { print("\($0)") }  // 0x0000000100003088

print(MemoryLayout<Foo>.size)      // 25
print(MemoryLayout<Foo>.stride)    // 32
print(MemoryLayout<Foo>.alignment) // 8

// (lldb) memory read 0x0000000100003088
// 0x100003088: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
// 0x100003098: 00 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00  ................

Optional

frame variable -R <VAR> / fr v -R <VAR>

LLDB 中的 frame 命令用来展示变量的当前栈帧(Stack Frame)。-R 参数意味着将结果原始输出(Raw Output),不加修饰。

可选类型的本质其实也是枚举,即 .some.none。这里我们主要看下多重可选的情况:

//           ┌──────────┐  ┌──────────┐
// ┌──────┐  │   Int??  │  │   Int??  │
// │ Int? │  │ ┌──────┐ │  │ ┌──────┐ │
// │      │  │ │ Int? │ │  │ │ Int? │ │
// │  10  │  │ │      │ │  │ │      │ │
// └──────┘  │ │  10  │ │  │ │  10  │ │
//           └─┴──────┴─┘  └─┴──────┴─┘
//   foo1        foo2         foo3
var foo1: Int? = 10
var foo2: Int?? = foo1
var foo3: Int?? = 10

print(foo2 == foo3) // true

// LLDB:
// (lldb) fr v -R foo1
// (Swift.Optional<Swift.Int>) foo1 = some {
//   some = {
//     _value = 10
//   }
// }
// (lldb) fr v -R foo2
// (Swift.Optional<Swift.Optional<Swift.Int>>) foo2 = some {
//   some = some {
//     some = {
//       _value = 10
//     }
//   }
// }
// (lldb) fr v -R foo3
// (Swift.Optional<Swift.Optional<Swift.Int>>) foo3 = some {
//   some = some {
//     some = {
//       _value = 10
//     }
//   }
// }

通过 fr v -R 我们可以看出,foo2foo3 均为两重可选,且均为 .some,最内部实际包装的值也为 10,因此两者相同。而下面这种情况略有不同:

//           ┌──────────┐  ┌──────────┐
// ┌──────┐  │   Int??  │  │   Int??  │
// │ Int? │  │ ┌──────┐ │  │          │
// │      │  │ │ Int? │ │  │          │
// │      │  │ │      │ │  │          │
// └──────┘  │ │      │ │  │          │
//           └─┴──────┴─┘  └──────────┘
//   bar1        bar2         bar3

var bar1: Int? = nil
var bar2: Int?? = bar1
var bar3: Int?? = nil

print(bar1 == bar3) // false
print(bar2 == bar3) // false

// LLDB:
// (lldb) fr v -R bar1
// (Swift.Optional<Swift.Int>) bar1 = none {
//   some = {
//     _value = 0
//   }
// }
// (lldb) fr v -R bar2
// (Swift.Optional<Swift.Optional<Swift.Int>>) bar2 = some {
//   some = none {
//     some = {
//       _value = 0
//     }
//   }
// }
// (lldb) fr v -R bar3
// (Swift.Optional<Swift.Optional<Swift.Int>>) bar3 = none {
//   some = some {
//     some = {
//       _value = 0
//     }
//   }
// }

不同于之前例子中的赋非 nil 值,这里的 bar1bar3 固然因为类型都无法匹配而无法判等;而 bar2bar3 似乎看起来都被赋值了 nil,但其实前者本质上被赋值了 Int? 类型的 nil,即第二层可选仍有值,后者则是被赋值了 Int??nil,两层可选均为空,因此结果也不相等。