Swift 枚舉本質(zhì)

我們先來(lái)問(wèn)大家一個(gè)問(wèn)題 下面打印結(jié)果是多少

var abc: Int = 100
var efg: Int? = 200

/*
 Memlayout.size(ofValue value: T) // 獲取變量實(shí)際占用的內(nèi)存大小

 Memlayout.stride(ofValue value: T)     // 獲取創(chuàng)建變量所需要的分配的內(nèi)存大小

 MemoryLayout.alignment(ofValue: T) // 獲取變量的內(nèi)存對(duì)齊數(shù)
 
 */

print("abc 實(shí)際占用的內(nèi)存 ======= \(MemoryLayout.size(ofValue: abc))")
print("efg 實(shí)際占用的內(nèi)存 ======= \(MemoryLayout.size(ofValue: efg))")

下面看一下打印結(jié)果

abc 實(shí)際占用的內(nèi)存 ======= 8
efg 實(shí)際占用的內(nèi)存 ======= 9
Program ended with exit code: 0

那么現(xiàn)在問(wèn)題來(lái)了為什么可選類型(Int眼虱?)比不可選類型(Int)多一個(gè)字節(jié)屉栓?

那我們先來(lái)看一下可選類型代碼

@frozen public enum Optional<Wrapped> : ExpressibleByNilLiteral {

    /// The absence of a value.
    ///
    /// In code, the absence of a value is typically written using the `nil`
    /// literal rather than the explicit `.none` enumeration case.
    case none

    /// The presence of a value, stored as `Wrapped`.
    case some(Wrapped)

    /// Creates an instance that stores the given value.
    public init(_ some: Wrapped)
    /// Creates an instance initialized with `nil`.
    ///
    /// Do not call this initializer directly. It is used by the compiler when you
    /// initialize an `Optional` instance with a `nil` literal. For example:
    ///
    ///     var i: Index? = nil
    ///
    /// In this example, the assignment to the `i` variable calls this
    /// initializer behind the scenes.
    public init(nilLiteral: ())
}

查看Optional的源碼得知,其實(shí)在swift中可選類型就是一個(gè)添加了關(guān)聯(lián)值的枚舉衡楞,例如:

Int?`就等價(jià)于 `Optional<Int>
let age: Int? = 2` 就等價(jià)于 `let age: Optional<Int> = Optional.some(2)

ExpressibleByNilLiteral是一個(gè)nil的字面量協(xié)議,代表可以使用nil這個(gè)關(guān)鍵字來(lái)進(jìn)行初始化,Optional實(shí)現(xiàn)了這個(gè)協(xié)議的方法init(nilLiteral: ()) { self = .none }图张,所以let age: Int? = nil就等價(jià)于 let age: Optional<Int> = Optional.init(nilLiteral: ())

只是編譯器在背后幫我們做了一些轉(zhuǎn)換而已诈悍。

枚舉占多少內(nèi)存

1.1祸轮、普通枚舉

我們先來(lái)創(chuàng)建一個(gè)普通的枚舉用MemoryLayout獲取一下

enum Direction {
    case north, south, east, west
}

enum Direction {
    case north, south, east, west
}

func textEnum() {
    let dir = Direction.south
    print("Direction 實(shí)際占用內(nèi)存 ===== \(MemoryLayout<Direction>.size)")
    print("dir 實(shí)際占用內(nèi)存 ===== \(MemoryLayout.size(ofValue: dir))")
    
}
// 打印結(jié)果
Direction 實(shí)際占用內(nèi)存 ===== 1
dir 實(shí)際占用內(nèi)存 ===== 1
Program ended with exit code: 0

那么現(xiàn)在我們知道了枚舉占用的內(nèi)存是一個(gè)字節(jié) 那么這一個(gè)字節(jié)里面裝的是什么呢?

通常如果我們知道了一個(gè)內(nèi)存地址侥钳,我們可以通過(guò)下面兩種方式查看地址對(duì)應(yīng)內(nèi)存空間存放的數(shù)據(jù):

1适袜、我們可以在xcode -> Debug -> Debug workflow -> View Memory中輸入內(nèi)存地址定位到那塊內(nèi)存空間

2、在lldb中使用指令memory read + 內(nèi)存地址讀取指針對(duì)應(yīng)的內(nèi)存舷夺。也可以直接使用指令x簡(jiǎn)化書(shū)寫(xiě)苦酱,效果等同于memory read

現(xiàn)在問(wèn)題就變成我們?cè)撊绾潍@取Swift變量的內(nèi)存地址了,但由于xcode對(duì)Swift語(yǔ)言做了非常多的封裝和屏蔽给猾,斷點(diǎn)調(diào)試時(shí)疫萤,我們不能直接像oc/c語(yǔ)言那樣直接看到枚舉變量的地址,我們只能通過(guò)Swift的方式獲取內(nèi)存地址敢伸。

func getPointer<T>(of value: inout T) -> UnsafeRawPointer {
  return withUnsafePointer(to: &value, { UnsafeRawPointer($0) })
}

我們來(lái)看一下內(nèi)存數(shù)據(jù)

func textEnum() {
    var dir = Direction.south
    print("\(getPointer(of: &dir))")
    print("====================")
}
1.png

或者使用View Memory` 如下步驟

4.png

上面我們知道Direction枚舉只占用一個(gè)字節(jié)扯饶,所以我們只需要查看第1個(gè)字節(jié)的數(shù)據(jù):可以看到原來(lái)dir變量在內(nèi)存中的真實(shí)存儲(chǔ)數(shù)據(jù)是0x1,同樣的我們也可以測(cè)試到Direction.north在內(nèi)存中的值是0x0详拙,Direction.east在內(nèi)存中的值是0x2帝际,Direction.west在內(nèi)存中的值是0x3。

?提醒:Swift和OC混編時(shí)饶辙,Swift中的enum要想在OC中使用蹲诀,需要添加@objc修飾符,而添加完@objc修飾符之后弃揽,swift的枚舉占用的內(nèi)存大小就不是由枚舉類型的數(shù)目決定的了脯爪,而是固定為和Int類型大小一致则北。

1.2、帶初始值的枚舉

我們來(lái)看一下帶初始值的枚舉占多少內(nèi)存

6.png

枚舉的內(nèi)存還是一個(gè)字節(jié)存儲(chǔ)的是case的值 那么問(wèn)題就來(lái)了那我們的初始值去哪了痕慢?

其實(shí)熟悉原始值使用語(yǔ)法的同學(xué)都知道尚揣,枚舉的原始值并不是直接拿來(lái)使用的,而是通過(guò)枚舉的一個(gè)名為rawValue的屬性才可以訪問(wèn)到的掖举,我們是不是可以根據(jù)剛才看到的內(nèi)存結(jié)構(gòu)大膽的猜測(cè)一下:是不是定義枚舉變量時(shí)快骗,原始值并不會(huì)被存儲(chǔ)在枚舉的內(nèi)存空間中,而有可能只是編譯器幫我們生成了一個(gè)rawValue的計(jì)算屬性塔次,然后在計(jì)算屬性的內(nèi)部判斷枚舉自身的類型來(lái)返回不同的原始值方篮。

我們來(lái)看一下rawValue的匯編是什么樣的

7.png

8.png

通過(guò)rawValue的匯編可以看到是調(diào)用了rawValue的getter的函數(shù) 從這里可以猜測(cè)出計(jì)算屬性就是函數(shù)調(diào)用

給枚舉添加原始值就是編譯器幫我們實(shí)現(xiàn)了RawRepresentable協(xié)議,實(shí)現(xiàn)了rawValue励负、init(_ rawValue)函數(shù)藕溅,rawValue函數(shù)在內(nèi)部對(duì)self參數(shù)進(jìn)行switch判斷,以此返回不同的的原始值继榆。

帶關(guān)聯(lián)至的枚舉
10.png

可以看到三種類型的Achievement輸出的size都是9巾表,為什么都是9個(gè)字節(jié)呢?

那么關(guān)聯(lián)值的實(shí)現(xiàn)是不是也可能像原始值那樣略吨,是編譯器幫我們生成一些計(jì)算屬性集币、方法之類的,幫我們保存關(guān)聯(lián)值晋南?仔細(xì)思考一下答案應(yīng)該是否定的惠猿,關(guān)聯(lián)值是不同于原始值的羔砾,因?yàn)樵贾凳且粋€(gè)確定值负间,在程序編譯時(shí)期就可以確定下來(lái)的值,而關(guān)聯(lián)值是不確定的姜凄,每一個(gè)枚舉變量綁定的關(guān)聯(lián)值都是不同的政溃,值是在程序運(yùn)行的時(shí)候才能確定的,我們可以使用case let語(yǔ)法從枚舉中解析出不同的關(guān)聯(lián)值态秧, 那么這個(gè)關(guān)聯(lián)值一定是和枚舉變量有密切關(guān)聯(lián)的董虱,所以關(guān)聯(lián)值是不是被直接存放在枚舉變量中呢?我們分析一下englishScore枚舉申鱼,一個(gè)Int類型在64位系統(tǒng)占用8個(gè)字節(jié)愤诱,除此之外通過(guò)第一部分的學(xué)習(xí)我們知道枚舉自己還需要一個(gè)字節(jié)來(lái)區(qū)分枚舉類型,所以8 + 1 = 9捐友,正好可以解釋ach變量的大小為什么是9淫半。

大家會(huì)接著疑惑為什么ach1、ach2也占用9個(gè)字節(jié)呢匣砖?按照剛才的計(jì)算法則Bool變量只占1個(gè)字節(jié)科吭,加上枚舉自身的一個(gè)字節(jié)應(yīng)該是1 + 1 = 2個(gè)字節(jié)就可以了昏滴,為什么還需要占用9個(gè)字節(jié)呢?這個(gè)時(shí)候我們不能只考慮自身所占用的數(shù)據(jù)大小对人,大家想一下枚舉的一些使用場(chǎng)景谣殊,比如如果我們要將ach1重新賦值為其他的類型如ach1=Achievement.mathScore(100)`,ach1的兩個(gè)字節(jié)還夠用嗎牺弄,又比如我們定義一個(gè)枚舉數(shù)組姻几,如果每一個(gè)元素的占用的內(nèi)存大小都不一樣,數(shù)組該怎么根據(jù)下標(biāo)尋址呢势告,所以枚舉枚舉變量的size是固定的鲜棠,而大小是取決于需要占用內(nèi)存空間最大的那個(gè)類型。

1.3培慌、默認(rèn)實(shí)現(xiàn)的協(xié)議

大家有沒(méi)有發(fā)現(xiàn)一個(gè)現(xiàn)象:我們定義的簡(jiǎn)單枚舉類型<沒(méi)有關(guān)聯(lián)值>默認(rèn)就可以進(jìn)行==豁陆、!=運(yùn)算,要知道在Swift語(yǔ)言中==不再是一個(gè)運(yùn)算符了吵护,==是一個(gè)函數(shù)盒音,是屬于Equatable協(xié)議中的一個(gè)函數(shù),但我們的枚舉又沒(méi)有實(shí)現(xiàn)Equatable怎么也可以進(jìn)行比較呢馅而?

編譯器默認(rèn)會(huì)幫我們實(shí)現(xiàn)Hashable/Equatable協(xié)議祥诽,這就是為什么我們的枚舉可以調(diào)用hashValue屬性,可以進(jìn)行==運(yùn)算的原因瓮恭。接著我們給枚舉添加關(guān)聯(lián)值后再試一下雄坪,這個(gè)時(shí)候你會(huì)發(fā)現(xiàn)編譯器什么協(xié)議也沒(méi)幫我們添加,想必大家在開(kāi)發(fā)過(guò)程中也發(fā)現(xiàn)了屯蹦,設(shè)置關(guān)聯(lián)值之后的枚舉確實(shí)是不能進(jìn)行==運(yùn)算的维哈,大家猜想一下是為什么呢,為什么設(shè)置了關(guān)聯(lián)值編譯器就不幫我們實(shí)現(xiàn)協(xié)議了呢登澜?

其實(shí)通過(guò)第二三部分的探索我們大概可以知道答案了阔挠,還是要從枚舉底層的內(nèi)存結(jié)構(gòu)來(lái)看,枚舉在沒(méi)有綁定關(guān)聯(lián)值的時(shí)候脑蠕,本身其實(shí)就是一個(gè)整型值购撼,類似Int,Swift系統(tǒng)的Int默認(rèn)也是實(shí)現(xiàn)了Hashable/Equatable協(xié)議的谴仙,系統(tǒng)當(dāng)然可以像對(duì)待Int一樣幫我們實(shí)現(xiàn)Hashable/Equatable協(xié)議迂求,而當(dāng)我們添加了關(guān)聯(lián)值之后,枚舉在的內(nèi)存中的數(shù)據(jù)結(jié)構(gòu)就是由枚舉本身和關(guān)聯(lián)值兩部分組成了晃跺,編譯器是不能確定具體要怎么樣比較揩局,怎么樣hash了,則需要由我們開(kāi)發(fā)者自己實(shí)現(xiàn)了哼审。

1.4谐腰、總結(jié)

下面來(lái)總結(jié)一下我們學(xué)到的知識(shí)吧孕豹。

1、簡(jiǎn)單枚舉<沒(méi)有關(guān)聯(lián)值>的本質(zhì)就是一個(gè)整型值十气,整型值的大小取決于該枚舉所定義的類型的數(shù)量励背。

2、給枚舉添加原始值不會(huì)影響枚舉自身的任何結(jié)構(gòu)砸西,設(shè)置原始值其實(shí)是編譯器幫我們添加了rawValue屬性叶眉,init(rawValue)方法(RawRepresentable協(xié)議)。

3芹枷、添加關(guān)聯(lián)值會(huì)影響枚舉內(nèi)存結(jié)構(gòu)衅疙,關(guān)聯(lián)值被儲(chǔ)存在枚舉變量中,枚舉變量的大小取決于占用內(nèi)存最大的那個(gè)類型鸳慈。

4饱溢、添加/調(diào)用"實(shí)例方法"、"類型方法"走芋、計(jì)算屬性以及實(shí)現(xiàn)協(xié)議的本質(zhì)都是添加/調(diào)用函數(shù)绩郎。

5、對(duì)于沒(méi)有添加關(guān)聯(lián)值的枚舉系統(tǒng)會(huì)默認(rèn)幫我們實(shí)現(xiàn)Hashable/Equatable協(xié)議翁逞。

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末肋杖,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子挖函,更是在濱河造成了極大的恐慌状植,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,198評(píng)論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件怨喘,死亡現(xiàn)場(chǎng)離奇詭異津畸,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)哲思,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門(mén)洼畅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)吩案,“玉大人棚赔,你說(shuō)我怎么就攤上這事∨枪” “怎么了靠益?”我有些...
    開(kāi)封第一講書(shū)人閱讀 167,643評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)残揉。 經(jīng)常有香客問(wèn)我胧后,道長(zhǎng),這世上最難降的妖魔是什么抱环? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,495評(píng)論 1 296
  • 正文 為了忘掉前任壳快,我火速辦了婚禮纸巷,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘眶痰。我一直安慰自己瘤旨,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布竖伯。 她就那樣靜靜地躺著存哲,像睡著了一般。 火紅的嫁衣襯著肌膚如雪七婴。 梳的紋絲不亂的頭發(fā)上祟偷,一...
    開(kāi)封第一講書(shū)人閱讀 52,156評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音打厘,去河邊找鬼修肠。 笑死,一個(gè)胖子當(dāng)著我的面吹牛户盯,可吹牛的內(nèi)容都是我干的氛赐。 我是一名探鬼主播,決...
    沈念sama閱讀 40,743評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼先舷,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼艰管!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起蒋川,我...
    開(kāi)封第一講書(shū)人閱讀 39,659評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤牲芋,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后捺球,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體缸浦,經(jīng)...
    沈念sama閱讀 46,200評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評(píng)論 3 340
  • 正文 我和宋清朗相戀三年氮兵,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了裂逐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,424評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡泣栈,死狀恐怖卜高,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情南片,我是刑警寧澤掺涛,帶...
    沈念sama閱讀 36,107評(píng)論 5 349
  • 正文 年R本政府宣布,位于F島的核電站疼进,受9級(jí)特大地震影響薪缆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜伞广,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評(píng)論 3 333
  • 文/蒙蒙 一拣帽、第九天 我趴在偏房一處隱蔽的房頂上張望疼电。 院中可真熱鬧,春花似錦减拭、人聲如沸澜沟。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,264評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)茫虽。三九已至,卻和暖如春既们,著一層夾襖步出監(jiān)牢的瞬間濒析,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,390評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工啥纸, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留号杏,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,798評(píng)論 3 376
  • 正文 我出身青樓斯棒,卻偏偏與公主長(zhǎng)得像盾致,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子荣暮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評(píng)論 2 359