Swift 枚舉(enum)詳解
[TOC]
本文將介紹Swift
中枚舉的一些用法和其底層原理的一些探索牍帚,以及探索一下OC
中的枚舉與Swift
中枚舉互相調(diào)用和枚舉類型的內(nèi)存占用情況印机。
1. 枚舉
1.1 C中枚舉
首先我們來看看C
語言中枚舉的寫法夯尽。這里我們以一周7天作為示例凡怎。
普通寫法:
enum Week {
MON, TUE, WED, THU, FRI, SAT, SUN
}
以上就是C
語言中枚舉的常見寫法enum
關(guān)鍵字盟榴,加上枚舉名稱郑口,大括號里面的不同的枚舉值使用逗號分隔開來竿屹。此時的枚舉值默認從0開始枫虏,依次是1妇穴,2爬虱,3……
自定義枚舉值:
如果我們不想使用默認的枚舉值,則可以這樣寫
enum Week {
MON = 1, TUE, WED, THU, FRI, SAT, SUN
};
此時枚舉值就會從1開始依次向后排列腾它,你也可以給每個枚舉都定義不同的枚舉值跑筝,如果直接給TUE
定義為2,而沒給MON
定義瞒滴,則MON
的枚舉值會是0曲梗。
枚舉變量的定義:
enum Week {
MON = 1, TUE, WED, THU, FRI, SAT, SUN
};
enum Week week;
enum Week{
MON = 1, TUE, WED, THU, FRI, SAT, SUN
}week;
enum{
MON = 1, TUE, WED, THU, FRI, SAT, SUN
}week;
我們可以通過以上三種方法創(chuàng)建枚舉變量:
- 創(chuàng)建一個枚舉,然后聲明一個枚舉變量
- 創(chuàng)建一個枚舉并聲明一個枚舉變量
- 也可以省略枚舉名稱妓忍,直接聲明一個枚舉變量
1.2 Swift中枚舉
Swift
中最常見的枚舉寫法:
enum Week{
case MON
case TUE
case WED
case THU
case FRI
case SAT
case SUN
}
在Swift
中也可以簡化為如下寫法:
enum Week{
case MON, TUE, WED, THU, FRI, SAT, SUN
}
Swift
中枚舉很強大虏两,我們可以創(chuàng)建一個枚舉值是String
類型的enum
,其實也不應(yīng)該說是枚舉值世剖,而是枚舉的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"
}
當(dāng)然定罢,如果我們不想寫后面的字符串,也可以簡寫成如下的形式:
enum Week: String{
case MON, TUE, WED, THU, FRI, SAT, SUN
}
定義枚舉變量:
var w: Week = .MON
枚舉變量的定義很簡單旁瘫,跟普通變量的定義沒什么差別祖凫。
枚舉的訪問:
我們可以訪問枚舉的變量和枚舉的rawValue
首先我們需要注意的是如果沒有聲明枚舉的類型,是沒有rawValue
屬性可以訪問的境蜕。
一般情況下我們可以通過以下方式訪問枚舉:
enum Week: String{
case MON, TUE, WED, THU, FRI, SAT, SUN
}
print(Week.MON)
print(Week.MON.rawValue)
打印結(jié)果如下:
1.3 枚舉值和其RawValue的存儲
在上面枚舉的訪問中我們可以看到關(guān)于MON
字符串的打印蝙场,既然可以打印出來,說明是存儲了相關(guān)的字符串的粱年,那么是怎么存儲的呢售滤?又是怎么獲取出來的呢?下面我們通過sil
代碼進行分析(使用如下命令生成并打開sil
代碼)
swiftc -emit-sil main.swift >> ./main.sil && open main.sil
為了方便分析我們將代碼修改為如下:
enum Week: String{
case MON, TUE, WED, THU, FRI, SAT, SUN
}
var w = Week.MON.rawValue
print(w)
執(zhí)行生成sil
代碼的名后:
enum week : String {
case MON
case TUE
case WED
case SUN
typealias RawValue = String
init?(rawValue: String)
var rawValue: String { get }
}
通過sil
代碼中對枚舉的定義可以看到:
- 跟
Swift
中一致的枚舉 - 取了一個別名台诗,也就是
String
類型是RawValue
- 添加了一個可選類型的
init
方法 - 一個計算屬性
rawValue
完箩,通過其get
方法獲取枚舉的原始值
下面我們在main
函數(shù)中看看:
關(guān)于w
變量的初始化即分析注釋寫在了截圖中。
- 首先創(chuàng)建一個全局變量w拉队,并為變量w開辟內(nèi)存地址
- 將枚舉類型Week.MON存儲到%5
- 將枚舉Week的rawValue.getter函數(shù)存儲到%6
- 調(diào)用%6中存儲的函數(shù)弊知,%5作為參數(shù),返回值存儲到%7
- 將%7中獲取到額值存儲到%3粱快,至此變量w初始化完成
下面我們看看rawValue
的getter
方法:
我們可以看到在rawValue
的getter
方法中主要實現(xiàn)是:
- 通過接收到的枚舉值去匹配一個分支
- 在分支中構(gòu)建對于的
String
- 返回上一步構(gòu)建的
String
那么這個字符串是從哪里來的呢秩彤?根據(jù)匹配的分支中的方法名稱我們可以知道這是獲取一個內(nèi)置的字符串的字面量。其實就是從Mach-O
文件的__TEXT.cstring
中事哭。下面我們通過查看Mach-O
來驗證漫雷。
所以說rawValue
的值是通過調(diào)用枚舉的rawValue。getter
函數(shù)鳍咱,從Mach-O
對應(yīng)的地址中取出字符串并返回降盹。
那么枚舉值呢?其實在上面關(guān)于rawValue
探索的時候就可以知道了谤辜,枚舉值在sil
代碼中就是:#Week.MON!enumelt
蓄坏,枚舉值和rawValue
本質(zhì)上是不一樣的价捧,從下面的例子可以得到結(jié)論:
按照以上的寫法是會報編譯錯誤的。
1.4 枚舉.init
1.4.1 觸發(fā)方式
在上面的分析時我們知道枚舉會有一個init
方法涡戳,那么這個方法是什么時候調(diào)用的呢结蟋?我們添加如下符號斷點:
添加如下代碼:
var w: Week = .MON
print(w.rawValue)
運行后并沒有觸發(fā)該符號斷點。
下面我們在添加如下代碼:
var w = Week(rawValue: "MON")
print(w)
運行后即可觸發(fā)符號斷點:
所以這里init
方法是為枚舉通過rawValue
初始化的時候調(diào)用的妹蔽。
1.4.2 init分析
首先我們來看看如下代碼的打印結(jié)果:
print(Week.init(rawValue: "MON"))
print(Week.init(rawValue: "Hello"))
<!--打印結(jié)果-->
Optional(SwiftEnum.Week.MON)
nil
從打印結(jié)果中可以看到椎眯,第一個輸出的是可選值SwiftEnum.Week.MON
挠将,第二個是nil
胳岂,很顯然Hello
不是我們的枚舉,那么這些是怎么實現(xiàn)的呢舔稀?我們再次查看sil
代碼乳丰,此時我們可以直接看Week.init
方法的實現(xiàn)。
方法比較長内贮,在里面添加了相關(guān)的注釋和分析产园,折疊的代碼基本上是與其上面的代碼一致。現(xiàn)在總結(jié)如下:
- 首先開辟一塊內(nèi)存用于后續(xù)存儲構(gòu)建出來的枚舉
- 通過
_allocateUninitializedArray
函數(shù)創(chuàng)建一個元組夜郁,元組中包含- 與枚舉個數(shù)大小一樣的數(shù)組什燕,用于存儲枚舉中的
rawValue
在本示例中是staticString
, - 數(shù)組的首地址
- 與枚舉個數(shù)大小一樣的數(shù)組什燕,用于存儲枚舉中的
- 開始一個一個的構(gòu)建枚舉
rawValue
存儲到數(shù)組中 - 通過
_findStringSwitchCase
函數(shù)查找處要構(gòu)建的枚舉在數(shù)組中的位置index
- 從0到count-1依次與
index
作比較- 如果相等則構(gòu)建對于的枚舉
- 如果不相等則構(gòu)建一個
Optional.none!enumelt
的枚舉
- 將構(gòu)建的枚舉存儲到開辟的地址
- 最后返回構(gòu)建的枚舉
關(guān)于上面提到的兩個函數(shù)源碼可以Swift
源碼中找到
_allocateUninitializedArray源碼:
@inlinable @inline(__always) @_semantics("array.uninitialized_intrinsic") public func _allocateUninitializedArray<Element>(_ builtinCount: Builtin.Word) -> (Swift.Array<Element>, Builtin.RawPointer) {
let count = Int(builtinCount)
if count > 0 {
// Doing the actual buffer allocation outside of the array.uninitialized
// semantics function enables stack propagation of the buffer.
let bufferObject = Builtin.allocWithTailElems_1(
_ContiguousArrayStorage<Element>.self, builtinCount, Element.self)
let (array, ptr) = Array<Element>._adoptStorage(bufferObject, count: count)
return (array, ptr._rawValue)
}
// For an empty array no buffer allocation is needed.
let (array, ptr) = Array<Element>._allocateUninitialized(count)
return (array, ptr._rawValue)
}
可以看到此處就是根據(jù)傳入的count
和Builtin.Word
初始化一個數(shù)組竞端,將其以元組的形式返回數(shù)組和數(shù)組首地址屎即。
_findStringSwitchCase源碼:
/// The compiler intrinsic which is called to lookup a string in a table
/// of static string case values.
@_semantics("findStringSwitchCase")
public // COMPILER_INTRINSIC
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
}
我們可以看到這里接收一個數(shù)組和要匹配的字符串,然后通過一個for
循環(huán)匹配字符串事富,如果匹配到了則返回數(shù)組中對應(yīng)的index
技俐,否則返回-1。
1.5 枚舉的遍歷
一般我們很少會對枚舉進行遍歷操作统台,在Swift
中可以通過遵守CaseIterable
協(xié)議來實現(xiàn)對枚舉的遍歷雕擂。
enum Week: String, CaseIterable{
case MON, TUE, WED, THU, FRI, SAT, SUN
}
// 使用for循環(huán)遍歷
var allCase = Week.allCases
for c in allCase{
print(c)
}
// 函數(shù)是編程遍歷
let allCase = Week.allCases.map({"\($0)"}).joined(separator: ", ")
print(allCase)
1.6 關(guān)聯(lián)值
在Swift
中如果想要表示復(fù)雜的含義,可以在枚舉中關(guān)聯(lián)更多的信息贱勃。下面我們舉個例子井赌,如果需要有一個形狀的枚舉,里面有圓形和矩形贵扰。圓形有半徑仇穗,矩形有長寬,那么這個枚舉就可以寫成如下代碼:
enum Shape{
case circle(radius: Double)
case rectangle(width: Int, height: Int)
}
- 其中括號中的
radius
以及width
和height
就是關(guān)聯(lián)值 - 如果沒枚舉中使用關(guān)聯(lián)值則枚舉就沒有
rawValue
屬性了拔鹰,因為關(guān)聯(lián)值是一組值仪缸,而rawValue
是單個值,可以通過sil
代碼驗證
在sil
代碼中我們并沒有發(fā)現(xiàn)init
方法RawValue
別名以及rawValue
的get
方法列肢。
在這個枚舉中radius恰画、width宾茂、height
這些都是自定義的標(biāo)簽,也可以不寫拴还,如下所示跨晴,但并不推薦這種方式,因為可讀性非常差
enum Shape{
case circle(Double)
case rectangle(Int, Int)
}
那么有關(guān)聯(lián)值的枚舉該如何初始化呢片林?其實也很簡單端盆,下面我們就來創(chuàng)建一下
var shape = Shape.circle(radius: 10.0)
shape = Shape.circle(radius: 15)
shape = Shape.rectangle(width: 10, height: 10)
2. 其他用法
2.1 模式匹配
2.1.1 簡單的模式匹配
顧明思議,模式匹配就是匹配每一個枚舉值费封,通常我們可以使用switch
語句來進行模式匹配焕妙。如果使用switch
進行模式匹配:
- 必須列舉當(dāng)前所有可能的情況,否則就會報編譯錯誤
- 如果不想匹配這么多
case
則可以使用defalut
- 在同一個
case
中可以列舉多種情況
enum Week{
case MON
case TUE
case WED
case THU
case FRI
case SAT
case SUN
}
var week = Week.MON
switch week {
case .MON:
print("周一")
case .TUE:
print("周二")
case .WED:
print("周三")
case .SAT, .SUN:
print("happy day")
default : print("unknow day")
}
其實這個匹配也很簡單弓摘,我們通過查看sil
代碼就可以知道:
2.1.2 關(guān)聯(lián)值枚舉的模式匹配
如果我們不關(guān)心關(guān)聯(lián)值焚鹊,關(guān)聯(lián)值枚舉的寫法與普通枚舉沒有什么區(qū)別:
enum Shape{
case circle(radius: Double)
case rectangle(width: Int, height: Int)
}
let shape = Shape.circle(radius: 10.0)
switch shape {
case .circle:
print("the shape is circle")
case .rectangle:
print("the shape is rectangle")
}
但是我們使用關(guān)聯(lián)值枚舉,肯定是會關(guān)心關(guān)聯(lián)值的韧献,當(dāng)關(guān)心關(guān)聯(lián)值時其寫法如下:
switch shape{
case let .circle(radius):
print("circle radius: \(radius)")
case .rectangle(let width, var height):
height += 1
print("rectangle width: \(width) height: \(height)")
}
可以發(fā)現(xiàn)末患,這里的每個case
中都使用了let
或者var
,這里因為要使用關(guān)聯(lián)值锤窑,所以需要使用let
聲明一下璧针。或者放在最前面渊啰,或者對每個需要使用的變量前都添加let
探橱。如果使用var
則可在當(dāng)前case
中修改其修飾的關(guān)聯(lián)值。當(dāng)然你也可以不使用枚舉定義中的關(guān)聯(lián)值的名字虽抄,可以自定義走搁。
關(guān)于關(guān)聯(lián)值枚舉的模式匹配我們也可以看看sil
代碼:
2.1.3 其他匹配
有時候在業(yè)務(wù)邏輯處理中,我們只是想匹配單個case
迈窟,我們可以這樣寫:
if case let Shape.circle(radius) = shape {
print("circle radius: \(radius)")
}
當(dāng)然如果我們只關(guān)心不同case
的相同關(guān)聯(lián)值時就可以這樣寫:
enum Shape{
case circle(radius: Double)
case rectangle(width: Double, height: Double)
case square(width: Double, width: Double)
}
let shape = Shape.rectangle(width: 20, height: 10)
switch shape {
case let .rectangle(x, 10), let .square(x, 10):
print(x)
default: break
}
此時的打印是20私植,對于上面的例子,必須case
是rectangle
或square
车酣,而且rectangle
必須是10曲稼,square
后面的width
是10。
如果對于10的匹配不那么嚴(yán)格我們則可以使用通配符_
switch shape {
case let .rectangle(x, _), let .square(x, _):
print(x)
default: break
}
注意: 以上命名必須一致湖员,比如都使用x
贫悄,如果一個x
一個y
就不行了。
2.2 枚舉的嵌套
2.2.1 枚舉嵌套枚舉
顧名思義娘摔,枚舉嵌套枚舉就是在枚舉中還有枚舉窄坦,比如我們玩游戲時會有上下左右四個方向鍵,有時候也需要兩兩組合去使用,所以我們通過這個例子可以編寫如下枚舉:
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 leftup = CombineDirect.leftUp(combineElement1: .left, combineElement2: .up)
2.2.1 結(jié)構(gòu)體嵌套枚舉
Swift
允許在結(jié)構(gòu)體中嵌套枚舉鸭津,具體使用如下:
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")
}
}
}
使用起來也很簡單:
let s = Skill(key: .up)
s.launchSkill()
2.3 枚舉中包含屬性
Swift
中允許在枚舉中包含計算屬性和類型屬性彤侍,但不能包含存儲屬性。
enum Shape {
case circle(radius: Double)
case rectangle(width: Double, height: Double)
// var radius: Double // Enums must not contain stored properties
var width: Double{
get {
return 10.0
}
}
static let height = 20.0
}
2.3 枚舉中包含方法
Swift
中的枚舉也可以包含方法逆趋,可以是實例方法也可以是類方法
enum Week: Int{
case MON, TUE, WED, THU, FRI, SAT, SUN
mutating func nextDay(){
if self == .SUN{
self = Week.MON
}else{
self = Week(rawValue: self.rawValue+1)!
}
}
static func test() {
print("test")
}
}
使用起來依舊很簡單:
var w = Week.SUN
w.nextDay()
print(w)
Week.test()
此處的方法都是靜態(tài)調(diào)用:
2.4 indiret在枚舉中的應(yīng)用
2.4.1 indiret
如果我們想要使用enum
表達一個復(fù)雜的關(guān)鍵數(shù)據(jù)結(jié)構(gòu)的時候盏阶,我們可以通過使用indrect
關(guān)鍵字來讓enum
更簡潔。
比如我們想要通過枚舉來表達一個鏈表的結(jié)構(gòu)闻书,鏈表需要存儲數(shù)據(jù)以及指向它的下一個節(jié)點的指針名斟,如果不使用indiret
修飾則會報編譯錯誤:
此時我們可以寫成如下兩種方式就不會報錯了:
enum List<T> {
case end
indirect case node(T, next: List<T>)
}
indirect enum List<T> {
case end
case node(T, next: List<T>)
}
那么為什么要添加indirect
關(guān)鍵字呢?
因為enum
是值類型魄眉,它的大小在編譯期就需要確定砰盐,如果按照開始的寫法是不能夠確定當(dāng)前enum
的大小的,所以從系統(tǒng)的角度來說杆融,在不知道給enum
分配多大的空間楞卡,所以就需要使用indirect
關(guān)鍵字霜运,官方文檔是這樣解釋的:
You indicate that an enumeration case is recursive by writing indirect before it, which tells the compiler to insert the necessary layer of indirection.
譯:您可以通過在枚舉案例之前寫indirect來表明枚舉案例是遞歸的脾歇,這告訴編譯器插入必要的間接層。
2.4.2 內(nèi)存占用
我們打印一下使用indirect
修飾的枚舉內(nèi)存占用是多少呢淘捡?
enum List<T> {
case end
indirect case node(T, next: List<T>)
}
print(MemoryLayout<List<Int>>.size)
print(MemoryLayout<List<Int>>.stride)
<!--打印結(jié)果-->
8
8
如果我們的泛型使用的是String
呢?
print(MemoryLayout<List<String>>.size)
print(MemoryLayout<List<String>>.stride)
<!--打印結(jié)果-->
8
8
此時我們發(fā)現(xiàn)泛型的更換內(nèi)存占用保持不變,此時我們創(chuàng)建一個使用indirect
修飾的枚舉類型的變量:
var node = List<Int>.node(10, next: List<Int>.end)
通過lldb
查看node
的內(nèi)存:
可以看到node
像一個對象的結(jié)構(gòu)橙数,所以說這里面存儲的是一個指針英古,當(dāng)不確定枚舉類型大小的時候,將分配一個8字節(jié)大小的指針膘魄,指向一塊堆空間用于存儲這不確定大小的枚舉乌逐。
如果是end
,此時存儲的就是case
值
那么這些是如何實現(xiàn)的呢创葡?我們通過sil
代碼來看一下:
這里我們可以看到使用了alloc_box
浙踢,我們打開SIL參考文檔,并找到alloc-box
我們可以看到alloc_box
就是在堆上分配一個引用計數(shù)@box灿渴,該值足夠大洛波,可以容納T類型的值,以及一個retain count
和運行時所需的任何其他元數(shù)據(jù)骚露。
其本質(zhì)是調(diào)用了swift_allocObject
蹬挤,這點可以通過匯編代碼驗證:
3. 枚舉的大小
3.1 普通枚舉大小分析
首先看看下面這段代碼的打印結(jié)果:
enum NoMean{
case a
}
print(MemoryLayout<NoMean>.size)
print(MemoryLayout<NoMean>.stride)
<!--打印結(jié)果-->
0
1
如果我們在增加一個case
呢?
enum NoMean{
case a
case b
}
print(MemoryLayout<NoMean>.size)
print(MemoryLayout<NoMean>.stride)
<!--打印結(jié)果-->
1
1
如果在多增加幾個呢棘幸?
enum NoMean{
case a
case b
case c
case d
case e
}
print(MemoryLayout<NoMean>.size)
print(MemoryLayout<NoMean>.stride)
<!--打印結(jié)果-->
1
1
可以看到焰扳,打印結(jié)果還是1,所以普通枚舉應(yīng)該就是以1字節(jié)存儲在內(nèi)存中的,下面我們來分析一下:
首先我們添加如下代碼:
var a = NoMean.a
var b = NoMean.b
var c = NoMean.c
var d = NoMean.d
lldb調(diào)試:
所以這里當(dāng)前枚舉的步長是1字節(jié)吨悍,也就意味著如果內(nèi)存中連續(xù)存儲NoMean
光绕,需要跨越一個字節(jié)的長度。一個字節(jié)也就是8位畜份,最大可以表達255個數(shù)字诞帐。由于太長就不測試了,如果真的需要寫255及以上爆雹,還是建議以別的方式優(yōu)化一下停蕉。
如果枚舉后面寫了類型,比如:
enum NoMean: Int{
case a
case b
case c
case d
}
此時打印枚舉的大小和步長還是1钙态,這里面的類型指的是rawValue
慧起,并不是case
的值。
- 所以枚舉中默認是以
UInt8
存儲的册倒,最大可以存儲0~255蚓挤,如果不夠則會自動轉(zhuǎn)換為UInt16
,以此類推驻子。 - 當(dāng)只有一個
case
的時候灿意,size
是0,表示這個枚舉是沒有意義的 - 枚舉中后面聲明的類型只的是
rawValue
的類型崇呵,不會影響枚舉的大小 - 這些
rawValue
的值會存儲在Mach-O
文件中缤剧,在使用的時候取查找,這個在上面提到過域慷,與枚舉大小沒有關(guān)系
3.2 關(guān)聯(lián)值枚舉的大小
如果枚舉中有關(guān)聯(lián)值荒辕,那么它的大小是多少呢?
enum Shape{
case circle(radius: Int)
case rectangle(width: Int, height: Int)
}
print(MemoryLayout<Shape>.size)
print(MemoryLayout<Shape>.stride)
<!--打印結(jié)果-->
17
24
從打印結(jié)果我們可以知道犹褒,具有關(guān)聯(lián)值的枚舉的大小取決于關(guān)聯(lián)值的大小抵窒,此時circle
中的關(guān)聯(lián)值Int
占用內(nèi)存大小是8,而rectangle
中兩個Int
加起來是16叠骑,那么打印的這個17是怎么來的呢李皇?其實還有存儲枚舉值,所以枚舉的大小此處枚舉的size = 8+8+1 = 17座云,由于內(nèi)存對齊疙赠,所以要分配8的整數(shù)倍,所以stride
就是24朦拖。這是該枚舉中最大需要的內(nèi)存圃阳,這個內(nèi)存足夠容納circle
需要的9字節(jié)的大小。
下面我們璧帝,修改一下代碼順序捍岳,創(chuàng)建一下具有關(guān)聯(lián)值的枚舉,看看其內(nèi)存分布:
我們可以看到circle
是分配了24字節(jié)的內(nèi)存空間的,內(nèi)存分布首先是存儲關(guān)聯(lián)自锣夹,然后在存儲枚舉值页徐,circle
的枚舉值是存儲在第三個8字節(jié)上的,也就是存儲在最后银萍。
- 具有關(guān)聯(lián)值的枚舉的大小取決于關(guān)聯(lián)值的大小
- 具有關(guān)聯(lián)值的枚舉的大小是枚舉中最大的那個關(guān)聯(lián)值枚舉的大小 + 1(case 需要占用1字節(jié))变勇,
- 如果大于255可能需要占用2字節(jié),這里沒有進行測試
3.3 枚舉嵌套枚舉的大小分析
下面我們看看枚舉嵌套枚舉中內(nèi)存占用的大刑健:
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)
}
print(MemoryLayout<CombineDirect>.size)
print(MemoryLayout<CombineDirect>.stride)
<!--打印結(jié)果-->
2
2
根據(jù)打印結(jié)果我們可以知道嵌套的枚舉搀绣,其實也就是枚舉中關(guān)聯(lián)了枚舉,它的大小同樣取決于關(guān)聯(lián)值的大小戳气,因為BaseDirect
是基本的枚舉链患,其內(nèi)存占用為1,那么按照關(guān)聯(lián)值枚舉中的內(nèi)存占用應(yīng)該是1+1+1 = 3瓶您,那么為什么是2呢麻捻?
下面我們通過創(chuàng)建枚舉變量,看看其內(nèi)存分布是什么樣的呀袱,首先添加如下代碼:
// 2 0 3 0 2 1 3 1
var a = CombineDirect.leftUp(combineElement1: .left, combineElement2: .up)
var b = CombineDirect.rightUp(combineElement1: .right, combineElement2: .up)
var c = CombineDirect.leftDown(combineElement1: .left, combineElement2: .down)
var d = CombineDirect.rightDown(combineElement1: .right, combineElement2: .down)
lldb查看內(nèi)存
通過lldb
調(diào)試的結(jié)果我們可以看到贸毕,在每個字節(jié)的低4位上存儲著關(guān)聯(lián)值的值,而在最后那個關(guān)聯(lián)值的高四位分別存儲了0压鉴,4崖咨,8,12(c)油吭,所以對于枚舉中嵌套枚舉應(yīng)該是做了相應(yīng)的優(yōu)化,借用未使用的高位存儲關(guān)聯(lián)值枚舉的枚舉值署拟。
下面我們測試一下婉宰,多寫幾個:
enum CombineDirect{
enum BaseDirect{
case up
case down
case left
case right
}
case upup(combineElement1: BaseDirect, combineElement2: BaseDirect)
case updown(combineElement1: BaseDirect, combineElement2: BaseDirect)
case upleft(combineElement1: BaseDirect, combineElement2: BaseDirect)
case upright(combineElement1: BaseDirect, combineElement2: BaseDirect)
case downup(combineElement1: BaseDirect, combineElement2: BaseDirect)
case downdown(combineElement1: BaseDirect, combineElement2: BaseDirect)
case downleft(combineElement1: BaseDirect, combineElement2: BaseDirect)
case downright(combineElement1: BaseDirect, combineElement2: BaseDirect)
case leftUp(combineElement1: BaseDirect, combineElement2: BaseDirect)
case leftDown(combineElement1: BaseDirect, combineElement2: BaseDirect)
case leftleft(combineElement1: BaseDirect, combineElement2: BaseDirect)
case leftright(combineElement1: BaseDirect, combineElement2: BaseDirect)
case rightup(combineElement1: BaseDirect, combineElement2: BaseDirect)
case rightdown(combineElement1: BaseDirect, combineElement2: BaseDirect)
case rightleft(combineElement1: BaseDirect, combineElement2: BaseDirect)
case rightright(combineElement1: BaseDirect, combineElement2: BaseDirect)
}
print(MemoryLayout<CombineDirect>.size)
print(MemoryLayout<CombineDirect>.stride)
var a = CombineDirect.upup(combineElement1: .up, combineElement2: .up)
var b = CombineDirect.updown(combineElement1: .up, combineElement2: .down)
var c = CombineDirect.upleft(combineElement1: .up, combineElement2: .left)
var d = CombineDirect.upright(combineElement1: .up, combineElement2: .right)
var e = CombineDirect.downup(combineElement1: .down, combineElement2: .up)
var f = CombineDirect.downdown(combineElement1: .down, combineElement2: .down)
var g = CombineDirect.downleft(combineElement1: .down, combineElement2: .left)
var h = CombineDirect.downright(combineElement1: .down, combineElement2: .right)
var i = CombineDirect.leftUp(combineElement1: .left, combineElement2: .up)
var j = CombineDirect.leftDown(combineElement1: .left, combineElement2: .down)
var k = CombineDirect.leftleft(combineElement1: .left, combineElement2: .left)
var l = CombineDirect.leftright(combineElement1: .left, combineElement2: .right)
var m = CombineDirect.rightup(combineElement1: .right, combineElement2: .up)
var n = CombineDirect.rightdown(combineElement1: .right, combineElement2: .down)
var o = CombineDirect.rightleft(combineElement1: .right, combineElement2: .left)
var p = CombineDirect.rightright(combineElement1: .right, combineElement2: .right)
lldb調(diào)試結(jié)果:
此時我們發(fā)現(xiàn),最后那個關(guān)聯(lián)值的高四位分別存儲了0推穷,1心包,2,3馒铃,4蟹腾,5,6区宇,7娃殖,8,9议谷,a炉爆,b,c,d芬首,e赴捞,f。
如果我們隨便注釋幾個:
我們發(fā)現(xiàn)郁稍,對應(yīng)的結(jié)果與枚舉中的順序是一致的赦政。
如果只剩一個,但不是第一個:
此時我們發(fā)現(xiàn)是7耀怜,與枚舉中的屬性還是一致的昼钻。
下面我們開始注釋枚舉中的:
最后發(fā)現(xiàn):
- 如果是兩個枚舉就是0,8
- 如果是3或4的時候是按照0封寞,4然评,8,c
- 大約4小于等于16的時候是0狈究,1碗淌,2,3抖锥,4......f
- 如果大于16就不能看出上面的規(guī)律了亿眠,所以從二進制位看
對于枚舉中嵌套枚舉,使用關(guān)聯(lián)值磅废,又或者說不嵌套纳像,具有關(guān)聯(lián)值的枚舉中的關(guān)聯(lián)值是枚舉類型的時候,會優(yōu)先借用最后關(guān)聯(lián)的那個枚舉的二進制位存儲具有關(guān)聯(lián)值枚舉的值拯勉,借用的位數(shù)為關(guān)聯(lián)值枚舉的個數(shù)小于等于2的冪最小值竟趾,也就是2的幾次冪才能大于等于關(guān)聯(lián)枚舉的個數(shù)。
這里我有進一步測試宫峦,如果普通枚舉的個數(shù)不足以使用低四位表示岔帽,比如低四位最少表示16個,如果多了的話导绷,就會借用關(guān)聯(lián)值中倒數(shù)第二個犀勒,也就上面例子中的第一個關(guān)聯(lián)值的高位進行借位存儲妥曲。按照這個邏輯檐盟,大膽猜想褂萧,如果普通枚舉的個數(shù)為256個遵堵,也就是不能借任何一個位怨规,這種具有關(guān)聯(lián)值枚舉是不是會另外開辟內(nèi)存存關(guān)聯(lián)值枚舉的值?其實不需要是256個锡足,只要不夠借的時候就會開辟內(nèi)存去存儲關(guān)聯(lián)值枚舉的值舶得。
舉個例子:
enum BaseDirect{
case up
case down
case left
case right
case a
case b
case c
case d
case e
case f
case g
case h
case i
case j
case k
case l,m,n,o,p,q,r,s,t,u,v,w,x,y,z
case l1,m1,n1,o1,p1,q1,r1,s1,t1,u1,v1,w1,x1,y1,z1
case l2,m2,n2,o2,p2,q2,r2,s2,t2,u2,v2,w2,x2,y2,z2
case l3,m3,n3,o3,p3,q3,r3,s3,t3,u3,v3,w3,x3,y3,z3
}
enum CombineDirect{
case upup(combineElement1: BaseDirect, combineElement2: BaseDirect)
case updown(combineElement1: BaseDirect, combineElement2: BaseDirect)
case upleft(combineElement1: BaseDirect, combineElement2: BaseDirect)
case upright(combineElement1: BaseDirect, combineElement2: BaseDirect)
case downup(combineElement1: BaseDirect, combineElement2: BaseDirect)
case downdown(combineElement1: BaseDirect, combineElement2: BaseDirect)
case downleft(combineElement1: BaseDirect, combineElement2: BaseDirect)
case downright(combineElement1: BaseDirect, combineElement2: BaseDirect)
case leftUp(combineElement1: BaseDirect, combineElement2: BaseDirect)
case leftDown(combineElement1: BaseDirect, combineElement2: BaseDirect)
case leftleft(combineElement1: BaseDirect, combineElement2: BaseDirect)
case leftright(combineElement1: BaseDirect, combineElement2: BaseDirect)
case rightup(combineElement1: BaseDirect, combineElement2: BaseDirect)
case rightdown(combineElement1: BaseDirect, combineElement2: BaseDirect)
case rightleft(combineElement1: BaseDirect, combineElement2: BaseDirect)
case rightright(combineElement1: BaseDirect, combineElement2: BaseDirect)
case aa(combineElement1: BaseDirect, combineElement2: BaseDirect)
}
print(MemoryLayout<CombineDirect>.size)
print(MemoryLayout<CombineDirect>.stride)
<!--打印結(jié)果-->
3
3
這個例子并沒有嵌套纫骑,其實與嵌套沒有任何關(guān)系先馆,嵌套的枚舉也是單獨存儲的煤墙,只不過嵌套的枚舉作用域只在嵌套的大括號內(nèi)仿野。
另外脚作,如果枚舉值過多的時候球涛,我們看sil
代碼:
此時我們可以發(fā)現(xiàn):
- 多了一個
hashValue
的計算屬性 - 一個遵守了
Equatable
協(xié)議的__derived_enum_equals
imp - 以及一個
hash
函數(shù)
我猜想宾符,對于過多case
的枚舉,swift
為了更好更快的匹配肝箱,使用了蘋果慣用的哈希煌张。我嘗試在源碼中搜索了一下derived_enum_equals
并沒有找到相關(guān)方法骏融,貌似是過期被移除了档玻,后面使用==
代替误趴。
3.4 結(jié)構(gòu)體嵌套枚舉的大小分析
首先還是看一下打印結(jié)果:
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")
}
}
}
print(MemoryLayout<Skill>.size)
print(MemoryLayout<Skill>.stride)
<!--打印結(jié)果-->
1
1
如果只是嵌套了枚舉呢枣申?
struct Skill{
enum KeyType{
case up
case down
case left
case right
}
}
print(MemoryLayout<Skill>.size)
print(MemoryLayout<Skill>.stride)
<!--打印結(jié)果-->
0
1
如果添加了其他屬性忠藤,則打印結(jié)果與添加的屬性類型有關(guān)系模孩,這里就不一一驗證了瓜贾。
總的來說祭芦,結(jié)構(gòu)體中嵌套枚舉與枚舉嵌套枚舉是一樣的龟劲,他們都不存儲枚舉昌跌,只是作用域在其中而已蚕愤。
4. 與OC混編
綜上萍诱,我們可以看到Swift
中的枚舉非常強大裕坊,而在OC
中枚舉僅僅只是一個整數(shù)值籍凝,那么在與OC
混編的時候声诸,該如何在OC
中使用Swift
的枚舉呢双絮?下面我們就來探索一下:
4.1 OC調(diào)用Swift中的枚舉
如果想要在OC
中使用Swift
枚舉要求會很嚴(yán)格:
- 使用
@objc
標(biāo)記 - 類型必須是
Int
囤攀,也就是Swift
的rawValue
- 必須導(dǎo)入
import Foundation
也就是這樣:
import Foundation
@objc enum Week: Int{
case MON, TUE, WED, THU, FRI, SAT, SUN
}
此時編譯后就可以正在project-Swift.h
中看到轉(zhuǎn)換后的對應(yīng)的OC
枚舉:
調(diào)用的話就是:
4.2 Swift調(diào)用OC中的枚舉
4.2.1 NS_ENUM
NS_ENUM(NSInteger, OCENUM){
Value1,
Value2
};
如果使用NS_ENUM
創(chuàng)建的枚舉會自動轉(zhuǎn)換成swift
中的enum
可以在ocfileName.h
中查看轉(zhuǎn)換后的枚舉:
使用的話需要在橋接文件中導(dǎo)入OC
頭文件漓骚,然后在swift
中使用:
let value = OCENUM.Value1
4.2.2 使用typedef enum
typedef enum {
Enum1,
Enum2,
Enum3
}OCENum;
如果使用typedef enum
這種形式的枚舉蝌蹂,會轉(zhuǎn)換成結(jié)構(gòu)體剃允,同樣可以在ocfileName.h
中查看轉(zhuǎn)換后的結(jié)果齐鲤,轉(zhuǎn)換后的代碼如下:
可以看到里面有一個rawValue
屬性给郊,以及init
方法淆九。還遵守了Equatable, RawRepresentable
兩個協(xié)議炭庙。
使用的話也是需要導(dǎo)入頭文件:
let num = OCEnum(0)
let num1 = OCEnum.init(0)
let num2 = OCEnum.init(rawValue: 3)
print(num)
<!--打印結(jié)果-->
OCNum(rawValue: 0)
這里我們只能通過init
方法去初始化煤搜,不能訪問枚舉中的變量嘲驾。
4.2.3 使用typedef NS_ENUM
typedef NS_ENUM(NSInteger, OCENUM){
OCEnumInvalid = 0,
OCEnumA = 1,
OCEnumB,
OCEnumC
};
使用typedef NS_ENUM
也會自動轉(zhuǎn)換為Swift
的枚舉辽故,轉(zhuǎn)換后的代碼如下:
使用也是需要導(dǎo)入頭文件:
let ocenum = OCENUM.OCEnumInvalid
let ocenumRawValue = OCENUM.OCEnumA.rawValue
4.3 混編時需要使用String類型的枚舉
這里的意思是,Swift
中需要使用String
類型的枚舉喂走,但是又要與OC
混編芋肠,暴露給OC
使用帖池。
如果直接聲明為String
類型編譯時不會通過的睡汹,這里只能弄個假的囚巴。Swift
中的枚舉還是聲明為Int
文兢,可以在枚舉中聲明一個變量或者方法姆坚,用于返回想要的字符串。
@objc enum Week: Int{
case MON, TUE, WED
var value: String?{
switch self {
case .MON:
return "MON"
case .TUE:
return "TUE"
case .WED:
return "WED"
default:
return nil
}
}
func weekName() -> String? {
switch self {
case .MON: return "MON"
case .TUE: return "TUE"
case .WED: return "WED"
default:
return nil
}
}
}
用法:
<!--OC用法-->
Week mon = WeekMON;
<!--Swift用法-->
let value = Week.MON.value
let value1 = Week.MON.weekName()
5. 總結(jié)
其實感覺該篇不太適合總結(jié)击喂,因為直接給結(jié)論會使人不是很好理解懂昂,但是還是記錄一下吧凌彬。
-
Swift
中的枚舉很強大 -
enum
中的rawValue
是其中的計算屬性 - 如果聲明的時候不指定枚舉類型就沒有
rawValue
屬性(包括關(guān)聯(lián)值) -
rawValue
中的值存儲在Mach-O
中褐澎,不占用枚舉的存儲空間 - 枚舉值與
rawValue
不是同一個東西 -
rawValue
可以不寫工三,如果是Int
默認0俭正,1段审,2...String
等于枚舉名稱的字符串 - 如果枚舉中存在
rawValue
同時也會存在init(rawValue:)
方法,用于通過rawValue
值初始化枚舉 - 如果枚舉遵守了
CaseIterable
協(xié)議姥闪,且不是關(guān)聯(lián)值的枚舉筐喳,我們可以通過enum.allCases
獲取到所有的枚舉避归,然后通過for
循環(huán)遍歷 - 我們可以使用
switch
對枚舉進行模式匹配梳毙,如果只關(guān)系一個枚舉還可以使用if case
- 關(guān)聯(lián)值枚舉可以表示復(fù)雜的枚舉結(jié)構(gòu)
- 關(guān)聯(lián)值的枚舉沒有init方法,沒有
RawValue
別名奸柬,沒有rawValue
計算屬性 -
enum
可以嵌套enum
廓奕,被嵌套的作用域只在嵌套內(nèi)部 - 結(jié)構(gòu)體也可以嵌套
enum
懂从,此時enum
的作用域也只在結(jié)構(gòu)體內(nèi) -
enum
中可以包含計算屬性
,類型屬性
但不能包含存儲屬性
-
enum
中可以定義實例方法和使用static
修飾的方法缘薛,不能定義class
修飾的方法 - 如果想使用復(fù)雜結(jié)構(gòu)的枚舉宴胧,或者說是具有遞歸結(jié)構(gòu)的枚舉可以使用
indirect
關(guān)鍵字
-
關(guān)于枚舉的大小
- 默認情況下枚舉占用1字節(jié)也就是
UInt8
恕齐,如果不夠用也就是超過256個的時候會使用UInt16,UInt32...
(太多沒有去驗證士骤,如果真的有這么多枚舉拷肌,建議通過其他方式去優(yōu)化) - 如果枚舉個數(shù)過多會使用哈希來進行優(yōu)化巨缘,以便快速匹配
- 使用關(guān)聯(lián)值的枚舉大小取決于關(guān)聯(lián)值的大小,還要考慮內(nèi)存對齊
- 關(guān)聯(lián)值的枚舉中的關(guān)聯(lián)值如果是普通枚舉類型拴清,系統(tǒng)會通過借位優(yōu)化的方式節(jié)省內(nèi)存的占用
- 如果借位不夠了口予,會單獨開辟內(nèi)存存儲關(guān)聯(lián)值枚舉的枚舉值
- 在嵌套的時候,無論結(jié)構(gòu)體還是枚舉中都是不占用內(nèi)存的木张,被嵌套的枚舉是單獨存儲的舷礼,只是作用域在其內(nèi)部而已
- 默認情況下枚舉占用1字節(jié)也就是
-
關(guān)于和
OC
混編- OC中只能使用
Swift
中Int
類型的枚舉 - 需要使用
@objc
關(guān)鍵字進行修飾 - 還要
import Foundation
- 在
swift
中使用OC中的NS_ENUM
的枚舉就跟普通枚舉一致 - 如果使用
OC
中typedef enum
枚舉則需要通過init
方法進行初始化
- OC中只能使用