Swift底层探索:enum

C enum

C语言枚举格式

enum 枚举名 {
    枚举值 1,
    枚举值 2,
    ……
};

比如要表示星期几的枚举:

enum week {
    MON,
    TUE,
    WED,
    THU,
    FRI,
    SAT,
    SUN
};

第一个枚举成员的默认值为0,依次加1。要改变枚举值直接赋值就可以了。
声明一个枚举变量如下:

enum week {//这里枚举名可以省略
    MON,
    TUE,
    WED,
    THU,
    FRI,
    SAT,
    SUN
} week;

Swift 枚举

同样的一个week枚举,在swift中定义如下:

enum week {
    case MONDAY
    case TUEDAY
    case WEDDAY
    case THUDAY
    case FRIDAY
    case SATDAY
    case SUNDAY
}
var w: week = week.MONDAY

swift枚举不需要,;,不过需要case。可以简写只有一个case

enum week {
 case MONDAY, TUEDAY, WEDDAY, THUDAY, FRIDAY, SATDAY, SUNDAY
}

Swift中枚举默认也是整形,区别是根据枚举的数量编译器会处理整形的类型比如:Int8Int16Int32Int64

遍历

枚举可以像集合那样遍历,需要遵循CaseIterable协议。

enum week {
 case MONDAY, TUEDAY, WEDDAY, THUDAY, FRIDAY, SATDAY, SUNDAY
}

extension week : CaseIterable {}

for enumCase in week.allCases {
    print(enumCase)
}
MONDAY
TUEDAY
WEDDAY
THUDAY
FRIDAY
SATDAY
SUNDAY

rawValue(原始值)

如果想用Sring表达Enum,在Swift中可以如下:

enum week: String { //这里必须注明类型
    case MONDAY = "MONDAY"
    case TUEDAY = "TUEDAY"
    case WEDDAY = "WEDDAY"
    case THUDAY  //不赋值 “THUDAY”
    case FRIDAY
    case SATDAY
    case SUNDAY
}

在这里=左边的值我们叫做rawValue。想要访问rawValue必须设置类型(Int也需要)。
那么这里Swift是怎么处理rawValue的呢?

var w = week.MONDAY.rawValue

查看sil

enum week : String {
  case MONDAY
  case TUEDAY
  case WEDDAY
  case THUDAY
  case FRIDAY
  case SATDAY
  case SUNDAY
  typealias RawValue = String
  init?(rawValue: String)
  var rawValue: String { get }
}

可以看到String有一个别名RawValue。有一个可选的初始化方法,有一个计算属性rawValue。所以本质上我们访问rawValue是在访问计算属性。
变量w的生成过程如下:

// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer>>):
  alloc_global @main.w : Swift.String                     // id: %2
  //全局变量w
  %3 = global_addr @main.w : Swift.String : $*String      // user: %8
  %4 = metatype $@thin week.Type
  //拿到MONDAY枚举
  %5 = enum $week, #week.MONDAY!enumelt           // user: %7
  // function_ref week.rawValue.getter
  //获取枚举计算属性rawValue的get方法
  %6 = function_ref @main.week.rawValue.getter : Swift.String : $@convention(method) (week) -> @owned String // user: %7
  //枚举值传给rawValue的get方法,返回一个String
  %7 = apply %6(%5) : $@convention(method) (week) -> @owned String // user: %8
  //把String给比变量w
  store %7 to %3 : $*String                       // id: %8
  %9 = integer_literal $Builtin.Int32, 0          // user: %10
  %10 = struct $Int32 (%9 : $Builtin.Int32)       // user: %11
  return %10 : $Int32                             // id: %11
} // end sil function 'main'

main.week.rawValue.getter方法比较长,截取一部分如下:

// week.rawValue.getter
sil hidden @main.week.rawValue.getter : Swift.String : $@convention(method) (week) -> @owned String {
// %0 "self"                                      // users: %2, %1
//接收一个枚举值参数week
bb0(%0 : $week):
  //默认有个self=枚举值
  debug_value %0 : $week, let, name "self", argno 1 // id: %1
  //匹配枚举跳转对应分支
  switch_enum %0 : $week, case #week.MONDAY!enumelt: bb1, case #week.TUEDAY!enumelt: bb2, case #week.WEDDAY!enumelt: bb3, case #week.THUDAY!enumelt: bb4, case #week.FRIDAY!enumelt: bb5, case #week.SATDAY!enumelt: bb6, case #week.SUNDAY!enumelt: bb7 // id: %2

bb1:                                              // Preds: bb0
  //构建字符串MONDAY
  %3 = string_literal utf8 "MONDAY"               // user: %8
  %4 = integer_literal $Builtin.Word, 6           // user: %8
  %5 = integer_literal $Builtin.Int1, -1          // user: %8
  %6 = metatype $@thin String.Type                // user: %8
  // function_ref String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
  %7 = function_ref @Swift.String.init(_builtinStringLiteral: Builtin.RawPointer, utf8CodeUnitCount: Builtin.Word, isASCII: Builtin.Int1) -> Swift.String : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %8
  %8 = apply %7(%3, %4, %5, %6) : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %9
  //跳转bb8
  br bb8(%8 : $String)         

// %52                                            // user: %53
bb8(%52 : $String):                               // Preds: bb7 bb6 bb5 bb4 bb3 bb2 bb1
  //返回字符串
  return %52 : $String                            // id: %53
} // end sil function 'main.week.rawValue.getter : Swift.String'                   

所以在这个过程中访问rawValuegetter方法,根据枚举值找到对应分支构建字符串然后返回。
那么MONDAY存在哪里呢?

Swift底层探索:enum_第1张图片
image.png

可以看到是连续的内存空间来存储字符串。在程序运行的时候会通过地址+偏移量取找到这个字符串。

case和rawValue

print(week.MONDAY)
print(week.MONDAY.rawValue)
MONDAY
MONDAY

caserawValue输出一样,那么这两个相同么?

image.png

Swift底层探索:enum_第2张图片
image.png

显然不是一个东西,不是同一个类型。这里肯定也不能互相赋值,并且也不能对rawValue赋值(只有getter方法)。

init?

上面分析的sil里面有一个init方法

print(week.init(rawValue:"MONDAY")!)//MONDAY
print(week.init(rawValue:"123"))//nil
MONDAY
nil
Swift底层探索:enum_第3张图片
image.png

可以打个符号断点具体看一下,

//走不到断点
print(week.MONDAY.rawValue)//MONDAY
//可以断点
print(week(rawValue:"MONDAY")!)//MONDAY
print(week.init(rawValue:"123"))//nil

发现访问rawValue不走init方法,通过rawValue初始化才走init方法。

分析下SIL文件

// week.init(rawValue:)
sil hidden @main.week.init(rawValue: Swift.String) -> main.week? : $@convention(method) (@owned String, @thin week.Type) -> Optional {
// %0 "rawValue"                                  // users: %164, %158, %79, %3
// %1 "$metatype"
bb0(%0 : $String, %1 : $@thin week.Type):
  %2 = alloc_stack $week, var, name "self"        // users: %162, %154, %143, %132, %121, %110, %99, %88, %165, %159
  debug_value %0 : $String, let, name "rawValue", argno 1 // id: %3
  %4 = integer_literal $Builtin.Word, 7           // user: %6
  // function_ref _allocateUninitializedArray(_:)
  //调用方法创建一个数组
  %5 = function_ref @Swift._allocateUninitializedArray(Builtin.Word) -> ([A], Builtin.RawPointer) : $@convention(thin) <τ_0_0> (Builtin.Word) -> (@owned Array<τ_0_0>, Builtin.RawPointer) // user: %6
 //返回一个元组
  %6 = apply %5(%4) : $@convention(thin) <τ_0_0> (Builtin.Word) -> (@owned Array<τ_0_0>, Builtin.RawPointer) // users: %8, %7
 //返回值是一个元组类型(Array,RawPointer当前指针)
  %7 = tuple_extract %6 : $(Array, Builtin.RawPointer), 0 // users: %80, %79
  %8 = tuple_extract %6 : $(Array, Builtin.RawPointer), 1 // user: %9
  //访问数组首地址
  %9 = pointer_to_address %8 : $Builtin.RawPointer to [strict] $*StaticString // users: %17, %69, %59, %49, %39, %29, %19
  //构建第一个字符串,存储在 array index 0
  %10 = string_literal utf8 "MONDAY"              // user: %12
  %11 = integer_literal $Builtin.Word, 6          // user: %16
  %12 = builtin "ptrtoint_Word"(%10 : $Builtin.RawPointer) : $Builtin.Word // user: %16
  br bb1                                          // id: %13

bb1:                                              // Preds: bb0
  %14 = integer_literal $Builtin.Int8, 2          // user: %16
  br bb2                                          // id: %15

bb2:                                              // Preds: bb1
  //构建StaticString
  %16 = struct $StaticString (%12 : $Builtin.Word, %11 : $Builtin.Word, %14 : $Builtin.Int8) // user: %17
  store %16 to %9 : $*StaticString                // id: %17
  // 1
  %18 = integer_literal $Builtin.Word, 1          // user: %19
  //%9数组首地址,19%存储当前数组index 1的地址
  %19 = index_addr %9 : $*StaticString, %18 : $Builtin.Word // user: %27
  //构建第二个字符串,存储在array index 1的位置
  %20 = string_literal utf8 "TUEDAY"              // user: %22
  %21 = integer_literal $Builtin.Word, 6          // user: %26
  %22 = builtin "ptrtoint_Word"(%20 : $Builtin.RawPointer) : $Builtin.Word // user: %26
  br bb3                                          // id: %23

bb14:                                             // Preds: bb13
  %76 = struct $StaticString (%72 : $Builtin.Word, %71 : $Builtin.Word, %74 : $Builtin.Int8) // user: %77
  store %76 to %69 : $*StaticString               // id: %77
  // function_ref _findStringSwitchCase(cases:string:)
  //拿到函数
  %78 = function_ref @Swift._findStringSwitchCase(cases: [Swift.StaticString], string: Swift.String) -> Swift.Int : $@convention(thin) (@guaranteed Array, @guaranteed String) -> Int // user: %79
  //拿到数组中元素的index
  %79 = apply %78(%7, %0) : $@convention(thin) (@guaranteed Array, @guaranteed String) -> Int // users: %149, %138, %127, %116, %105, %94, %83, %147, %136, %125, %114, %103, %92, %81
  release_value %7 : $Array         // id: %80
  debug_value %79 : $Int, let, name "$match"      // id: %81
  //0
  %82 = integer_literal $Builtin.Int64, 0         // user: %84
  //拿到结构体的index
  %83 = struct_extract %79 : $Int, #Int._value    // user: %84
  //比较两个index
  %84 = builtin "cmp_eq_Int64"(%82 : $Builtin.Int64, %83 : $Builtin.Int64) : $Builtin.Int1 // user: %85
  //cond_br 成功跳转bb15,否则跳转bb16
  cond_br %84, bb15, bb16                         // id: %85

bb15:                                             // Preds: bb14
  %86 = metatype $@thin week.Type
  //返回枚举值
  %87 = enum $week, #week.MONDAY!enumelt          // user: %89
  %88 = begin_access [modify] [static] %2 : $*week // users: %89, %90
  store %87 to %88 : $*week                       // id: %89
  end_access %88 : $*week                         // id: %90
  br bb29                                         // id: %91

bb16:                                             // Preds: bb14
  debug_value %79 : $Int, let, name "$match"      // id: %92
  %93 = integer_literal $Builtin.Int64, 1         // user: %95
  %94 = struct_extract %79 : $Int, #Int._value    // user: %95
  //把1和index比较,一直循环比较
  %95 = builtin "cmp_eq_Int64"(%93 : $Builtin.Int64, %94 : $Builtin.Int64) : $Builtin.Int1 // user: %96
  cond_br %95, bb17, bb18                         // id: %96

bb28:                                             // Preds: bb26
  release_value %0 : $String                      // id: %158
  dealloc_stack %2 : $*week                       // id: %159
  //没有匹配到返回none,也就是nil
  %160 = enum $Optional, #Optional.none!enumelt // user: %161
  br bb30(%160 : $Optional)                 // id: %161

bb29:                                             // Preds: bb27 bb25 bb23 bb21 bb19 bb17 bb15
  %162 = load %2 : $*week                         // user: %163
  //匹配到了
  %163 = enum $Optional, #Optional.some!enumelt, %162 : $week // user: %166
  release_value %0 : $String                      // id: %164
  dealloc_stack %2 : $*week                       // id: %165
  br bb30(%163 : $Optional)                 // id: %166

// %167                                           // user: %168
bb30(%167 : $Optional):                     // Preds: bb29 bb28
//返回可选值
  return %167 : $Optional                   // id: %168
} // end sil function 'main.week.init(rawValue: Swift.String) -> main.week?' 

首先构造数组,从macho文件里面逐一读取cstring存放到数组中。
这里可以看到有一个_findStringSwitchCase方法,在swift源码中这个方法:

func _findStringSwitchCase(
  cases: [StaticString],
  string: String) -> Int {

  for (idx, s) in cases.enumerated() {
    if String(_builtinStringLiteral: s.utf8Start._rawValue,
              utf8CodeUnitCount: s._utf8CodeUnitCount,
              isASCII: s.isASCII._value) == string {
      return idx
    }
  }
  return -1
}

接收一个数组和目标字符串。返回index。如果找不到返回-1。所以枚举在底层就相当于Int。查找枚举也就是查找index。

  • 创建一个Array,把所有的macho中的字符串取出来依次存储到数组中。
  • 通过init参数中的值去数组中找index。然后依次与0、1、2比较。
  • 通过比较index找到相等则返回对应枚举,否则返回none。是一个可选值。

Associated Value (关联值)

从上面看到原始值只能对应一个值。如果想用枚举表达更复杂的信息时rawValue就不够用了。这个时候就可以使用Associated Value
比如:

enum Shape{
    case circle(radious: Double) //这里标签可以省略,不推荐
    case rectangle(width: Int, height: Int)
}
var circle = Shape.circle(radious: 10)
  • 添加关联值后就没有原始值了。(因为已经允许使用一组值了,通过SIL查看也没有对应的rawValue和init方法了)
  • 标签可以省略

enum模式匹配

对于一般的枚举我们直接switch case匹配就可以了如:

enum week: String {
    case MONDAY
    case TUEDAY
    case WEDDAY
    case THUDAY
    case FRIDAY
    case SATDAY
    case SUNDAY
}

var w: week?
switch w {
    case .MONDAY: print("MONDAY")
    case .SUNDAY: print("SUNDAY")
    default: print("default")
}
  • 不匹配所有case需要default,不然会报错。
  • 对于多个caseoc不同的是要用,隔开匹配。

原理是把w存储在全局变量中,匹配对应的case做代码跳转。

对于关联值想要匹配

enum Shape{
    case circle(radious: Double)
    case rectangle(width: Int, height: Int)
}

var circle = Shape.circle(radious: 10)

switch circle {
    case let .circle(radious) :  print("\(radious)")//这里相当于把常量10给了radious。
    case let .rectangle(width, height):  print("\(width) \(height)")
}
  • 进行了value - binding,如果case匹配上把值给了常量/变量。

也可以把修饰符放在变量前。

switch rectangle {
    case .circle(var radious):
        radious += 1
        print("\(radious)")
    case .rectangle(let width, var height):
        height += 10
        print("\(width) \(height)")
}

为了方便查看SIL代码,简化下代码

enum Shape{
    case circle(radious: Double)
    case rectangle(width: Int, height: Int)
}

var circle = Shape.circle(radious: 10)
var temp: Double
var w: Int
var h: Int

switch circle {
    case .circle(let radious):
        temp = radious
    case .rectangle(let width, var height):
        w = width
        h = height
}

对应sil如下:

// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer>>):
  alloc_global @main.circle : main.Shape         // id: %2
  %3 = global_addr @main.circle : main.Shape : $*Shape // users: %9, %16
  %4 = metatype $@thin Shape.Type
  %5 = float_literal $Builtin.FPIEEE64, 0x4024000000000000 // 10 // user: %6
  %6 = struct $Double (%5 : $Builtin.FPIEEE64)    // user: %7
  %7 = tuple $(radious: Double) (%6)              // user: %8
  //创建枚举值,枚举值关联值是元组(%6),也就是10
  %8 = enum $Shape, #Shape.circle!enumelt, %7 : $(radious: Double) // user: %9
  //枚举值连同关联值给到%3
  store %8 to %3 : $*Shape                        // id: %9
  alloc_global @main.temp : Swift.Double                  // id: %10
  %11 = global_addr @main.temp : Swift.Double : $*Double  // user: %23
  alloc_global @main.w : Swift.Int                     // id: %12
  %13 = global_addr @main.w : Swift.Int : $*Int        // user: %33
  alloc_global @main.h : Swift.Int                     // id: %14
  %15 = global_addr @main.h : Swift.Int : $*Int        // user: %36
  %16 = begin_access [read] [dynamic] %3 : $*Shape // users: %17, %18
  //读取%3
  %17 = load %16 : $*Shape                        // user: %19
  end_access %16 : $*Shape                        // id: %18
  //跳转bb1
  switch_enum %17 : $Shape, case #Shape.circle!enumelt: bb1, case #Shape.rectangle!enumelt: bb2 // id: %19

// %20                                            // user: %21
bb1(%20 : $(radious: Double)):                    // Preds: bb0
  //取出关联值
  %21 = tuple_extract %20 : $(radious: Double), 0 // users: %24, %22
  //关联值给到radious
  debug_value %21 : $Double, let, name "radious"  // id: %22
  %23 = begin_access [modify] [dynamic] %11 : $*Double // users: %24, %25
  store %21 to %23 : $*Double                     // id: %24
  end_access %23 : $*Double                       // id: %25
  br bb3                                          // id: %26

这也就解释了为什么switch中能访问到关联值。
1.构建一个元组
2.根据当前枚举值匹配case
3.取出元组值,声明常量使用这个值初始化

只匹配一个case的情况,除了写default的情况还可以通过if case处理

if case let Shape.circle(radious) = circle {
    print("\(radious)")
}

这里SIL代码和switch差不多,原理相同。

多个case匹配

enum Shape{
    case circle(radious: Double)
    case rectangle(width: Int, height: Int)
    case square(width: Int, height: Int)
}

var rectangle = Shape.rectangle(width: 10, height: 10)
var square = Shape.square(width: 10, height: 20)

var temp: Int

switch rectangle {
    case let .rectangle(10, x), let .square(10, x):
        temp = x
    default:
        print("not found case")
}

对应的sil(省略了main中的switch_enum相关逻辑,和上面的一样):

// %28                                            // users: %30, %29
bb1(%28 : $(width: Int, height: Int)):            // Preds: bb0
  //取出关联值 width
  %29 = tuple_extract %28 : $(width: Int, height: Int), 0 // users: %31, %33
  //取出关联值 height
  %30 = tuple_extract %28 : $(width: Int, height: Int), 1 // users: %36, %37
  debug_value %29 : $Int, let, name "$match"      // id: %31
  //常量 10
  %32 = integer_literal $Builtin.Int64, 10        // user: %34
  //从结构体取 width
  %33 = struct_extract %29 : $Int, #Int._value    // user: %34
  //比较 10 和 width
  %34 = builtin "cmp_eq_Int64"(%32 : $Builtin.Int64, %33 : $Builtin.Int64) : $Builtin.Int1 // user: %35
  //相等走bb2,否则走bb3
  cond_br %34, bb2, bb3                           // id: %35

bb2:                                              // Preds: bb1
  //将 height 给到 x
  debug_value %30 : $Int, let, name "x"           // id: %36
  //bb6是将x给到temp
  br bb6(%30 : $Int)                              // id: %37

取出case元组值,比较第一个元素,赋值第二个元素。
如果匹配的第一个元素是通配符呢?

switch rectangle {
    case let .rectangle(_, x), let .square(_, x):
        temp = x
    default:
        print("not found case")
}

可以看到直接取了height赋值给x了,这里就不需要检查第一个元素了。

// %28                                            // user: %29
bb1(%28 : $(width: Int, height: Int)):            // Preds: bb0
  %29 = tuple_extract %28 : $(width: Int, height: Int), 1 // users: %30, %31
  debug_value %29 : $Int, let, name "x"           // id: %30
  br bb3(%29 : $Int)                              // id: %31

对于一个case多个匹配的情况,常量/变量要匹配,如下:
对于.rectangle有两个参数,.square有3个参数。

enum Shape{
    case circle(radious: Double)
    case rectangle(width: Int, height: Int)
    case square(width: Int, height: Int, length: Int)
}

var rectangle = Shape.rectangle(width: 10, height: 10)
var square = Shape.square(width: 10, height: 20, length: 30)

var temp: Int

switch square {
    case let .rectangle(20, x), let .square(10, x, 30):
        temp = x
        print("\(temp)")
    default:
        print("not found case")
}

在上面.square的第三个参数必须是一个确定的值,当然也可以第二个参数是确定值,第三个参数是x

switch square {
    case let .rectangle(20, x), let .square(10, 10, x):
        temp = x
        print("\(temp)")
    default:
        print("not found case")
}

也就是说case里面的常量/变量每个case必须都有。

嵌套

如果一个复杂枚举是由单个枚举构成的,那么我们可以用嵌套来解决。

enum CombineDirect {
    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 combine = CombineDirect.leftUp(combineElement1: .left, combineElement2: .up)

当然在结构体中也可以嵌套enum:

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 .down,.up:
            print("up, down")
        }
    }
}

实际上枚举只是在不同作用于内,结构上没有影响

enum属性

enum中能包含计算属性(本身是方法)、类型属性(本身是全局变量)。不能包含存储属性。(结构体能包含存储属性是因为结构体大小就是存储属性大小)

enum Shape{
    case circle(radious: Double)
    case rectangle(width: Int, height: Int)
    static var height = 20
    //    var width: Double
    var width: Int {
        get {
            return 10
        }
    }
}

计算属性要通过case调用,类型属性通过枚举调用:

print(Shape.rectangle(width: 10, height: 10).width)
print(Shape.height)

enum方法

如果想知道星期枚举的下一天:

enum WEEK: Int {
    case MONDAY
    case TUEDAY
    case WEDDAY
    case THUDAY
    case FRIDAY
    case SATDAY
    case SUNDAY
    
    func nextDay() -> WEEK {
        if self == .SUNDAY {
            return WEEK(rawValue: 0)!
        } else {
            return WEEK(rawValue: self.rawValue + 1)!
        }
    }
}
print(WEEK.SUNDAY.nextDay())

这样做的好处是我们不需要在外面再专门定义一个方法了,直接在枚举中就可以定义方法处理了。

  • 可以定义实例方法和类型方法(static)

enum大小

rawValue枚举大小

enum Hotpot {
    case cat
}

var  hotpot = Hotpot.cat
print(MemoryLayout.size(ofValue: hotpot))
print(MemoryLayout.size)
print(MemoryLayout.stride)//步长为1

0
0
1

可以看到size0stride1。那么增加一个case看下:

enum Hotpot {
    case cat
    case dog
}
print(MemoryLayout.size)
print(MemoryLayout.stride)
1
1

sizestride都为1,那也就意味着enum在内存存储过程当中就是以1个字节长度存储的。

声明一个变量打个断点看一下:

var hotpot = Hotpot.cat

image.png

这里把0x0给到了[rip + 0x49c8],那么修改一下:

var hotpot = Hotpot.dog

Swift底层探索:enum_第5张图片
image.png

变成了把0x1给到[rip + 0x49c8]
这里对于默认的enumcaseUInt8也就是1字节。最大255,超过会自动变成UInt16
验证下:

enum Hotpot {
    case cat1
    ……
    case cat257
}
print(MemoryLayout.size)
print(MemoryLayout.stride)
2
2

断点验证:

var hotpot = Hotpot.cat257

Swift底层探索:enum_第6张图片
image.png

0x100也就是256。从0开始对应Hotpot.cat257

读取一下内存地址:

var hotpot = Hotpot.cat1
var hotpot1 = Hotpot.cat2
var hotpot2 = Hotpot.cat3
var hotpot3 = Hotpot.cat4

枚举数量小于256:

(lldb) po withUnsafePointer(to: &hotpot){print($0)}
0x000000010000805c
0 elements

(lldb) memory read 0x000000010000805c
0x10000805c: 00 01 02 03 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x10000806c: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

枚举数量大于256:

(lldb) po withUnsafePointer(to: &hotpot){print($0)}
0x000000010000c05c
0 elements

(lldb) memory read 0x000000010000c05c
0x10000c05c: 00 00 01 00 02 00 03 00 00 00 00 00 00 00 00 00  ................
0x10000c06c: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

那么在内存中通过分配对应字节大小(这里分别是1和2),0x1、0x2……都存储在内存中。

  • 如果枚举有原始值,大小取决于case的多少。
  • 枚举类型其实标识的是rawValue的值,不是case的值。
  • 对于只有一个case的情况是0,因为这个enum没有意义,多于一个则有意义。

关联值枚举大小

只有一个case的情况:

enum Shape {
    case circle(radious: Double)
}

print(MemoryLayout.size)
print(MemoryLayout.stride)
8
8

多个case的情况:

enum Shape {
    case circle(radious: Double)
    case rectangle(width: Int, height: Int)
}

print(MemoryLayout.size)
print(MemoryLayout.stride)
17
24

这里sizerectangle的大小为准8 (width)+ 8(height) + 1(case)的大小为17
内存验证下:

var  rectangle = Shape.rectangle(width: 10, height: 20)
Swift底层探索:enum_第7张图片
image.png

rectangle修改下:

enum Shape {
    case circle(radious: Double)
    case rectangle(width: Int)
}

print(MemoryLayout.size)
print(MemoryLayout.stride)

var  rectangle = Shape.rectangle(width: 10)

Swift底层探索:enum_第8张图片
image.png

size9,补齐8 stride变为16

  • 有关联值的枚举大小取决于最大的那个case+1(这里1为case本身,如果只有一个case则不加1)。

嵌套枚举大小

enum CombineDirect {
    enum BaseDirect {
        case up //00
        case down //01
        case left //02
        case right //03

    }
//0
    case leftUp(combineElement1: BaseDirect, combineElement2: BaseDirect)
//4
    case rightUp(combineElement1: BaseDirect, combineElement2: BaseDirect)
//8
    case leftDown(combineElement1: BaseDirect, combineElement2: BaseDirect)
//12
    case rightDown(combineElement1: BaseDirect, combineElement2: BaseDirect)
}

var combine = CombineDirect.leftDown(combineElement1: .left, combineElement2: .down)
print(MemoryLayout.size)
print(MemoryLayout.stride)

Swift底层探索:enum_第9张图片
image.png

这里02和BaseDirect相关,例子中是.left81中的1(因为BaseDirect占一字节)和第二个枚举值相关.down 01,系统进行了优化。8代表case leftDown
这里并不是递增,如果case很多的话会从0、1、2、3、4开始,编译器做了优化

  • 本质上取决于BaseDirect枚举的大小。

结构体中枚举大小

struct Skill {
   enum KeyType {
          case up
          case down
          case left
          case right
   }
}

print(MemoryLayout.size)
print(MemoryLayout.stride)
0
1
  • 结构体中枚举大小与只有一个case的情况相同(有关联值的枚举也一样)

这里相当于结构体中没有定义枚举,相当于没有。其实也就是结构体的大小。

内存对齐&字节对齐

内存对齐

8字节对齐

字节对齐

倍数对齐

struct Hotpot {
    var age: Int //8
    var height: UInt8 //1
    var width: UInt16 //2
}
12
16

按理说应该是11啊,调整下顺序:

struct Hotpot {
    var age: Int //8
    var width: UInt16 //2
    var height: UInt8 //1
}

print(MemoryLayout.size)
print(MemoryLayout.stride)
11
16

这里是因为编译器做了优化,这里UInt8编译器做了优化,为了偶地址。

上面说的结构体,枚举也一样:

enum Hotpot {
    //1
    case cat
    // 8 + 2 + 1 + 1(case)
    case cat1(Int,UInt16,UInt8)
    //8 + 2(偶地址) + 2 + 1(case)
    case cat2(Int,UInt8,UInt16)
}

print(MemoryLayout.size)
print(MemoryLayout.stride)
13
16
  • 计算size的时候要考虑进去偶地址
  • 计算size的时候要考虑进去字节对齐

枚举大小案例

enum Shape{
    case circle(radious: UInt8)
    //2 + 8 + 2 + 1 =  13
    case rectangle(width: UInt8, height: Int, length: UInt16)
}

print(MemoryLayout.size)
print(MemoryLayout.stride)
18
24

按照前面所说应该是2 + 8 + 2 + 1 = 13,这里由于需要8字节对齐,所以应该是8 + 8 + 2 + 1 = 19为什么是18呢?
编译器在这里做了优化

var shape = Shape.rectangle(width: 1, height: 2, length: 3)

Swift底层探索:enum_第10张图片
image.png

memory read这里是小端,验证了width占了8字节,case1字节放在了0x80,这个是枚举值的标志位。相当于对case做了优化。
rectangle换个顺序看下:

    case rectangle(width: UInt8,length: UInt16, height: Int)

应该是(2 + 2)(这里补齐到8) + 8 + 1(优化到前面) = 16

16
16

Swift底层探索:enum_第11张图片
image.png

可以看到widthheightcase存在了前8字节上。01存储的就是width偶地址补位为00010002存储的就是length补3字节000000 0002,剩余1字节存放case``ox80

枚举大小总结

  • rawValue的枚举大小默认是1字节(UInt8),case超过256个会UInt8->UInt16->UInt32……
  • 关联值枚举大小与最大case关联值相关,需要加1(case)和考虑偶地址以及字节对齐。

indirect

如果enum是一个复杂的数据结构,可以通过indirectenum简洁,比如要用结构体写一个链表:

enum List {
    case end
    case node(Element,next:List)
}

这样写直接报错:

image.png

系统要求增加indirect关键字。
由于enum是值类型,大小在编译期就确定了,在上面的例子中List大小并不能确定(由于case中有List)。
官方原文:

You indicate that an enumeration case is recursive by writing indi rect before it, which tells the compiler to insert the necessary l ayer of indirection.

enum BinaryTree {
    case empty
    indirect case node(left: BinaryTree,value:T, right:BinaryTree)
}

var node = BinaryTree.node(left: BinaryTree.empty, value: 10, right: BinaryTree.node(left: BinaryTree.empty, value: 20, right: BinaryTree.empty))
print(MemoryLayout.size(ofValue: node))
print(MemoryLayout.stride(ofValue: node))

indirect也可以放在枚举之前:

indirect  enum BinaryTree {
    case empty
    case node(left: BinaryTree,value:T, right:BinaryTree)
}
8
8

可以看到大小都是8,读取下内存:

Swift底层探索:enum_第12张图片
image.png

看内存结构像是HeapObject,分析下SIL:
Swift底层探索:enum_第13张图片
image.png

alloc_box

sil-instruction ::= 'alloc_box' sil-type (',' debug-var-attr)*
%1 = alloc_box $T
//   %1 has type $@box T

Allocates a reference-counted @box on the heap large enough to hold a value of type T, along with a retain count and any other metadata required by the runtime. The result of the instruction is the reference-counted @box reference that owns the box. The project_box instruction is used to retrieve the address of the value inside the box.
The box will be initialized with a retain count of 1; the storage will be uninitialized. The box owns the contained value, and releasing it to a retain count of zero destroys the contained value as if by destroy_addr. Releasing a box is undefined behavior if the box's value is uninitialized. To deallocate a box whose value has not been initialized, dealloc_box should be used.

大概意思是在堆空间上分配了一块内存区域,内存区域用来存放value,在例子中也就是Int值。在取值的时候取box,box是把value包裹了一层,也就是取的是地址。

alloc_box本质是swift_allocObject,所以indirect就是通知编译器当前的枚举是递归的,大小自然无法确定,所以分配堆空间存放。

Swift底层探索:enum_第14张图片
image.pn

两种方式的比较:

indirect  enum BinaryTree {
    case empty
    case node(left: BinaryTree,value:T, right:BinaryTree)
}

enum BinaryTree1 {
    case empty
    indirect case node(left: BinaryTree1,value:T, right:BinaryTree1)
}

var node = BinaryTree.node(left: BinaryTree.empty, value: 10, right: BinaryTree.empty)
var node1 = BinaryTree.empty
var node2 = BinaryTree1.node(left: BinaryTree1.empty, value: 10, right: BinaryTree1.empty)
var node3 = BinaryTree1.empty

node


Swift底层探索:enum_第15张图片
image.png

node1


Swift底层探索:enum_第16张图片
n

node2
Swift底层探索:enum_第17张图片
image.png

node3


Swift底层探索:enum_第18张图片
image.png
  • indrect关键字的本质把对应case的值放在了堆空间上。
  • 通知编译器当前的枚举是递归的,分配堆空间存放。
  • 放在case前这个case是引用类型,在enum前整个枚举类型都用引用类型存储。

Swift与OC枚举混编

  • Swift枚举非常强大,可以添加方法、extension、计算属性、类型属性。
  • OC枚举仅仅是一个整形值。

Swift枚举暴露给OC

  • @objc标记
  • 枚举应该是Int类型
@objc enum WEEK: Int  {
    case MONDAY
    case TUEDAY
    case WEDDAY
    case THUDAY
    case FRIDAY
    case SATDAY
    case SUNDAY
}

暴露给OC的是:

typedef SWIFT_ENUM(NSInteger, WEEK, closed) {
  WEEKMONDAY = 0,
  WEEKTUEDAY = 1,
  WEEKWEDDAY = 2,
  WEEKTHUDAY = 3,
  WEEKFRIDAY = 4,
  WEEKSATDAY = 5,
  WEEKSUNDAY = 6,
};

oc中调用

#import "SwiftEnum-Swift.h"

     WEEK week = WEEKMONDAY;

那么对于非Int类型的枚举呢?
1.通过swift中包装一层,也就是Int枚举,只不过Swift中封装个方法调用。对OC来说转换的只是case,没有方法。

@objc public enum WEEK: Int {
    case MONDAY
    case TUEDAY
    case WEDDAY
    func weekName() -> String {
        switch self {
        case .MONDAY: return "MONDAY"
        case .TUEDAY: return "TUEDAY"
        case .WEDDAY: return "WEDDAY"
       }
    }
}

swift中调用

var week = WEEK.MONDAY.weekName()

oc中调用

WEEK week = WEEKMONDAY;

2.通过class封装,这里相当于把class共享给OC了,自然能访问到enum。这个时候通过class getName方法 OC也能读取rawValue

class WeekName: NSObject {
    @objc enum WEEK : Int {
        case MONDAY
        case TUEDAY
        case WEDDAY

        var weekName: String {
            return WeekName.getName(self)
        }
    }

    @objc class func getName(_ fieldName:WEEK) -> String {
        switch fieldName {
            case .MONDAY: return "MONDAY"
            case .TUEDAY: return "TUEDAY"
            case .WEDDAY: return "WEDDAY"
       }
    }
}

转换如下:

enum WEEK : NSInteger;

SWIFT_CLASS("_TtC9SwiftEnum8WeekName")
@interface WeekName : NSObject
+ (NSString * _Nonnull)getName:(enum WEEK)fieldName SWIFT_WARN_UNUSED_RESULT;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end

typedef SWIFT_ENUM(NSInteger, WEEK, closed) {
  WEEKMONDAY = 0,
  WEEKTUEDAY = 1,
  WEEKWEDDAY = 2,
};

swift中调用

var week = WeekName.WEEK.MONDAY. weekName

oc中调用

WEEK week = WEEKMONDAY;
NSString *str = [WeekName getName:week];

在这里如果不想swift中使用Int枚举,可以重写rawValue,这里需要注意不能private,可以直接assertdebugcrash

Property 'rawValue' must be declared public because it matches a requirement in public protocol 'RawRepresentable'

    public var rawValue: Int {
        assert(false, "This is  a string enum,please use weekName()")
       return 0
    }

swift调用OC枚举

NS_ENUM枚举

在oc中使用NS_ENUM定义的枚举都会自动转化为swift中的枚举

typedef NS_ENUM(NSUInteger, OCEnum) {
    value1,
    value2
};
Swift底层探索:enum_第19张图片
image.png

自动转换代码:

public enum OCEnum : UInt {
    case value1 = 0
    case value2 = 1
}

在swift中调用需要在bridging中导入头文件:

//SwiftEnum-Bridging-Header.h中
#import "OCTest.h"

//调用:
var ocEnum = OCEnum.value1

非NS_ENUM枚举

不是NS_ENUM宏定义的枚举,或者用NS_OPTIONS定义的枚举:

typedef enum {
    num1,
    num2
} OCNum;

可以看到转换成了结构体:

public struct OCNum : Equatable, RawRepresentable {

    public init(_ rawValue: UInt32)

    public init(rawValue: UInt32)

    public var rawValue: UInt32
}
open class OCTest : NSObject {
}

swift调用:

var ocNumEnum = OCNum.init(rawValue: 1)
print(ocNumEnum)

输出

OCNum(rawValue: 1)

c的enum

typedef NS_ENUM(NSInteger, CEnum) {
    CEnumInValid = 0,
    CEnumA = 1,
    CEnumB = 2,
    CEnumC = 3,
};

转换后代码

public enum CEnum : Int {
    case inValid = 0
    case A = 1
    case B = 2
    case C = 3
}

调用

var cEnum = CEnum.A
cEnum = CEnum.init(rawValue: 10)!
print(cEnum.rawValue)
10

对比swiftenum

var swiftEnum = WEEK.init(rawValue: 10)
print(swiftEnum?.rawValue)
nil

对比可知,在swift中调用oc/c的枚举,对于不存在的case会返回rawValue,对于纯swift枚举返回nil

你可能感兴趣的:(Swift底层探索:enum)