一、Enum 原始值
1.1 基本用法
Swift 中通过 enum 关键字来声明一个枚举:
enum SSLEnum {
case test_one
case test_two
case test_three
}
我们知道在 C 和 OC 中默认是接受整数类型,也就意味着下面的例子中:A,B,C 分别默认代表 0,1,2
typedef NS_ENUM(NSInteger, SSLEnum) {
A,
B,
C,
};
Swift 中的枚举则更加灵活,并且不需给枚举中的每一个成员都提供值。如果要为枚举成员提供值(所谓原始值),那么这个值可以是字符串、字符、任意的整数值,或者是浮点类型。
enum Color: String {
case red = "Red"
case amber = "Amber"
case green = "Green"
}
enum SSLEnum: Double {
case a = 10.0
case b = 20.0
case c = 30.0
case d = 40.0
}
隐式 RawValue 分配是建立在 Swift 的类型推断机制上的。
enum DayOfWeek: String {
case mon, tue, wed, thu, fri = "Hello World", sat, sun
}
print(DayOfWeek.rawValue)
print(DayOfWeek.fri.rawValue)
print(DayOfWeek.rawValue)
输出结果:
mon
Hello World
sat
我们可以看到 mon、sta 的枚举值和原始值是一样的,接下来看一看这是如何做到的。
1.2 sil 分析取值过程
添加如下代码:
enum DayOfWeek: String {
case mon, tue, wed, thu, fri = "Hello World", sat, sun
}
var x = DayOfWeek.mon.rawValue
swiftc -emit-sil main.swift > ./main.sil && open main.sil 生成 sil 文件:
enum DayOfWeek : String {
case mon, tue, wed, thu, fri, sat, sun // case 值
init?(rawValue: String) // 可失败初始化器
typealias RawValue = String // 取别名
var rawValue: String { get } // get 函数
}
由上可知获取 rawValue 的本质,就是调用这个计算属性的 get 方法,在 sil 文件中找到 get 方法:
这里的取值过程是先判断是不是 mon,如果是就调用 bb1 代码块,代码块中取的是 “mon”字符串常量,字符串常量存在哪儿里呢?
存储位置见下面:
1.3 枚举值和原始值
枚举值和原始值不能相互赋值:
二、关联值
2.1 基本用法
有的时候我们想用枚举值表达一个更复杂的东西,比如说形状:
enum Shape {
case circle(radios: Double) // 圆形
case rectangle(width: Double, height: Double) // 长方形
}
// 半径为 10 的圆形
var circle = Shape.circle(radios: 10)
2.2 模式匹配
switch 模式匹配基本用法:
enum Weak: String {
case MONDAY
case TUEDAY
case WEDDAY
case THUDAY
case FRIDAY
case SATDAY
case SUNDAY
}
let currentWeak: Weak = Weak.MONDAY
switch currentWeak {
case .MONDAY: print(Weak.MONDAY.rawValue)
case .TUEDAY: print(Weak.MONDAY.rawValue)
case .WEDDAY: print(Weak.WEDDAY.rawValue)
case .THUDAY: print(Weak.THUDAY.rawValue)
case .FRIDAY: print(Weak.FRIDAY.rawValue)
case .SATDAY: print(Weak.SATDAY.rawValue)
case .SUNDAY: print(Weak.SUNDAY.rawValue)
}
如果不想匹配所有的 case,使用 default 关键字:
enum Weak: String {
case MONDAY
case TUEDAY
case WEDDAY
case THUDAY
case FRIDAY
case SATDAY
case SUNDAY
}
let currentWeak: Weak = Weak.MONDAY
switch currentWeak {
case .SATDAY, .SUNDAY: print("Happy Day")
default : print("SAD Day")
}
如果我们要匹配关联值的话:
enum Shape{
case circle(radious: Double)
case rectangle(width: Int, height: Int)
}
let shape = Shape.circle(radious: 10.0)
switch shape{
case let .circle(radious):
print("Circle radious:(radious)")
case let .rectangle(width, height):
print("rectangle width:(width),height(height)")
}
还可以这么写:
enum Shape{
case circle(radious: Double)
case rectangle(width: Int, height: Int)
}
var shape = Shape.circle(radious: 10.0)
switch shape{
case .circle(let radious):
print("Circle radious:(radious)")
case .rectangle(let width, let height):
print("rectangle width:(width),height(height)")
}
三、枚举大小
3.1 No-payload enums
接下来我们来讨论一下枚举占用的内存大小,这里我们区分几种不同的情况,首先第一种就是 No-payload enums 也就是没有关联值的枚举。
enum Week {
case MONDAY
case TUEDAY
case WEDDAY
case THUDAY
case FRIDAY
case SATDAY
case SUNDAY
}
大家可以看到这种枚举类型类似我们在 C 语言中的枚举,当前类型默认是 Int 类型,那么对于这一类的枚举在内存中是如何布局?以及在内存中占用的大小是多少那?这里我们就可以直接使用 MemoryLayout 来测量一下当前枚举
- 可以看到这里我们测试出来的不管是 size 还是 stribe 都是 1
- 在 Swift 中进行枚举布局的时候一直是尝试使用最少的空间来存储 enum,1 字节能够表示 256 个case
- 也就是说一个默认枚举类型且没有关联值且 case 少于 256,当前枚举类型的大小都是 1 字节,枚举类型
UInt8
通过上面的打印我们可以直观的看到,当前变量 a,b,c 这三个变量存储的内容分别是 00,01,02 这和我们上面说的布局理解是一致的。
3.2 Single-payload enums
接下来我们再来看一下 Single-payload enums 的内存布局,字面意思就是只有一个负载的 enum, 比如下面这个例子
enum SSLEnum {
case test_one(Bool)
case test_two
case test_three
case test_four
}
enum SSLEnum2 {
case test_one(Int)
case test_two
case test_three
case test_four
}
print(MemoryLayout<SSLEnum>.size)
print(MemoryLayout<SSLEnum2>.size)
输出结果:
1
9
- 这里为什么都是单个负载,但是当前占用的大小却不一致呢!
- 注意,
Swift中的 enum 中的Single-payload enums会使用负载类型中的额外空间来记录没有负载的 case 值。 - 对于
Bool类型对负载,Bool类型是 1 字节,但其实它只需要 1 位就够存了,对于UInt8类型的枚举来说,还有 7 位的空间可以用来表示 128 个 case,所以 1 个字节就可以了。 - 对于
Int类型的负载来说,其实系统是没有办法推算当前的负载所要使用的位数,也就意味着当前Int类型的负载是没有额外的剩余空间的,这个时候我们就需要额外开辟内存空间来存储我们的 case 值,也就是 8 + 1 = 9 字节。
3.3 Mutil-payload enums
接下来我们说第三种情况 Mutil-payload enums,有多个负载的情况时,enum 是如何进行布局的。
看下面的例子:
enum SSLEnum {
case test_one(Bool)
case test_two(Bool)
case test_three
case test_four
}
enum SSLEnum2 {
case test_one(Int)
case test_two(Int)
case test_three(Int)
case test_four(Int)
}
enum SSLEnum3 {
case test_one(Bool)
case test_two(Int)
case test_three
case test_four
}
enum SSLEnum4 {
case test_one(Int, Int, Int)
case test_two
case test_three
case test_four
}
print(MemoryLayout<SSLEnum>.size)
print(MemoryLayout<SSLEnum2>.size)
print(MemoryLayout<SSLEnum3>.size)
print(MemoryLayout<SSLEnum4>.size)
输出结果:
1
9
9
25
- 通过输出结果我们可以了解到,有多个负载的枚举时,枚举的大小取决于当前最大关联值的大小
- 如果最大关联值需要的位数小于 8 位,并且剩下的空间仍然可以表示所有的 case,枚举的大小就是
1 - 如果最大关联值需要的位数大于 8 位,枚举的大小 =
最大关联值 + 1
3.4 特殊情况
最后这里还有一个特殊情况我们需要理解一下,我们来看下面的案例
enum SSLEnum {
case test_one
}
对于当前的 SSLEnum 只有一个 case,我们不需要用任何东⻄来去区分当前的 case,所以当我们打印当前的 SSLEnum 大小你会发现是 0。
四、indirect关键字
我们先看下面报错的代码:
为什么报错呢,因为枚举是值类型,编译时期大小就要确定完成,而这里的关联值也是枚举类型,这样的话就没法确定了。
在枚举前面加上 indirect 就不会报错了,indirect 的作用就是把 BinaryTree 分配在堆空间上
打印一下 BinaryTree 的大小:
print(MemoryLayout<BinaryTree<Int>>.size)
输出结果:8
可以看到示例大小是 8,是引用类型。
indirect 除了放在枚举前面,还可以放在 case 的前面
enum BinaryTree<T> {
case empty(Int)
indirect case node(left: BinaryTree, value: T, right: BinaryTree)
}
复制代码
这个时候就不是整个 enum 都是引用类型了,只有 node 是引用类型。
总结:
1.原始值不占用枚举变量的内存;
2.枚举使用1个字节来存储成员值;
3.枚举使用N个字节来存储关联值;