Swift 枚举(enum)详解
[TOC]
本文将介绍Swift
中枚举的一些用法和其底层原理的一些探索,以及探索一下OC
中的枚举与Swift
中枚举互相调用和枚举类型的内存占用情况。
1. 枚举
1.1 C中枚举
首先我们来看看C
语言中枚举的写法。这里我们以一周7天作为示例。
普通写法:
enum Week {
MON, TUE, WED, THU, FRI, SAT, SUN
}
以上就是C
语言中枚举的常见写法enum
关键字,加上枚举名称,大括号里面的不同的枚举值使用逗号分隔开来。此时的枚举值默认从0开始,依次是1,2,3……
自定义枚举值:
如果我们不想使用默认的枚举值,则可以这样写
enum Week {
MON = 1, TUE, WED, THU, FRI, SAT, SUN
};
此时枚举值就会从1开始依次向后排列,你也可以给每个枚举都定义不同的枚举值,如果直接给TUE
定义为2,而没给MON
定义,则MON
的枚举值会是0。
枚举变量的定义:
enum Week {
MON = 1, TUE, WED, THU, FRI, SAT, SUN
};
enum Week week;
enum Week{
MON = 1, TUE, WED, THU, FRI, SAT, SUN
}week;
enum{
MON = 1, TUE, WED, THU, FRI, SAT, SUN
}week;
我们可以通过以上三种方法创建枚举变量:
- 创建一个枚举,然后声明一个枚举变量
- 创建一个枚举并声明一个枚举变量
- 也可以省略枚举名称,直接声明一个枚举变量
1.2 Swift中枚举
Swift
中最常见的枚举写法:
enum Week{
case MON
case TUE
case WED
case THU
case FRI
case SAT
case SUN
}
在Swift
中也可以简化为如下写法:
enum Week{
case MON, TUE, WED, THU, FRI, SAT, SUN
}
Swift
中枚举很强大,我们可以创建一个枚举值是String
类型的enum
,其实也不应该说是枚举值,而是枚举的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"
}
当然,如果我们不想写后面的字符串,也可以简写成如下的形式:
enum Week: String{
case MON, TUE, WED, THU, FRI, SAT, SUN
}
定义枚举变量:
var w: Week = .MON
枚举变量的定义很简单,跟普通变量的定义没什么差别。
枚举的访问:
我们可以访问枚举的变量和枚举的rawValue
首先我们需要注意的是如果没有声明枚举的类型,是没有rawValue
属性可以访问的。
一般情况下我们可以通过以下方式访问枚举:
enum Week: String{
case MON, TUE, WED, THU, FRI, SAT, SUN
}
print(Week.MON)
print(Week.MON.rawValue)
打印结果如下:
1.3 枚举值和其RawValue的存储
在上面枚举的访问中我们可以看到关于MON
字符串的打印,既然可以打印出来,说明是存储了相关的字符串的,那么是怎么存储的呢?又是怎么获取出来的呢?下面我们通过sil
代码进行分析(使用如下命令生成并打开sil
代码)
swiftc -emit-sil main.swift >> ./main.sil && open main.sil
为了方便分析我们将代码修改为如下:
enum Week: String{
case MON, TUE, WED, THU, FRI, SAT, SUN
}
var w = Week.MON.rawValue
print(w)
执行生成sil
代码的名后:
enum week : String {
case MON
case TUE
case WED
case SUN
typealias RawValue = String
init?(rawValue: String)
var rawValue: String { get }
}
通过sil
代码中对枚举的定义可以看到:
- 跟
Swift
中一致的枚举 - 取了一个别名,也就是
String
类型是RawValue
- 添加了一个可选类型的
init
方法 - 一个计算属性
rawValue
,通过其get
方法获取枚举的原始值
下面我们在main
函数中看看:
关于w
变量的初始化即分析注释写在了截图中。
- 首先创建一个全局变量w,并为变量w开辟内存地址
- 将枚举类型Week.MON存储到%5
- 将枚举Week的rawValue.getter函数存储到%6
- 调用%6中存储的函数,%5作为参数,返回值存储到%7
- 将%7中获取到额值存储到%3,至此变量w初始化完成
下面我们看看rawValue
的getter
方法:
我们可以看到在rawValue
的getter
方法中主要实现是:
- 通过接收到的枚举值去匹配一个分支
- 在分支中构建对于的
String
- 返回上一步构建的
String
那么这个字符串是从哪里来的呢?根据匹配的分支中的方法名称我们可以知道这是获取一个内置的字符串的字面量。其实就是从Mach-O
文件的__TEXT.cstring
中。下面我们通过查看Mach-O
来验证。
所以说rawValue
的值是通过调用枚举的rawValue。getter
函数,从Mach-O
对应的地址中取出字符串并返回。
那么枚举值呢?其实在上面关于rawValue
探索的时候就可以知道了,枚举值在sil
代码中就是:#Week.MON!enumelt
,枚举值和rawValue
本质上是不一样的,从下面的例子可以得到结论:
按照以上的写法是会报编译错误的。
1.4 枚举.init
1.4.1 触发方式
在上面的分析时我们知道枚举会有一个init
方法,那么这个方法是什么时候调用的呢?我们添加如下符号断点:
添加如下代码:
var w: Week = .MON
print(w.rawValue)
运行后并没有触发该符号断点。
下面我们在添加如下代码:
var w = Week(rawValue: "MON")
print(w)
运行后即可触发符号断点:
所以这里init
方法是为枚举通过rawValue
初始化的时候调用的。
1.4.2 init分析
首先我们来看看如下代码的打印结果:
print(Week.init(rawValue: "MON"))
print(Week.init(rawValue: "Hello"))
<!--打印结果-->
Optional(SwiftEnum.Week.MON)
nil
从打印结果中可以看到,第一个输出的是可选值SwiftEnum.Week.MON
,第二个是nil
,很显然Hello
不是我们的枚举,那么这些是怎么实现的呢?我们再次查看sil
代码,此时我们可以直接看Week.init
方法的实现。
方法比较长,在里面添加了相关的注释和分析,折叠的代码基本上是与其上面的代码一致。现在总结如下:
- 首先开辟一块内存用于后续存储构建出来的枚举
- 通过
_allocateUninitializedArray
函数创建一个元组,元组中包含- 与枚举个数大小一样的数组,用于存储枚举中的
rawValue
在本示例中是staticString
, - 数组的首地址
- 与枚举个数大小一样的数组,用于存储枚举中的
- 开始一个一个的构建枚举
rawValue
存储到数组中 - 通过
_findStringSwitchCase
函数查找处要构建的枚举在数组中的位置index
- 从0到count-1依次与
index
作比较- 如果相等则构建对于的枚举
- 如果不相等则构建一个
Optional.none!enumelt
的枚举
- 将构建的枚举存储到开辟的地址
- 最后返回构建的枚举
关于上面提到的两个函数源码可以Swift
源码中找到
_allocateUninitializedArray源码:
@inlinable @inline(__always) @_semantics("array.uninitialized_intrinsic") public func _allocateUninitializedArray<Element>(_ builtinCount: Builtin.Word) -> (Swift.Array<Element>, Builtin.RawPointer) {
let count = Int(builtinCount)
if count > 0 {
// Doing the actual buffer allocation outside of the array.uninitialized
// semantics function enables stack propagation of the buffer.
let bufferObject = Builtin.allocWithTailElems_1(
_ContiguousArrayStorage<Element>.self, builtinCount, Element.self)
let (array, ptr) = Array<Element>._adoptStorage(bufferObject, count: count)
return (array, ptr._rawValue)
}
// For an empty array no buffer allocation is needed.
let (array, ptr) = Array<Element>._allocateUninitialized(count)
return (array, ptr._rawValue)
}
可以看到此处就是根据传入的count
和Builtin.Word
初始化一个数组,将其以元组的形式返回数组和数组首地址。
_findStringSwitchCase源码:
/// The compiler intrinsic which is called to lookup a string in a table
/// of static string case values.
@_semantics("findStringSwitchCase")
public // COMPILER_INTRINSIC
func _findStringSwitchCase(
cases: [StaticString],
string: String) -> Int {
for (idx, s) in cases.enumerated() {
if String(_builtinStringLiteral: s.utf8Start._rawValue,
utf8CodeUnitCount: s._utf8CodeUnitCount,
isASCII: s.isASCII._value) == string {
return idx
}
}
return -1
}
我们可以看到这里接收一个数组和要匹配的字符串,然后通过一个for
循环匹配字符串,如果匹配到了则返回数组中对应的index
,否则返回-1。
1.5 枚举的遍历
一般我们很少会对枚举进行遍历操作,在Swift
中可以通过遵守CaseIterable
协议来实现对枚举的遍历。
enum Week: String, CaseIterable{
case MON, TUE, WED, THU, FRI, SAT, SUN
}
// 使用for循环遍历
var allCase = Week.allCases
for c in allCase{
print(c)
}
// 函数是编程遍历
let allCase = Week.allCases.map({"\($0)"}).joined(separator: ", ")
print(allCase)
1.6 关联值
在Swift
中如果想要表示复杂的含义,可以在枚举中关联更多的信息。下面我们举个例子,如果需要有一个形状的枚举,里面有圆形和矩形。圆形有半径,矩形有长宽,那么这个枚举就可以写成如下代码:
enum Shape{
case circle(radius: Double)
case rectangle(width: Int, height: Int)
}
- 其中括号中的
radius
以及width
和height
就是关联值 - 如果没枚举中使用关联值则枚举就没有
rawValue
属性了,因为关联值是一组值,而rawValue
是单个值,可以通过sil
代码验证
在sil
代码中我们并没有发现init
方法RawValue
别名以及rawValue
的get
方法。
在这个枚举中radius、width、height
这些都是自定义的标签,也可以不写,如下所示,但并不推荐这种方式,因为可读性非常差
enum Shape{
case circle(Double)
case rectangle(Int, Int)
}
那么有关联值的枚举该如何初始化呢?其实也很简单,下面我们就来创建一下
var shape = Shape.circle(radius: 10.0)
shape = Shape.circle(radius: 15)
shape = Shape.rectangle(width: 10, height: 10)
2. 其他用法
2.1 模式匹配
2.1.1 简单的模式匹配
顾明思议,模式匹配就是匹配每一个枚举值,通常我们可以使用switch
语句来进行模式匹配。如果使用switch
进行模式匹配:
- 必须列举当前所有可能的情况,否则就会报编译错误
- 如果不想匹配这么多
case
则可以使用defalut
- 在同一个
case
中可以列举多种情况
enum Week{
case MON
case TUE
case WED
case THU
case FRI
case SAT
case SUN
}
var week = Week.MON
switch week {
case .MON:
print("周一")
case .TUE:
print("周二")
case .WED:
print("周三")
case .SAT, .SUN:
print("happy day")
default : print("unknow day")
}
其实这个匹配也很简单,我们通过查看sil
代码就可以知道:
2.1.2 关联值枚举的模式匹配
如果我们不关心关联值,关联值枚举的写法与普通枚举没有什么区别:
enum Shape{
case circle(radius: Double)
case rectangle(width: Int, height: Int)
}
let shape = Shape.circle(radius: 10.0)
switch shape {
case .circle:
print("the shape is circle")
case .rectangle:
print("the shape is rectangle")
}
但是我们使用关联值枚举,肯定是会关心关联值的,当关心关联值时其写法如下:
switch shape{
case let .circle(radius):
print("circle radius: \(radius)")
case .rectangle(let width, var height):
height += 1
print("rectangle width: \(width) height: \(height)")
}
可以发现,这里的每个case
中都使用了let
或者var
,这里因为要使用关联值,所以需要使用let
声明一下。或者放在最前面,或者对每个需要使用的变量前都添加let
。如果使用var
则可在当前case
中修改其修饰的关联值。当然你也可以不使用枚举定义中的关联值的名字,可以自定义。
关于关联值枚举的模式匹配我们也可以看看sil
代码:
2.1.3 其他匹配
有时候在业务逻辑处理中,我们只是想匹配单个case
,我们可以这样写:
if case let Shape.circle(radius) = shape {
print("circle radius: \(radius)")
}
当然如果我们只关心不同case
的相同关联值时就可以这样写:
enum Shape{
case circle(radius: Double)
case rectangle(width: Double, height: Double)
case square(width: Double, width: Double)
}
let shape = Shape.rectangle(width: 20, height: 10)
switch shape {
case let .rectangle(x, 10), let .square(x, 10):
print(x)
default: break
}
此时的打印是20,对于上面的例子,必须case
是rectangle
或square
,而且rectangle
必须是10,square
后面的width
是10。
如果对于10的匹配不那么严格我们则可以使用通配符_
switch shape {
case let .rectangle(x, _), let .square(x, _):
print(x)
default: break
}
注意: 以上命名必须一致,比如都使用x
,如果一个x
一个y
就不行了。
2.2 枚举的嵌套
2.2.1 枚举嵌套枚举
顾名思义,枚举嵌套枚举就是在枚举中还有枚举,比如我们玩游戏时会有上下左右四个方向键,有时候也需要两两组合去使用,所以我们通过这个例子可以编写如下枚举:
enum CombineDirect{
enum BaseDirect{
case up
case down
case left
case right
}
case leftUp(combineElement1: BaseDirect, combineElement2: BaseDirect)
case rightUp(combineElement1: BaseDirect, combineElement2: BaseDirect)
case leftDown(combineElement1: BaseDirect, combineElement2: BaseDirect)
case rightDown(combineElement1: BaseDirect, combineElement2: BaseDirect)
}
使用起来也很简单:
let leftup = CombineDirect.leftUp(combineElement1: .left, combineElement2: .up)
2.2.1 结构体嵌套枚举
Swift
允许在结构体中嵌套枚举,具体使用如下:
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 .down,.up:
print("up, down")
}
}
}
使用起来也很简单:
let s = Skill(key: .up)
s.launchSkill()
2.3 枚举中包含属性
Swift
中允许在枚举中包含计算属性和类型属性,但不能包含存储属性。
enum Shape {
case circle(radius: Double)
case rectangle(width: Double, height: Double)
// var radius: Double // Enums must not contain stored properties
var width: Double{
get {
return 10.0
}
}
static let height = 20.0
}
2.3 枚举中包含方法
Swift
中的枚举也可以包含方法,可以是实例方法也可以是类方法
enum Week: Int{
case MON, TUE, WED, THU, FRI, SAT, SUN
mutating func nextDay(){
if self == .SUN{
self = Week.MON
}else{
self = Week(rawValue: self.rawValue+1)!
}
}
static func test() {
print("test")
}
}
使用起来依旧很简单:
var w = Week.SUN
w.nextDay()
print(w)
Week.test()
此处的方法都是静态调用:
2.4 indiret在枚举中的应用
2.4.1 indiret
如果我们想要使用enum
表达一个复杂的关键数据结构的时候,我们可以通过使用indrect
关键字来让enum
更简洁。
比如我们想要通过枚举来表达一个链表的结构,链表需要存储数据以及指向它的下一个节点的指针,如果不使用indiret
修饰则会报编译错误:
此时我们可以写成如下两种方式就不会报错了:
enum List<T> {
case end
indirect case node(T, next: List<T>)
}
indirect enum List<T> {
case end
case node(T, next: List<T>)
}
那么为什么要添加indirect
关键字呢?
因为enum
是值类型,它的大小在编译期就需要确定,如果按照开始的写法是不能够确定当前enum
的大小的,所以从系统的角度来说,在不知道给enum
分配多大的空间,所以就需要使用indirect
关键字,官方文档是这样解释的:
You indicate that an enumeration case is recursive by writing indirect before it, which tells the compiler to insert the necessary layer of indirection.
译:您可以通过在枚举案例之前写indirect来表明枚举案例是递归的,这告诉编译器插入必要的间接层。
2.4.2 内存占用
我们打印一下使用indirect
修饰的枚举内存占用是多少呢?
enum List<T> {
case end
indirect case node(T, next: List<T>)
}
print(MemoryLayout<List<Int>>.size)
print(MemoryLayout<List<Int>>.stride)
<!--打印结果-->
8
8
如果我们的泛型使用的是String
呢?
print(MemoryLayout<List<String>>.size)
print(MemoryLayout<List<String>>.stride)
<!--打印结果-->
8
8
此时我们发现泛型的更换内存占用保持不变,此时我们创建一个使用indirect
修饰的枚举类型的变量:
var node = List<Int>.node(10, next: List<Int>.end)
通过lldb
查看node
的内存:
可以看到node
像一个对象的结构,所以说这里面存储的是一个指针,当不确定枚举类型大小的时候,将分配一个8字节大小的指针,指向一块堆空间用于存储这不确定大小的枚举。
如果是end
,此时存储的就是case
值
那么这些是如何实现的呢?我们通过sil
代码来看一下:
这里我们可以看到使用了alloc_box
,我们打开SIL参考文档,并找到alloc-box
我们可以看到alloc_box
就是在堆上分配一个引用计数@box,该值足够大,可以容纳T类型的值,以及一个retain count
和运行时所需的任何其他元数据。
其本质是调用了swift_allocObject
,这点可以通过汇编代码验证:
3. 枚举的大小
3.1 普通枚举大小分析
首先看看下面这段代码的打印结果:
enum NoMean{
case a
}
print(MemoryLayout<NoMean>.size)
print(MemoryLayout<NoMean>.stride)
<!--打印结果-->
0
1
如果我们在增加一个case
呢?
enum NoMean{
case a
case b
}
print(MemoryLayout<NoMean>.size)
print(MemoryLayout<NoMean>.stride)
<!--打印结果-->
1
1
如果在多增加几个呢?
enum NoMean{
case a
case b
case c
case d
case e
}
print(MemoryLayout<NoMean>.size)
print(MemoryLayout<NoMean>.stride)
<!--打印结果-->
1
1
可以看到,打印结果还是1,所以普通枚举应该就是以1字节存储在内存中的,下面我们来分析一下:
首先我们添加如下代码:
var a = NoMean.a
var b = NoMean.b
var c = NoMean.c
var d = NoMean.d
lldb调试:
所以这里当前枚举的步长是1字节,也就意味着如果内存中连续存储NoMean
,需要跨越一个字节的长度。一个字节也就是8位,最大可以表达255个数字。由于太长就不测试了,如果真的需要写255及以上,还是建议以别的方式优化一下。
如果枚举后面写了类型,比如:
enum NoMean: Int{
case a
case b
case c
case d
}
此时打印枚举的大小和步长还是1,这里面的类型指的是rawValue
,并不是case
的值。
- 所以枚举中默认是以
UInt8
存储的,最大可以存储0~255,如果不够则会自动转换为UInt16
,以此类推。 - 当只有一个
case
的时候,size
是0,表示这个枚举是没有意义的 - 枚举中后面声明的类型只的是
rawValue
的类型,不会影响枚举的大小 - 这些
rawValue
的值会存储在Mach-O
文件中,在使用的时候取查找,这个在上面提到过,与枚举大小没有关系
3.2 关联值枚举的大小
如果枚举中有关联值,那么它的大小是多少呢?
enum Shape{
case circle(radius: Int)
case rectangle(width: Int, height: Int)
}
print(MemoryLayout<Shape>.size)
print(MemoryLayout<Shape>.stride)
<!--打印结果-->
17
24
从打印结果我们可以知道,具有关联值的枚举的大小取决于关联值的大小,此时circle
中的关联值Int
占用内存大小是8,而rectangle
中两个Int
加起来是16,那么打印的这个17是怎么来的呢?其实还有存储枚举值,所以枚举的大小此处枚举的size = 8+8+1 = 17,由于内存对齐,所以要分配8的整数倍,所以stride
就是24。这是该枚举中最大需要的内存,这个内存足够容纳circle
需要的9字节的大小。
下面我们,修改一下代码顺序,创建一下具有关联值的枚举,看看其内存分布:
我们可以看到circle
是分配了24字节的内存空间的,内存分布首先是存储关联自,然后在存储枚举值,circle
的枚举值是存储在第三个8字节上的,也就是存储在最后。
- 具有关联值的枚举的大小取决于关联值的大小
- 具有关联值的枚举的大小是枚举中最大的那个关联值枚举的大小 + 1(case 需要占用1字节),
- 如果大于255可能需要占用2字节,这里没有进行测试
3.3 枚举嵌套枚举的大小分析
下面我们看看枚举嵌套枚举中内存占用的大小:
enum CombineDirect{
enum BaseDirect{
case up
case down
case left
case right
}
case leftUp(combineElement1: BaseDirect, combineElement2: BaseDirect)
case rightUp(combineElement1: BaseDirect, combineElement2: BaseDirect)
case leftDown(combineElement1: BaseDirect, combineElement2: BaseDirect)
case rightDown(combineElement1: BaseDirect, combineElement2: BaseDirect)
}
print(MemoryLayout<CombineDirect>.size)
print(MemoryLayout<CombineDirect>.stride)
<!--打印结果-->
2
2
根据打印结果我们可以知道嵌套的枚举,其实也就是枚举中关联了枚举,它的大小同样取决于关联值的大小,因为BaseDirect
是基本的枚举,其内存占用为1,那么按照关联值枚举中的内存占用应该是1+1+1 = 3,那么为什么是2呢?
下面我们通过创建枚举变量,看看其内存分布是什么样的,首先添加如下代码:
// 2 0 3 0 2 1 3 1
var a = CombineDirect.leftUp(combineElement1: .left, combineElement2: .up)
var b = CombineDirect.rightUp(combineElement1: .right, combineElement2: .up)
var c = CombineDirect.leftDown(combineElement1: .left, combineElement2: .down)
var d = CombineDirect.rightDown(combineElement1: .right, combineElement2: .down)
lldb查看内存
通过lldb
调试的结果我们可以看到,在每个字节的低4位上存储着关联值的值,而在最后那个关联值的高四位分别存储了0,4,8,12(c),所以对于枚举中嵌套枚举应该是做了相应的优化,借用未使用的高位存储关联值枚举的枚举值。
下面我们测试一下,多写几个:
enum CombineDirect{
enum BaseDirect{
case up
case down
case left
case right
}
case upup(combineElement1: BaseDirect, combineElement2: BaseDirect)
case updown(combineElement1: BaseDirect, combineElement2: BaseDirect)
case upleft(combineElement1: BaseDirect, combineElement2: BaseDirect)
case upright(combineElement1: BaseDirect, combineElement2: BaseDirect)
case downup(combineElement1: BaseDirect, combineElement2: BaseDirect)
case downdown(combineElement1: BaseDirect, combineElement2: BaseDirect)
case downleft(combineElement1: BaseDirect, combineElement2: BaseDirect)
case downright(combineElement1: BaseDirect, combineElement2: BaseDirect)
case leftUp(combineElement1: BaseDirect, combineElement2: BaseDirect)
case leftDown(combineElement1: BaseDirect, combineElement2: BaseDirect)
case leftleft(combineElement1: BaseDirect, combineElement2: BaseDirect)
case leftright(combineElement1: BaseDirect, combineElement2: BaseDirect)
case rightup(combineElement1: BaseDirect, combineElement2: BaseDirect)
case rightdown(combineElement1: BaseDirect, combineElement2: BaseDirect)
case rightleft(combineElement1: BaseDirect, combineElement2: BaseDirect)
case rightright(combineElement1: BaseDirect, combineElement2: BaseDirect)
}
print(MemoryLayout<CombineDirect>.size)
print(MemoryLayout<CombineDirect>.stride)
var a = CombineDirect.upup(combineElement1: .up, combineElement2: .up)
var b = CombineDirect.updown(combineElement1: .up, combineElement2: .down)
var c = CombineDirect.upleft(combineElement1: .up, combineElement2: .left)
var d = CombineDirect.upright(combineElement1: .up, combineElement2: .right)
var e = CombineDirect.downup(combineElement1: .down, combineElement2: .up)
var f = CombineDirect.downdown(combineElement1: .down, combineElement2: .down)
var g = CombineDirect.downleft(combineElement1: .down, combineElement2: .left)
var h = CombineDirect.downright(combineElement1: .down, combineElement2: .right)
var i = CombineDirect.leftUp(combineElement1: .left, combineElement2: .up)
var j = CombineDirect.leftDown(combineElement1: .left, combineElement2: .down)
var k = CombineDirect.leftleft(combineElement1: .left, combineElement2: .left)
var l = CombineDirect.leftright(combineElement1: .left, combineElement2: .right)
var m = CombineDirect.rightup(combineElement1: .right, combineElement2: .up)
var n = CombineDirect.rightdown(combineElement1: .right, combineElement2: .down)
var o = CombineDirect.rightleft(combineElement1: .right, combineElement2: .left)
var p = CombineDirect.rightright(combineElement1: .right, combineElement2: .right)
lldb调试结果:
此时我们发现,最后那个关联值的高四位分别存储了0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f。
如果我们随便注释几个:
我们发现,对应的结果与枚举中的顺序是一致的。
如果只剩一个,但不是第一个:
此时我们发现是7,与枚举中的属性还是一致的。
下面我们开始注释枚举中的:
最后发现:
- 如果是两个枚举就是0,8
- 如果是3或4的时候是按照0,4,8,c
- 大约4小于等于16的时候是0,1,2,3,4......f
- 如果大于16就不能看出上面的规律了,所以从二进制位看
对于枚举中嵌套枚举,使用关联值,又或者说不嵌套,具有关联值的枚举中的关联值是枚举类型的时候,会优先借用最后关联的那个枚举的二进制位存储具有关联值枚举的值,借用的位数为关联值枚举的个数小于等于2的幂最小值,也就是2的几次幂才能大于等于关联枚举的个数。
这里我有进一步测试,如果普通枚举的个数不足以使用低四位表示,比如低四位最少表示16个,如果多了的话,就会借用关联值中倒数第二个,也就上面例子中的第一个关联值的高位进行借位存储。按照这个逻辑,大胆猜想,如果普通枚举的个数为256个,也就是不能借任何一个位,这种具有关联值枚举是不是会另外开辟内存存关联值枚举的值?其实不需要是256个,只要不够借的时候就会开辟内存去存储关联值枚举的值。
举个例子:
enum BaseDirect{
case up
case down
case left
case right
case a
case b
case c
case d
case e
case f
case g
case h
case i
case j
case k
case l,m,n,o,p,q,r,s,t,u,v,w,x,y,z
case l1,m1,n1,o1,p1,q1,r1,s1,t1,u1,v1,w1,x1,y1,z1
case l2,m2,n2,o2,p2,q2,r2,s2,t2,u2,v2,w2,x2,y2,z2
case l3,m3,n3,o3,p3,q3,r3,s3,t3,u3,v3,w3,x3,y3,z3
}
enum CombineDirect{
case upup(combineElement1: BaseDirect, combineElement2: BaseDirect)
case updown(combineElement1: BaseDirect, combineElement2: BaseDirect)
case upleft(combineElement1: BaseDirect, combineElement2: BaseDirect)
case upright(combineElement1: BaseDirect, combineElement2: BaseDirect)
case downup(combineElement1: BaseDirect, combineElement2: BaseDirect)
case downdown(combineElement1: BaseDirect, combineElement2: BaseDirect)
case downleft(combineElement1: BaseDirect, combineElement2: BaseDirect)
case downright(combineElement1: BaseDirect, combineElement2: BaseDirect)
case leftUp(combineElement1: BaseDirect, combineElement2: BaseDirect)
case leftDown(combineElement1: BaseDirect, combineElement2: BaseDirect)
case leftleft(combineElement1: BaseDirect, combineElement2: BaseDirect)
case leftright(combineElement1: BaseDirect, combineElement2: BaseDirect)
case rightup(combineElement1: BaseDirect, combineElement2: BaseDirect)
case rightdown(combineElement1: BaseDirect, combineElement2: BaseDirect)
case rightleft(combineElement1: BaseDirect, combineElement2: BaseDirect)
case rightright(combineElement1: BaseDirect, combineElement2: BaseDirect)
case aa(combineElement1: BaseDirect, combineElement2: BaseDirect)
}
print(MemoryLayout<CombineDirect>.size)
print(MemoryLayout<CombineDirect>.stride)
<!--打印结果-->
3
3
这个例子并没有嵌套,其实与嵌套没有任何关系,嵌套的枚举也是单独存储的,只不过嵌套的枚举作用域只在嵌套的大括号内。
另外,如果枚举值过多的时候,我们看sil
代码:
此时我们可以发现:
- 多了一个
hashValue
的计算属性 - 一个遵守了
Equatable
协议的__derived_enum_equals
imp - 以及一个
hash
函数
我猜想,对于过多case
的枚举,swift
为了更好更快的匹配,使用了苹果惯用的哈希。我尝试在源码中搜索了一下derived_enum_equals
并没有找到相关方法,貌似是过期被移除了,后面使用==
代替。
3.4 结构体嵌套枚举的大小分析
首先还是看一下打印结果:
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 .down,.up:
print("up, down")
}
}
}
print(MemoryLayout<Skill>.size)
print(MemoryLayout<Skill>.stride)
<!--打印结果-->
1
1
如果只是嵌套了枚举呢?
struct Skill{
enum KeyType{
case up
case down
case left
case right
}
}
print(MemoryLayout<Skill>.size)
print(MemoryLayout<Skill>.stride)
<!--打印结果-->
0
1
如果添加了其他属性,则打印结果与添加的属性类型有关系,这里就不一一验证了。
总的来说,结构体中嵌套枚举与枚举嵌套枚举是一样的,他们都不存储枚举,只是作用域在其中而已。
4. 与OC混编
综上,我们可以看到Swift
中的枚举非常强大,而在OC
中枚举仅仅只是一个整数值,那么在与OC
混编的时候,该如何在OC
中使用Swift
的枚举呢?下面我们就来探索一下:
4.1 OC调用Swift中的枚举
如果想要在OC
中使用Swift
枚举要求会很严格:
- 使用
@objc
标记 - 类型必须是
Int
,也就是Swift
的rawValue
- 必须导入
import Foundation
也就是这样:
import Foundation
@objc enum Week: Int{
case MON, TUE, WED, THU, FRI, SAT, SUN
}
此时编译后就可以正在project-Swift.h
中看到转换后的对应的OC
枚举:
调用的话就是:
4.2 Swift调用OC中的枚举
4.2.1 NS_ENUM
NS_ENUM(NSInteger, OCENUM){
Value1,
Value2
};
如果使用NS_ENUM
创建的枚举会自动转换成swift
中的enum
可以在ocfileName.h
中查看转换后的枚举:
使用的话需要在桥接文件中导入OC
头文件,然后在swift
中使用:
let value = OCENUM.Value1
4.2.2 使用typedef enum
typedef enum {
Enum1,
Enum2,
Enum3
}OCENum;
如果使用typedef enum
这种形式的枚举,会转换成结构体,同样可以在ocfileName.h
中查看转换后的结果,转换后的代码如下:
可以看到里面有一个rawValue
属性,以及init
方法。还遵守了Equatable, RawRepresentable
两个协议。
使用的话也是需要导入头文件:
let num = OCEnum(0)
let num1 = OCEnum.init(0)
let num2 = OCEnum.init(rawValue: 3)
print(num)
<!--打印结果-->
OCNum(rawValue: 0)
这里我们只能通过init
方法去初始化,不能访问枚举中的变量。
4.2.3 使用typedef NS_ENUM
typedef NS_ENUM(NSInteger, OCENUM){
OCEnumInvalid = 0,
OCEnumA = 1,
OCEnumB,
OCEnumC
};
使用typedef NS_ENUM
也会自动转换为Swift
的枚举,转换后的代码如下:
使用也是需要导入头文件:
let ocenum = OCENUM.OCEnumInvalid
let ocenumRawValue = OCENUM.OCEnumA.rawValue
4.3 混编时需要使用String类型的枚举
这里的意思是,Swift
中需要使用String
类型的枚举,但是又要与OC
混编,暴露给OC
使用。
如果直接声明为String
类型编译时不会通过的,这里只能弄个假的。Swift
中的枚举还是声明为Int
,可以在枚举中声明一个变量或者方法,用于返回想要的字符串。
@objc enum Week: Int{
case MON, TUE, WED
var value: String?{
switch self {
case .MON:
return "MON"
case .TUE:
return "TUE"
case .WED:
return "WED"
default:
return nil
}
}
func weekName() -> String? {
switch self {
case .MON: return "MON"
case .TUE: return "TUE"
case .WED: return "WED"
default:
return nil
}
}
}
用法:
<!--OC用法-->
Week mon = WeekMON;
<!--Swift用法-->
let value = Week.MON.value
let value1 = Week.MON.weekName()
5. 总结
其实感觉该篇不太适合总结,因为直接给结论会使人不是很好理解,但是还是记录一下吧。
-
Swift
中的枚举很强大 -
enum
中的rawValue
是其中的计算属性 - 如果声明的时候不指定枚举类型就没有
rawValue
属性(包括关联值) -
rawValue
中的值存储在Mach-O
中,不占用枚举的存储空间 - 枚举值与
rawValue
不是同一个东西 -
rawValue
可以不写,如果是Int
默认0,1,2...String
等于枚举名称的字符串 - 如果枚举中存在
rawValue
同时也会存在init(rawValue:)
方法,用于通过rawValue
值初始化枚举 - 如果枚举遵守了
CaseIterable
协议,且不是关联值的枚举,我们可以通过enum.allCases
获取到所有的枚举,然后通过for
循环遍历 - 我们可以使用
switch
对枚举进行模式匹配,如果只关系一个枚举还可以使用if case
- 关联值枚举可以表示复杂的枚举结构
- 关联值的枚举没有init方法,没有
RawValue
别名,没有rawValue
计算属性 -
enum
可以嵌套enum
,被嵌套的作用域只在嵌套内部 - 结构体也可以嵌套
enum
,此时enum
的作用域也只在结构体内 -
enum
中可以包含计算属性
,类型属性
但不能包含存储属性
-
enum
中可以定义实例方法和使用static
修饰的方法,不能定义class
修饰的方法 - 如果想使用复杂结构的枚举,或者说是具有递归结构的枚举可以使用
indirect
关键字
-
关于枚举的大小
- 默认情况下枚举占用1字节也就是
UInt8
,如果不够用也就是超过256个的时候会使用UInt16,UInt32...
(太多没有去验证,如果真的有这么多枚举,建议通过其他方式去优化) - 如果枚举个数过多会使用哈希来进行优化,以便快速匹配
- 使用关联值的枚举大小取决于关联值的大小,还要考虑内存对齐
- 关联值的枚举中的关联值如果是普通枚举类型,系统会通过借位优化的方式节省内存的占用
- 如果借位不够了,会单独开辟内存存储关联值枚举的枚举值
- 在嵌套的时候,无论结构体还是枚举中都是不占用内存的,被嵌套的枚举是单独存储的,只是作用域在其内部而已
- 默认情况下枚举占用1字节也就是
-
关于和
OC
混编- OC中只能使用
Swift
中Int
类型的枚举 - 需要使用
@objc
关键字进行修饰 - 还要
import Foundation
- 在
swift
中使用OC中的NS_ENUM
的枚举就跟普通枚举一致 - 如果使用
OC
中typedef enum
枚举则需要通过init
方法进行初始化
- OC中只能使用