Swift底层原理探索2----枚举

686 阅读15分钟

枚举的基本用法

enum Direction_1 {
    case north, south, east, west
}

enum Direction {
    case north
    case south
    case east
    case west
}
var dir = Direction.west
dir = Direction.east
dir = .north
print(dir)

switch dir {
case .north:
    print("north")
case .south:
    print("south")
case .east:
    print("east")
case .west:
    print("west")
}

关联值(Associated Values)

关联值是直接存在枚举变量的内存里面的,这点要牢记,对于一个有固定取值范围的变量,设计成枚举比较合适

enum Score {
    case points(Int)
    case grade(Character)
}
var score = Score.points(96)
score = .grade("A")

switch score {
case let .points(i):
    print(i, "points")
case let .grade(i):
    print("grade", i)
} // grade A

enum Date {
    case digit(year: Int, month: Int, day: Int)
    case string(String)
}
var date = Date.digit(year: 2020, month: 02, day: 29)
date = .string("2020-02-29")
switch date {
case let .digit(year, month, day):  
    print(year, month, day)
case let .string(dateStr):
    print(dateStr)
    
} // "2020-02-29"

注意上看switch内部对let/var关键字的使用,如果下载枚举值左边,那么关联值只能统一绑定给let常量或者var变量

case let .digit(year, month, day): //year、month、day都是let常量
case var .digit(year, month, day): //year、month、day都是var变量

如果let/var关键字写在关联值括号内,就比较灵活

case .digit(let year, var month, let day)

另外一些枚举举例

enum Password {
    case number(Int, Int, Int, Int)
    case gesture(String)
}
var pwd = Password.number(3, 5, 7, 9)
pwd = .gesture("3259")
switch pwd {
case let .number(n1 , n2 , n3 , n4 ): //数字密码
    print("number is", n1, n2, n3, n4)
case let .gesture(pwdStr):// 字符串密码
    print("gestrue is", pwdStr)
}

原始值(Raw Values)

枚举成员可以只用相同类型的默认值预先关联,这个默认值叫做 原始值

enum PokerSuit: Character { //这里的Character表示的是枚举值所关联的原始值
    case spade = "️"
    case heart = "️"
    case diamond = "️"
    case club = "️"
}
var suit = PokerSuit.spade
print(suit)
print(suit.rawValue)
print(PokerSuit.club.rawValue)

enum Grade: String {
    case perfect = "A"
    case great = "B"
    case good = "C"
    case bad = "D"
}
print(Grade.perfect.rawValue) // A
print(Grade.great.rawValue) // B
print(Grade.good.rawValue) // C
print(Grade.bad.rawValue) // D


隐式原始值(Implicitly Assigned Raw Values)

enum Direction1: String {
    case north, south, east, west
}
print(Direction1.north.rawValue)

enum Direction2: String {
    case north = "nor", south, east, west
}
print(Direction2.north.rawValue)//有赋值,就用赋值的字符串
print(Direction2.south.rawValue)//没赋值, 就用case名字符串

enum Season: Int {
    case spring, summer, autumn, winter
}
print(Season.spring.rawValue)//0
print(Season.summer.rawValue)//1
print(Season.autumn.rawValue)//2
print(Season.winter.rawValue)//3

enum Season2: Int {
    case spring = 2, summer, autumn = 6, winter
}
print(Season2.spring.rawValue) //2
print(Season2.summer.rawValue) //3
print(Season2.autumn.rawValue) //6
print(Season2.winter.rawValue) //7

递归枚举(Recursive Enumeration)

//书写方法一
indirect enum ArithExpr_1 {
    case number(Int)
    case sum(ArithExpr, ArithExpr)
    case difference(ArithExpr, ArithExpr)
}

//书写方法二
enum ArithExpr {
    case number(Int)
    indirect case sum(ArithExpr, ArithExpr)
    indirect case difference(ArithExpr, ArithExpr)
}

let five = ArithExpr.number(5)
let four = ArithExpr.number(4)
let two = ArithExpr.number(2)
let sum = ArithExpr.sum(five, four)
let difference = ArithExpr.difference(sum, two)


func calculate(_ expr: ArithExpr) -> Int{
    switch expr {
    case let .number(value):
        return value
    case let .sum(left, right):
        return calculate(left) + calculate(right)
    case let .difference(left, right):
        return calculate(left) - calculate(right)
    }
}

MemoryLayout

我们可以使用 MemoryLayout 来获取数据类型占用的内存大小,相当于C里面使用的sizeof

enum Password2 {
    case number(Int, Int, Int, Int)
    case other
}
MemoryLayout<Password2>.stride //系统分配给变量的内存大小--40
MemoryLayout<Password2>.size //实际被使用的内存大小--33
MemoryLayout<Password2>.alignment //对其参数--8

var pd = Password2.number(9, 8, 7, 6)
pd = .other
print(pd) //"other/n"
MemoryLayout.stride(ofValue: pd)  //40
MemoryLayout.size(ofValue: pd)  //33
MemoryLayout.alignment(ofValue: pd)  //8

枚举在内存中是如何存储的?

通过MemoryLayout,我们只能简单查看一些内存相关的信息,但还不足以看清枚举在内存中的具体细节,由于Xcode调试工具无法为我们提供枚举变量的内存地址,因此需要借助一些额外的工具,这里推介一下大牛李明杰的一个工具

(1)首先来看下一种简单的情况~~~~~~~

enum TestEnum {
    case test1, test2, test3
}
print("系统实际分配内存",MemoryLayout<TestEnum>.stride)
print("实际使用的内存",MemoryLayout<TestEnum>.size)
print("内存对齐参数",MemoryLayout<TestEnum>.alignment)

var t = TestEnum.test1
print("枚举变量t的内存地址:",Mems.ptr(ofVal: &t)) //这里可以输出变量t的内存地址
t = .test2
t = .test3
print("Stop for debug")

Mems.ptr(ofVal: &t)可以帮我们获得变量t的内存地址,准备好3个断点 image 然后将程序运行值断点1处,此时我们已经获得t的内存地址,根据该地址,调出内存界面,我们来观察一下此时的内存细节 image 在继续走到断点2、断点3处,对比一下各自的内存情况如下 image 在这里插入图片描述

小结:enum TestEnum { case test1, test2, test3 }

  • 系统为TestEnum类型的变量分配1个字节的内存空间
  • test1 、 test2、 test3 三个case对应在内存中用整数0、1、2来表示

(2)把场景调整为有Int型原始值的情形如下~~~~~~~

enum TestEnum: Int {
    case test1
    case test2 = 3
    case test3
    case test4 = 10
    case test5
}
print("系统实际分配内存",MemoryLayout<TestEnum>.stride)
print("实际使用的内存",MemoryLayout<TestEnum>.size)
print("内存对齐参数",MemoryLayout<TestEnum>.alignment)

var t = TestEnum.test1
print("枚举变量t的内存地址:",Mems.ptr(ofVal: &t)) //这里可以输出变量t的内存地址
t = .test2
t = .test3
t = .test4
t = .test5
print("Stop for debug")

按照上面同样的方法,对比各自case的内存情况如下 image image image image image 我们在查看一下各自caserawValue

print("test1的rawValue:", TestEnum.test1.rawValue)
print("test2的rawValue:", TestEnum.test2.rawValue)
print("test2的rawValue:", TestEnum.test3.rawValue)
print("test2的rawValue:", TestEnum.test4.rawValue)
print("test2的rawValue:", TestEnum.test5.rawValue)

***********运行结果
test1的rawValue: 0
test2的rawValue: 3
test2的rawValue: 4
test2的rawValue: 10
test2的rawValue: 11

看得出,如果原始值类型为Int

  • 那么在不手动设定的情况下,首个case的原始值默为整数0,非首个case的默认值为上一个case的默认值+1
  • 如果手动设定了,那么原始值即为设定值。

(3)看过了带Int型原始值的情况之后,在看一下带String型原始值的情况,改造如下~~~~~~~

enum TestEnum: String {
    case test1
    case test2 = "AA"
    case test3 = "汉字"
    case test4 = "🦕"
    case test5
}
print("系统实际分配内存",MemoryLayout<TestEnum>.stride)
print("实际使用的内存",MemoryLayout<TestEnum>.size)
print("内存对齐参数",MemoryLayout<TestEnum>.alignment)

var t = TestEnum.test1
print("枚举变量t的内存地址:",Mems.ptr(ofVal: &t)) //这里可以输出变量t的内存地址
t = .test2
t = .test3
t = .test4
t = .test5
print("Stop for debug")


print("test1的rawValue:", TestEnum.test1.rawValue)
print("test2的rawValue:", TestEnum.test2.rawValue)
print("test2的rawValue:", TestEnum.test3.rawValue)
print("test2的rawValue:", TestEnum.test4.rawValue)
print("test2的rawValue:", TestEnum.test5.rawValue)

****************运行结果
系统实际分配内存 1
实际使用的内存 1
内存对齐参数 1
枚举变量t的内存地址: 0x0000000100008218
Stop for debug
test1的rawValue: test1
test2的rawValue: AA
test2的rawValue: 汉字
test2的rawValue: 🦕
test2的rawValue: test5
Program ended with exit code: 0

内存的情况这里省略,和上面Int型的时候是一样的,根据调试输出的情况,我们可以看出

  • 如果不设置原始值,那么case的原始值为该case名称的字符串
  • 如果设置了原始值,那吗case的原始值即为设定值

总结 带原始值的枚举

  • 枚举变量本身的就占一个字节
  • 枚举变量所对应的内存里所存放的具体值:对应第一个case为0,并且往后逐个+1

(4)带关联值的场景~~~~~~~

enum TestEnum {
    case test1(a: Int, b: Int, c: Int)
    case test2(d: Int, e: Int)
    case test3(f: Int)
    case test4(g: Bool)
    case test5
}
print("系统实际分配内存",MemoryLayout<TestEnum>.stride)
print("实际使用的内存",MemoryLayout<TestEnum>.size)
print("内存对齐参数",MemoryLayout<TestEnum>.alignment)

var t = TestEnum.test1(a: 1, b: 2, c: 3)
//这里可以输出变量t的内存地址
print("枚举变量t的内存地址:",Mems.ptr(ofVal: &t))
t = .test2(d: 4, e: 5)
t = .test3(f: 6)
t = .test4(g: true)
t = .test5
print("Stop for debug")

*****************运行结果
系统实际分配内存 32
实际使用的内存 25
内存对齐参数 8
枚举变量t的内存地址: 0x0000000100008208

接下来照例在过一遍内存,下面直接贴上内存查看的结果

  • t = test1(a: 1, b: 2, c: 3)
01 00 00 00 00 00 00 00  --> 对应a
02 00 00 00 00 00 00 00  --> 对应b
03 00 00 00 00 00 00 00  --> 对应c
00 00 00 00 00 00 00 00  --> 对应case test1
  • t = test2(d: 4, e: 5)
04 00 00 00 00 00 00 00  --> 对应d
05 00 00 00 00 00 00 00  --> 对应e
00 00 00 00 00 00 00 00  --> 此时没用到
01 00 00 00 00 00 00 00  --> 对应case test2
  • t = test3(f: 6)
06 00 00 00 00 00 00 00  --> 对应f
00 00 00 00 00 00 00 00  --> 此时没用到
00 00 00 00 00 00 00 00  --> 此时没用到
02 00 00 00 00 00 00 00  --> 对应case test3
  • t = test4(g: true)
01 00 00 00 00 00 00 00  --> 对应g
00 00 00 00 00 00 00 00  --> 此时没用到
00 00 00 00 00 00 00 00  --> 此时没用到
03 00 00 00 00 00 00 00  --> 对应case test4
  • t = test5
00 00 00 00 00 00 00 00  --> 此时没用到
00 00 00 00 00 00 00 00  --> 此时没用到
00 00 00 00 00 00 00 00  --> 此时没用到
04 00 00 00 00 00 00 00  --> 对应case test5

总结 带关联值的枚举

  • 枚举变量的成员case的值只用了其内存空间的1字节来存放
  • 枚举的case关联值也存放在枚举变量的内存中
  • 系统为枚举的case关联值所分配的内存空间,必须保证可以放下所需内存最大的那个关联值
  • 枚举变量的内存空间里,先存放存放的是case关联值成员case的值被放在最后
  • 枚举变量的内存总空间按内存对齐参数进行补齐(计算机常识)

(5)一些极端场景~~~~~~~

enum TestEnum {
    case test
}

print("系统实际分配内存",MemoryLayout<TestEnum>.stride)
print("实际使用的内存",MemoryLayout<TestEnum>.size)
print("内存对齐参数",MemoryLayout<TestEnum>.alignment)
var t = TestEnum.test
print(print("枚举变量t的内存地址:",Mems.ptr(ofVal: &t)))

****************运行结果
系统实际分配内存 1
实际使用的内存 0
内存对齐参数 1
枚举变量t的内存地址: 0x0000000000000001
Program ended with exit code: 0

可以看到,系统确实是分配了1个字节给枚举,但是实际上用到了0个,因为一种情况不需要做任何区分,所以也就不需要存储,当然貌似没人会这么用,所以系统针对这种情况下的处理,就不难理解了。在看看带关联值的情况:

enum TestEnum {
    case test(Int)
}

print("系统实际分配内存",MemoryLayout<TestEnum>.stride)
print("实际使用的内存",MemoryLayout<TestEnum>.size)
print("内存对齐参数",MemoryLayout<TestEnum>.alignment)
var t = TestEnum.test(10)
print("枚举变量t的内存地址:",Mems.ptr(ofVal: &t))
print("Stop for debug")

***************运行结果
系统实际分配内存 8
实际使用的内存 8
内存对齐参数 8
枚举变量t的内存地址: 0x0000000100007200
Stop for debug
Program ended with exit code: 0

***************汇编结果
0A 00 00 00 00 00 00 00 

可以看到系统直接分配了8个字节来存储枚举里面的Int型关联值,没有分配空间来存储成员case的值,原因和上面很想,因为现在就是一种case,没有必要再存储成员变量的值,只需要关心case关联值就好。那如果有一个以上的case,是不是就会给成员case分配空间了?咱们试试看,如下

enum TestEnum {
    case other
    case test(Int)
}

print("系统实际分配内存",MemoryLayout<TestEnum>.stride)
print("实际使用的内存",MemoryLayout<TestEnum>.size)
print("内存对齐参数",MemoryLayout<TestEnum>.alignment)

var t = TestEnum.other
//Mem.memStr是大神李明杰提供的工具,文中有链接,可以帮我直接获取变量的内存里面的值
print("枚举变量t = other 时的内存情况:  ",Mems.memStr(ofVal: &t)) 
t = TestEnum.test(10)
print("枚举变量t = test(10)时的内存地址:",Mems.memStr(ofVal: &t))
print("Stop for debug")

***************运行结果
系统实际分配内存 16
实际使用的内存 9
内存对齐参数 8
枚举变量t = other 时的内存情况:   0x0000000000000000 0x0000000000000001
枚举变量t = test(10)时的内存地址: 0x000000000000000a 0x0000000000000000

可以看出,只要case大于1个,除了Int型关联值需要占用8个字节外,枚举变量还使用了1个字节来存储成员case的值,根据内存对齐参数8,系统给枚举变量分配了16字节空间。上面的结果中,最后一个字节是用来存放成员case的值也就是case other 对应了01case test 对应了00,但是感觉顺序不太对,明明是other在前,test在后的,带着这个疑问,我们把用例改造如下

enum TestEnum {
    case aaa
    case test(Int)
    case ccc
    case test3(Int, Int)
    case test2(Int,Int, Int)
    case other
}
print("系统实际分配内存",MemoryLayout<TestEnum>.stride)
print("实际使用的内存",MemoryLayout<TestEnum>.size)
print("内存对齐参数",MemoryLayout<TestEnum>.alignment)

var t = TestEnum.aaa
print("t = .aaa的内存情况:              ",Mems.memStr(ofVal: &t))
t = TestEnum.test(10)
print("t = .test(10)的内存情况:         ",Mems.memStr(ofVal: &t))
t = TestEnum.ccc
print("t = .ccc的内存情况:              ",Mems.memStr(ofVal: &t))
t = TestEnum.test3(16, 32)
print("t = .test3(16, 32)的内存情况:    ",Mems.memStr(ofVal: &t))
t = TestEnum.test2(20, 20, 20)
print("t = .test2(20, 20, 20)的内存情况:",Mems.memStr(ofVal: &t))
t = TestEnum.other
print("t = .other的内存情况:            ",Mems.memStr(ofVal: &t))
print("Stop for debug")



*************************************运行结果
系统实际分配内存 32
实际使用的内存 25
内存对齐参数 8
t = .aaa的内存情况:               0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000003
t = .test(10)的内存情况:          0x000000000000000a 0x0000000000000000 0x0000000000000000 0x0000000000000000
t = .ccc的内存情况:               0x0000000000000001 0x0000000000000000 0x0000000000000000 0x0000000000000003
t = .test3(16, 32)的内存情况:     0x0000000000000010 0x0000000000000020 0x0000000000000000 0x0000000000000001
t = .test2(20, 20, 20)的内存情况: 0x0000000000000014 0x0000000000000014 0x0000000000000014 0x0000000000000002
t = .other的内存情况:             0x0000000000000002 0x0000000000000000 0x0000000000000000 0x0000000000000003
Stop for debug
Program ended with exit code: 0

从上面的调试,又挖掘了一点小细节: image

  • 对于有关联值的成员case,它的case值会根据定义的顺序,默认从0开始+1累加,
  • 其余所有不带关联值的成员case,它们的case值相同,而且都等于最后一个可关联成员case 的值+1

关联值 VS 原始值rawValue

以上我们看清楚了简单枚举、关联值枚举、原始值枚举在内存中分别是如何存储的,可以看出,枚举的关联值和原始值又以下区别:

  • 内存角度:关联值是直接存储在枚举变量内存里面的,而原始值则不是,因为原始值是通过xx.rawValue访问的,因此它的值完全不需要存储,可以在枚举定义完之后通过方法提供给外部。
  • 使用角度:原始值必须在枚举定义的时候确定原始值类型,才能被使用 enum Direction : String/Int/... {...}。关联值则必须在枚举定义的时候,确定好case所对应的关联值类型
  • 赋值:关联值只能在枚举case被赋值给变量的时候进行赋值,因为同一个case每次被赋值给变量,都需要设定一个关联值,因此也可以说关联值是可以改变的,如下
enum Score {
    case points(Int)
    case grade(Character)
}
var score = Score.points(96)
score = .grade("A")
score = .grade("B") -->相同的case,不同的关联值

而原始值,只能在枚举定义的时候进行赋值,不赋值则系统会给定相应的默认值,也就是只有一次机会可以赋值,定义完枚举之后,就没有办法可以更改原始值了,示例如下

enum Grade: String {
    case perfect = "A"
    case great
    case good = "C"
    case bad = "D"
}
print(Grade.perfect.rawValue) --> A
print(Grade.great.rawValue) --> 定义时无赋值,系统默认为case的名称 great
print(Grade.good.rawValue) --> C
print(Grade.bad.rawValue) -> D

switch的实现原理(待续...)