Swift 枚举本质

我们先来问大家一个问题 下面打印结果是多少

var abc: Int = 100
var efg: Int? = 200

/*
 Memlayout.size(ofValue value: T) // 获取变量实际占用的内存大小

 Memlayout.stride(ofValue value: T)     // 获取创建变量所需要的分配的内存大小

 MemoryLayout.alignment(ofValue: T) // 获取变量的内存对齐数
 
 */

print("abc 实际占用的内存 ======= \(MemoryLayout.size(ofValue: abc))")
print("efg 实际占用的内存 ======= \(MemoryLayout.size(ofValue: efg))")

下面看一下打印结果

abc 实际占用的内存 ======= 8
efg 实际占用的内存 ======= 9
Program ended with exit code: 0

那么现在问题来了为什么可选类型(Int?)比不可选类型(Int)多一个字节?

那我们先来看一下可选类型代码

@frozen public enum Optional : ExpressibleByNilLiteral {

    /// The absence of a value.
    ///
    /// In code, the absence of a value is typically written using the `nil`
    /// literal rather than the explicit `.none` enumeration case.
    case none

    /// The presence of a value, stored as `Wrapped`.
    case some(Wrapped)

    /// Creates an instance that stores the given value.
    public init(_ some: Wrapped)
    /// Creates an instance initialized with `nil`.
    ///
    /// Do not call this initializer directly. It is used by the compiler when you
    /// initialize an `Optional` instance with a `nil` literal. For example:
    ///
    ///     var i: Index? = nil
    ///
    /// In this example, the assignment to the `i` variable calls this
    /// initializer behind the scenes.
    public init(nilLiteral: ())
}

查看Optional的源码得知,其实在swift中可选类型就是一个添加了关联值的枚举,例如:

Int?`就等价于 `Optional
let age: Int? = 2` 就等价于 `let age: Optional = Optional.some(2)

ExpressibleByNilLiteral是一个nil的字面量协议,代表可以使用nil这个关键字来进行初始化,Optional实现了这个协议的方法init(nilLiteral: ()) { self = .none },所以let age: Int? = nil就等价于 let age: Optional = Optional.init(nilLiteral: ())

只是编译器在背后帮我们做了一些转换而已。

枚举占多少内存

1.1、普通枚举

我们先来创建一个普通的枚举用MemoryLayout获取一下

enum Direction {
    case north, south, east, west
}

enum Direction {
    case north, south, east, west
}

func textEnum() {
    let dir = Direction.south
    print("Direction 实际占用内存 ===== \(MemoryLayout.size)")
    print("dir 实际占用内存 ===== \(MemoryLayout.size(ofValue: dir))")
    
}
// 打印结果
Direction 实际占用内存 ===== 1
dir 实际占用内存 ===== 1
Program ended with exit code: 0

那么现在我们知道了枚举占用的内存是一个字节 那么这一个字节里面装的是什么呢?

通常如果我们知道了一个内存地址,我们可以通过下面两种方式查看地址对应内存空间存放的数据:

1、我们可以在xcode -> Debug -> Debug workflow -> View Memory中输入内存地址定位到那块内存空间

2、在lldb中使用指令memory read + 内存地址读取指针对应的内存。也可以直接使用指令x简化书写,效果等同于memory read

现在问题就变成我们该如何获取Swift变量的内存地址了,但由于xcode对Swift语言做了非常多的封装和屏蔽,断点调试时,我们不能直接像oc/c语言那样直接看到枚举变量的地址,我们只能通过Swift的方式获取内存地址。

func getPointer(of value: inout T) -> UnsafeRawPointer {
  return withUnsafePointer(to: &value, { UnsafeRawPointer($0) })
}

我们来看一下内存数据

func textEnum() {
    var dir = Direction.south
    print("\(getPointer(of: &dir))")
    print("====================")
}
Swift 枚举本质_第1张图片
1.png

或者使用View Memory` 如下步骤

Swift 枚举本质_第2张图片
4.png

上面我们知道Direction枚举只占用一个字节,所以我们只需要查看第1个字节的数据:可以看到原来dir变量在内存中的真实存储数据是0x1,同样的我们也可以测试到Direction.north在内存中的值是0x0,Direction.east在内存中的值是0x2,Direction.west在内存中的值是0x3。

⚠提醒:Swift和OC混编时,Swift中的enum要想在OC中使用,需要添加@objc修饰符,而添加完@objc修饰符之后,swift的枚举占用的内存大小就不是由枚举类型的数目决定的了,而是固定为和Int类型大小一致。

1.2、带初始值的枚举

我们来看一下带初始值的枚举占多少内存

Swift 枚举本质_第3张图片
6.png

枚举的内存还是一个字节存储的是case的值 那么问题就来了那我们的初始值去哪了?

其实熟悉原始值使用语法的同学都知道,枚举的原始值并不是直接拿来使用的,而是通过枚举的一个名为rawValue的属性才可以访问到的,我们是不是可以根据刚才看到的内存结构大胆的猜测一下:是不是定义枚举变量时,原始值并不会被存储在枚举的内存空间中,而有可能只是编译器帮我们生成了一个rawValue的计算属性,然后在计算属性的内部判断枚举自身的类型来返回不同的原始值

我们来看一下rawValue的汇编是什么样的

Swift 枚举本质_第4张图片
7.png

Swift 枚举本质_第5张图片
8.png

通过rawValue的汇编可以看到是调用了rawValue的getter的函数 从这里可以猜测出计算属性就是函数调用

给枚举添加原始值就是编译器帮我们实现了RawRepresentable协议,实现了rawValueinit(_ rawValue)函数,rawValue函数在内部对self参数进行switch判断,以此返回不同的的原始值。

带关联至的枚举
Swift 枚举本质_第6张图片
10.png

可以看到三种类型的Achievement输出的size都是9,为什么都是9个字节呢?

那么关联值的实现是不是也可能像原始值那样,是编译器帮我们生成一些计算属性、方法之类的,帮我们保存关联值?仔细思考一下答案应该是否定的,关联值是不同于原始值的,因为原始值是一个确定值,在程序编译时期就可以确定下来的值,而关联值是不确定的,每一个枚举变量绑定的关联值都是不同的,值是在程序运行的时候才能确定的,我们可以使用case let语法从枚举中解析出不同的关联值, 那么这个关联值一定是和枚举变量有密切关联的,所以关联值是不是被直接存放在枚举变量中呢?我们分析一下englishScore枚举,一个Int类型在64位系统占用8个字节,除此之外通过第一部分的学习我们知道枚举自己还需要一个字节来区分枚举类型,所以8 + 1 = 9,正好可以解释ach变量的大小为什么是9。

大家会接着疑惑为什么ach1、ach2也占用9个字节呢?按照刚才的计算法则Bool变量只占1个字节,加上枚举自身的一个字节应该是1 + 1 = 2个字节就可以了,为什么还需要占用9个字节呢?这个时候我们不能只考虑自身所占用的数据大小,大家想一下枚举的一些使用场景,比如如果我们要将ach1重新赋值为其他的类型如ach1=Achievement.mathScore(100)`,ach1的两个字节还够用吗,又比如我们定义一个枚举数组,如果每一个元素的占用的内存大小都不一样,数组该怎么根据下标寻址呢,所以枚举枚举变量的size是固定的,而大小是取决于需要占用内存空间最大的那个类型。

1.3、默认实现的协议

大家有没有发现一个现象:我们定义的简单枚举类型<没有关联值>默认就可以进行==!=运算,要知道在Swift语言中==不再是一个运算符了,==是一个函数,是属于Equatable协议中的一个函数,但我们的枚举又没有实现Equatable怎么也可以进行比较呢?

编译器默认会帮我们实现Hashable/Equatable协议,这就是为什么我们的枚举可以调用hashValue属性,可以进行==运算的原因。接着我们给枚举添加关联值后再试一下,这个时候你会发现编译器什么协议也没帮我们添加,想必大家在开发过程中也发现了,设置关联值之后的枚举确实是不能进行==运算的,大家猜想一下是为什么呢,为什么设置了关联值编译器就不帮我们实现协议了呢?

其实通过第二三部分的探索我们大概可以知道答案了,还是要从枚举底层的内存结构来看,枚举在没有绑定关联值的时候,本身其实就是一个整型值,类似Int,Swift系统的Int默认也是实现了Hashable/Equatable协议的,系统当然可以像对待Int一样帮我们实现Hashable/Equatable协议,而当我们添加了关联值之后,枚举在的内存中的数据结构就是由枚举本身和关联值两部分组成了,编译器是不能确定具体要怎么样比较,怎么样hash了,则需要由我们开发者自己实现了。

1.4、总结

下面来总结一下我们学到的知识吧。

1、简单枚举<没有关联值>的本质就是一个整型值,整型值的大小取决于该枚举所定义的类型的数量。

2、给枚举添加原始值不会影响枚举自身的任何结构,设置原始值其实是编译器帮我们添加了rawValue属性,init(rawValue)方法(RawRepresentable协议)。

3、添加关联值会影响枚举内存结构,关联值被储存在枚举变量中,枚举变量的大小取决于占用内存最大的那个类型。

4、添加/调用"实例方法"、"类型方法"、计算属性以及实现协议的本质都是添加/调用函数。

5、对于没有添加关联值的枚举系统会默认帮我们实现Hashable/Equatable协议。

参考

你可能感兴趣的:(Swift 枚举本质)