枚举
为一组相关的值定义了一个共同的类型
,使你可以在你的代码中以类型安全
的方式来使用这些值。
我们熟悉 的C 语言
,枚举会为一组整型值
分配相关联的名称。Swift
中的枚举更加灵活,不必给每一个枚举成员提供一个值。如果给枚举成员提供一个值(称为原始值
),则该值的类型可以是字符串
、字符
,或是一个整型值
或浮点数
。
此外,枚举成员可以指定任意类型的关联值
存储到枚举成员中,就像其他语言中的联合体(unions
)和变体(variants)
。你可以在一个枚举中定义一组相关的枚举
成员,每一个枚举成员都可以有适当类型的关联值
。
在 Swift
中,枚举类型是一等(first-class)类型
。它们采用了很多在传统上只被类(class)所支持的特性,例如计算属性(computed properties
),用于提供枚举值的附加信息;实例方法(instance methods)
,用于提供和枚举值相关联的功能;枚举也可以定义构造函数(initializers)
来提供一个初始值;可以在原始实现的基础上扩展
它们的功能;还可以遵循协议(protocols)
来提供标准的功能。
一、枚举语法
Swift
中,枚举支持Int
、Double
、String
等基础类型,也有默认枚举值
(String
类型默认枚举值为case
的key
名称,Int
、Double
数值型默认枚举值为0
开始,+1
递增。
代码:
// 写法一
// 不需要逗号隔开
enum Weak1 {
case MON
case TUE
case WED
case THU
case FRI
case SAT
case SUN
}
// 写法二
// 也可以直接一个case,然后使用逗号隔开
enum Weak2 {
case MON, TUE, WED, THU, FRI, SAT, SUN
}
// 定义一个枚举变量
var w: Weak1 = .MON
/*
String类型的enum
- =左边的值是枚举值,例如 MON
- =右边的值在swift中称为 RawValue(原始值),例如 "MON"
- 两者的关系为:case 枚举值 = 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"
}
如果不想写枚举值后的字符串,也可以使用隐式RawValue
分配。
enum Week: String{
case MON, TUE, WED, THU, FRI, SAT, SUN
}
var w = Week.MON.rawValue
print(w)
//打印结果:MON
1.2 枚举的访问
那么Swift是如何获取rawValue的值
,我们可以通过SIL
文件分析。
SIL命令:
swiftc -emit-sil main.swift |xcrun swift-demangle >> ./main.sil && open main.sil
SIL文件
enum Week : String {
case MON, TUE, WED, THU, FRI, SAT, SUN
init?(rawValue: String) //默认添加了一个可选类型的init方法
typealias RawValue = String //给枚举值的类型,通过typealias取了一个别名RawValue
var rawValue: String { get } //增加一个计算属性rawValue,用于获取枚举值的原始值
}
main
函数流程:
rawValue
的getter
方法:bb8
代码段:总结:
rawValue
的底层就是调用的getter
方法,getter
方法中构造了字符串
,但是这个字符串的值(例如“MON”)从哪里取出的呢?他们在编译期
已经确定了,我们查看Mach-O
,在TEXT的__cstring段
就能看到。
1.3 case枚举值 & rawValue原始值
代码:
//输出 case枚举值
print(Week.MON)
//输出 rawValue
print(Week.MON.rawValue)
//打印结果:MON MON
从结果来看是没有什么区别的,输出值都一样,但他们本质是不一样的。
第一个,输出的case枚举值
;第二个,是通过rawValue访问的rawValue的get方法
。
下面这种写法,编译器就会报错:
1.4 枚举的init
枚举的init
会在什么时候调用,我们通过断点看一下:
①,不设置
Condition
,进不到方法里面;②原生代码第一个断点,进入的是getter
方法,进不去init
。即,
enum
中init方法
的调用是通过枚举.init(rawValue:)
或者枚举(rawValue:)
触发的。
继续:
print(Week.init(rawValue: "MON"))
print(Week.init(rawValue: "Hello"))
//打印结果:
//Optional(_6_EnumTest.Week.MON)
//nil
第一个输出的可选值
,第二个输出的是nil
。表示,没有找到对应的case枚举值
。
分析SIL文件中的Week.init
方法,主要有以下几步:
1、在init
方法中是将所有enum
的字符串从Mach-O
文件中取出,依次放入数组
中;
2、放完后,然后调用_findStringSwitchCase
方法进行匹配。
如下:
index_addr:表示获取当前数组中的第n个元素值的地址,然后再把构建好的字符串放到当前地址中
struct_extract:表示取出当前的Int值,Int类型在系统中也是结构体
cond_br:表示比较的表达式,即分支条件跳转
- 如果匹配成功,则构建一个 .some的Optional 返回
- 如果匹配不成功,则继续匹配,知道最后还是没有匹配上,则构建一个.none的Optional返回
_findStringSwitchCase
在swift-source
中,接收两个参数,分别是 数组
+ 需要匹配的String
。①遍历数组
,如果匹配则返回对应的index
;②如果不匹配
,则返回-1
。
所以,这也是为什么一个打印可选值
,一个打印nil
的原因。
1.5枚举的遍历
CaseIterable协议
通常用于没有关联值的枚举
,用来访问所有的枚举值,只需要对应的枚举遵守该协议即可,然后通过allCases
获取所有枚举值,如下:
// Double类型
enum Week1: Double, CaseIterable {
case Mon,Tue, Wed, Thu, Fri, Sat, Sun
}
Week1.allCases.forEach { print($0.rawValue)}
// String类型
enum Week2: String {
case Mon,Tue, Wed, Thu, Fri, Sat, Sun
}
extension Week2: CaseIterable {}
Week2.allCases.forEach { print($0.rawValue)}
二、关联值枚举、模式匹配、属性方法
如果希望用枚举表示复杂的含义,关联更多的信息,就需要使用关联值
了。
他与普通类型的枚举
不同
:没有rawValue,没有rawValue的getter方法;没有初始化init方法
。
//注:当使用了关联值后,就没有RawValue了
//因为:case可以用一组值来表示,而rawValue是单个的值
enum Shape{
//case枚举值后括号内的就是关联值,如 radius
case circle(radius: Double)
case rectangle(width: Int, height: Int)
}
//创建
var circle = Shape.circle(radius: 10.0)
//重新分配
circle = Shape.rectangle(width: 10, height: 10)
模式匹配
enum
中的模式匹配其实就是匹配case枚举值
,根据枚举类型,分为2种:
1、简单类型
的枚举的模式匹配;
2、自定义类型
的枚举(关联值)的模式匹配。
简单enum的模式匹配
注:swift中的enum模式匹配需要将所有情况都列举,或者使用default
表示默认情况,否则会报错
enum Week: String{
case MON
case TUE
case WED
case THU
case FRI
case SAT
case SUN
}
var current: Week?
switch current {
case .MON: print(Week.MON.rawValue)
case .TUE: print(Week.MON.rawValue)
default:print("unknow day")
}
//打印结果:unknow day
关联值类型的模式匹配
关联值类型
的模式匹配
有两种方式:1、switch - case, 匹配所有case;2、if - case, 匹配单个case
。
switch - case
定义关联值枚举:
enum Shape{
case circle(radius: Double)
case rectangle(width: Int, height: Int)
}
可以let var
修饰关联值的入参:
let shape = Shape.circle(radius: 10.0)
switch shape{
//相当于将10.0赋值给了声明的radius常量
case let .circle(radius):
print("circle radius: \(radius)")
case .rectangle(let width, var height):
height += 1
print("rectangle width: \(width) height: \(height)")
}
- 通过
if-case
匹配单个case
,如下:
let circle = Shape.circle(radius: 10)
//匹配单个case
if case let Shape.circle(radius) = circle {
print("circle radius: \(radius)")
}
- 如果我们只关心
不同case的相同关联值(即关心不同case的某一个值)
,需要使用同一个参数
。
例如,案例中的x
,如果分别使用x、y
, 编译器会报错:
enum Shape{
case circle(radius: Double)
case rectangle(width: Double, height: Double)
case square(width: Double, height: Double)
}
let shape = Shape.circle(radius: 10)
switch shape{
case let .circle(x), let .square(20, x):
print(x)
default:
break
}
也可以使用通配符_(表示匹配一切)
的方式:
let shape = Shape.rectangle(width: 10, height:20)
switch shape{
case let .rectangle(x, _), let .square(_, x):
print("x = \(x)")
default:
break
}
注:枚举使用过程中不关心某一个关联值
,可以使用通配符_
表示。OC只能调用swift中Int类型
的枚举。
属性 & 函数
enum中可以包含计算属性
、类型属性
,不能包含存储属性
。
enum Direct: Int {
case up
case down
case left
case right
// 计算型属性
var description: String{
switch self {
case .up:
return "这是上面"
default:
return "这是\(self)"
}
}
//存储属性:编译器报错,Enums must not contain stored properties
//var radius: Double
//类型属性 - 是一个全局变量
static let height = 20.0
// 函数
func printSelf() {
print(description)
}
mutating func nextDay(){
if self == .up{
self = Direct(rawValue: 1)!
}else{
self = Direct(rawValue: self.rawValue+1)!
}
}
}
Direct.down.printSelf()
//打印结果:这是down
var direct = Direct.left;
direct.nextDay();
direct.printSelf()
//打印结果:这是right
为什么
struct
中可以放存储属性
,而enum不可以?
struct
中可以包含存储属性,是因为其大小就是存储属性的大小
。而enum
是不一样的(请查阅后文的enum大小讲解),enum枚举的大小是取决于case的个数的
,如果没有超过255,enum的大小就是1字节(8位)
可以在enum中定义实例方法
、static修饰的方法
:
enum Week: Int{
case MON, TUE, WED, THU, FRI, SAT, SUN
mutating func nextDay(){
if self == .SUN{
self = Week(rawValue: 0)!
}else{
self = Week(rawValue: self.rawValue+1)!
}
}
}
<!--使用-->
var w = Week.MON
w.nextDay()
print(w)
三、枚举的嵌套
枚举的嵌套主要用于以下场景:
1、枚举嵌套枚举
:一个复杂枚举是由一个或多个枚举组成;
2、结构体嵌套枚举
:enum是不对外公开的,即是私有
的。
3.1 enum嵌套enum
枚举嵌套枚举,改动上面的例子:
enum CombineDirect{
//枚举中嵌套的枚举
enum BaseDirect{
case up
case down
case left
case right
}
//通过内部枚举组合的枚举值
case leftUp(baseDIrect1: BaseDirect, baseDirect2: BaseDirect)
case leftDown(baseDIrect1: BaseDirect, baseDirect2: BaseDirect)
case rightUp(baseDIrect1: BaseDirect, baseDirect2: BaseDirect)
case rightDown(baseDIrect1: BaseDirect, baseDirect2: BaseDirect)
}
//使用
let leftUp = CombineDirect.leftUp(baseDIrect1: CombineDirect.BaseDirect.left, baseDirect2: CombineDirect.BaseDirect.up)
结构体嵌套枚举
//结构体嵌套枚举
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 .up, .down:
print("up, down")
}
}
}
枚举的递归:indirect
递归枚举
是一种枚举类型,它有一个或多个枚举成员使用该枚举类型的实例作为关联值。使用递归枚举时,编译器会插入一个间接层。你可以在枚举成员前加上 indirect
来表示该成员可递归。
//一、用枚举表示链表结构
enum List<T>{
case end
//表示case使是引用来存储
indirect case node(T, next: List<T>)
}
//二、也可以将indirect放在enum前
//表示整个enum是用引用来存储
indirect enum List<T>{
case end
case node(T, next: List<T>)
}
第一种写法,如果没有关键字indirect
,编译报错。原因是使用该枚举时,enum的大小需要case来确定,而case的大小又需要使用到enum大小。所以无法计算enmu的大小,于是报错!
根据编译器提示,需要使用关键字indirect
,意思就是将该枚举标记位递归,同时也支持标记单个case。
enum
内存大小:
enum List<T>{
case end
indirect case node(T, next: List<T>)
}
print(MemoryLayout<Int>.size)
print(MemoryLayout<List<Int>>.size)
print(MemoryLayout<List<Int>>.stride)
print(MemoryLayout<String>.size)
print(MemoryLayout<List<String>>.size)
print(MemoryLayout<List<String>>.stride)
//打印结果:
//8 8 8
//16 8 8
发现Int
、String
都是8。为什么?
下面通过LLDB分析查看一下:
如果是end,此时存储的是case值,为0
,而case为node时存储的是引用地址
。所以,
indirect
关键字其实就是通知编译器,我当前的enum是递归的,大小是不确定的,需要分配一块堆区的内存空间
,用来存放enum。
四、swift和OC混编enum
在swift中,enum
非常强大,而在OC
中,enum
仅仅只是一个整数值
。
因此,OC调用Swift枚举,必须具备2个条件:①、用@objc关键字标记enum
;②、当前enum应该是Int类型
。
// Swift中定义枚举
@objc enum Weak: Int{
case MON, TUE, WED, THU, FRI, SAT, SUN
}
// OC使用
- (void)test{
Weak mon = WeakMON;
}
Swift
使用OC
枚举,OC
中的枚举会自动转换成swift中的enum
。
// OC定义1:NS_ENUM
NS_ENUM(NSInteger, ENUM_OC_TYPE){
Value1,
Value2
};
// OC定义2:typedef enum
typedef enum {
Num1,
Num2
}OCEnumType;
// swift使用
//1、将OC头文件导入桥接文件xxx-Bridging-Header.h
#import "xxx.h"
//2、使用
let ocEnum1 = ENUM_OC_TYPE.Value1
let ocEnum2 = OCEnumType.init(0)
print("\(ocEnum1) + \(ocEnum2.rawValue)")
//打印结果:ENUM_OC_TYPE + 0
OC自动转换成swift方式:上图可知,通过
typedef enum
定义的enum
,在swift
中变成了一个结构体
,并遵循了两个协议:Equatable
和 RawRepresentable
。OC使用Swift中String类型的枚举方式
@objc enum Weak: Int{
case MON, TUE, WED
var val: String?{
switch self {
case .MON:
return "MON"
case .TUE:
return "TUE"
case .WED:
return "WED"
default:
return nil
}
}
}
// OC中使用
Weak mon = WeakMON;
// swift中使用
let weak = Weak.MON.val
即:swift中的enum成Int整型;enum再声明一个变量/方法,用于返回固定的字符串,用于在swift中使用。
五、Enum内存大小
我们主要分析两个函数的区别:
-
size
:实际占用内存大小; -
stride
:系统分配的内存大小。
5.1 普通enum
一个case
的情况:
enum Weak {
case MON
//case TUE
}
print(MemoryLayout<Weak>.size)
print(MemoryLayout<Weak>.stride)
//打印结果:0 1
再加一个case:
enum Weak {
case MON
case TUE
}
print(MemoryLayout<Weak>.size)
print(MemoryLayout<Weak>.stride)
//打印结果:1 1
继续增加多个case:
enum Weak {
case MON
case TUE
case WED
case THU
case FRI
case SAT
case SUN
}
print(MemoryLayout<Weak>.size)
print(MemoryLayout<Weak>.stride)
//打印结果:1 1
以上可以看出,当case个数为1时,枚举size为0
,个数>=2时,size的大小始终是1
,即,说明enum就是以1字节存储在内存中的
。why?
读取内存可以看出,
case都是1字节大小
,1个字节是8个byte,那么有255种
排列组合(0x00000000 - 0x11111111
)。所以,
当case为1个的时候,size的大小是0(二进制是0x0)
;case数<=255时,size都是1,是UInt8类型
。超过255个时,会自动扩容,size和stride都会增加
。
总结:
1、如果enum
中有原始值,即rawValue
,其大小取决于case的多少
,如果没有超过UInt8
即255,则就是1字节
存储case,Int
标识的其实就是RawValue
的值。
2、当只有一个case
的情况下,size是0
,表示这个enum是没有意义
的。
3、当有两个及以上case
时,如果没有超过255,则case的步长是1字节
;如果超过,则UInt8->UInt16...
,以此类推。
5.2 关联值的enum
自定义类型的枚举,即关联值
类型,size
和stride
值的变化,如下:
enum Shape{
case circle(radius: Double)
case rectangle(width: Double, height: Double)
}
print(MemoryLayout<Shape>.size)
print(MemoryLayout<Shape>.stride)
//打印结果:17 24
分析:
-
case
(枚举值)size
是1
; - 枚举是
共用内存
,有关联值时,取最大值
;
2.1circle
的参数是double
类型,内存size
是8
;
2.2rectangle
的参数是两个double
类型,内存size
是16
; - 取
最大值
,为16
。即,内存size
为:16(最大关联值大小) + 1(case枚举值)= 17
。 -
内存对齐
,所以stride为24
。
断点读取内存验证一下:
5.3 enum嵌套enum
enum CombineDirect{
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<CombineDirect>.size)
print(MemoryLayout<CombineDirect>.stride)
//打印结果:2 2
说明:enum嵌套enum
同具有关联值的enum是一样的,同样取决于关联值的大小,其内存大小是最大关联值的大小
。
定义一个变量,观察一下内存:
var combine = CombineDirect.leftDown(baseDirect1: .left, baseDirect2: .down)
总结,有待进一步验证:
enum嵌套enum同样取决于最大case的关联值大小;
当嵌套enum的case只有2个时,case在内存中的存储是0、8;
当嵌套enum的case大于2,小于等于4时,case在内存中的存储是 0、4、8、12;
当嵌套enum的case大于4时,case在内存中的存储是从0、1、2...以此类推。
5.4 结构体嵌套enum
struct Skill {
enum KeyType{
case up
case down
case left
case right
}
}
print(MemoryLayout<Skill>.size)
print(MemoryLayout<Skill>.stride)
//打印结果:0 1
如果只嵌套了enum,没有声明变量。size的大小取决于成员变量,但是struct中目前没有属性,所以size是1
。
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 .up, .down:
print("up, down")
}
}
}
print(MemoryLayout<Skill>.size)
print(MemoryLayout<Skill>.stride)
//打印结果:1 1
结构体的大小计算,跟函数无关,所以只看成员变量key的大小
,key是枚举Skill类型,大小为1,所以结构体大小为1
。
如果在添加一个成员变量:
struct Skill {
enum KeyType{
case up
case down
case left
case right
}
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<Skill>.size)
print(MemoryLayout<Skill>.stride)
//打印结果:2 2
在添加一个Int属性:
struct Skill {
enum KeyType{
case up
case down
case left
case right
}
let key: KeyType //1字节
var height: UInt8 //1字节
var width: Int //8字节
func launchSkill(){
switch key {
case .left, .right:
print("left, right")
case .up, .down:
print("up, down")
}
}
}
print(MemoryLayout<Skill>.size)
print(MemoryLayout<Skill>.stride)
//打印结果:16 16
//打印结果2:如果把Int属性放在最前面,则为,10 16
为什么属性位置不一样
,打印结果不一样,也是因为内存对齐
。内存对齐规则如下:
/**
数据成员的对齐规则可以理解为min(m, n) 的公式
m,表示当前成员的开始位置;n,表示当前成员所需要的位数。
如果满足条件 m 整除 n (即 m % n == 0), n 从 m 位置开始存储;
反之继续检查 m+1 能否整除 n, 直到可以整除, 从而就确定了当前成员的开始位置。
*/
struct Mystruct4{
int a; //4字节 min(0,4)--- (0,1,2,3)
struct Mystruct5{ //从4开始,存储开始位置必须是最大的整数倍(最大成员为8),min(4,8)不符合 4,5,6,7,8 -- min(8,8)满足,从8开始存储
double b; //8字节 min(8,8) --- (8,9,10,11,12,13,14,15)
short c; //1字节,从16开始,min(16,1) -- (16,17)
}Mystruct5;
}Mystruct4;
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"%lu %lu", sizeof(Mystruct4), sizeof(Mystruct4.Mystruct5));
//打印结果:24 16
NSLog(@"Hello, World!");
}
return 0;
}
结构体嵌套了enum总结:
1、如果没有声明变量,此时的size是0,stride是1
;
2、如果结构体中没有其他属性,只有枚举变量
,那么结构体的大小就是枚举的大小
,即size为1
;
3、如果结构体中还有其他属性,则按照OC
中的结构体内存对齐原则
进行分析。
内存对齐 & 字节对齐 区分
内存对齐
:iOS中是8字节
对齐,苹果实际分配采用16字节
对齐,这种只会在分配对象
时出现。
字节对齐
:存储属性的位置必须是偶地址,即OC内存对齐中的min(m,n)
,其中m表示存储的位置
,n表示属性的大小
,需要满足位置m整除n
时,才能从该位置存放属性。简单来说,就是必须在自身的倍数位置
开始。
外部调用对象时,对象是服从内存对齐
。
单纯从结构上说,结构内部服从最大字节对齐
。即,枚举size为1的情况。
总结:
一、枚举定义:
1、enum
中使用rawValue
的本质是调用get方法
,即在get方法中从Mach-O
对应地址中取出字符串
并返回的操作;
2、enum
中init方法
的调用是通过枚举.init(rawValue:)
或者枚举(rawValue:)
触发的;
3、没有关联值的enum
,如果希望获取所有枚举值,需要遵循CaseIterable协议
,然后通过枚举名.allCase
的方式获取;
4、case枚举值
和rawValue原始值
的关系:case 枚举值 = rawValue原始值
;
5、具有关联值
的枚举,可以称为三无
enum,因为没有别名RawValue、init、计算属性rawValue
;
6、enum
的模式匹配
方式,主要有两种:switch-case
/if-case
7、enum可以嵌套enum
,也可以在结构体
中嵌套enum,表示该enum是struct私有
的;
8、enum中还可以包含计算属性
、类型属性
,但是不能包含存储属性
;
9、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中内存对齐
原则进行计算。
参考:枚举教程