Swift-進階 08:枚舉enum

Swift 進階之路 文章匯總

本文主要介紹enum的常見使用形式,以及枚舉大小是如何計算的

補充:添加腳本自動生成SIL

  • 通過target -> +,選擇 other -> Aggregate,,然后命名為CJLScript
    image
image
  • 選中CJLScript,選擇Build Phases -> New Run Script Phase
    image
  • Run Script中輸入以下命令
swiftc -emit-sil ${SRCROOT}/06、EnumTest/main.swift | xcrun swift-demangle > ./main.sil && code main.sil

然后我們就可以通過腳本自動生成SIL并自動打開啦 ??ヽ(°▽°)ノ??

C中的枚舉

在介紹swift中的枚舉之前,首先我們來回顧下C中的枚舉寫法,如下所示

enum 枚舉名{
    枚舉值1,
    枚舉值2,
    ......
};

<!--舉例:表示一周7天-->
enum Week{
    MON, TUE, WED, THU, FRI, SAT, SUN
};

<!--更改C中枚舉默認值-->
//如果沒有設置枚舉默認值,一般第一個枚舉成員的默認值為整型0,后面依次遞推
enum Week{
    MON = 1, TUE, WED, THU, FRI, SAT, SUN
};

<!--C中定義一個枚舉變量-->
//表明創建了一個枚舉,并聲明了一個枚舉變量Week
enum Week{
    MON = 1, TUE, WED, THU, FRI, SAT, SUN
}week;
//或者下面這種寫法,省略枚舉名稱
enum{
    MON = 1, TUE, WED, THU, FRI, SAT, SUN
}week;

Swift中的枚舉

在swift中,枚舉的創建方式如下所示,如果沒有指定枚舉值的類型,那么enum默認枚舉值是整型

<!--1、寫法一-->
enum Week{
    case MON
    case TUE
    case WED
    case THU
    case FRI
    case SAT
    case SUN
}

<!--2、寫法二-->
//也可以直接一個case,然后使用逗號隔開
enum Week{
    case MON, TUE, WED, THU, FRI, SAT, SUN
}

<!--定義一個枚舉變量-->
var w: Week = .MON
  • 如果此時想創建一個枚舉值是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屬性的

image

枚舉的訪問方式如下所示

enum Week: String{
    case MON, TUE, WED, THU, FRI, SAT, SUN
}
var w = Week.MON.rawValue
<!--訪問-->
print(w)

<!--打印結果-->
MON

這里就有一個疑問,swift是如何做到打印 MON的?我們通過SIL文件分析

  • 首先查看SIL文件中的enum,底層多增加了一些東西
    • 1、給枚舉值的類型,通過typealias取了一個別名RawValue

    • 2、默認添加了一個可選類型的init方法

    • 3、增加一個計算屬性rawValue,用于獲取枚舉值的原始值

image
  • 查看SIL中的main方法,可以得知w是通過枚舉值的rawValueget方法獲取
    image
  • 查看SIL文件rawValue的get方法,主要有以下幾步:
    • 1、接收一個枚舉值,用于匹配對應的分支

    • 2、在對應分支創建對應的String

    • 3、返回對應的String

image

結論1:使用rawValue的本質是調用get方法

但是get方法中的String是從哪里來的呢?String存儲在哪里?

  • 其實這些對應分支的字符串在編譯時期就已經存儲好了,即存放在Maach-O文件的__TEXT.cstring中,且是連續的內存空間,可以通過編譯后查看Mach-O文件來驗證
    image

結論2rawValueget方法中的分支構建的字符串,主要是從Mach-O文件對應地址取出的字符串,然后再返回給w

總結

  • 使用rawValue的本質就是在底層調用get方法,即在get方法中從Mach-O對應地址中取出字符串并返回的操作

區分 case枚舉值 & rawValue原始值

請問下面這段代碼的打印結果是什么?

//輸出 case枚舉值
print(Week.MON)
//輸出 rawValue 
print(Week.MON.rawValue)

<!--打印結果-->
MON
MON

雖然這兩個輸出的值從結果來看是沒有什么區別的,雖然輸出的都是MON,但并不是同一個東西

  • 第一個輸出的case枚舉值

  • 第二個是通過rawValue訪問的rawValueget方法

如果我們像下面這種寫法,編譯器就會報錯


image

枚舉的init調用時機

主要是探索枚舉的init會在什么時候調用

  • 定義一個符號斷點Week.init
    image
  • 定義如下代碼
print(Week.MON.rawValue)

let w = Week.MON.rawValue

通過運行結果發現,都是不會走init方法的

  • 如果是通過init方式創建enum呢?
print(Week.init(rawValue: "MON"))

運行結果如下


image

注:這個斷點首先需要通過init前的一個斷點 + Week.init符號斷點+init符號斷點,一起配合,才能斷住

總結:enum中init方法的調用是通過枚舉.init(rawValue:)或者枚舉(rawValue:)觸發的

我們再繼續來分析init方法,來看下面這段代碼的打印結果是什么?

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方法進行匹配

image

其中
- index_addr 表示獲取當前數組中的第n個元素值的地址,然后再把構建好的字符串放到當前地址中

- `struct_extract` 表示`取出當前的Int值`,Int類型在系統中也是結構體
- `cond_br` 表示比較的表達式,即分支條件跳轉
    - 如果匹配成功,則構建一個`.some的Optional`返回
    - 如果匹配不成功,則繼續匹配,知道最后還是沒有匹配上,則構建一個`.none的Optional`返回
  • swift-source中查找_findStringSwitchCase方法,接收兩個參數,分別是 數組 + 需要匹配的String
    • 1、遍歷數組,如果匹配則返回對應的index
    • 2、如果不匹配,則返回-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方法
    • 1、如果沒有匹配成功,則構建一個.none類型的Optional,表示nil
    • 2、如果匹配成功,則構建一個.some類型的Optional,表示有值
image

所以,這也是為什么一個打印可選值,一個打印nil的原因

枚舉的遍歷

CaseIterable協議通常用于沒有關聯值的枚舉,用來訪問所有的枚舉值,只需要對應的枚舉遵守該協議即可,然后通過allCases獲取所有枚舉值,如下所示

<!--1、定義無關聯值枚舉,并遵守協議-->
enum Week: String{
    case MON, TUE, WED, THU, FRI, SAT, SUN
}
extension Week: CaseIterable{}

<!--2、通過for循環遍歷-->
var allCase = Week.allCases
for c in allCase{
    print(c)
}

<!--3、通過函數式編程遍歷-->
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了,簡稱三無枚舉(個人叫法,大家隨意哈)

    image

  • 其中關聯值中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中的模式匹配其實就是匹配case枚舉值

簡單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,做對應的代碼跳轉

image

具有關聯值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中的關聯值的模式匹配,如下圖所示

  • 1、首先構建一個關聯值的元組

  • 2、根據當前case枚舉值,匹配對應的case,并跳轉

  • 3、取出元組中的值,將其賦值給匹配case中的參數


    image
  • 通過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
}

注:

  • 枚舉使用過程中不關心某一個關聯值,可以使用通配符_表示
  • OC只能調用swift中Int類型的枚舉

枚舉的嵌套

枚舉的嵌套主要用于以下場景:

  • 1、【枚舉嵌套枚舉】一個復雜枚舉是由一個或多個枚舉組成

  • 2、【結構體嵌套枚舉】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大小講解),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)

indirect關鍵字

如果我們想要表達的enum是一個復雜的關鍵數據結構時,可以通過indirect關鍵字來讓當前的enum更簡潔

//用枚舉表示鏈表結構
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>)
}

為什么呢?

  • 因為enum值類型,也就意味著他們的大小在編譯時期就確定了,那么這個過程中對于當前的enum的大小是不能確定的,從系統的角度來說,不知道需要給enum分配多大的空間,以下是官方文檔的解釋
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的大小
enum List<T>{
    case end
    indirect case node(T, next: List<T>)
}
print(MemoryLayout<List<Int>>.size)
print(MemoryLayout<List<Int>>.stride)

<!--打印結果-->
8 //size大小是8
8 //stride大小是8

如果傳入的類型是String呢?


image

從結果發現,換成其他類型,其結果依舊是8,這是為什么呢?

下面來分析其內存結構,首先需要定義一個全局變量

enum List<T>{
    case end
    indirect case node(T, next: List<T>)
}

var node = List<Int>.node(10, next: List<Int>.end)

print(MemoryLayout.size(ofValue: node))
print(MemoryLayout.stride(ofValue: node))

通過lldb分析其內存


image

所以indirect關鍵字其實就是通知編譯器,我當前的enum是遞歸的,大小是不確定的,需要分配一塊堆區的內存空間,用來存放enum

  • 如果是end,此時存儲的是case值,而case為node時存儲的是引用地址


    image

    然后再通過插件來查看哪個地址在堆上,哪個地址在棧上


    image
  • 這一點也可以通過SIL來驗證


    image
  • 也可以通過node的斷點來驗證,確實是執行了swift_allocObject

    image

swift和OC混編enum

在swift中,enum非常強大,可以添加方法、添加extension
而在OC中,enum僅僅只是一個整數值

如果想將swift中的enum暴露給OC使用:

  • @objc關鍵字標記enum
  • 當前enum應該是Int類型

OC調用Swift的enum

<!--swift中定義-->
@objc enum Week: Int{
    case MON, TUE, WED, THU, FRI, SAT, SUN
}

<!--OC使用-->
- (void)test{
    Week mon = WeekMON;
}

Swift調用OC的enum
OC中的枚舉會自動轉換成swift中的enum

<!--OC定義-->
//會自動轉換成swift的enum
NS_ENUM(NSInteger, OCENUM){
    Value1,
    Value2
};

<!--swift使用-->
//1、將OC頭文件導入橋接文件
#import "CJLTest.h"
//2、使用
let ocEnum = OCENUM.Value1

如果OC中是使用typedef enum定義的,自動轉換成swift就成了下面這樣

typedef enum {
    Num1,
    Num2
}OCNum;

<!--swift中使用-->
let ocEnum = OCNum.init(0)
print(ocEnum)

//*******打印結果*******
OCNum(rawValue: 0)

自動轉換成swift中的如下所示,通過typedef enum定義的enum,在swift中變成了一個結構體,并遵循了兩個協議:EquatableRawRepresentable

image

如果在OC中使用typedef NS_ENUM定義枚舉呢?

typedef NS_ENUM(NSInteger, CENUM){
    CEnumInvalid = 0,
    CEnumA = 1,
    CEnumB,
    CEnumC
};

自動轉換成swift后的結果如下


image

問題:OC如何訪問swift中String類型的enum?

  • swift中的enum盡量聲明成Int整型
  • 然后OC調用時,使用的是Int整型的
  • enum在聲明一個變量/方法,用于返回固定的字符串,用于在swift中使用
@objc enum Week: 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中使用-->
Week mon = WeekMON;

<!--swift中使用-->
let Week = Week.MON.val

枚舉的大小

主要分析以下幾種情況的大?。?/p>

  • 1、普通enum

  • 2、具有關聯值的enum

  • 3、enum嵌套enum

  • 4、struct嵌套enum

1、普通enum大小分析

在前面提及enum中不能包含存儲屬性,其根本在于enum的大小與Struct的計算方式是不一樣的,這里我們將展開詳細的分析

  • 首先,我們先來看看下面這段代碼的打印結果是什么?
enum NoMean{
    case a
}
print(MemoryLayout<NoMean>.size)
print(MemoryLayout<NoMean>.stride)

<!--打印結果-->
0 //size大小是0
1 //表示訪問下一個NoMean的case時,需要跨越1字節的步長
  • 如果此時增加一個 case b,此時的打印結果是什么?
enum NoMean{
    case a
    case b
}
print(MemoryLayout<NoMean>.size)
print(MemoryLayout<NoMean>.stride)

<!--打印結果-->
1 //size大小是1
1 //步長是1
  • 如果在增加多個呢?
enum NoMean{
    case a
    case b
    case c
    case d
}
print(MemoryLayout<NoMean>.size)
print(MemoryLayout<NoMean>.stride)

<!--打印結果-->
1
1

從結果來看,仍然是1,說明enum就是以1字節存儲在內存中的,這是為什么呢?我們來分析下

斷點分析

  • 首先通過斷點來分析,case分別a、b、c的情況
    image

    從斷點可以看出,
    • case是UInt8,即1字節(8位),最大可以存儲255

    • 如果超過了255,會自動從UInt8 -> UInt16 -> UInt32 -> UInt64 ...

LLDB分析

  • 分別定義4個全局變量tmp、tmp1、tmp2、tmp3
enum NoMean{
    case a
    case b
    case c
    case d
}

var tmp = NoMean.a
var tmp1 = NoMean.b
var tmp2 = NoMean.c
var tmp3 = NoMean.d

通過lldb查看內存情況如下,case都是1字節大小


image

普通enum總結

  • 1、如果enum中有原始值,即rawValue,其大小取決于case的多少,如果沒有超過UInt8即255,則就是1字節存儲case

  • 2、Int標識的其實就是 RawValue的值

  • 3、當只有一個case的情況下,size0,表示這個enum是沒有意義的,

  • 4、當有兩個及以上case時,此時的enum是有意義的,如果沒有超過255,則case的步長是1字節,如果超過,則UInt8->UInt16...,以此類推

2、具有關聯值enum的大小分析

如果enum中有關聯值,其大小又是多少呢?有如下代碼,打印其size和stride

enum Shape{
    case circle(radius: Double)
    case rectangle(width: Double, height: Double)
}
print(MemoryLayout<Shape>.size)
print(MemoryLayout<Shape>.stride)

<!--打印結果-->
17 //size的大小是17
24 //stride的步長是24

說明從打印結果可以說明 enum中有關聯值時,其內存大小取決于關聯值的大小

  • enum有關聯值時,關聯值的大小 取 對應枚舉關聯值 最大的,例如circle中關聯值大小是8,而rectangle中關聯值大小是16,所以取16。所以enum的size = 最大關聯值大小 + case(枚舉值)大小 = 16 + 1 = 17,而stride由于8字節對齊,所以自動補齊到24
    • 定義一個全局變量,觀察其內存


      image

總結

  • 1、具有關聯值的enum大小,取決于最大case的內存大小【枚舉大小的本質】

  • 2、關聯值枚舉的大小 = 最大case的內存大小 + 1(case的大?。?/p>

  • 3、size 表示 實際大小

  • 4、stride 表示 對齊后的大?。▋却婵臻g中真實占用的大?。?/p>

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 //size大小,enum有關聯值取決于關聯值的大小,每個case都有2個大小為1的enum,所以為2
2 //stride大小

從結果中說明enum嵌套enum同具有關聯值的enum是一樣的,同樣取決于關聯值的大小,其內存大小是最大關聯值的大小

通過嵌套枚舉定義一個全局變量

var combine = CombineDirect.leftDown(baseDirect1: .left, baseDirect2: .down)

查看其內存情況如下

image

這里我們會有一個疑問,其中的81到底指的是什么?這里先提前劇透下:8表示 case leftDown的枚舉值,1表示其中down的枚舉值,下面我們來驗證

在上面這個例子中,是有4個case,其case在內存中是用0、4、8、12體現的,如果是有很多個case,是否還滿足我們現在這樣的規律呢?

  • 【嘗試1】:在4個case的基礎上增加了10個case

    • 查看case downDown1,在內存中為0x1,即1
      嘗試1-1
    • 查看case rightUp,在內存中為0xb,即11
      嘗試1-2

      從這里可以發現case是從0、1、2....這樣依次往后的順序
  • 【嘗試2】:如果去掉其中的幾種情況呢,發現case依舊是0、1、2....


    嘗試2
  • 【嘗試3】:當只有2個case時,發現case的枚舉值是0、8

    嘗試3

  • 【嘗試4】:當有3個case時,發現case的枚舉值是 0、4、8

    嘗試4

PS:至于為什么會是這樣的結果,目前也沒找到任何依據,后續如果有了依據,再來補充吧(有知道的童鞋,歡迎留言~)

總結

  • 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...以此類推

4、結構體嵌套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 .up, .down:
            print("up, down")
        }
    }
}
print(MemoryLayout<Skill>.size)
print(MemoryLayout<Skill>.stride)

<!--打印結果-->
1
1
  • 如果只嵌套了enum,沒有聲明變量,結構體的大小是多少呢?
struct Skill {
    enum KeyType{
        case up
        case down
        case left
        case right
    }
}
print(MemoryLayout<Skill>.size)
print(MemoryLayout<Skill>.stride)

<!--打印結果-->
0 //size的大小取決于成員變量,但是struct中目前沒有屬性
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
    }
    
    var width: Int //8字節

    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)

<!--打印結果-->
10 //size大?。ㄅcOC中的結構體大小計算是一致的,min(m,n),其中m表示存儲的位置,n表示屬性的大小,要求是:m必須整除n)
16 //stride大小

結論

  • 1、如果結構體中沒有其他屬性,只有枚舉變量,那么結構體的大小就是枚舉的大小,即size為1

  • 2、如果結構體中嵌套了enum,但是沒有聲明變量,此時的size是0,stride是1

  • 3、如果結構體中還有其他屬性,則按照OC中的結構體內存對齊三原則進行分析(參考iOS-底層原理 05:內存對齊原理這篇文章)

內存對齊 & 字節對齊 區分

  • 內存對齊:iOS中是8字節對齊,蘋果實際分配采用16字節對齊,這種只會在分配對象時出現

  • 字節對齊:存儲屬性的位置必須是地址,即OC內存對齊中的min(m,n),其中m表示存儲的位置,n表示屬性的大小,需要滿足位置m整除n時,才能從該位置存放屬性。簡單來說,就是必須在自身的倍數位置開始

  • 外部調用對象時,對象是服從內存對齊

  • 單純從結構上說,結構內部服從最大字節對齊。

例如下面這個例子

struct Skill {
    var age: Int //8字節
    var height: UInt8 //1字節
    var width: UInt16 //2字節
}
print(MemoryLayout<Skill>.size)
print(MemoryLayout<Skill>.stride)

<!--打印結果-->
12
16
  • size為12的原因是:內存從0位置開始Int是占據0-7,UInt8占據8,下一個位置是9,但是UInt16是2字節對齊的要在它的倍數位置開始所以找下一個可以整除它的位置也就是UInt16占據10-11正好整個size在0-11,所以size為12

  • stride為16的原因:stride是實際分配的,必須是最大屬性大小的整數倍,即8的倍數,所以是16

總結

  • 枚舉說明:
    • 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 / 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中內存對齊原則進行計算

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念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

推薦閱讀更多精彩內容

  • swift進階 學習大綱[http://www.lxweimin.com/p/0fc67b373540] 本節,分...
    markhetao閱讀 1,664評論 0 5
  • C enum C語言枚舉格式 比如要表示星期幾的枚舉: 第一個枚舉成員的默認值為0,依次加1。要改變枚舉值直接賦值...
    HotPotCat閱讀 646評論 1 3
  • C語言的枚舉 C語言的枚舉寫法 我們通過枚舉表示一周的七天 c語言中,枚舉的第一個成員默認是為0,后面的枚舉值一次...
    浪的出名閱讀 404評論 0 1
  • C語言枚舉 一周七天可以寫成 第?個枚舉成員的默認值為整型的 0,后?的枚舉值依次類推,如果我們想更改,只需要這樣...
    Mjs閱讀 241評論 0 1
  • title: "Swift 中枚舉高級用法及實踐"date: 2015-11-20tags: [APPVENTUR...
    guoshengboy閱讀 2,602評論 0 2