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 read
或x
加上内存地址,输出内存占用信息。
当 case 个数只有一个时,size
为 0
,即实际占用的内存大小为 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
我们可以看出,foo2
和 foo3
均为两重可选,且均为 .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
值,这里的 bar1
和 bar3
固然因为类型都无法匹配而无法判等;而 bar2
和 bar3
似乎看起来都被赋值了 nil
,但其实前者本质上被赋值了 Int?
类型的 nil
,即第二层可选仍有值,后者则是被赋值了 Int??
的 nil
,两层可选均为空,因此结果也不相等。