我正在参加「掘金·启航计划」
主要内容:
- 枚举的底层结构体认识
- rawValue的实现原理
- init的实现原理
- 模式匹配过程
- 内存大小分析
- Swift和OC混编
1. 枚举的底层结构体认识
1.1 原始值
代码:
/*
1、原始值
*/
enum Week: String{
case MON = "MON"
case TUE = "TUE"
case WED = "WED"
case THU = "THU"
case FRI = "FRI"
case SAT = "SAT"
case SUN = "SUN"
}
SIL:
//只有原始值
//自动生成init?初始化方法,已经rawValue计算属性
enum Week : String {
case MON
case TUE
case WED
case THU
case FRI
case SAT
case SUN
//参数是rawValue
init?(rawValue: String)
//RawValue类型也就是String
typealias RawValue = String
//rawValue只读计算属性
var rawValue: String { get }
}
说明:
1.2 关联值
代码:
SIL:
说明:
- 当使用了关联值后,就没有RawValue了,主要是因为case可以用一组值来表示,而rawValue是单个的值
- 注意只要枚举中存在一个关联值,那么这个枚举就不存在rawValue了。(后面好好分析一下)
2. rawValue的实现原理
代码:
/*
3、rawValue的实现原理
*/
enum Week: String{
case MON = "MON"
case TUE = "TUE"
case WED = "WED"
case THU = "THU"
case FRI = "FRI"
case SAT = "SAT"
case SUN = "SUN"
}
let mon = Week.MON.rawValue
SIL的main方法:
说明:
- 拿到枚举
- 获取到getter方法
- apply调用方法,并且传入枚举
- 赋值给全局变量
- 因此这里最重要的就是getter方法传入枚举获取到rawValue值
SIL的getter方法:
说明:
- 通过传入的枚举通过匹配枚举项找到对应的分支
- 在不同的分支上构建对应的字符串
- 跳转到b8返回这个字符串,而这个字符串就是rawValue拿到的值
- 构建字符串本质就是到底层获取到字符串
在底层存储的字符串:
3. init的实现原理
3.1 调用时机
代码:
/*
4、init方法
*/
enum Week: String{
case MON = "MON"
case TUE = "TUE"
case WED = "WED"
case THU = "THU"
case FRI = "FRI"
case SAT = "SAT"
case SUN = "SUN"
}
print(Week.MON.rawValue)
let w = Week.MON.rawValue
Week.init(rawValue: "MON")
print("end")
定义一个符号断点
说明:
- 可以看到只有通过Init方法进行调用时才会执行init方法
- 直接获取ravwValue,是不会执行init流程的
3.2 底层分析
代码:
SIL的init方法:
说明:
- 在init方法中是将所有enum的字符串从Mach-O文件中取出,依次放入数组中
- 放完后,然后调用_findStringSwitchCase方法进行匹配
- 1、先创建一个数组
- 2、将第一个值存储到数组中
- 3、计算得到第二个值的地址,将第二个值存储到数组中
- 4、依次执行,把所有元素放到数组中
_findStringSwitchCase: 通过init方法来创建枚举对象时,需要判断枚举中是否存在该元素
说明:
- 1、遍历数组,如果匹配则返回对应的index
- 2、如果不匹配,则返回-1
匹配之后的判断:
说明:
- 如果没有匹配成功,则构建一个.none类型的Optional,表示nil
- 如果匹配成功,则构建一个.some类型的Optional,表示有值
4. 模式匹配过程
4.1 原始值
代码:
/*
5、模式匹配,原始值
*/
/*
enum Week: String{
case MON
case TUE
case WED
case THU
case FRI
case SAT
case SUN
}
var current: Week?
switch current {
case .MON:print(Week.MON.rawValue)
case .TUE:print(Week.MON.rawValue)
case .WED:print(Week.MON.rawValue)
default:print("unknow day")
}
SIL:
说明:
- 其内部是将nil放入current全局变量,然后匹配case,做对应的代码跳转
4.2 关联值
代码:
/*
6、模式匹配,关联值
*/
enum Shape{
case circle(radius: Double)
case rectangle(width: Int, height: Int)
}
let shape = Shape.circle(radius: 10)
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:
说明:
- 首先构建一个关联值的元组
- 根据当前case枚举值,匹配对应的case,并跳转
- 取出元组中的值,将其赋值给匹配case中的参数
5. 内存大小分析
5.1 原始值
代码:
/*
7、内存大小
*/
//只有一个成员
enum NoMean1{
case a
}
print(MemoryLayout<NoMean1>.size)
print(MemoryLayout<NoMean1>.stride)
//多个成员
enum NoMean2{
case a
case b
}
print(MemoryLayout<NoMean2>.size)
print(MemoryLayout<NoMean2>.stride)
结果:
说明:
- 一个成员,不占用内存
- 多个成员时,会占用一个字节大小的内存
LLDB查看:
enum NoMean{
case a
case b
case c
case d
}
var tmp = NoMean.a
var tmp1 = NoMean.b
var tmp2 = NoMean.c
var tmp3 = NoMean.d
说明:
- 在枚举内存中存储的是序号,用来标记枚举成员(从0开始,总共4个序号)
- case是UInt8,即1字节(8位),最大可以存储255
- 如果超过了255,会自动从UInt8 -> UInt16 -> UInt32 -> UInt64 ...(再看下教案)
总结:
- 如果enum中有原始值,即rawValue,其大小取决于case的多少,如果没有超过UInt8即255,则就是1字节存储case
- 只有一个成员时,此时不需要使用序号标记,因此枚举内存没有存储任何值
- 如果有多个成员,就需要使用需要标记成员,枚举就会占用1个字节大小的内存
5.2 关联值
代码:
/*
8、内存大小:关联值
*/
enum Shape{
case circle(radius: Double)
case rectangle(width: Double, height: Double)
}
print(MemoryLayout<Shape>.size)
print(MemoryLayout<Shape>.stride)
结果:
LLDB:
说明:
- num有关联值时,关联值的大小 取 对应枚举关联值 最大的
- circle中关联值大小是8,而rectangle中关联值大小是16,所以取16
- 但是因为有多个成员,所以还需要一个字节大小用来存储序号,所以就是17个
- 分配内存时遵循字节对齐原则,因此分配了24个字节的空间
5.3 递归枚举
使用关键字indirect就可以实现枚举的递归 ,可以修饰在enum前,也可以修饰在枚举内的成员前
5.3.1 使用:
详细的可以看另一篇博客
/*
9、递归枚举
*/
//用枚举表示链表结构
//放在case前
enum List1<T>{
case end
//表示case使是引用来存储
indirect case node(T, next: List1<T>)
}
//放在enum前
indirect enum List2<T>{
case end
case node(T, next: List2<T>)
}
5.3.2 查看大小:
代码:
说明:
- 不管是String类型还是Int类型,此时都是8个字节,而且占用内存大小就是8个字节
- 此时枚举包含两个成员,但是end是原始值,所以内存大小取决于node的成员的大小
5.3.3 LLDB查看内存结构
1、先查看node
代码:
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))
LLDB:
说明:
- 如果枚举被indirect修饰,编译器就不再计算这个枚举的大小
- 在堆中开辟了一个空间,将这个地址放到枚举中
- 因此递归的时候,是无法确定实际大小的,只能通过运行中递归的判断条件来决定
2、再查看end
代码:
LLDB:
说明:
- 如果存储的是end,那么他就和普通的枚举一样
- 只不过这里存储的值是的当前序号的2倍
6. Swift和OC混编
上面我们可以知道swift有很强大的功能,可以添加方法属性,还有关联值原始值,并且成员有多种类型,而OC的枚举只能是Int类型,局限性比较大,所以如果要进行混编,就需要进行一定的特殊考虑
6.1 OC调用Swift
代码:
<!--swift中定义-->
@objc enum Week: Int{
case MON, TUE, WED, THU, FRI, SAT, SUN
}
<!--OC使用-->
- (void)test{
Week mon = WeekMON;
}
说明:
- 用@objc进行修饰,暴露给OC
- 必须是Int类型
OC如何访问swift中String类型的enum:
@objc enum Week: 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中使用-->
Week mon = WeekMON;
<!--swift中使用-->
let Week = Week.MON.val
说明:
- swift中的enum尽量声明成Int整型
- 然后OC调用时,使用的是Int整型的
- enum在声明一个变量/方法,用于返回固定的字符串,用于在swift中使用
6.2 Swift调用OC
6.3.1 方式一:typedef enum
OC代码:
Swift代码:
说明:
- OC的这种声明方式,会转换成Swift的结构体
- 之后就需要注意使用时的样式
- 并遵循了两个协议:Equatable 和 RawRepresentable
6.3.2 方式二:typedef NS_ENUM
OC代码:
Swift代码:
说明:
- OC指定类型的方式,Swift也会指定类型
6.3.3 方式三:NS_ENUM
OC代码:
Swift代码:
说明:
- 自动转换