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<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  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存在哪里呢?

image.png

可以看到是連續的內存空間來存儲字符串。在程序運行的時候會通過地址+偏移量取找到這個字符串。

case和rawValue

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

caserawValue輸出一樣,那么這兩個相同么?

image.png

image.png

顯然不是一個東西,不是同一個類型。這里肯定也不能互相賦值,并且也不能對rawValue賦值(只有getter方法)。

init?

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

print(week.init(rawValue:"MONDAY")!)//MONDAY
print(week.init(rawValue:"123"))//nil
MONDAY
nil
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<week> {
// %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<A>(_:)
  //調用方法創建一個數組
  %5 = function_ref @Swift._allocateUninitializedArray<A>(Builtin.Word) -> ([A], Builtin.RawPointer) : $@convention(thin) <τ_0_0> (Builtin.Word) -> (@owned Array<τ_0_0>, Builtin.RawPointer) // user: %6
 //返回一個元組
  %6 = apply %5<StaticString>(%4) : $@convention(thin) <τ_0_0> (Builtin.Word) -> (@owned Array<τ_0_0>, Builtin.RawPointer) // users: %8, %7
 //返回值是一個元組類型(Array,RawPointer當前指針)
  %7 = tuple_extract %6 : $(Array<StaticString>, Builtin.RawPointer), 0 // users: %80, %79
  %8 = tuple_extract %6 : $(Array<StaticString>, 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<StaticString>, @guaranteed String) -> Int // user: %79
  //拿到數組中元素的index
  %79 = apply %78(%7, %0) : $@convention(thin) (@guaranteed Array<StaticString>, @guaranteed String) -> Int // users: %149, %138, %127, %116, %105, %94, %83, %147, %136, %125, %114, %103, %92, %81
  release_value %7 : $Array<StaticString>         // 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<week>, #Optional.none!enumelt // user: %161
  br bb30(%160 : $Optional<week>)                 // id: %161

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

// %167                                           // user: %168
bb30(%167 : $Optional<week>):                     // Preds: bb29 bb28
//返回可選值
  return %167 : $Optional<week>                   // 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<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  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<Hotpot>.size)
print(MemoryLayout<Hotpot>.stride)//步長為1

0
0
1

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

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

sizestride都為1,那也就意味著enum在內存存儲過程當中就是以1個字節長度存儲的。

聲明一個變量打個斷點看一下:

var hotpot = Hotpot.cat

image.png

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

var hotpot = Hotpot.dog

image.png

變成了把0x1給到[rip + 0x49c8]
這里對于默認的enumcaseUInt8也就是1字節。最大255,超過會自動變成UInt16
驗證下:

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

斷點驗證:

var hotpot = Hotpot.cat257

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<Shape>.size)
print(MemoryLayout<Shape>.stride)
8
8

多個case的情況:

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

print(MemoryLayout<Shape>.size)
print(MemoryLayout<Shape>.stride)
17
24

這里sizerectangle的大小為準8 (width)+ 8(height) + 1(case)的大小為17
內存驗證下:

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

rectangle修改下:

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

print(MemoryLayout<Shape>.size)
print(MemoryLayout<Shape>.stride)

var  rectangle = Shape.rectangle(width: 10)

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<CombineDirect>.size)
print(MemoryLayout<CombineDirect>.stride)

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<Skill>.size)
print(MemoryLayout<Skill>.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<Hotpot>.size)
print(MemoryLayout<Hotpot>.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<Hotpot>.size)
print(MemoryLayout<Hotpot>.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<Shape>.size)
print(MemoryLayout<Shape>.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)

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

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<Element> {
    case end
    case node(Element,next:List<Element>)
}

這樣寫直接報錯:

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<T> {
    case empty
    indirect case node(left: BinaryTree,value:T, right:BinaryTree)
}

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

indirect也可以放在枚舉之前:

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

可以看到大小都是8,讀取下內存:

image.png

看內存結構像是HeapObject,分析下SIL:
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就是通知編譯器當前的枚舉是遞歸的,大小自然無法確定,所以分配堆空間存放。

image.pn

兩種方式的比較:

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

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

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

node


image.png

node1


n

node2
image.png

node3


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
};
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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容