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<Wrapped> : 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<Int>
let age: Int? = 2` 就等价于 `let age: Optional<Int> = Optional.some(2)

ExpressibleByNilLiteral是一个nil的字面量协议,代表可以使用nil这个关键字来进行初始化,Optional实现了这个协议的方法init(nilLiteral: ()) { self = .none },所以let age: Int? = nil就等价于 let age: Optional<Int> = 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<Direction>.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<T>(of value: inout T) -> UnsafeRawPointer {
  return withUnsafePointer(to: &value, { UnsafeRawPointer($0) })
}

我们来看一下内存数据

func textEnum() {
    var dir = Direction.south
    print("\(getPointer(of: &dir))")
    print("====================")
}
1.png

或者使用View Memory` 如下步骤

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、带初始值的枚举

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

6.png

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

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

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

7.png

8.png

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

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

带关联至的枚举
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协议。

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342