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`返回
	    - 如果匹配不成功,则继续匹配,知道最后还是没有匹配上,则构建一个`.noneOptional`返回
  • 在 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之深入解析“泛型”的底层原理

Swift之深入解析反射Mirror的底层原理

Swift之深入解析内存管理的底层原理

Swift之深入解析可选类型Optional的底层原理

Enum深入解析

iOS之深入解析类方法+load与+initialize的底层原理