12

Swift 拾遗 - enum

 3 years ago
source link: https://kingcos.me/posts/2020/swift_enum/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
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,两层可选均为空,因此结果也不相等。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK