enum Barcode {
case upc(Int, Int, Int, Int) //关联值定义时,只需要注明类型
case qrCode(String)
}
枚举的 case 可以携带附加信息,这些信息在创建实例时提供。可以像这样声明使用
var productBarcode = Barcode.upc(8, 85909, 51226, 3)
productBarcode = .qrCode("ABCDEFGHIJKLMNOP")
怎么提取关联值呢?在 switch 中可以使用 let 或 var 来提取关联值:
switch productBarcode {
case .upc(let numberSystem, let manufacturer, let product, let check):
print("UPC: \(numberSystem), \(manufacturer), \(product), \(check).")
case .qrCode(let productCode):
print("QR code: \(productCode).")
}
Swift 在内部使用一种 tagged union(带标签的联合体) 来实现这种结构。
enum Value {
case int(Int)
case text(String)
}
这样的枚举相当于
struct Value {
uint8_t tag; // 哪一个 case
union {
Int intValue;
String stringValue;
} payload;
};
你可以把 union 理解成:多种字段共用同一块内存,同一时刻“逻辑上只应有一个有效值”。编译器会保证:这个联合体的大小足够容纳所有 case 中的最大内容。
仅有 union 不够,因为运行时不知道当前有效成员是哪一个。
所以要加 tag 标识当前是哪一个 case(0 -> int, 1 -> text)
switch 时编译器先看 tag,再按对应 case 解包 payload:
switch v {
case .int(let x):
// 按 Int 解包 payload
case .text(let s):
// 按 String 解包 payload
}
上述的struct代码示例是“总有一个显式 tag 字段”。
Swift 更聪明:很多时候不需要单独存这个 tag,或者 tag 很小。
这里举一些例子说明swift可能如何对上述的struct Value结构进行优化
1、对某些指针类型,某些比特模式本来就不会是有效对象地址
Optional<T> 其中的T是指针类型,就可以用union部分这样来表示
- 全 0x00...00 => .none
- 非 0 => .some(pointer)
这只适用指针类型,如果是Int,全0这种情况会真的用来标志值为0的情况,这种情况就不能省略tag了
2、multi-payload enum(多个 case 都带关联值)
这种情况比Optional难,因为要区分的不止 .none/.some。
Swift 的策略是:先“借”payload里的无效位模式(extra inhabitants / spare bits)编码一部分 tag,不够再加显式 discriminator 字节。
为了说清楚这里的无效位,我们先了解一下地址宽度和内存对齐
例如地址宽度为64bit,那么可以理解指针为8字节大小
8 字节对齐,可以通过以下这个struct示例来理解:
struct S1 {
var a: Int8 // 1字节, 对齐1
var b: Int // 8字节, 对齐8
}
//a 放在 offset 0(占 1 字节)
//b 必须从 8 的倍数地址开始 -> 会在中间补 7 字节 padding
//总大小常为 16(末尾还要满足整体对齐)
了解了这些还不够,因为multi-payload enum在发生无效位借用时常常存储的是指针关联值,64bit的寻址,说明地址就是64bit,8字节对齐的情况下,无效位是从哪里得到的?
enum E2 {
case a(AnyObject)
case b(AnyObject)
case c(AnyObject)
case d(AnyObject)
}
答案是由对齐带来的低位恒定带来的。对齐为什么会带来低位恒定很好理解
我们访问地址时,要么是0x1000 二进制:0000 0000 0000 1000
要么是0x1008 二进制:0001 0000 0000 1000
但不会是0x1007 二进制:0001 0000 0000 0111
可以看出针对8字节对齐的地址访问,都是可以整除2的3次方的,这就像我们要求10进制的数字要整除10的3次方。这两种情况都会导致末尾3位恒0。
既然末尾3位一定为0,编译器就可以“从中作梗”,使用这3位来表示tag,例如:想要取出原地址只需要与1111 1111 1111 1000进行与操作就好,这只是一种假设,具体编译器如何做我们可以不必在意。
如果本文对你有帮助,可以关注微信公众号iOS开发小挖,学海无涯,但学到就是赚到!