Swift 枚舉(enum)詳解
[TOC]
本文將介紹Swift
中枚舉的一些用法和其底層原理的一些探索,以及探索一下OC
中的枚舉與Swift
中枚舉互相調(diào)用和枚舉類型的內(nèi)存占用情況。
1. 枚舉
1.1 C中枚舉
首先我們來(lái)看看C
語(yǔ)言中枚舉的寫法。這里我們以一周7天作為示例。
普通寫法:
enum Week {
MON, TUE, WED, THU, FRI, SAT, SUN
}
以上就是C
語(yǔ)言中枚舉的常見(jiàn)寫法enum
關(guān)鍵字,加上枚舉名稱,大括號(hào)里面的不同的枚舉值使用逗號(hào)分隔開(kāi)來(lái)。此時(shí)的枚舉值默認(rèn)從0開(kāi)始,依次是1,2,3……
自定義枚舉值:
如果我們不想使用默認(rèn)的枚舉值,則可以這樣寫
enum Week {
MON = 1, TUE, WED, THU, FRI, SAT, SUN
};
此時(shí)枚舉值就會(huì)從1開(kāi)始依次向后排列,你也可以給每個(gè)枚舉都定義不同的枚舉值,如果直接給TUE
定義為2,而沒(méi)給MON
定義,則MON
的枚舉值會(huì)是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;
我們可以通過(guò)以上三種方法創(chuàng)建枚舉變量:
- 創(chuàng)建一個(gè)枚舉,然后聲明一個(gè)枚舉變量
- 創(chuàng)建一個(gè)枚舉并聲明一個(gè)枚舉變量
- 也可以省略枚舉名稱,直接聲明一個(gè)枚舉變量
1.2 Swift中枚舉
Swift
中最常見(jiàn)的枚舉寫法:
enum Week{
case MON
case TUE
case WED
case THU
case FRI
case SAT
case SUN
}
在Swift
中也可以簡(jiǎn)化為如下寫法:
enum Week{
case MON, TUE, WED, THU, FRI, SAT, SUN
}
Swift
中枚舉很強(qiáng)大,我們可以創(chuàng)建一個(gè)枚舉值是String
類型的enum
,其實(shí)也不應(yīng)該說(shuō)是枚舉值,而是枚舉的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)然,如果我們不想寫后面的字符串,也可以簡(jiǎn)寫成如下的形式:
enum Week: String{
case MON, TUE, WED, THU, FRI, SAT, SUN
}
定義枚舉變量:
var w: Week = .MON
枚舉變量的定義很簡(jiǎn)單,跟普通變量的定義沒(méi)什么差別。
枚舉的訪問(wèn):
我們可以訪問(wèn)枚舉的變量和枚舉的rawValue
首先我們需要注意的是如果沒(méi)有聲明枚舉的類型,是沒(méi)有rawValue
屬性可以訪問(wèn)的。
一般情況下我們可以通過(guò)以下方式訪問(wèn)枚舉:
enum Week: String{
case MON, TUE, WED, THU, FRI, SAT, SUN
}
print(Week.MON)
print(Week.MON.rawValue)
打印結(jié)果如下:
1.3 枚舉值和其RawValue的存儲(chǔ)
在上面枚舉的訪問(wèn)中我們可以看到關(guān)于MON
字符串的打印,既然可以打印出來(lái),說(shuō)明是存儲(chǔ)了相關(guān)的字符串的,那么是怎么存儲(chǔ)的呢?又是怎么獲取出來(lái)的呢?下面我們通過(guò)sil
代碼進(jìn)行分析(使用如下命令生成并打開(kāi)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 }
}
通過(guò)sil
代碼中對(duì)枚舉的定義可以看到:
- 跟
Swift
中一致的枚舉 - 取了一個(gè)別名,也就是
String
類型是RawValue
- 添加了一個(gè)可選類型的
init
方法 - 一個(gè)計(jì)算屬性
rawValue
,通過(guò)其get
方法獲取枚舉的原始值
下面我們?cè)?code>main函數(shù)中看看:
關(guān)于w
變量的初始化即分析注釋寫在了截圖中。
- 首先創(chuàng)建一個(gè)全局變量w,并為變量w開(kāi)辟內(nèi)存地址
- 將枚舉類型Week.MON存儲(chǔ)到%5
- 將枚舉Week的rawValue.getter函數(shù)存儲(chǔ)到%6
- 調(diào)用%6中存儲(chǔ)的函數(shù),%5作為參數(shù),返回值存儲(chǔ)到%7
- 將%7中獲取到額值存儲(chǔ)到%3,至此變量w初始化完成
下面我們看看rawValue
的getter
方法:
我們可以看到在rawValue
的getter
方法中主要實(shí)現(xiàn)是:
- 通過(guò)接收到的枚舉值去匹配一個(gè)分支
- 在分支中構(gòu)建對(duì)于的
String
- 返回上一步構(gòu)建的
String
那么這個(gè)字符串是從哪里來(lái)的呢?根據(jù)匹配的分支中的方法名稱我們可以知道這是獲取一個(gè)內(nèi)置的字符串的字面量。其實(shí)就是從Mach-O
文件的__TEXT.cstring
中。下面我們通過(guò)查看Mach-O
來(lái)驗(yàn)證。
所以說(shuō)rawValue
的值是通過(guò)調(diào)用枚舉的rawValue。getter
函數(shù),從Mach-O
對(duì)應(yīng)的地址中取出字符串并返回。
那么枚舉值呢?其實(shí)在上面關(guān)于rawValue
探索的時(shí)候就可以知道了,枚舉值在sil
代碼中就是:#Week.MON!enumelt
,枚舉值和rawValue
本質(zhì)上是不一樣的,從下面的例子可以得到結(jié)論:
按照以上的寫法是會(huì)報(bào)編譯錯(cuò)誤的。
1.4 枚舉.init
1.4.1 觸發(fā)方式
在上面的分析時(shí)我們知道枚舉會(huì)有一個(gè)init
方法,那么這個(gè)方法是什么時(shí)候調(diào)用的呢?我們添加如下符號(hào)斷點(diǎn):
添加如下代碼:
var w: Week = .MON
print(w.rawValue)
運(yùn)行后并沒(méi)有觸發(fā)該符號(hào)斷點(diǎn)。
下面我們?cè)谔砑尤缦麓a:
var w = Week(rawValue: "MON")
print(w)
運(yùn)行后即可觸發(fā)符號(hào)斷點(diǎn):
所以這里init
方法是為枚舉通過(guò)rawValue
初始化的時(shí)候調(diào)用的。
1.4.2 init分析
首先我們來(lái)看看如下代碼的打印結(jié)果:
print(Week.init(rawValue: "MON"))
print(Week.init(rawValue: "Hello"))
<!--打印結(jié)果-->
Optional(SwiftEnum.Week.MON)
nil
從打印結(jié)果中可以看到,第一個(gè)輸出的是可選值SwiftEnum.Week.MON
,第二個(gè)是nil
,很顯然Hello
不是我們的枚舉,那么這些是怎么實(shí)現(xiàn)的呢?我們?cè)俅尾榭?code>sil代碼,此時(shí)我們可以直接看Week.init
方法的實(shí)現(xiàn)。
方法比較長(zhǎng),在里面添加了相關(guān)的注釋和分析,折疊的代碼基本上是與其上面的代碼一致。現(xiàn)在總結(jié)如下:
- 首先開(kāi)辟一塊內(nèi)存用于后續(xù)存儲(chǔ)構(gòu)建出來(lái)的枚舉
- 通過(guò)
_allocateUninitializedArray
函數(shù)創(chuàng)建一個(gè)元組,元組中包含- 與枚舉個(gè)數(shù)大小一樣的數(shù)組,用于存儲(chǔ)枚舉中的
rawValue
在本示例中是staticString
, - 數(shù)組的首地址
- 與枚舉個(gè)數(shù)大小一樣的數(shù)組,用于存儲(chǔ)枚舉中的
- 開(kāi)始一個(gè)一個(gè)的構(gòu)建枚舉
rawValue
存儲(chǔ)到數(shù)組中 - 通過(guò)
_findStringSwitchCase
函數(shù)查找處要構(gòu)建的枚舉在數(shù)組中的位置index
- 從0到count-1依次與
index
作比較- 如果相等則構(gòu)建對(duì)于的枚舉
- 如果不相等則構(gòu)建一個(gè)
Optional.none!enumelt
的枚舉
- 將構(gòu)建的枚舉存儲(chǔ)到開(kāi)辟的地址
- 最后返回構(gòu)建的枚舉
關(guān)于上面提到的兩個(gè)函數(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
初始化一個(gè)數(shù)組,將其以元組的形式返回?cái)?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
}
我們可以看到這里接收一個(gè)數(shù)組和要匹配的字符串,然后通過(guò)一個(gè)for
循環(huán)匹配字符串,如果匹配到了則返回?cái)?shù)組中對(duì)應(yīng)的index
,否則返回-1。
1.5 枚舉的遍歷
一般我們很少會(huì)對(duì)枚舉進(jìn)行遍歷操作,在Swift
中可以通過(guò)遵守CaseIterable
協(xié)議來(lái)實(shí)現(xiàn)對(duì)枚舉的遍歷。
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)更多的信息。下面我們舉個(gè)例子,如果需要有一個(gè)形狀的枚舉,里面有圓形和矩形。圓形有半徑,矩形有長(zhǎng)寬,那么這個(gè)枚舉就可以寫成如下代碼:
enum Shape{
case circle(radius: Double)
case rectangle(width: Int, height: Int)
}
- 其中括號(hào)中的
radius
以及width
和height
就是關(guān)聯(lián)值 - 如果沒(méi)枚舉中使用關(guān)聯(lián)值則枚舉就沒(méi)有
rawValue
屬性了,因?yàn)殛P(guān)聯(lián)值是一組值,而rawValue
是單個(gè)值,可以通過(guò)sil
代碼驗(yàn)證
在sil
代碼中我們并沒(méi)有發(fā)現(xiàn)init
方法RawValue
別名以及rawValue
的get
方法。
在這個(gè)枚舉中radius、width、height
這些都是自定義的標(biāo)簽,也可以不寫,如下所示,但并不推薦這種方式,因?yàn)榭勺x性非常差
enum Shape{
case circle(Double)
case rectangle(Int, Int)
}
那么有關(guān)聯(lián)值的枚舉該如何初始化呢?其實(shí)也很簡(jiǎn)單,下面我們就來(lái)創(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 簡(jiǎn)單的模式匹配
顧明思議,模式匹配就是匹配每一個(gè)枚舉值,通常我們可以使用switch
語(yǔ)句來(lái)進(jìn)行模式匹配。如果使用switch
進(jìn)行模式匹配:
- 必須列舉當(dāng)前所有可能的情況,否則就會(huì)報(bào)編譯錯(cuò)誤
- 如果不想匹配這么多
case
則可以使用defalut
- 在同一個(gè)
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")
}
其實(shí)這個(gè)匹配也很簡(jiǎn)單,我們通過(guò)查看sil
代碼就可以知道:
2.1.2 關(guān)聯(lián)值枚舉的模式匹配
如果我們不關(guān)心關(guān)聯(lián)值,關(guān)聯(lián)值枚舉的寫法與普通枚舉沒(méi)有什么區(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)值枚舉,肯定是會(huì)關(guān)心關(guān)聯(lián)值的,當(dāng)關(guān)心關(guān)聯(lián)值時(shí)其寫法如下:
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),這里的每個(gè)case
中都使用了let
或者var
,這里因?yàn)橐褂藐P(guān)聯(lián)值,所以需要使用let
聲明一下。或者放在最前面,或者對(duì)每個(gè)需要使用的變量前都添加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 其他匹配
有時(shí)候在業(yè)務(wù)邏輯處理中,我們只是想匹配單個(gè)case
,我們可以這樣寫:
if case let Shape.circle(radius) = shape {
print("circle radius: \(radius)")
}
當(dāng)然如果我們只關(guān)心不同case
的相同關(guān)聯(lián)值時(shí)就可以這樣寫:
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
}
此時(shí)的打印是20,對(duì)于上面的例子,必須case
是rectangle
或square
,而且rectangle
必須是10,square
后面的width
是10。
如果對(duì)于10的匹配不那么嚴(yán)格我們則可以使用通配符_
switch shape {
case let .rectangle(x, _), let .square(x, _):
print(x)
default: break
}
注意: 以上命名必須一致,比如都使用x
,如果一個(gè)x
一個(gè)y
就不行了。
2.2 枚舉的嵌套
2.2.1 枚舉嵌套枚舉
顧名思義,枚舉嵌套枚舉就是在枚舉中還有枚舉,比如我們玩游戲時(shí)會(huì)有上下左右四個(gè)方向鍵,有時(shí)候也需要兩兩組合去使用,所以我們通過(guò)這個(gè)例子可以編寫如下枚舉:
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)
}
使用起來(lái)也很簡(jiǎn)單:
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")
}
}
}
使用起來(lái)也很簡(jiǎn)單:
let s = Skill(key: .up)
s.launchSkill()
2.3 枚舉中包含屬性
Swift
中允許在枚舉中包含計(jì)算屬性和類型屬性,但不能包含存儲(chǔ)屬性。
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
中的枚舉也可以包含方法,可以是實(shí)例方法也可以是類方法
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")
}
}
使用起來(lái)依舊很簡(jiǎn)單:
var w = Week.SUN
w.nextDay()
print(w)
Week.test()
此處的方法都是靜態(tài)調(diào)用:
2.4 indiret在枚舉中的應(yīng)用
2.4.1 indiret
如果我們想要使用enum
表達(dá)一個(gè)復(fù)雜的關(guān)鍵數(shù)據(jù)結(jié)構(gòu)的時(shí)候,我們可以通過(guò)使用indrect
關(guān)鍵字來(lái)讓enum
更簡(jiǎn)潔。
比如我們想要通過(guò)枚舉來(lái)表達(dá)一個(gè)鏈表的結(jié)構(gòu),鏈表需要存儲(chǔ)數(shù)據(jù)以及指向它的下一個(gè)節(jié)點(diǎn)的指針,如果不使用indiret
修飾則會(huì)報(bào)編譯錯(cuò)誤:
此時(shí)我們可以寫成如下兩種方式就不會(huì)報(bào)錯(cuò)了:
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)鍵字呢?
因?yàn)?code>enum是值類型,它的大小在編譯期就需要確定,如果按照開(kāi)始的寫法是不能夠確定當(dāng)前enum
的大小的,所以從系統(tǒng)的角度來(lái)說(shuō),在不知道給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.
譯:您可以通過(guò)在枚舉案例之前寫indirect來(lái)表明枚舉案例是遞歸的,這告訴編譯器插入必要的間接層。
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
此時(shí)我們發(fā)現(xiàn)泛型的更換內(nèi)存占用保持不變,此時(shí)我們創(chuàng)建一個(gè)使用indirect
修飾的枚舉類型的變量:
var node = List<Int>.node(10, next: List<Int>.end)
通過(guò)lldb
查看node
的內(nèi)存:
可以看到node
像一個(gè)對(duì)象的結(jié)構(gòu),所以說(shuō)這里面存儲(chǔ)的是一個(gè)指針,當(dāng)不確定枚舉類型大小的時(shí)候,將分配一個(gè)8字節(jié)大小的指針,指向一塊堆空間用于存儲(chǔ)這不確定大小的枚舉。
如果是end
,此時(shí)存儲(chǔ)的就是case
值
那么這些是如何實(shí)現(xiàn)的呢?我們通過(guò)sil
代碼來(lái)看一下:
這里我們可以看到使用了alloc_box
,我們打開(kāi)SIL參考文檔,并找到alloc-box
我們可以看到alloc_box
就是在堆上分配一個(gè)引用計(jì)數(shù)@box,該值足夠大,可以容納T類型的值,以及一個(gè)retain count
和運(yùn)行時(shí)所需的任何其他元數(shù)據(jù)。
其本質(zhì)是調(diào)用了swift_allocObject
,這點(diǎn)可以通過(guò)匯編代碼驗(yàn)證:
3. 枚舉的大小
3.1 普通枚舉大小分析
首先看看下面這段代碼的打印結(jié)果:
enum NoMean{
case a
}
print(MemoryLayout<NoMean>.size)
print(MemoryLayout<NoMean>.stride)
<!--打印結(jié)果-->
0
1
如果我們?cè)谠黾右粋€(gè)case
呢?
enum NoMean{
case a
case b
}
print(MemoryLayout<NoMean>.size)
print(MemoryLayout<NoMean>.stride)
<!--打印結(jié)果-->
1
1
如果在多增加幾個(gè)呢?
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é)存儲(chǔ)在內(nèi)存中的,下面我們來(lái)分析一下:
首先我們添加如下代碼:
var a = NoMean.a
var b = NoMean.b
var c = NoMean.c
var d = NoMean.d
lldb調(diào)試:
所以這里當(dāng)前枚舉的步長(zhǎng)是1字節(jié),也就意味著如果內(nèi)存中連續(xù)存儲(chǔ)NoMean
,需要跨越一個(gè)字節(jié)的長(zhǎng)度。一個(gè)字節(jié)也就是8位,最大可以表達(dá)255個(gè)數(shù)字。由于太長(zhǎng)就不測(cè)試了,如果真的需要寫255及以上,還是建議以別的方式優(yōu)化一下。
如果枚舉后面寫了類型,比如:
enum NoMean: Int{
case a
case b
case c
case d
}
此時(shí)打印枚舉的大小和步長(zhǎng)還是1,這里面的類型指的是rawValue
,并不是case
的值。
- 所以枚舉中默認(rèn)是以
UInt8
存儲(chǔ)的,最大可以存儲(chǔ)0~255,如果不夠則會(huì)自動(dòng)轉(zhuǎn)換為UInt16
,以此類推。 - 當(dāng)只有一個(gè)
case
的時(shí)候,size
是0,表示這個(gè)枚舉是沒(méi)有意義的 - 枚舉中后面聲明的類型只的是
rawValue
的類型,不會(huì)影響枚舉的大小 - 這些
rawValue
的值會(huì)存儲(chǔ)在Mach-O
文件中,在使用的時(shí)候取查找,這個(gè)在上面提到過(guò),與枚舉大小沒(méi)有關(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)值的大小,此時(shí)circle
中的關(guān)聯(lián)值Int
占用內(nèi)存大小是8,而rectangle
中兩個(gè)Int
加起來(lái)是16,那么打印的這個(gè)17是怎么來(lái)的呢?其實(shí)還有存儲(chǔ)枚舉值,所以枚舉的大小此處枚舉的size = 8+8+1 = 17,由于內(nèi)存對(duì)齊,所以要分配8的整數(shù)倍,所以stride
就是24。這是該枚舉中最大需要的內(nèi)存,這個(gè)內(nèi)存足夠容納circle
需要的9字節(jié)的大小。
下面我們,修改一下代碼順序,創(chuàng)建一下具有關(guān)聯(lián)值的枚舉,看看其內(nèi)存分布:
我們可以看到circle
是分配了24字節(jié)的內(nèi)存空間的,內(nèi)存分布首先是存儲(chǔ)關(guān)聯(lián)自,然后在存儲(chǔ)枚舉值,circle
的枚舉值是存儲(chǔ)在第三個(gè)8字節(jié)上的,也就是存儲(chǔ)在最后。
- 具有關(guān)聯(lián)值的枚舉的大小取決于關(guān)聯(lián)值的大小
- 具有關(guān)聯(lián)值的枚舉的大小是枚舉中最大的那個(gè)關(guān)聯(lián)值枚舉的大小 + 1(case 需要占用1字節(jié)),
- 如果大于255可能需要占用2字節(jié),這里沒(méi)有進(jìn)行測(cè)試
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é)果我們可以知道嵌套的枚舉,其實(shí)也就是枚舉中關(guān)聯(lián)了枚舉,它的大小同樣取決于關(guān)聯(lián)值的大小,因?yàn)?code>BaseDirect是基本的枚舉,其內(nèi)存占用為1,那么按照關(guān)聯(lián)值枚舉中的內(nèi)存占用應(yīng)該是1+1+1 = 3,那么為什么是2呢?
下面我們通過(guò)創(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)存
通過(guò)lldb
調(diào)試的結(jié)果我們可以看到,在每個(gè)字節(jié)的低4位上存儲(chǔ)著關(guān)聯(lián)值的值,而在最后那個(gè)關(guān)聯(lián)值的高四位分別存儲(chǔ)了0,4,8,12(c),所以對(duì)于枚舉中嵌套枚舉應(yīng)該是做了相應(yīng)的優(yōu)化,借用未使用的高位存儲(chǔ)關(guān)聯(lián)值枚舉的枚舉值。
下面我們測(cè)試一下,多寫幾個(gè):
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é)果:
此時(shí)我們發(fā)現(xiàn),最后那個(gè)關(guān)聯(lián)值的高四位分別存儲(chǔ)了0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f。
如果我們隨便注釋幾個(gè):
我們發(fā)現(xiàn),對(duì)應(yīng)的結(jié)果與枚舉中的順序是一致的。
如果只剩一個(gè),但不是第一個(gè):
此時(shí)我們發(fā)現(xiàn)是7,與枚舉中的屬性還是一致的。
下面我們開(kāi)始注釋枚舉中的:
最后發(fā)現(xiàn):
- 如果是兩個(gè)枚舉就是0,8
- 如果是3或4的時(shí)候是按照0,4,8,c
- 大約4小于等于16的時(shí)候是0,1,2,3,4......f
- 如果大于16就不能看出上面的規(guī)律了,所以從二進(jìn)制位看
對(duì)于枚舉中嵌套枚舉,使用關(guān)聯(lián)值,又或者說(shuō)不嵌套,具有關(guān)聯(lián)值的枚舉中的關(guān)聯(lián)值是枚舉類型的時(shí)候,會(huì)優(yōu)先借用最后關(guān)聯(lián)的那個(gè)枚舉的二進(jìn)制位存儲(chǔ)具有關(guān)聯(lián)值枚舉的值,借用的位數(shù)為關(guān)聯(lián)值枚舉的個(gè)數(shù)小于等于2的冪最小值,也就是2的幾次冪才能大于等于關(guān)聯(lián)枚舉的個(gè)數(shù)。
這里我有進(jìn)一步測(cè)試,如果普通枚舉的個(gè)數(shù)不足以使用低四位表示,比如低四位最少表示16個(gè),如果多了的話,就會(huì)借用關(guān)聯(lián)值中倒數(shù)第二個(gè),也就上面例子中的第一個(gè)關(guān)聯(lián)值的高位進(jìn)行借位存儲(chǔ)。按照這個(gè)邏輯,大膽猜想,如果普通枚舉的個(gè)數(shù)為256個(gè),也就是不能借任何一個(gè)位,這種具有關(guān)聯(lián)值枚舉是不是會(huì)另外開(kāi)辟內(nèi)存存關(guān)聯(lián)值枚舉的值?其實(shí)不需要是256個(gè),只要不夠借的時(shí)候就會(huì)開(kāi)辟內(nèi)存去存儲(chǔ)關(guān)聯(lián)值枚舉的值。
舉個(gè)例子:
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
這個(gè)例子并沒(méi)有嵌套,其實(shí)與嵌套沒(méi)有任何關(guān)系,嵌套的枚舉也是單獨(dú)存儲(chǔ)的,只不過(guò)嵌套的枚舉作用域只在嵌套的大括號(hào)內(nèi)。
另外,如果枚舉值過(guò)多的時(shí)候,我們看sil
代碼:
此時(shí)我們可以發(fā)現(xiàn):
- 多了一個(gè)
hashValue
的計(jì)算屬性 - 一個(gè)遵守了
Equatable
協(xié)議的__derived_enum_equals
imp - 以及一個(gè)
hash
函數(shù)
我猜想,對(duì)于過(guò)多case
的枚舉,swift
為了更好更快的匹配,使用了蘋果慣用的哈希。我嘗試在源碼中搜索了一下derived_enum_equals
并沒(méi)有找到相關(guān)方法,貌似是過(guò)期被移除了,后面使用==
代替。
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)系,這里就不一一驗(yàn)證了。
總的來(lái)說(shuō),結(jié)構(gòu)體中嵌套枚舉與枚舉嵌套枚舉是一樣的,他們都不存儲(chǔ)枚舉,只是作用域在其中而已。
4. 與OC混編
綜上,我們可以看到Swift
中的枚舉非常強(qiáng)大,而在OC
中枚舉僅僅只是一個(gè)整數(shù)值,那么在與OC
混編的時(shí)候,該如何在OC
中使用Swift
的枚舉呢?下面我們就來(lái)探索一下:
4.1 OC調(diào)用Swift中的枚舉
如果想要在OC
中使用Swift
枚舉要求會(huì)很嚴(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
}
此時(shí)編譯后就可以正在project-Swift.h
中看到轉(zhuǎn)換后的對(duì)應(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)建的枚舉會(huì)自動(dò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
這種形式的枚舉,會(huì)轉(zhuǎn)換成結(jié)構(gòu)體,同樣可以在ocfileName.h
中查看轉(zhuǎn)換后的結(jié)果,轉(zhuǎn)換后的代碼如下:
可以看到里面有一個(gè)rawValue
屬性,以及init
方法。還遵守了Equatable, RawRepresentable
兩個(gè)協(xié)議。
使用的話也是需要導(dǎo)入頭文件:
let num = OCEnum(0)
let num1 = OCEnum.init(0)
let num2 = OCEnum.init(rawValue: 3)
print(num)
<!--打印結(jié)果-->
OCNum(rawValue: 0)
這里我們只能通過(guò)init
方法去初始化,不能訪問(wèn)枚舉中的變量。
4.2.3 使用typedef NS_ENUM
typedef NS_ENUM(NSInteger, OCENUM){
OCEnumInvalid = 0,
OCEnumA = 1,
OCEnumB,
OCEnumC
};
使用typedef NS_ENUM
也會(huì)自動(dòng)轉(zhuǎn)換為Swift
的枚舉,轉(zhuǎn)換后的代碼如下:
使用也是需要導(dǎo)入頭文件:
let ocenum = OCENUM.OCEnumInvalid
let ocenumRawValue = OCENUM.OCEnumA.rawValue
4.3 混編時(shí)需要使用String類型的枚舉
這里的意思是,Swift
中需要使用String
類型的枚舉,但是又要與OC
混編,暴露給OC
使用。
如果直接聲明為String
類型編譯時(shí)不會(huì)通過(guò)的,這里只能弄個(gè)假的。Swift
中的枚舉還是聲明為Int
,可以在枚舉中聲明一個(gè)變量或者方法,用于返回想要的字符串。
@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é)
其實(shí)感覺(jué)該篇不太適合總結(jié),因?yàn)橹苯咏o結(jié)論會(huì)使人不是很好理解,但是還是記錄一下吧。
-
Swift
中的枚舉很強(qiáng)大 -
enum
中的rawValue
是其中的計(jì)算屬性 - 如果聲明的時(shí)候不指定枚舉類型就沒(méi)有
rawValue
屬性(包括關(guān)聯(lián)值) -
rawValue
中的值存儲(chǔ)在Mach-O
中,不占用枚舉的存儲(chǔ)空間 - 枚舉值與
rawValue
不是同一個(gè)東西 -
rawValue
可以不寫,如果是Int
默認(rèn)0,1,2...String
等于枚舉名稱的字符串 - 如果枚舉中存在
rawValue
同時(shí)也會(huì)存在init(rawValue:)
方法,用于通過(guò)rawValue
值初始化枚舉 - 如果枚舉遵守了
CaseIterable
協(xié)議,且不是關(guān)聯(lián)值的枚舉,我們可以通過(guò)enum.allCases
獲取到所有的枚舉,然后通過(guò)for
循環(huán)遍歷 - 我們可以使用
switch
對(duì)枚舉進(jìn)行模式匹配,如果只關(guān)系一個(gè)枚舉還可以使用if case
- 關(guān)聯(lián)值枚舉可以表示復(fù)雜的枚舉結(jié)構(gòu)
- 關(guān)聯(lián)值的枚舉沒(méi)有init方法,沒(méi)有
RawValue
別名,沒(méi)有rawValue
計(jì)算屬性 -
enum
可以嵌套enum
,被嵌套的作用域只在嵌套內(nèi)部 - 結(jié)構(gòu)體也可以嵌套
enum
,此時(shí)enum
的作用域也只在結(jié)構(gòu)體內(nèi) -
enum
中可以包含計(jì)算屬性
,類型屬性
但不能包含存儲(chǔ)屬性
-
enum
中可以定義實(shí)例方法和使用static
修飾的方法,不能定義class
修飾的方法 - 如果想使用復(fù)雜結(jié)構(gòu)的枚舉,或者說(shuō)是具有遞歸結(jié)構(gòu)的枚舉可以使用
indirect
關(guān)鍵字
-
關(guān)于枚舉的大小
- 默認(rèn)情況下枚舉占用1字節(jié)也就是
UInt8
,如果不夠用也就是超過(guò)256個(gè)的時(shí)候會(huì)使用UInt16,UInt32...
(太多沒(méi)有去驗(yàn)證,如果真的有這么多枚舉,建議通過(guò)其他方式去優(yōu)化) - 如果枚舉個(gè)數(shù)過(guò)多會(huì)使用哈希來(lái)進(jìn)行優(yōu)化,以便快速匹配
- 使用關(guān)聯(lián)值的枚舉大小取決于關(guān)聯(lián)值的大小,還要考慮內(nèi)存對(duì)齊
- 關(guān)聯(lián)值的枚舉中的關(guān)聯(lián)值如果是普通枚舉類型,系統(tǒng)會(huì)通過(guò)借位優(yōu)化的方式節(jié)省內(nèi)存的占用
- 如果借位不夠了,會(huì)單獨(dú)開(kāi)辟內(nèi)存存儲(chǔ)關(guān)聯(lián)值枚舉的枚舉值
- 在嵌套的時(shí)候,無(wú)論結(jié)構(gòu)體還是枚舉中都是不占用內(nèi)存的,被嵌套的枚舉是單獨(dú)存儲(chǔ)的,只是作用域在其內(nèi)部而已
- 默認(rèn)情況下枚舉占用1字節(jié)也就是
-
關(guān)于和
OC
混編- OC中只能使用
Swift
中Int
類型的枚舉 - 需要使用
@objc
關(guān)鍵字進(jìn)行修飾 - 還要
import Foundation
- 在
swift
中使用OC中的NS_ENUM
的枚舉就跟普通枚舉一致 - 如果使用
OC
中typedef enum
枚舉則需要通過(guò)init
方法進(jìn)行初始化
- OC中只能使用