Swift 枚舉本質(zhì)

我們先來問大家一個問題 下面打印結(jié)果是多少

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

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

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

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

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

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

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

那么現(xiàn)在問題來了為什么可選類型(Int?)比不可選類型(Int)多一個字節(jié)?

那我們先來看一下可選類型代碼

@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的源碼得知,其實在swift中可選類型就是一個添加了關(guān)聯(lián)值的枚舉,例如:

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

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

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

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

1.1、普通枚舉

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

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

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

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

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

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

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

2、在lldb中使用指令memory read + 內(nèi)存地址讀取指針對應(yīng)的內(nèi)存。也可以直接使用指令x簡化書寫,效果等同于memory read

現(xiàn)在問題就變成我們該如何獲取Swift變量的內(nèi)存地址了,但由于xcode對Swift語言做了非常多的封裝和屏蔽,斷點調(diào)試時,我們不能直接像oc/c語言那樣直接看到枚舉變量的地址,我們只能通過Swift的方式獲取內(nèi)存地址。

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

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

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

或者使用View Memory` 如下步驟

4.png

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

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

1.2、帶初始值的枚舉

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

6.png

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

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

我們來看一下rawValue的匯編是什么樣的

7.png

8.png

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

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

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

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

那么關(guān)聯(lián)值的實現(xiàn)是不是也可能像原始值那樣,是編譯器幫我們生成一些計算屬性、方法之類的,幫我們保存關(guān)聯(lián)值?仔細(xì)思考一下答案應(yīng)該是否定的,關(guān)聯(lián)值是不同于原始值的,因為原始值是一個確定值,在程序編譯時期就可以確定下來的值,而關(guān)聯(lián)值是不確定的,每一個枚舉變量綁定的關(guān)聯(lián)值都是不同的,值是在程序運行的時候才能確定的,我們可以使用case let語法從枚舉中解析出不同的關(guān)聯(lián)值, 那么這個關(guān)聯(lián)值一定是和枚舉變量有密切關(guān)聯(lián)的,所以關(guān)聯(lián)值是不是被直接存放在枚舉變量中呢?我們分析一下englishScore枚舉,一個Int類型在64位系統(tǒng)占用8個字節(jié),除此之外通過第一部分的學(xué)習(xí)我們知道枚舉自己還需要一個字節(jié)來區(qū)分枚舉類型,所以8 + 1 = 9,正好可以解釋ach變量的大小為什么是9。

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

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

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

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

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

1.4、總結(jié)

下面來總結(jié)一下我們學(xué)到的知識吧。

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

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

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

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

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

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容