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

枚举的基本你用法

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)

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

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变量github

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

若是let/var关键字写在关联值括号内,就比较灵活spring

case .digit(let year, var month, let day)
复制代码

另一些枚举举例swift

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)

枚举成员能够只用相同类型的默认值预先关联,这个默认值叫作 原始值markdown

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里面使用的sizeofide

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)首先来看下一种简单的状况~~~~~~~oop

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 在继续走到断点二、断点3处,对比一下各自的内存状况以下 image 在这里插入图片描述ui

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

  • 系统为TestEnum类型的变量分配1个字节的内存空间
  • test1 、 test二、 test3 三个case对应在内存中用整数0、一、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的实现原理(待续...)

相关文章
相关标签/搜索