前言
本篇文章將講述Swift中很常用的也很重要的一個知識點 ?? Enum枚舉
。首先會介紹與OC中枚舉的差別,接著會從底層分析Enum
的使用場景,包含枚舉的嵌套
和遞歸
,與OC的混編
的場景,最后分析枚舉的內存大小
的計算方式,希望大家能夠掌握。
一、OC&Swift枚舉的區別
1.1 OC中的NS_ENUM
OC中的枚舉和C/C++中的枚舉基本一樣,具有以下特點??
- 僅支持
Int類型
,默認首元素值為0
,后續元素值依次+1
-
中間的元素
有賦值,那么以此賦值為準
,后續沒賦值
的元素值依舊依次+1
枚舉使用示例代碼??
typedef NS_ENUM(NSInteger, WEEK) {
Mon,
Tue = 10,
Wed,
Thu,
Fri,
Sat,
Sun
};
// 調用代碼??
WEEK a = Mon;
WEEK b = Tue;
NSLog(@"a = %d, b = %d", (int)a, (int)b);
1.2 Swift中的Enum
Swift中的枚舉比OC的強大很多
!其特點如下??
- 格式: 不用逗號分隔,類型需使用case聲明
- 內容:
- 支持
Int、Double、String
等基礎類型
,也有默認枚舉值
(String類型
默認枚舉值為key的名稱
,Int、Double
數值型默認枚舉值為0
開始+1遞增
- 支持
自定義選項
??不指定
支持類型
,就沒有rawValue
,但同樣支持case枚舉
,可自定義關聯內容
- 支持
注意:
rawValue
在后面枚舉的訪問
中會詳細的講解。
示例代碼??
// 寫法一
// 不需要逗號隔開
enum Weak1 {
case MON
case TUE
case WED
case THU
case FRI
case SAT
case SUN
}
// 寫法二
// 也可以直接一個case,然后使用逗號隔開
enum Weak2 {
case MON, TUE, WED, THU, FRI, SAT, SUN
}
// 定義一個枚舉變量
var w: Weak1 = .MON
1.2.2 自定義選項類型
如果在聲明枚舉時不指定類型
,那么可給枚舉項添加拓展內容
(即自定義類型
)。switch-case
訪問時,可取出拓展類型進行相應的操作。例如??
// 自定義類型的使用
enum Shape {
case square(width: Double)
case circle(radius: Double, borderWidth:Double)
}
func printValue(_ v: Shape) {
// switch區分case(不想每個case處理,可使用default)
switch v {
case .square(let width):
print(width)
case .circle(let radius, _):
print(radius)
}
}
let s = Shape.square(width: 10)
let c = Shape.circle(radius: 20, borderWidth: 1)
printValue(s)
printValue(c)
二、Swift枚舉的使用
接下來我們看看Swift枚舉的使用,包含一些特殊的場景的情況。
2.1 枚舉的訪問
說到枚舉的訪問,就必須得提一個關鍵字rawValue
,使用案例??
enum Weak: String{
case MON, TUE, WED, THU, FRI, SAT, SUN
}
var w = Weak.MON.rawValue
print(w)
運行結果??
注意:如果enum
沒有聲明類型
,是沒有rawValue
屬性的??
現在問題來了 ?? rawValue
對應在底層是如何做到讀取到MON
值的?
rawValue的取值流程
老規矩,找入口,之前我們都是查看SIL,當然這里也不例外??
swiftc -emit-sil xx.swift | xcrun swift-demangle >> ./xx.sil && vscode xx.sil
先看看枚舉Week??
接著看看main函數的流程??
最后看看rawValue的getter方法??
然后看bb8代碼段??
至此,我們現在知道了,rawValue
的底層就是調用的getter方法
,getter方法中構造了字符串
,但是這個字符串的值
(例如“MON”)從哪里取出
的呢?其實我們能猜出來,應該是在編譯期確定
了的,所以,我們打開工程的exec可執行文件,查看Mach-O
??
可見,在__TEXT, __cstring
的section段,這些字符串在編譯期已經存儲好了,而且內存地址是連續的
。所以,rawValue
的getter方法 ?? case分支中構建的字符串,主要是在Mach-O
文件中從對應地址
取出的字符串,然后再返回給變量w
。
case值 & rawValue值
現在我們弄清楚了rawValue
值的來源,那么又有一個問題:枚舉case
值和 rawValue
值如何區分
呢?下面的代碼輸出打印結果是什么?
//輸出 case值
print(Weak.MON)
//輸出 rawValue值
print(Weak.MON.rawValue)
雖然輸出的都是MON
,但其實并不是相同的,why?看下圖??
上圖可知,并不能將枚舉的case值
賦給字符串類型常量w
,同時,也不能將字符串"MON"
賦給枚舉值t
。
2.2 枚舉初始化init
在OC中,枚舉沒有初始化一說,而在Swift中,枚舉是有init初始化方法
??
Weak.init(rawValue:)
接下來我們來看看這個初始化方法在底層的流程
,首先添加代碼??,打上斷點
print(Weak.init(rawValue: "MON")!)
打開匯編,運行??
接著我們還是看SIL代碼,關于Weak.init(rawValue:)
部分??
其中,上圖中涉及的SIL的指令釋義??
指令名稱 | 指令釋義 |
---|---|
index_addr | 獲取當前數組中的第n個元素值 的地址(即指針 ),存儲到當前地址中 |
struct_extract | 表示在結構體中取出當前的Int值 ,Int類型在系統中也是結構體 |
cond_br | 表示比較的表達式 ,即分支條件跳轉 (類似于三元表達式) |
接著來看看這個關鍵的函數_findStringSwitchCase
的源碼??
我們繼續看Weak.init
的最終處理代碼 ?? bb29
代碼段??
至此,我們分析完了Weak.init
的底層流程,于是修改之前的調用代碼(去掉
了之前的感嘆號!
)??
print(Weak.init(rawValue: "MON"))
print(Weak.init(rawValue: "Hello"))
編譯器會爆出警告(返回的結果是可選型
),運行結果??
所以,現在我們就能明白,為什么一個打印的是可選值,一個打印的是nil。
2.3 枚舉遍歷:CaseIterable協議
CaseIterable協議
,有allCases
屬性,支持遍歷所有
case,例如??
// Double類型
enum Week1: Double, CaseIterable {
case Mon,Tue, Wed, Thu, Fri, Sat, Sun
}
Week1.allCases.forEach { print($0.rawValue)}
// String類型
enum Week2: String, CaseIterable {
case Mon,Tue, Wed, Thu, Fri, Sat, Sun
}
Week2.allCases.forEach { print($0.rawValue)}
2.4 枚舉關聯值
關聯值
就是上面講過的自定義類型
的枚舉,它能表示更復雜的信息,與普通類型的枚舉不同點在于??
- 沒有rawValue
- 沒有rawValue的getter方法
- 沒有初始化init方法
例如
// 自定義類型的使用
enum Shape {
case square(width: Double)
case circle(radius: Double, borderWidth:Double)
}
查看其SIL代碼??
中間層代碼真的什么都沒有!??
2.5 模式匹配
模式匹配
就是針對case的匹配
,根據枚舉類型,分為2種:
- 簡單類型的枚舉的模式匹配
- 自定義類型的枚舉(關聯值)的模式匹配
2.5.1 簡單類型
swift中的簡單類型enum匹配需要將
所有情況都列舉
,或者使用default表示默認情況
,否則會報錯
!
enum Weak: String{
case MON
case TUE
case WED
case THU
case FRI
case SAT
case SUN
}
var current: Weak?
switch current {
case .MON:print(Weak.MON.rawValue)
case .TUE:print(Weak.MON.rawValue)
case .WED:print(Weak.MON.rawValue)
default:print("unknow day")
}
如果去掉default
,會報錯??
我們看看SIL代碼??
所以運行上面代碼,應該匹配的是default分支??
2.5.2 關聯值類型
關聯值類型的模式匹配有兩種方式??
- switch - case ?? 匹配所有case
- if - case ?? 匹配單個case
switch - case
enum Shape{
case circle(radius: Double)
case rectangle(width: Int, height: Int)
}
let
修飾case
值??
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)")
}
也可以let var
修飾關聯值的入參
??
let shape = Shape.circle(radius: 10.0)
switch shape{
case .circle(let radius):
print("circle radius: \(radius)")
case .rectangle(let width, var height):
height += 1
print("rectangle width: \(width) height: \(height)")
}
查看SIL層的代碼,看看是怎么匹配的??
if - case
let circle = Shape.circle(radius: 10.0)
if case let Shape.circle(radius) = circle {
print("circle radius: \(radius)")
}
通用關聯值
如果只關心不同case
下的某一個關聯值
,可以將該關聯值用同一個入參
替換,例如下面例子中的x
??
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:
print("未匹配")
break
}
注意:不能使用多于1個的通用入參,例如下面的
y
??
也可以使用通配符 _
??
let shape = Shape.rectangle(width: 10, height:20)
switch shape{
case let .rectangle(_, x), let .square(_, x):
print("x = \(x)")
default:
break
}
還可以這么寫??
let shape = Shape.rectangle(width: 10, height:20)
switch shape{
case let .rectangle(x, _), let .square(_, x):
print("x = \(x)")
default:
break
}
大家平時在使用枚舉時,還是要注意下面2點??
- 枚舉使用過程中不關心某一個關聯值,可以使用
通配符_
標識- OC只能調用Swift中
Int類型
的枚舉
2.6 支持計算型屬性 & 函數
Swift枚舉中還支持計算屬性
和函數
,例如??
enum Direct: Int {
case up
case down
case left
case right
// 計算型屬性
var description: String{
switch self {
case .up:
return "這是上面"
default:
return "這是\(self)"
}
}
// 函數
func printSelf() {
print(description)
}
}
Direct.down.printSelf()
三、枚舉嵌套
枚舉的嵌套主要有2種場景??
- 枚舉嵌套枚舉
- 結構體嵌套枚舉
3.1 enum嵌套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)
3.2 struct嵌套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")
}
}
}
3.3 枚舉的遞歸(indirect)
還有一種特殊的場景 遞歸
?? 枚舉中case關聯內容
使用自己的枚舉類型
。例如??
enum Binary<T> {
case empty
case node(left: Binary, value:T, right:Binary)
}
一個樹
結構,其左右節點的類型也是自己本身,這時編譯器會報錯??
報錯原因 ?? 使用該枚舉時,enum的大小
需要case
來確定,而case的大小又需要使用到enum大小
。所以無法計算
enmu的大小,于是報錯!
安排 ?? 根據編譯器提示,需要使用關鍵字indirect
,意思就是將該枚舉標記位遞歸
,同時也支持標記單個case
,所以可以??
那么問題來了,indirect
在底層干了什么呢?
indirect底層原理
我們先來看這個例子??
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))
size和stride都是8,換成String類型??
仍然也是8,看來枚舉的大小不受其模板類型大小的影響。
lldb分析
我們先lldb看看其內存的分布??
上圖中我們發現,node的metadata
對應的地址0x0000000100562660
是分配在堆
上的,所以,indirect關鍵字其實就是通知編譯器
,需要分配一塊堆區
的內存空間,用來存放enum
或 case
。此時case為node
時,存儲的是引用地址0x0000000100562660
,而case為end
時,則??
那為何說地址是堆區呢?我們接著看看SIL代碼??
SIL代碼中,是通過alloc_box申請的內存,alloc_box底層調用的是swift_allocObject
,所以是堆區,我們可以再node打上斷點,查看匯編??
四、##swift和OC混編枚舉
接下來我們看看swift和OC的枚舉的混編場景。
4.1 OC使用Swift枚舉
首先看看OC調用Swift枚舉,那么此時枚舉必須具備以下2個條件??
- 用
@objc
關鍵字標記enum - 當前enum必須是
Int類型
// Swift中定義枚舉
@objc enum Weak: Int{
case MON, TUE, WED, THU, FRI, SAT, SUN
}
// OC使用
- (void)test{
Weak mon = WeakMON;
}
4.2 Swift使用OC枚舉
反過來,就沒限制了,OC中的枚舉會自動轉換
成swift中的enum。
// OC定義
NS_ENUM(NSInteger, OCENUM){
Value1,
Value2
};
// swift使用
//1、將OC頭文件導入橋接文件
#import "OCFile.h"
//2、使用
let ocEnum = OCENUM.Value1
typedef enum
// OC定義
typedef enum {
Num1,
Num2
}OCNum;
// swift使用
let ocEnum = OCNum.init(0)
print(ocEnum)
上圖可知,通過typedef enum
定義的enum,在swift中變成了一個結構體,并遵循了兩個協議:Equatable
和 RawRepresentable
。
typedef NS_ENUM
// OC定義
typedef NS_ENUM(NSInteger, OCNum) {
Num1,
Num2
};
// swift使用
let ocEnum = OCNum.init(rawValue: 0)
print(ocEnum!)
那么自動生成的swift中是這樣??
并沒有遵循任何協議!
4.3 OC使用Swift中String類型的枚舉
這也是一種常見的場景,解決方案??
- swift中的enum盡量聲明成Int整型
- 然后OC調用時,使用的是Int整型的
- enum再聲明一個
變量/方法
,用于返回固定的字符串
,給swift中使用
示例??
@objc enum Weak: 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中使用
Weak mon = WeakMON;
// swift中使用
let weak = Weak.MON.val
五、枚舉的大小
主要分析以下幾種情況??
- 普通enum
- 具有關聯值的enum
- enum嵌套enum
- struct嵌套enum
枚舉的大小也是面試中經常問到的問題,重點在于兩個函數的區別??
size
:實際占用
內存大小
stride
:系統分配
的內存大小
5.1 普通enum
最普通的情況,即非嵌套,非自定義類型
的枚舉,例如??
enum Weak {
case MON
}
print(MemoryLayout<Weak>.size)
print(MemoryLayout<Weak>.stride)
再添加一個case,運行??
繼續增加多個case,運行??
以上可以看出,當case個數為1時,枚舉size為0,個數>=2時,size的大小始終是1,why?下面我們來分析分析??
上圖打斷點,讀取內存可以看出,case都是1字節
大小,1個字節是8個byte
,按照二進制轉換成十進制,那么有255種排列組合(0x00000000 - 0x11111111)
,所以當case為1個的時候,size的大小是0
(二進制是0x0
),case數<=255
時,size都是1
。而超過255
個時,會自動擴容
,size
和stride
都會增加
。
5.2 具有關聯值的enum
如果是自定義類型的枚舉,即關聯值類型,size和stride的值會發生什么變化呢?看下面的例子??
enum Shape{
case circle(radius: Double)
case rectangle(width: Double, height: Double)
}
print(MemoryLayout<Shape>.size)
print(MemoryLayout<Shape>.stride)
看來關聯值的枚舉大小和關聯值入參有關系,??
5.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)
從結果中可以看出,enum嵌套enum,和具有關聯值的enum
的情況是一樣的,同樣取決于關聯值的大小
,其內存大小是最大關聯值的大小
。
接著我們看看具體的分布,可以先定義一個變量??
var combine = CombineDirect.leftDown(baseDirect1: .left, baseDirect2: .down)
lldb查看其內存分布??
將第一個入參left 改為 up??
所以,02
表示是caseleftDown
的關聯值的第一個入參
的枚舉值,那第2個入參是.down,按照規律來算應該是01
,但卻是81
,why?接下來我們看看81
代表的是什么值?
- 在enum CombineDirect中
多加4個case
結果是c1
- 減少一個case
減少一個case項后,是a1
- 再減少一個case
再減少一個是81,說明81
中的8
是
- 繼續減少,保證case只有2個
結果頁是81
- 添加保證case>10個
如果leftDown 的case索引值大于8,例如上圖,leftDown是第1個case,結果e1
中的e
就是15(十進制),即case選項的索引值。
- 再減少,保證case leftDown在第9個
果然,上圖中leftDown的case索引值是9,打印出來的91
中的第一位也是9
。
綜上, 81
中的第一位8
這個值,有以下幾種情況區分??
- 當嵌套enum的case只有2個時,case在內存中的存儲是0、8
- 當嵌套enum的case
大于2,小于等于4
時,case在內存中的存儲是0、4、8、12
- 當嵌套enum的case
大于4
時,case在內存中的存儲是從0、1、2...
類推
81
中的1
,代表什么意思呢?我們改變下關聯值入參??
所以,leftDown減少一個入參,結果是80
,加一個入參,結果是01 80
,繼續再加一個入參??
關聯值的入參是up,down,right,right
,對應的枚舉值是0,1,3,3
,所以可以得出結論??
- enum嵌套enum同樣取決于
最大case的關聯值大小
- case中關聯值的
內存分布
,又是根據入參的個數
和大小
來分布的
2.1 每個入參占一個字節
大小的空間(即2個byte位
),第2位byte里面存儲的是內層枚舉的case值
,第1位的byte值通常是0
2.2最后一個入參
的byte空間分布 ?? 第2位是內層
枚舉的case值,第1位是外層
枚舉的case值,其規律又如下??
- 當外層enum的case
只有2個
時,第1位byte值按照0、8
依次分布- 當外層enum的case個數
>2,<=4
時,第1位byte值按照0、4、8、12
依次分布- 當外層enum的case個數
>4
時,第1位byte值按照0、1、2、3、...
依次分布
5.4 struct嵌套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)
size和stride都是1。結構體的大小計算,跟函數無關,所以只看成員變量key的大小
,key是枚舉Skill
類型,大小為1,所以結構體大小為1。繼續,去掉
成員key??
沒有任何成員變量時,size為0,stride為1(系統默認分配
的)。如果加一個成員??
因為添加的是UInt8
,占1個字節,所以size和stride都+1,均為2。再添加一個成員??
添加的成員是Int類型,占8字節,8+1+1=10,而stride是系統分配的,8的倍數來分配,所以是16。你以為就這么簡單的相加嗎?我們換一下width的位置
??
將width成員放到最后面,size變為16,why?因為size的大小,是按照結構體內存對齊原則
來計算的,可參考我之前的文章內存對齊分析。
總結
本篇文章主要講解了Swift中的枚舉,開始與OC的枚舉作比較,引出Swift枚舉的不同點,進而分析了rawValue
和初始化init
的底層實現流程,然后講解了幾個重要的場景 ?? OC和Swift的橋接
場景,枚舉嵌套
的場景,最后重點分析了枚舉的大小
,即內存分布的情況,這也是面試中經常出的題目,希望大家掌握,謝謝!