Swift之深入解析枚举enum的底层原理
Posted Forever_wj
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Swift之深入解析枚举enum的底层原理相关的知识,希望对你有一定的参考价值。
一、Swift 枚举
- 枚举简单的说也是一种数据类型,只不过是这种数据类型只包含自定义的特定数据,它是一组有共同特性的数据的集合。
- Swift 的枚举类似于 Objective C 和 C 的结构,枚举的功能为:
-
- 它声明在类中,可以通过实例化类来访问它的值。
-
- 枚举也可以定义构造函数(initializers)来提供一个初始成员值;可以在原始的实现基础上扩展它们的功能。
-
- 可以遵守协议(protocols)来提供标准的功能。
- Swift 中使用 enum 关键词来创建枚举,并且把它们的整个定义放在一对大括号内:
enum enumName {
// 枚举定义放在这里
}
- 如下所示,定义以下表示星期的枚举:
import Cocoa
// 定义枚举
enum DaysofaWeek {
case Sunday
case Monday
case TUESDAY
case WEDNESDAY
case THURSDAY
case FRIDAY
case Saturday
}
var weekDay = DaysofaWeek.THURSDAY
weekDay = .THURSDAY
switch weekDay
{
case .Sunday:
print("星期天")
case .Monday:
print("星期一")
case .TUESDAY:
print("星期二")
case .WEDNESDAY:
print("星期三")
case .THURSDAY:
print("星期四")
case .FRIDAY:
print("星期五")
case .Saturday:
print("星期六")
}
- 以上程序执行输出结果为:
星期四
- 如果此时想创建一个枚举值是 String 类型的 enum,可以通过指定 enum 的枚举值的类型来创建,其中枚举值和原始值 rawValue 的关系为 case 枚举值 = rawValue原始值。
/*
- =左边的值是枚举值,例如 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 分配,如下所示:
// String类型
enum Week: String {
case MON, TUE, WED = "WED", THU, FRI, SAT, SUN
}
// Int类型
// MON是从0开始一次递推,而WED往后是从10开始一次递推
enum Week: Int {
case MON, TUE, WED = 10, THU, FRI, SAT, SUN
}
二、枚举的访问
- 如果 enum 没有声明类型,是没有 rawValue 属性的:
- 枚举的访问方式如下所示:
enum Week: String {
case MON, TUE, WED, THU, FRI, SAT, SUN
}
var w = Week.MON.rawValue
// 访问
print(w)
// 打印结果
MON
- 那么,Swift 是如何做到打印 MON 的呢?通过 SIL 文件分析:
-
- 首先查看 SIL 文件中的 enum,可以看见底层增加了一些属性:
-
-
- 给枚举值的类型,通过 typealias 命名一个别名 RawValue;
-
-
-
- 默认添加一个可选类型的 init 方法;
-
-
-
- 增加一个计算属性 rawValue,用于获取枚举值的原始值。
-
enum Week: String {
case MON, TUE, WED, THU, FRI, SAT, SUN
// 别名,即将String类型命名为RawValue
typealias RawValue = String
// 可选的初始化方法,允许返回一个nil
init?(rawValue:String)
// 计算属性 rawValue
var rawValue : String { get }
}
-
- 查看 SIL 中的 main 方法,可以得知 w 是通过枚举值的 rawValue 的 get 方法获取:
-
- 查看 SIL 文件 rawValue 的 get 方法:
-
-
- 接收一个枚举值,用于匹配对应的分支;
-
-
-
- 在对应分支创建对应的 String;
-
-
-
- 返回对应的 String。
-
- 使用 rawValue 的本质是调用 get 方法。但是 get 方法中的 String 是从哪里来的呢?String 又存储在哪里?
- 其实,这些对应分支的字符串在编译时期就已经存储完成,即存放在 Maach-O 文件的 __TEXT.cstring 中,且是连续的内存空间,可以通过编译后查看 Mach-O 文件来验证:
- rawValue 的 get 方法中的分支构建的字符串,主要是从 Mach-O 文件对应地址取出的字符串,然后再返回给 w。
三、区分 case 枚举值与 rawValue 原始值
- 有如下代码,请问打印结果是什么呢?
// 输出 case枚举值
print(Week.MON)
// 输出 rawValue
print(Week.MON.rawValue)
// 打印结果
MON
MON
- 虽然两个输出的结果来看是没有什么区别的,都是 MON,但其实并不是同一回事:第一个输出的 case 枚举值,第二个是通过 rawValue 访问的 rawValue 的 get 方法。
- 两种错误的示例,如下所示:
三、枚举的 init 调用时机
- 定义一个符号断点 Week.init,如下所示:
- 定义如下代码,通过运行结果,可以发现并不会执行 init 方法:
print(Week.MON.rawValue)
let w = Week.MON.rawValue
- 如果通过 init 方式创建 enum 呢?
print(Week.init(rawValue: "MON"))
- 运行结果如下:
- 注意:这个断点需要通过 init 前的一个断点 + Week.init 符号断点 + 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 方法:
-
- 在 init 方法中是将所有 enum 的字符串从 Mach-O 文件中取出,依次加入数组中;
-
- 加入完成后,然后调用 _findStringSwitchCase 方法进行匹配。
-
- index_addr 表示获取当前数组中的第 n 个元素值的地址,然后再把构建好的字符串放到当前地址中:
- `struct_extract` 表示`取出当前的Int值`,Int类型在系统中也是结构体
- `cond_br` 表示比较的表达式,即分支条件跳转
- 如果匹配成功,则构建一个`.some的Optional`返回
- 如果匹配不成功,则继续匹配,知道最后还是没有匹配上,则构建一个`.none的Optional`返回
- 在 swift-source 中查找 _findStringSwitchCase 方法,接收两个参数,分别是数组 + 需要匹配的 String:
-
- 遍历数组,如果匹配则返回对应的 index;
-
- 如果不匹配,则返回-1;
@_semantics("findStringSwitchCase")
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
}
- 继续分析 SIL 中的 week.init 方法:
-
- 如果没有匹配成功,则构建一个 .none 类型的 Optional,表示 nil;
-
- 如果匹配成功,则构建一个 .some 类型的 Optional,表示有值。
- 这就是一个打印可选值,一个打印 nil 的原因。
四、枚举的遍历
- CaseIterable 协议通常用于没有关联值的枚举,用来访问所有的枚举值,只需要对应的枚举遵守该协议即可,然后通过 allCases 获取所有枚举值,如下所示:
// 定义无关联值枚举,并遵守协议
enum Week: String{
case MON, TUE, WED, THU, FRI, SAT, SUN
}
extension Week: CaseIterable{}
// 通过for循环遍历
var allCase = Week.allCases
for c in allCase{
print(c)
}
// 通过函数式编程遍历
let allCase = Week.allCases.map({"\\($0)"}).joined(separator: ", ")
print(allCase)
// 打印结果
MON, TUE, WED, THU, FRI, SAT, SUN
五、关联值
- 如果希望用枚举表示复杂的含义,关联更多的信息,就需要使用关联值。
- 使用 enum 表达一个形状,其中有圆形、长方形等,圆形有半径,长方形有宽、高,可以通过下面具有关联值的 enum 来表示:
// 当使用了关联值后,就没有RawValue了,主要是因为case可以用一组值来表示,而rawValue是单个的值
enum Shape {
// case枚举值后括号内的就是关联值,例如 radius
case circle(radius: Double)
case rectangle(width: Int, height: Int)
}
- 具有关联值的枚举,就没有 rawValue 属性了,主要是因为一个 case 可以用一个或者多个值来表示,而 rawValue 只有单个的值。
- SIL 验证:
-
- 查看 SIL 文件,发现此时的 enum 中既没有别名,也没有 init 方法,计算属性 rawValue:
-
- 其中,关联值中 radius、width、height 都是自定义的标签,也可以不写,如下所示,但并不推荐这种方式,因为可读性比较差:
enum Shape {
// case枚举值后括号内的就是关联值,例如 radius
case circle(Double)
case rectangle(Int, Int)
}
- 那么如何创建一个有关联值的枚举值呢?可以直接在使用时给定值来创建一个关联的枚举值:
// 创建
var circle = Shape.circle(radius: 10.0)
// 重新分配
circle = Shape.rectangle(width: 10, height: 10)
六、枚举的其它用法
① 模式匹配
- 简单 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)
case .WED:print(Week.MON.rawValue)
default:print("unknow day")
}
// 打印结果
unknow day
-
- 查看其 SIL 文件,其内部是将 nil 放入 current 全局变量,然后匹配 case,做对应的代码跳转:
- 具有关联值 enum 的模式匹配:
-
- 通过 switch 匹配所有 case:
enum Shape {
case circle(radius: Double)
case rectangle(width: Int, height: Int)
}
let shape = Shape.circle(radius: 10.0)
switch shape {
// 相当于将10.0赋值给了声明的radius常量
case let .circle(radius):
print("circle radius: \\(radius)")
case let .rectangle(width, height):
print("rectangle width: \\(width) height: \\(height)")
}
// 打印结果
circle radius: 10.0
-
- 也可以将关联值的参数使用 let、var 修饰:
enum Shape {
case circle(radius: Double)
case rectangle(width: Int, height: Int)
}
let shape = Shape.circle(radius: 10)
switch shape {
// 做了Value-Binding,相当于将10.0赋值给了声明的radius常量
case .circle(let radius):
print("circle radius: \\(radius)")
case .rectangle(let width, var height):
height += 1
print("rectangle width: \\(width) height: \\(height)")
}
// 打印结果
circle radius: 10.0
-
- SIL 中的关联值的模式匹配,如下图所示:
-
- 首先构建一个关联值的元组;
-
- 根据当前 case 枚举值,匹配对应的 case,并跳转;
-
- 取出元组中的值,将其赋值给匹配 case 中的参数。
-
- 通过 if case 匹配单个 case,如下所示:
enum Shape {
case circle(radius: Double)
case rectangle(width: Int, height: Int)
}
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
}
- 使用通配符_(表示匹配一切)的方式:
enum Shape{
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 Shape{
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 是不对外公开的,即是私有的。
- 枚举嵌套枚举:
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")
}
}
}
③ 枚举中包含属性
- enum 中只能包含计算属性、类型属性,不能包含存储属性:
enum Shape {
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
}
- 为什么 struct 中可以放存储属性,而 enum 不可以?因为 struct 中可以包含存储属性是因为其大小就是存储属性的大小,而对 enum 来说就是不一样的,enum 枚举的大小是取决于 case 的个数的,如果没有超过 255,enum 的大小就是 1 字节(8 位)。
④ 枚举中包含方法
- 可以在 enum 中定义实例方法、static 修饰的方法:
enum Swift之深入解析“泛型”的底层原理