Swift列舉關聯值的記憶體探究

Sunxb發表於2020-08-18
enum Season {
   case Spring, Summer, Autumn, Winter
}
let s = Season.Spring

這是列舉最基礎的用法,但是在swift中,對列舉的功能進行了加強,也就是關聯值。

關聯值可以將額外資訊附加到 enum case中,像下面這樣子。


enum Test {
    case test1(v1: Int, v2: Int, v3: Int)
    case test2(v1: Int, v2: Int)
    case test3(v1: Int)
    case test4
}
let t = Test.test1(v1: 1, v2: 2, v3: 3)
    
switch t {
case .test1(let v1, let v2, let v3):
    print(v1, v2, v3)
default:
    break
}
// 輸出: 1 2 3

我們可以看到,在我們建立一個列舉值t的時候,設定他的選項為test1,同時可以關聯3個Int型別的值,然後在switch中,我們還可以把這3個Int值取出來進行使用。

我們今天的主要任務就是探索一下有關聯值的列舉型別,再底層的記憶體佈局是什麼樣子的,這些值都是怎麼儲存的。

在OC中我們使用sizeOf此類方法,可以輸出一個變數佔用記憶體的大小,在swift中也有此類的工作類,那就是MemoryLayout。

print(MemoryLayout<Int>.size)// 實際使用記憶體大小
print(MemoryLayout<Int>.stride)//分配記憶體大小
print(MemoryLayout<Int>.alignment)//記憶體對其引數

// 輸出 8 8 8 

上面的例子是隻是簡單的例項MemoryLayout的用法,這個我們知道,在64位的系統中Int型別確實是佔用8個位元組(64位)。接下來我們就看一下列舉的記憶體佔用情況。

點選Xcode選單欄中的Debug -> Debug Workflow -> View Memory,然後在下面紅色框中輸入變數的記憶體地址,就可以看到變數的記憶體使用情況。

使用swift後,從xcode沒法直接列印變數的記憶體地址,這裡我們使用了github上的一個工具類(github連結)來幫助我們輸出變數的記憶體地址。

1.png

準備工作完成後,我們先從最基礎的列舉開始。


enum Season {
    case Spring, Summer, Autumn, Winter
}
print("實際佔用:",MemoryLayout<Season>.size)
print("分配:",MemoryLayout<Season>.stride)
print("對齊引數:", MemoryLayout<Season>.alignment)
    
var s = Season.Spring
print("記憶體地址",Mems.ptr(ofVal: &s))
    
print("記憶體資料",Mems.memStr(ofVal: &s, alignment: .one))
    
s = Season.Summer
print("記憶體資料",Mems.memStr(ofVal: &s, alignment: .one))
    
s = Season.Autumn
print("記憶體資料",Mems.memStr(ofVal: &s, alignment: .one))
    
s = Season.Winter
print("記憶體資料",Mems.memStr(ofVal: &s, alignment: .one))

注:Mems.memStr可以直接列印記憶體資料,這樣我們就不用每次拿到地址再去工具中看了

實際佔用: 1
分配: 1
對齊引數: 1
記憶體地址 0x00007ffee753f0f0
記憶體資料 0x00
記憶體資料 0x01
記憶體資料 0x02
記憶體資料 0x03

我們可以看到這種普通的列舉型別,只佔用一個位元組。而且通過我們對變數設定不同的列舉值,列印的這一個位元組的資料也是不同的,其實也就是使用這一個位元組通過設定不同的數值來表示不同的列舉值,這樣的話其實可以至少儲存0x00-0xFF共256個值。那如果超過256個case呢?其實我覺得沒有必要考慮這種情況,列舉本來設計出就是為了區分有限中情況,如果太多,就像200多個,那完全可以使用Int來設定不同的值了,就沒必要用列舉了,當然,如果您願意探究一下的話也是可以的。

接下來我們使用一個帶關聯值的列舉來看一下。


enum Test {
    case test1(v1: Int, v2: Int, v3: Int)
    case test2(v1: Int, v2: Int)
    case test3(v1: Int)
    case test4
}
    
print("實際佔用:",MemoryLayout<Test>.size)
print("分配:",MemoryLayout<Test>.stride)
print("對齊引數:", MemoryLayout<Test>.alignment)
    
var t = Test.test1(v1: 1, v2: 2, v3: 3)
print("記憶體地址",Mems.ptr(ofVal: &t))
    
print("記憶體資料",Mems.memStr(ofVal: &t, alignment: .one))
    
t = Test.test2(v1: 4, v2: 5)
print("記憶體資料",Mems.memStr(ofVal: &t, alignment: .one))
    
t = Test.test3(v1: 6)
print("記憶體資料",Mems.memStr(ofVal: &t, alignment: .one))
    
t = Test.test4
print("記憶體資料",Mems.memStr(ofVal: &t, alignment: .one))

下面是輸出, 為了能直觀一下,我給插了幾個換行

實際佔用: 25
分配: 32
對齊引數: 8
記憶體地址 0x00007ffee0afe0d8
記憶體資料 
0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 
0x02 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x03 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 
0x00 0x00 0x00 0x00 0x00 0x00 0x00
記憶體資料 
0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x05 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x01 
0x00 0x00 0x00 0x00 0x00 0x00 0x00
記憶體資料 
0x06 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 
0x02 
0x00 0x00 0x00 0x00 0x00 0x00 0x00
記憶體資料 
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 
0x03 
0x00 0x00 0x00 0x00 0x00 0x00 0x00

實際佔用了25個位元組,我們至少可以確定,列舉的關聯值是儲存在列舉值的記憶體中的。

但是通過這一個例子其實可能還看不出有什麼規律,大家可以多用幾個例子來驗證,這是我就直接說結論了。

有關聯值得列舉實際佔用的記憶體是最多關聯值佔用的記憶體+1,在我們這個Test中,test1的關聯值是最多的,有3個Int型別的關聯值,所以要8*3=24位元組來存放關聯值,但是還需要一個位元組來儲存(辨別)是哪一個case。

帶著這個結論我們看一下輸出的結果:

當t=.test1時,前面24個位元組分配給3個Int型別關聯值,分別儲存了1,2,3, 第25個位元組是0。

當t=.test2時,前面24個位元組還是留給關聯值的,但是test2只有兩個關聯值,所以使用了前面16個位元組分配給他的關聯值,此時17到24這8位元組就空置,第25個位元組是1。

...

最後當t = test4 , 沒有關聯值,所以前面的位元組都是0, 只有第25個位元組是3

以此類推...

第25個位元組其實完全可以看成一個辨識位,或者說第25個位元組就是列舉的本質,通過不同值來區分不同case,只是因為有了關聯值,所以開闢了更多的空間來儲存而已。

後面多餘的位元組都是為了記憶體對齊,記憶體對其相關的知識大家可以自行上網查閱。

補充:
既然說到了關聯值,那就順便對列舉原始值說兩句。具通過你列印帶原始值的列舉的記憶體資料,發現是否帶有原始值對列舉的記憶體佔用並無影響,所以原始值應該不是儲存在列舉變數的內部的。大家可以自己試驗一下

相關文章