第七节课:枚举 & Optional
C语言枚举的写法回顾
enum 枚举名{
枚举值1,
枚举值2,
...
};
比如我们要展示一周7天,这个时候C语言的写法是这样的
enum week
{
MON,TUE,WED,THU,FRI,SAT,SUN
}
第一个枚举成员的默认值为整型0
,后面以此类推。如果我们想改,怎么办?
enum week
{
MON = 1,TUE,WED,THU,FRI,SAT,SUN
}
定义一个枚举变量
enum week
{
MON = 1,TUE,WED,THU,FRI,SAT,SUN
}week;
也可以这样写
enum
{
MON = 1,TUE,WED,THU,FRI,SAT,SUN
}week;
Swift中枚举写法类比
接下来回到Swift枚举中,还是写一个一周的枚举
enum week
{
case MON
case TUE
case WED
case THU
case FRI
case SAT
case SUN
}
上述代码也可以直接一个case
,然后用,
隔开
{
case MON,TUE,WED,THU,FRI,SAT,SUN
}
上述代码中我们的枚举值默认也是整形,与C语言一直。如果我们要表达String怎么办?
{
case MON = "MON"
case TUE = "TUE"
case WED = "WED"
case THU = "THU"
case FRI = "FRI"
case SAT = "SAT"
case SUN = "SUN "
}
=
号右边边的值在Swift中我们把他叫做RawValue
,如果我们不想写后面的字符串,这个时候我们就可以使用 "隐士RawValue分配"
mon, tue, wed, thu, fri = 10, sat, sun
}
tips:如果去掉Int类型,则不能访问.rawValue
这里不仅对Int类型使用,对String类型同样使用
case MON
case TUE
case WED
case THU
case FRI
case SAT
case SUN
}
print(week.MON.rawValue)
输出 : MON
这是为什么呢?我们添加一下代码let w = week.MON.rawValue
再查看SIL文件。
我们看到enum
里面的代码增加了
1.取了一个别名,把string
就交了RawValue
2.可选的初始化方法,允许返回nil
3.计算属性rawValue
,get
方法
由此我们可以看出,访问rawValue
实际就是访问枚举的get
方法
get
方法接收传进来一个枚举值,通过switch_enum
来匹配枚举值。匹配之后对应跳转到分支,我们以Mon
为例子
可以看到先以
MON
构建了字符串,然后将MON
作为参数传递给了bb8
,bb8
直接将传进来的String
参数返回。
这个就是我们访问
String
类型的rawVaule
本质
小拓展:构建的字符串存在哪里?
查看烂苹果,发现对应连续内存地址存储的字符串
上面
bb1
里构建字符串的过程本质上就是从对应地址的Value
取出来
case
与rawValue
虽然输出都是MON
,但是不是一个东西
枚举init详解
我们添加了一个符号断点,但是发现之前写的代码都断不住
只有我使用了week.init(rawValue: "MON")
,才可以断住。证明enum
中init
方法的调用是通过枚举.init(rawValue:)
或者枚举(rawValue:)
触发的
下面我们再进入SIL文件来看下初始化原理
可以看到bb0
是先接收一个字符串,function
创建了一个数组,在%6
返回了一个元组。
%7
元组存放第一个是元素的值,第二个存放的当前指针。
%9
访问当前首地址 ,就是"MON"
接下来就是跳转
bb2
中,首先构建了一个StaticString
%19
数组中index等于1的地址返回给%9
,所以%19
存储的就是当前地址
其实就是把取出来的字符串,依次放入数组里面,那么放好了之后呢?就是去匹配了
[图片上传失败...(image-91542a-1617203574015)]
掏出我们的Swift源码搜索_findStringSwitchCase(cases:string:)
public // COMPILER_INTRINSIC
// 接收一个数组 + 需要匹配的string
func _findStringSwitchCase(
cases: [StaticString],
string: String) -> Int {
// 遍历之前创建的字符串数组,如果匹配则返回对应的index
for (idx, s) in cases.enumerated() {
if String(_builtinStringLiteral: s.utf8Start._rawValue,
utf8CodeUnitCount: s._utf8CodeUnitCount,
isASCII: s.isASCII._value) == string {
return idx
}
}
// 如果不匹配,则返回-1
return -1
}
bb14
进行匹配
如果匹配到了,进入bb15
,最后跳到bb29
,构建一个.some类型的Optional
,表示有值,返回我们当前的enum
如果没有匹配到,进入bb16
继续匹配,如果一直没匹配到则跳到bb28
,构建一个.none类型的Optional
,表示nil
通过上面分析SIL,我们也就能理解这段代码为什么会输出如下内容了
总结:初始化的时候,把我们所有的字符串从Mach-O
文件中取出取出来,依次放到我们的数组里。放完后,然后调用_findStringSwitchCase
方法进行匹配,匹配成功返回枚举值,没有匹配到返回.
关联值
正常枚举值只能有一个值,如果我们想用枚举表达更复杂的信息,而不仅仅是一个RawValue
这么简单,这个时候我们就可以使用Associated Value
举个例子:我想表示一个形状
case circle(radious: Double)
case rectangle(width: Int,height: Int)
}
注意:具有关联值的枚举,就没有rawValue
属性了,主要是因为一个case
可以用一个或者多个值来表示,而rawValue
只有单个的值
SIL
文件里面也能看出来,没有了RawValue
,没有了init
,没有了get
方法
虽然也可以写成
case circle(Double)
case rectangle(Int,Int)
}
但是可读性太差,别人看不懂,自己时间长了也记不住,所以不推荐
关联值的赋值方式也很简单
circle = Shape.circle(radious: 20.0)
枚举的其他用法
模式匹配
case MON
case TUE
case WED
case THU
case FRI
case SAT
case SUN
}
let currentDay:week
var currentDay:week
switch currentDay{
case .MON: print(week.mon.rawvalue)
default:print("unkown day")
}
注意:
1.常量声明枚举会报错,因为没有找到初始化的步骤,变量就不报错。
2.通过Switch必须列举出所有情况,否则编译器报错
switch circle{
case let .circle(radious):
print("\(radious)")
case let .rectangle(width,height):
print("\(width)")
}
也可以这么写,将关联值的参数使用let、var修饰
case .circle(let radious):
print("\(radious)")
case .rectangle(let width, var height):
height += 1
print("\(height)")
}
查看SIL代码
有了之前的经验,我们更好分析了。
- 首先构建一个关联值的元组
- 根据当前
case枚举值
,匹配对应的case
,并跳转 - 取出元组中的值,将其赋值给匹配
case
中的参数
通过if case匹配单个case,如下所示
print("\(radious)")
}
如果我们只关心不同case
的相同关联值(即关心不同case
的某一个值),需要使用同一个参数,例如案例中的x
.如果我们把let .rectangle(10, width2:x):
改成let .square(10, width2:y):
会怎么样呢?答案是报错
因为对编译器产生了困惑,每次匹配case
只能进来一个代码分支,如果X X
两个位置名字相等,就声明一个代码常量,但是换成了Y
,代码的分支有可能不进来,所以Y
赋值不了,所以两个位置的未知变量名称要一致
case circle(radious: Double)
case rectangle(width: Int,height: Int)
case square(width1: Int,width2: Int)
}
var circle = Shape.rectangle(width: 10, height: 20)
var squar = Shape.rectangle(width1: 10, width2: 10)
switch circle{
case let .rectangle(10, x), let .square(10, width2:x):
print("\(x)")
default:
print ("nil")
}
<-- 输出结果 -->
20
switch squar{
case let .rectangle(10, x), let .square(10, width2:x):
print("\(x)")
default:
print ("nil")
}
<-- 输出结果 -->
10
也可以使用通配符_(表示匹配一切)的方式
case circle(radius: Double)
case rectangle(width: Double, height: Double)
case square(width: Double, height: Double)
}
let shape = Shape.rectangle(width: 10, height:20)
switch shape{
case let .rectangle(_, x), let .square(_, x):
print("x = \(x)")
default:
break
}
枚举的嵌套
枚举嵌套枚举
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(baseDIrect1:CombineDirect.BaseDirect.left,baseDirect2: CombineDirect.BaseDirect.up)
结构体嵌套枚举
enum KeyType{
case up
case down
case left
case right
}
let key: KeyType
func launchSkill(){
switch key {
case .left, .right:
print("left, right")
case .up, .down:
print("up, down")
}
}
}
枚举中包含属性
enum
中只能包含计算属性、类型属性,不能包含存储属性
case circle(radius: Double)
case rectangle(width: Double, height: Double)
//编译器报错:Enums must not contain stored properties 不能包含存储属性,因为enum本身是值类型
// var radius: Double
//计算属性 - 本质是方法(get、set方法)
var with: Double{
get{
return 10.0
}
}
//类型属性 - 是一个全局变量
static let height = 20.0
}
枚举中包含方法
可以在enum
中定义实例方法、static
修饰的方法
case MON, TUE, WED, THU, FRI, SAT, SUN
mutating func nextDay(){
if self == .SUN{
self = Weak(rawValue: 0)!
}else{
self = Weak(rawValue: self.rawValue+1)!
}
}
}
<-- 使用 -->
var w = Weak.MON
w.nextDay()
print(w)
枚举的大小
普通枚举大小
enum NoMean{
case a
}
print(MemoryLayout.stride) // 对齐之后的大小(内存空间中)
print(MemoryLayout.size) //实际大小
<-- 输出结果 -->
1
0
enum NoMean{
case a
case b
case c
}
print(MemoryLayout.stride) // 对齐之后的大小(内存空间中)
print(MemoryLayout.size) //实际大小
<-- 输出结果 -->
1
1
结论:
- RawValue枚举的大小:枚举值
- 所以枚举中默认是以UInt8存储的,最大可以存储0~255,如果不够则会自动转换为UInt16,以此类推。
- 当只有一个case的时候,size是0,表示这个枚举是没有意义的
- 枚举中后面声明的类型只的是rawValue的类型,不会影响枚举的大小。这些rawValue的值会存储在Mach-O文件中,在使用的时候取查找,这个在上面提到过,与枚举大小没有关系
关联枚举大小
enum Shape{
case circle(radious: Double)
case rectangle(width: Double) // 8 + 1(case)
}
print(MemoryLayout.stride) // 对齐之后的大小(内存空间中)
print(MemoryLayout.size) //实际大小
<-- 打印结果 -->
16
9
enum Shape{
case circle(radious: Double)
case rectangle(width: Double, height:Double) // 16 + 1(case)
}
print(MemoryLayout.stride) // 对齐之后的大小(内存空间中)
print(MemoryLayout.size) //实际大小
<-- 打印结果 -->
24
17
结论:
- 关联值枚举的大小,取决于最大case的内存大小
- enum有关联值时,关联值的大小 取 对应枚举关联值 最大的,例如circle中关联值大小是8,而rectangle中关联值大小是16,所以取16。所以enum的size = 最大关联值大小 + case(枚举值)大小 = 16 + 1 = 17,而stride由于8字节对齐,所以自动补齐到24
嵌套枚举大小
enum BaseDirect{
case up, down, left, right
}
case leftUp(baseDirect1: BaseDirect, baseDirect2: BaseDirect)
case rightUp(baseDirect1: BaseDirect, baseDirect2: BaseDirect)
case leftDown(baseDirect1: BaseDirect, baseDirect2: BaseDirect)
case rightDown(baseDirect1: BaseDirect, baseDirect2: BaseDirect)
}
print(MemoryLayout.size)
print(MemoryLayout.stride)
<-- 打印结果 -->
2 //size大小,enum有关联值取决于关联值的大小,每个case都有2个大小为1的enum,所以为2
2 //stride大小
结论:
从结果中说明enum嵌套enum同具有关联值的enum是一样的,同样取决于关联值的大小,其内存大小是最大关联值的大小
结构体嵌套enum的大小
enum KeyType{
case up
case down
case left
case right
}
let key: KeyType
func launchSkill(){
switch key {
case .left, .right:
print("left, right")
case .up, .down:
print("up, down")
}
}
}
print(MemoryLayout.size)
print(MemoryLayout.stride)
<-- 打印结果 -->
1
1
struct Skill {
enum KeyType{
case up
case down
case left
case right
}
}
print(MemoryLayout.size)
print(MemoryLayout.stride)
<-- 打印结果 -->
0 //size的大小取决于成员变量,但是struct中目前没有属性
1
struct Skill {
enum KeyType{
case up
case down
case left
case right
}
var width: Int //8字节
let key: KeyType //1字节
var height: UInt8 //1字节
func launchSkill(){
switch key {
case .left, .right:
print("left, right")
case .up, .down:
print("up, down")
}
}
}
print(MemoryLayout.size)
print(MemoryLayout.stride)
<-- 打印结果 -->
10 //size大小(与OC中的结构体大小计算是一致的,min(m,n),其中m表示存储的位置,n表示属性的大小,要求是:m必须整除n)
16 //stride大小
结论:
1、如果结构体中没有其他属性,只有枚举变量,那么结构体的大小就是枚举的大小,即size为1
2、如果结构体中嵌套了enum,但是没有声明变量,此时的size是0,stride是1
3、如果结构体中还有其他属性,则按照OC中的结构体内存对齐三原则进行分析
补充点:字节对齐&内存对齐
内存对齐:iOS中是8字节对齐,苹果实际分配采用16字节对齐,这种只会在分配对象时出现
字节对齐:存储属性的位置必须是偶地址,即OC内存对齐中的min(m,n),其中m表示存储的位置,n表示属性的大小,需要满足位置m整除n时,才能从该位置存放属性。简单来说,就是必须在自身的倍数位置开始
外部调用对象时,对象是服从内存对齐。
单纯从结构上说,结构内部服从最大字节对齐。
举个例子
var age: Int //8字节
var height: UInt8 //1字节
var width: UInt16 //2字节
}
print(MemoryLayout.size)
print(MemoryLayout.stride)
<-- 打印结果 -->
12
16
size为12的原因
:内存从0位置开始Int是占据0-7,UInt8占据8,下一个位置是9,但是UInt16是2字节对齐的要在它的倍数位置开始所以找下一个可以整除它的位置也就是UInt16占据10-11(就相当于系统自动把UInt8拉伸成2字节8~9,系统优化),正好整个size在0-11,所以size为12
如果先写UInt16再写UInt8,则size为11
stride为16的原因
:stride是实际分配的,必须是最大属性大小的整数倍,即8的倍数,所以是16
总结
枚举说明:
1、enum中使用rawValue
的本质是调用get
方法,即在get
方法中从Mach-O
对应地址中取出字符串并返回
的操作
2、enum中init
方法的调用是通过枚举.init(rawValue:)
或者枚举(rawValue:)
触发的
3、case枚举值和rawValue原始值的关系:case 枚举值 = rawValue原始值
4、具有关联值的枚举
,可以称为三无enum,因为没有别名RawValue、init、计算属性rawValue
5、enum的模式匹配方式
,主要有两种:switch / if case
6、enum可以嵌套enum,也可以在结构体中嵌套enum,表示该enum是struct私有的
7、enum中还可以包含计算属性、类型属性,但是不能包含存储属性
8、enum中可以定义实例 + static修饰
的方法
枚举内存大小结论:
1、普通enum
的内存大小一般是1字节
,如果只有一个case,则为0
,表示没有意义,如果case个数超过255,则枚举值的类型由UInt8->UInt16->UInt32
...
2、具有关联值的enum
大小,取决于最大case的内存大小+case的大小(1字节)
3、enum嵌套enum
同样取决于最大case的关联值大小
4、结构体嵌套enum
,如果没有属性,则size为0
,如果只有enum属性,size为1
,如果还有其他属性,则按照OC中内存对齐原则进行计算