前言
本篇文章将讲述Swift中很常用的也很重要的一个知识点 👉 Enum枚举
。首先会介绍与OC中枚举的差别,接着会从底层分析Enum
的使用场景,包含枚举的嵌套
和递归
,与OC的混编
的场景,最后分析枚举的内存大小
的计算方式,希望大家能够掌握。
一、OC&Swift枚举的区别
1.1 OC中的NS_ENUM
OC中的枚举和C/C++中的枚举基本一样,具有以下特点👇
- 仅支持
Int类型
,默认首元素值为0
,后续元素值依次+1
-
中间的元素
有赋值,那么以此赋值为准
,后续没赋值
的元素值依旧依次+1
枚举使用示例代码👇
typedef NS_ENUM(NSInteger, WEEK) {
Mon,
Tue = 10,
Wed,
Thu,
Fri,
Sat,
Sun
};
// 调用代码👇
WEEK a = Mon;
WEEK b = Tue;
NSLog(@"a = %d, b = %d", (int)a, (int)b);
1.2 Swift中的Enum
Swift中的枚举比OC的强大很多
!其特点如下👇
- 格式: 不用逗号分隔,类型需使用case声明
- 内容:
- 支持
Int、Double、String
等基础类型
,也有默认枚举值
(String类型
默认枚举值为key的名称
,Int、Double
数值型默认枚举值为0
开始+1递增
- 支持
自定义选项
👉不指定
支持类型
,就没有rawValue
,但同样支持case枚举
,可自定义关联内容
- 支持
注意:
rawValue
在后面枚举的访问
中会详细的讲解。
示例代码👇
// 写法一
// 不需要逗号隔开
enum Weak1 {
case MON
case TUE
case WED
case THU
case FRI
case SAT
case SUN
}
// 写法二
// 也可以直接一个case,然后使用逗号隔开
enum Weak2 {
case MON, TUE, WED, THU, FRI, SAT, SUN
}
// 定义一个枚举变量
var w: Weak1 = .MON
1.2.2 自定义选项类型
如果在声明枚举时不指定类型
,那么可给枚举项添加拓展内容
(即自定义类型
)。switch-case
访问时,可取出拓展类型进行相应的操作。例如👇
// 自定义类型的使用
enum Shape {
case square(width: Double)
case circle(radius: Double, borderWidth:Double)
}
func printValue(_ v: Shape) {
// switch区分case(不想每个case处理,可使用default)
switch v {
case .square(let width):
print(width)
case .circle(let radius, _):
print(radius)
}
}
let s = Shape.square(width: 10)
let c = Shape.circle(radius: 20, borderWidth: 1)
printValue(s)
printValue(c)
二、Swift枚举的使用
接下来我们看看Swift枚举的使用,包含一些特殊的场景的情况。
2.1 枚举的访问
说到枚举的访问,就必须得提一个关键字rawValue
,使用案例👇
enum Weak: String{
case MON, TUE, WED, THU, FRI, SAT, SUN
}
var w = Weak.MON.rawValue
print(w)
运行结果👇
注意:如果enum
没有声明类型
,是没有rawValue
属性的👇
现在问题来了 👉 rawValue
对应在底层是如何做到读取到MON
值的?
rawValue的取值流程
老规矩,找入口,之前我们都是查看SIL,当然这里也不例外👇
swiftc -emit-sil xx.swift | xcrun swift-demangle >> ./xx.sil && vscode xx.sil
先看看枚举Week👇
接着看看main函数的流程👇
最后看看rawValue的getter方法👇
然后看bb8代码段👇
至此,我们现在知道了,rawValue
的底层就是调用的getter方法
,getter方法中构造了字符串
,但是这个字符串的值
(例如“MON”)从哪里取出
的呢?其实我们能猜出来,应该是在编译期确定
了的,所以,我们打开工程的exec可执行文件,查看Mach-O
👇
可见,在__TEXT, __cstring
的section段,这些字符串在编译期已经存储好了,而且内存地址是连续的
。所以,rawValue
的getter方法 👉 case分支中构建的字符串,主要是在Mach-O
文件中从对应地址
取出的字符串,然后再返回给变量w
。
case值 & rawValue值
现在我们弄清楚了rawValue
值的来源,那么又有一个问题:枚举case
值和 rawValue
值如何区分
呢?下面的代码输出打印结果是什么?
//输出 case值
print(Weak.MON)
//输出 rawValue值
print(Weak.MON.rawValue)
虽然输出的都是MON
,但其实并不是相同的,why?看下图👇
上图可知,并不能将枚举的case值
赋给字符串类型常量w
,同时,也不能将字符串"MON"
赋给枚举值t
。
2.2 枚举初始化init
在OC中,枚举没有初始化一说,而在Swift中,枚举是有init初始化方法
👇
Weak.init(rawValue:)
接下来我们来看看这个初始化方法在底层的流程
,首先添加代码👇,打上断点
print(Weak.init(rawValue: "MON")!)
打开汇编,运行👇
接着我们还是看SIL代码,关于Weak.init(rawValue:)
部分👇
其中,上图中涉及的SIL的指令释义👇
指令名称 | 指令释义 |
---|---|
index_addr | 获取当前数组中的第n个元素值 的地址(即指针 ),存储到当前地址中 |
struct_extract | 表示在结构体中取出当前的Int值 ,Int类型在系统中也是结构体 |
cond_br | 表示比较的表达式 ,即分支条件跳转 (类似于三元表达式) |
接着来看看这个关键的函数_findStringSwitchCase
的源码👇
我们继续看Weak.init
的最终处理代码 👉 bb29
代码段👇
至此,我们分析完了Weak.init
的底层流程,于是修改之前的调用代码(去掉
了之前的感叹号!
)👇
print(Weak.init(rawValue: "MON"))
print(Weak.init(rawValue: "Hello"))
编译器会爆出警告(返回的结果是可选型
),运行结果👇
所以,现在我们就能明白,为什么一个打印的是可选值,一个打印的是nil。
2.3 枚举遍历:CaseIterable协议
CaseIterable协议
,有allCases
属性,支持遍历所有
case,例如👇
// Double类型
enum Week1: Double, CaseIterable {
case Mon,Tue, Wed, Thu, Fri, Sat, Sun
}
Week1.allCases.forEach { print($0.rawValue)}
// String类型
enum Week2: String, CaseIterable {
case Mon,Tue, Wed, Thu, Fri, Sat, Sun
}
Week2.allCases.forEach { print($0.rawValue)}
2.4 枚举关联值
关联值
就是上面讲过的自定义类型
的枚举,它能表示更复杂的信息,与普通类型的枚举不同点在于👇
- 没有rawValue
- 没有rawValue的getter方法
- 没有初始化init方法
例如
// 自定义类型的使用
enum Shape {
case square(width: Double)
case circle(radius: Double, borderWidth:Double)
}
查看其SIL代码👇
中间层代码真的什么都没有!😂
2.5 模式匹配
模式匹配
就是针对case的匹配
,根据枚举类型,分为2种:
- 简单类型的枚举的模式匹配
- 自定义类型的枚举(关联值)的模式匹配
2.5.1 简单类型
swift中的简单类型enum匹配需要将
所有情况都列举
,或者使用default表示默认情况
,否则会报错
!
enum Weak: String{
case MON
case TUE
case WED
case THU
case FRI
case SAT
case SUN
}
var current: Weak?
switch current {
case .MON:print(Weak.MON.rawValue)
case .TUE:print(Weak.MON.rawValue)
case .WED:print(Weak.MON.rawValue)
default:print("unknow day")
}
如果去掉default
,会报错👇
我们看看SIL代码👇
所以运行上面代码,应该匹配的是default分支👇
2.5.2 关联值类型
关联值类型的模式匹配有两种方式👇
- switch - case 👉 匹配所有case
- if - case 👉 匹配单个case
switch - case
enum Shape{
case circle(radius: Double)
case rectangle(width: Int, height: Int)
}
let
修饰case
值👇
let shape = Shape.circle(radius: 10.0)
switch shape{
//相当于将10.0赋值给了声明的radius常量
case let .circle(radius):
print("circle radius: \(radius)")
case let .rectangle(width, height):
print("rectangle width: \(width) height: \(height)")
}
也可以let var
修饰关联值的入参
👇
let shape = Shape.circle(radius: 10.0)
switch shape{
case .circle(let radius):
print("circle radius: \(radius)")
case .rectangle(let width, var height):
height += 1
print("rectangle width: \(width) height: \(height)")
}
查看SIL层的代码,看看是怎么匹配的👇
if - case
let circle = Shape.circle(radius: 10.0)
if case let Shape.circle(radius) = circle {
print("circle radius: \(radius)")
}
通用关联值
如果只关心不同case
下的某一个关联值
,可以将该关联值用同一个入参
替换,例如下面例子中的x
👇
enum Shape{
case circle(radius: Double)
case rectangle(width: Double, height: Double)
case square(width: Double, height: Double)
}
let shape = Shape.circle(radius: 10)
switch shape{
case let .circle(x), let .square(20, x):
print(x)
default:
print("未匹配")
break
}
注意:不能使用多于1个的通用入参,例如下面的
y
👇
也可以使用通配符 _
👇
let shape = Shape.rectangle(width: 10, height:20)
switch shape{
case let .rectangle(_, x), let .square(_, x):
print("x = \(x)")
default:
break
}
还可以这么写👇
let shape = Shape.rectangle(width: 10, height:20)
switch shape{
case let .rectangle(x, _), let .square(_, x):
print("x = \(x)")
default:
break
}
大家平时在使用枚举时,还是要注意下面2点👇
- 枚举使用过程中不关心某一个关联值,可以使用
通配符_
标识- OC只能调用Swift中
Int类型
的枚举
2.6 支持计算型属性 & 函数
Swift枚举中还支持计算属性
和函数
,例如👇
enum Direct: Int {
case up
case down
case left
case right
// 计算型属性
var description: String{
switch self {
case .up:
return "这是上面"
default:
return "这是\(self)"
}
}
// 函数
func printSelf() {
print(description)
}
}
Direct.down.printSelf()
三、枚举嵌套
枚举的嵌套主要有2种场景👇
- 枚举嵌套枚举
- 结构体嵌套枚举
3.1 enum嵌套enum
我们先来看看枚举嵌套枚举
,我们继续改下上面的例子👇
enum CombineDirect{
//枚举中嵌套的枚举
enum BaseDirect{
case up
case down
case left
case right
}
//通过内部枚举组合的枚举值
case leftUp(baseDirect1: BaseDirect, baseDirect2: BaseDirect)
case leftDown(baseDirect1: BaseDirect, baseDirect2: BaseDirect)
case rightUp(baseDirect1: BaseDirect, baseDirect2: BaseDirect)
case rightDown(baseDirect1: BaseDirect, baseDirect2: BaseDirect)
}
如果初始化一个左上方向,代码👇
let leftUp = CombineDirect.leftUp(baseDIrect1: CombineDirect.BaseDirect.left, baseDirect2: CombineDirect.BaseDirect.up)
3.2 struct嵌套enum
接下来就是结构体嵌套枚举
了,例如👇
//结构体嵌套枚举
struct Skill {
enum KeyType{
case up
case down
case left
case right
}
let key: KeyType
func launchSkill(){
switch key {
case .left, .right:
print("left, right")
case .up, .down:
print("up, down")
}
}
}
3.3 枚举的递归(indirect)
还有一种特殊的场景 递归
👉 枚举中case关联内容
使用自己的枚举类型
。例如👇
enum Binary<T> {
case empty
case node(left: Binary, value:T, right:Binary)
}
一个树
结构,其左右节点的类型也是自己本身,这时编译器会报错👇
报错原因 👉 使用该枚举时,enum的大小
需要case
来确定,而case的大小又需要使用到enum大小
。所以无法计算
enmu的大小,于是报错!
安排 👉 根据编译器提示,需要使用关键字indirect
,意思就是将该枚举标记位递归
,同时也支持标记单个case
,所以可以👇
那么问题来了,indirect
在底层干了什么呢?
indirect底层原理
我们先来看这个例子👇
enum List<T>{
case end
indirect case node(T, next: List<T>)
}
var node = List<Int>.node(10, next: List<Int>.end)
print(MemoryLayout.size(ofValue: node))
print(MemoryLayout.stride(ofValue: node))
size和stride都是8,换成String类型👇
仍然也是8,看来枚举的大小不受其模板类型大小的影响。
lldb分析
我们先lldb看看其内存的分布👇
上图中我们发现,node的metadata
对应的地址0x0000000100562660
是分配在堆
上的,所以,indirect关键字其实就是通知编译器
,需要分配一块堆区
的内存空间,用来存放enum
或 case
。此时case为node
时,存储的是引用地址0x0000000100562660
,而case为end
时,则👇
那为何说地址是堆区呢?我们接着看看SIL代码👇
SIL代码中,是通过alloc_box申请的内存,alloc_box底层调用的是swift_allocObject
,所以是堆区,我们可以再node打上断点,查看汇编👇
四、##swift和OC混编枚举
接下来我们看看swift和OC的枚举的混编场景。
4.1 OC使用Swift枚举
首先看看OC调用Swift枚举,那么此时枚举必须具备以下2个条件👇
- 用
@objc
关键字标记enum - 当前enum必须是
Int类型
// Swift中定义枚举
@objc enum Weak: Int{
case MON, TUE, WED, THU, FRI, SAT, SUN
}
// OC使用
- (void)test{
Weak mon = WeakMON;
}
4.2 Swift使用OC枚举
反过来,就没限制了,OC中的枚举会自动转换
成swift中的enum。
// OC定义
NS_ENUM(NSInteger, OCENUM){
Value1,
Value2
};
// swift使用
//1、将OC头文件导入桥接文件
#import "OCFile.h"
//2、使用
let ocEnum = OCENUM.Value1
typedef enum
// OC定义
typedef enum {
Num1,
Num2
}OCNum;
// swift使用
let ocEnum = OCNum.init(0)
print(ocEnum)
上图可知,通过typedef enum
定义的enum,在swift中变成了一个结构体,并遵循了两个协议:Equatable
和 RawRepresentable
。
typedef NS_ENUM
// OC定义
typedef NS_ENUM(NSInteger, OCNum) {
Num1,
Num2
};
// swift使用
let ocEnum = OCNum.init(rawValue: 0)
print(ocEnum!)
那么自动生成的swift中是这样👇
并没有遵循任何协议!
4.3 OC使用Swift中String类型的枚举
这也是一种常见的场景,解决方案👇
- swift中的enum尽量声明成Int整型
- 然后OC调用时,使用的是Int整型的
- enum再声明一个
变量/方法
,用于返回固定的字符串
,给swift中使用
示例👇
@objc enum Weak: Int{
case MON, TUE, WED
var val: String?{
switch self {
case .MON:
return "MON"
case .TUE:
return "TUE"
case .WED:
return "WED"
default:
return nil
}
}
}
// OC中使用
Weak mon = WeakMON;
// swift中使用
let weak = Weak.MON.val
五、枚举的大小
主要分析以下几种情况👇
- 普通enum
- 具有关联值的enum
- enum嵌套enum
- struct嵌套enum
枚举的大小也是面试中经常问到的问题,重点在于两个函数的区别👇
size
:实际占用
内存大小
stride
:系统分配
的内存大小
5.1 普通enum
最普通的情况,即非嵌套,非自定义类型
的枚举,例如👇
enum Weak {
case MON
}
print(MemoryLayout<Weak>.size)
print(MemoryLayout<Weak>.stride)
再添加一个case,运行👇
继续增加多个case,运行👇
以上可以看出,当case个数为1时,枚举size为0,个数>=2时,size的大小始终是1,why?下面我们来分析分析👇
上图打断点,读取内存可以看出,case都是1字节
大小,1个字节是8个byte
,按照二进制转换成十进制,那么有255种排列组合(0x00000000 - 0x11111111)
,所以当case为1个的时候,size的大小是0
(二进制是0x0
),case数<=255
时,size都是1
。而超过255
个时,会自动扩容
,size
和stride
都会增加
。
5.2 具有关联值的enum
如果是自定义类型的枚举,即关联值类型,size和stride的值会发生什么变化呢?看下面的例子👇
enum Shape{
case circle(radius: Double)
case rectangle(width: Double, height: Double)
}
print(MemoryLayout<Shape>.size)
print(MemoryLayout<Shape>.stride)
看来关联值的枚举大小和关联值入参有关系,👇
5.3 enum嵌套enum
枚举嵌套枚举,是一种特殊的情况,下面示例大小是多少?👇
enum CombineDirect{
enum BaseDirect{
case up, down, left, right
}
case leftUp(baseDirect1: BaseDirect, baseDirect2: BaseDirect)
case rightUp(baseDirect1: BaseDirect, baseDirect2: BaseDirect)
case leftDown(baseDirect1: BaseDirect, baseDirect2: BaseDirect)
case rightDown(baseDirect1: BaseDirect, baseDirect2: BaseDirect)
}
print(MemoryLayout<CombineDirect>.size)
print(MemoryLayout<CombineDirect>.stride)
从结果中可以看出,enum嵌套enum,和具有关联值的enum
的情况是一样的,同样取决于关联值的大小
,其内存大小是最大关联值的大小
。
接着我们看看具体的分布,可以先定义一个变量👇
var combine = CombineDirect.leftDown(baseDirect1: .left, baseDirect2: .down)
lldb查看其内存分布👇
将第一个入参left 改为 up👇
所以,02
表示是caseleftDown
的关联值的第一个入参
的枚举值,那第2个入参是.down,按照规律来算应该是01
,但却是81
,why?接下来我们看看81
代表的是什么值?
- 在enum CombineDirect中
多加4个case
结果是c1
- 减少一个case
减少一个case项后,是a1
- 再减少一个case
再减少一个是81,说明81
中的8
是
- 继续减少,保证case只有2个
结果页是81
- 添加保证case>10个
如果leftDown 的case索引值大于8,例如上图,leftDown是第1个case,结果e1
中的e
就是15(十进制),即case选项的索引值。
- 再减少,保证case leftDown在第9个
果然,上图中leftDown的case索引值是9,打印出来的91
中的第一位也是9
。
综上, 81
中的第一位8
这个值,有以下几种情况区分👇
- 当嵌套enum的case只有2个时,case在内存中的存储是0、8
- 当嵌套enum的case
大于2,小于等于4
时,case在内存中的存储是0、4、8、12
- 当嵌套enum的case
大于4
时,case在内存中的存储是从0、1、2...
类推
81
中的1
,代表什么意思呢?我们改变下关联值入参👇
所以,leftDown减少一个入参,结果是80
,加一个入参,结果是01 80
,继续再加一个入参👇
关联值的入参是up,down,right,right
,对应的枚举值是0,1,3,3
,所以可以得出结论👇
- enum嵌套enum同样取决于
最大case的关联值大小
- case中关联值的
内存分布
,又是根据入参的个数
和大小
来分布的
2.1 每个入参占一个字节
大小的空间(即2个byte位
),第2位byte里面存储的是内层枚举的case值
,第1位的byte值通常是0
2.2最后一个入参
的byte空间分布 👉 第2位是内层
枚举的case值,第1位是外层
枚举的case值,其规律又如下👇
- 当外层enum的case
只有2个
时,第1位byte值按照0、8
依次分布- 当外层enum的case个数
>2,<=4
时,第1位byte值按照0、4、8、12
依次分布- 当外层enum的case个数
>4
时,第1位byte值按照0、1、2、3、...
依次分布
5.4 struct嵌套enum
struct Skill {
enum KeyType{
case up
case down
case left
case right
}
let key: KeyType
func launchSkill(){
switch key {
case .left, .right:
print("left, right")
case .up, .down:
print("up, down")
}
}
}
print(MemoryLayout<Skill>.size)
print(MemoryLayout<Skill>.stride)
size和stride都是1。结构体的大小计算,跟函数无关,所以只看成员变量key的大小
,key是枚举Skill
类型,大小为1,所以结构体大小为1。继续,去掉
成员key👇
没有任何成员变量时,size为0,stride为1(系统默认分配
的)。如果加一个成员👇
因为添加的是UInt8
,占1个字节,所以size和stride都+1,均为2。再添加一个成员👇
添加的成员是Int类型,占8字节,8+1+1=10,而stride是系统分配的,8的倍数来分配,所以是16。你以为就这么简单的相加吗?我们换一下width的位置
👇
将width成员放到最后面,size变为16,why?因为size的大小,是按照结构体内存对齐原则
来计算的,可参考我之前的文章内存对齐分析。
总结
本篇文章主要讲解了Swift中的枚举,开始与OC的枚举作比较,引出Swift枚举的不同点,进而分析了rawValue
和初始化init
的底层实现流程,然后讲解了几个重要的场景 👉 OC和Swift的桥接
场景,枚举嵌套
的场景,最后重点分析了枚举的大小
,即内存分布的情况,这也是面试中经常出的题目,希望大家掌握,谢谢!