Swift底層原理探索2----枚舉

枚舉的基本你用法

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

enum Direction {
    case north
    case south
    case east
    case west
}
var dir = Direction.west
dir = Direction.east
dir = .north
print(dir)

switch dir {
case .north:
    print("north")
case .south:
    print("south")
case .east:
    print("east")
case .west:
    print("west")
}
複製代碼

關聯值(Associated Values)

關聯值是直接存在枚舉變量的內存裏面的,這點要牢記,對於一個有固定取值範圍的變量,設計成枚舉比較合適git

enum Score {
    case points(Int)
    case grade(Character)
}
var score = Score.points(96)
score = .grade("A")

switch score {
case let .points(i):
    print(i, "points")
case let .grade(i):
    print("grade", i)
} // grade A

enum Date {
    case digit(year: Int, month: Int, day: Int)
    case string(String)
}
var date = Date.digit(year: 2020, month: 02, day: 29)
date = .string("2020-02-29")
switch date {
case let .digit(year, month, day):  
    print(year, month, day)
case let .string(dateStr):
    print(dateStr)
    
} // "2020-02-29"
複製代碼

注意上看switch內部對let/var關鍵字的使用,若是下載枚舉值左邊,那麼關聯值只能統一綁定給let常量或者var變量github

case let .digit(year, month, day): //year、month、day都是let常量
case var .digit(year, month, day): //year、month、day都是var變量
複製代碼

若是let/var關鍵字寫在關聯值括號內,就比較靈活spring

case .digit(let year, var month, let day)
複製代碼

另一些枚舉舉例swift

enum Password {
    case number(Int, Int, Int, Int)
    case gesture(String)
}
var pwd = Password.number(3, 5, 7, 9)
pwd = .gesture("3259")
switch pwd {
case let .number(n1 , n2 , n3 , n4 ): //數字密碼
    print("number is", n1, n2, n3, n4)
case let .gesture(pwdStr):// 字符串密碼
    print("gestrue is", pwdStr)
}
複製代碼

原始值(Raw Values)

枚舉成員能夠只用相同類型的默認值預先關聯,這個默認值叫作 原始值markdown

enum PokerSuit: Character { //這裏的Character表示的是枚舉值所關聯的原始值
    case spade = "️"
    case heart = "️"
    case diamond = "️"
    case club = "️"
}
var suit = PokerSuit.spade
print(suit)
print(suit.rawValue)
print(PokerSuit.club.rawValue)

enum Grade: String {
    case perfect = "A"
    case great = "B"
    case good = "C"
    case bad = "D"
}
print(Grade.perfect.rawValue) // A
print(Grade.great.rawValue) // B
print(Grade.good.rawValue) // C
print(Grade.bad.rawValue) // D

複製代碼

隱式原始值(Implicitly Assigned Raw Values)

enum Direction1: String {
    case north, south, east, west
}
print(Direction1.north.rawValue)

enum Direction2: String {
    case north = "nor", south, east, west
}
print(Direction2.north.rawValue)//有賦值,就用賦值的字符串
print(Direction2.south.rawValue)//沒賦值, 就用case名字符串

enum Season: Int {
    case spring, summer, autumn, winter
}
print(Season.spring.rawValue)//0
print(Season.summer.rawValue)//1
print(Season.autumn.rawValue)//2
print(Season.winter.rawValue)//3

enum Season2: Int {
    case spring = 2, summer, autumn = 6, winter
}
print(Season2.spring.rawValue) //2
print(Season2.summer.rawValue) //3
print(Season2.autumn.rawValue) //6
print(Season2.winter.rawValue) //7
複製代碼

遞歸枚舉(Recursive Enumeration)

//書寫方法一
indirect enum ArithExpr_1 {
    case number(Int)
    case sum(ArithExpr, ArithExpr)
    case difference(ArithExpr, ArithExpr)
}

//書寫方法二
enum ArithExpr {
    case number(Int)
    indirect case sum(ArithExpr, ArithExpr)
    indirect case difference(ArithExpr, ArithExpr)
}

let five = ArithExpr.number(5)
let four = ArithExpr.number(4)
let two = ArithExpr.number(2)
let sum = ArithExpr.sum(five, four)
let difference = ArithExpr.difference(sum, two)


func calculate(_ expr: ArithExpr) -> Int{
    switch expr {
    case let .number(value):
        return value
    case let .sum(left, right):
        return calculate(left) + calculate(right)
    case let .difference(left, right):
        return calculate(left) - calculate(right)
    }
}
複製代碼

MemoryLayout

咱們可使用 MemoryLayout 來獲取數據類型佔用的內存大小,至關於C裏面使用的sizeofide

enum Password2 {
    case number(Int, Int, Int, Int)
    case other
}
MemoryLayout<Password2>.stride //系統分配給變量的內存大小--40
MemoryLayout<Password2>.size //實際被使用的內存大小--33
MemoryLayout<Password2>.alignment //對其參數--8

var pd = Password2.number(9, 8, 7, 6)
pd = .other
print(pd) //"other/n"
MemoryLayout.stride(ofValue: pd)  //40
MemoryLayout.size(ofValue: pd)  //33
MemoryLayout.alignment(ofValue: pd)  //8
複製代碼

枚舉在內存中是如何存儲的?

經過MemoryLayout,咱們只能簡單查看一些內存相關的信息,但還不足以看清枚舉在內存中的具體細節,因爲Xcode調試工具沒法爲咱們提供枚舉變量的內存地址,所以須要藉助一些額外的工具,這裏推介一下大牛李明傑的一個工具工具

(1)首先來看下一種簡單的狀況~~~~~~~oop

enum TestEnum {
    case test1, test2, test3
}
print("系統實際分配內存",MemoryLayout<TestEnum>.stride)
print("實際使用的內存",MemoryLayout<TestEnum>.size)
print("內存對齊參數",MemoryLayout<TestEnum>.alignment)

var t = TestEnum.test1
print("枚舉變量t的內存地址:",Mems.ptr(ofVal: &t)) //這裏能夠輸出變量t的內存地址
t = .test2
t = .test3
print("Stop for debug")
複製代碼

Mems.ptr(ofVal: &t)能夠幫咱們得到變量t的內存地址,準備好3個斷點 image 而後將程序運行值斷點1處,此時咱們已經得到t的內存地址,根據該地址,調出內存界面,咱們來觀察一下此時的內存細節 image 在繼續走到斷點二、斷點3處,對比一下各自的內存狀況以下 image 在這裏插入圖片描述ui

小結:enum TestEnum { case test1, test2, test3 }spa

  • 系統爲TestEnum類型的變量分配1個字節的內存空間
  • test1 、 test二、 test3 三個case對應在內存中用整數0、一、2來表示

(2)把場景調整爲有Int型原始值的情形以下~~~~~~~

enum TestEnum: Int {
    case test1
    case test2 = 3
    case test3
    case test4 = 10
    case test5
}
print("系統實際分配內存",MemoryLayout<TestEnum>.stride)
print("實際使用的內存",MemoryLayout<TestEnum>.size)
print("內存對齊參數",MemoryLayout<TestEnum>.alignment)

var t = TestEnum.test1
print("枚舉變量t的內存地址:",Mems.ptr(ofVal: &t)) //這裏能夠輸出變量t的內存地址
t = .test2
t = .test3
t = .test4
t = .test5
print("Stop for debug")
複製代碼

按照上面一樣的方法,對比各自case的內存狀況以下 image image image image image 咱們在查看一下各自caserawValue

print("test1的rawValue:", TestEnum.test1.rawValue)
print("test2的rawValue:", TestEnum.test2.rawValue)
print("test2的rawValue:", TestEnum.test3.rawValue)
print("test2的rawValue:", TestEnum.test4.rawValue)
print("test2的rawValue:", TestEnum.test5.rawValue)

***********運行結果
test1的rawValue: 0
test2的rawValue: 3
test2的rawValue: 4
test2的rawValue: 10
test2的rawValue: 11
複製代碼

看得出,若是原始值類型爲Int

  • 那麼在不手動設定的狀況下,首個case的原始值默爲整數0,非首個case的默認值爲上一個case的默認值+1
  • 若是手動設定了,那麼原始值即爲設定值。

(3)看過了帶Int型原始值的狀況以後,在看一下帶String型原始值的狀況,改造以下~~~~~~~

enum TestEnum: String {
    case test1
    case test2 = "AA"
    case test3 = "漢字"
    case test4 = "🦕"
    case test5
}
print("系統實際分配內存",MemoryLayout<TestEnum>.stride)
print("實際使用的內存",MemoryLayout<TestEnum>.size)
print("內存對齊參數",MemoryLayout<TestEnum>.alignment)

var t = TestEnum.test1
print("枚舉變量t的內存地址:",Mems.ptr(ofVal: &t)) //這裏能夠輸出變量t的內存地址
t = .test2
t = .test3
t = .test4
t = .test5
print("Stop for debug")


print("test1的rawValue:", TestEnum.test1.rawValue)
print("test2的rawValue:", TestEnum.test2.rawValue)
print("test2的rawValue:", TestEnum.test3.rawValue)
print("test2的rawValue:", TestEnum.test4.rawValue)
print("test2的rawValue:", TestEnum.test5.rawValue)

****************運行結果
系統實際分配內存 1
實際使用的內存 1
內存對齊參數 1
枚舉變量t的內存地址: 0x0000000100008218
Stop for debug
test1的rawValue: test1
test2的rawValue: AA
test2的rawValue: 漢字
test2的rawValue: 🦕
test2的rawValue: test5
Program ended with exit code: 0
複製代碼

內存的狀況這裏省略,和上面Int型的時候是同樣的,根據調試輸出的狀況,咱們能夠看出

  • 若是不設置原始值,那麼case的原始值爲該case名稱的字符串
  • 若是設置了原始值,那嗎case的原始值即爲設定值

總結 帶原始值的枚舉

  • 枚舉變量自己的就佔一個字節
  • 枚舉變量所對應的內存裏所存放的具體值:對應第一個case爲0,而且日後逐個+1

(4)帶關聯值的場景~~~~~~~

enum TestEnum {
    case test1(a: Int, b: Int, c: Int)
    case test2(d: Int, e: Int)
    case test3(f: Int)
    case test4(g: Bool)
    case test5
}
print("系統實際分配內存",MemoryLayout<TestEnum>.stride)
print("實際使用的內存",MemoryLayout<TestEnum>.size)
print("內存對齊參數",MemoryLayout<TestEnum>.alignment)

var t = TestEnum.test1(a: 1, b: 2, c: 3)
//這裏能夠輸出變量t的內存地址
print("枚舉變量t的內存地址:",Mems.ptr(ofVal: &t))
t = .test2(d: 4, e: 5)
t = .test3(f: 6)
t = .test4(g: true)
t = .test5
print("Stop for debug")

*****************運行結果
系統實際分配內存 32
實際使用的內存 25
內存對齊參數 8
枚舉變量t的內存地址: 0x0000000100008208
複製代碼

接下來照例在過一遍內存,下面直接貼上內存查看的結果

  • t = test1(a: 1, b: 2, c: 3)
01 00 00 00 00 00 00 00  --> 對應a
02 00 00 00 00 00 00 00  --> 對應b
03 00 00 00 00 00 00 00  --> 對應c
00 00 00 00 00 00 00 00  --> 對應case test1
複製代碼
  • t = test2(d: 4, e: 5)
04 00 00 00 00 00 00 00  --> 對應d
05 00 00 00 00 00 00 00  --> 對應e
00 00 00 00 00 00 00 00  --> 此時沒用到
01 00 00 00 00 00 00 00  --> 對應case test2
複製代碼
  • t = test3(f: 6)
06 00 00 00 00 00 00 00  --> 對應f
00 00 00 00 00 00 00 00  --> 此時沒用到
00 00 00 00 00 00 00 00  --> 此時沒用到
02 00 00 00 00 00 00 00  --> 對應case test3
複製代碼
  • t = test4(g: true)
01 00 00 00 00 00 00 00  --> 對應g
00 00 00 00 00 00 00 00  --> 此時沒用到
00 00 00 00 00 00 00 00  --> 此時沒用到
03 00 00 00 00 00 00 00  --> 對應case test4
複製代碼
  • t = test5
00 00 00 00 00 00 00 00  --> 此時沒用到
00 00 00 00 00 00 00 00  --> 此時沒用到
00 00 00 00 00 00 00 00  --> 此時沒用到
04 00 00 00 00 00 00 00  --> 對應case test5
複製代碼

總結 帶關聯值的枚舉

  • 枚舉變量的成員case的值只用了其內存空間的1字節來存放
  • 枚舉的case關聯值也存放在枚舉變量的內存中
  • 系統爲枚舉的case關聯值所分配的內存空間,必須保證能夠放下所需內存最大的那個關聯值
  • 枚舉變量的內存空間裏,先存放存放的是case關聯值成員case的值被放在最後
  • 枚舉變量的內存總空間按內存對齊參數進行補齊(計算機常識)

(5)一些極端場景~~~~~~~

enum TestEnum {
    case test
}

print("系統實際分配內存",MemoryLayout<TestEnum>.stride)
print("實際使用的內存",MemoryLayout<TestEnum>.size)
print("內存對齊參數",MemoryLayout<TestEnum>.alignment)
var t = TestEnum.test
print(print("枚舉變量t的內存地址:",Mems.ptr(ofVal: &t)))

****************運行結果
系統實際分配內存 1
實際使用的內存 0
內存對齊參數 1
枚舉變量t的內存地址: 0x0000000000000001
Program ended with exit code: 0

複製代碼

能夠看到,系統確實是分配了1個字節給枚舉,可是實際上用到了0個,由於一種狀況不須要作任何區分,因此也就不須要存儲,固然貌似沒人會這麼用,因此係統針對這種狀況下的處理,就不難理解了。在看看帶關聯值的狀況:

enum TestEnum {
    case test(Int)
}

print("系統實際分配內存",MemoryLayout<TestEnum>.stride)
print("實際使用的內存",MemoryLayout<TestEnum>.size)
print("內存對齊參數",MemoryLayout<TestEnum>.alignment)
var t = TestEnum.test(10)
print("枚舉變量t的內存地址:",Mems.ptr(ofVal: &t))
print("Stop for debug")

***************運行結果
系統實際分配內存 8
實際使用的內存 8
內存對齊參數 8
枚舉變量t的內存地址: 0x0000000100007200
Stop for debug
Program ended with exit code: 0

***************彙編結果
0A 00 00 00 00 00 00 00 
複製代碼

能夠看到系統直接分配了8個字節來存儲枚舉裏面的Int型關聯值,沒有分配空間來存儲成員case的值,緣由和上面很想,由於如今就是一種case,沒有必要再存儲成員變量的值,只須要關心case關聯值就好。那若是有一個以上的case,是否是就會給成員case分配空間了?我們試試看,以下

enum TestEnum {
    case other
    case test(Int)
}

print("系統實際分配內存",MemoryLayout<TestEnum>.stride)
print("實際使用的內存",MemoryLayout<TestEnum>.size)
print("內存對齊參數",MemoryLayout<TestEnum>.alignment)

var t = TestEnum.other
//Mem.memStr是大神李明傑提供的工具,文中有連接,能夠幫我直接獲取變量的內存裏面的值
print("枚舉變量t = other 時的內存狀況: ",Mems.memStr(ofVal: &t)) 
t = TestEnum.test(10)
print("枚舉變量t = test(10)時的內存地址:",Mems.memStr(ofVal: &t))
print("Stop for debug")

***************運行結果
系統實際分配內存 16
實際使用的內存 9
內存對齊參數 8
枚舉變量t = other 時的內存狀況:   0x0000000000000000 0x0000000000000001
枚舉變量t = test(10)時的內存地址: 0x000000000000000a 0x0000000000000000
複製代碼

能夠看出,只要case大於1個,除了Int型關聯值須要佔用8個字節外,枚舉變量還使用了1個字節來存儲成員case的值,根據內存對齊參數8,系統給枚舉變量分配了16字節空間。上面的結果中,最後一個字節是用來存放成員case的值也就是case other 對應了01case test 對應了00,可是感受順序不太對,明明是other在前,test在後的,帶着這個疑問,咱們把用例改造以下

enum TestEnum {
    case aaa
    case test(Int)
    case ccc
    case test3(Int, Int)
    case test2(Int,Int, Int)
    case other
}
print("系統實際分配內存",MemoryLayout<TestEnum>.stride)
print("實際使用的內存",MemoryLayout<TestEnum>.size)
print("內存對齊參數",MemoryLayout<TestEnum>.alignment)

var t = TestEnum.aaa
print("t = .aaa的內存狀況: ",Mems.memStr(ofVal: &t))
t = TestEnum.test(10)
print("t = .test(10)的內存狀況: ",Mems.memStr(ofVal: &t))
t = TestEnum.ccc
print("t = .ccc的內存狀況: ",Mems.memStr(ofVal: &t))
t = TestEnum.test3(16, 32)
print("t = .test3(16, 32)的內存狀況: ",Mems.memStr(ofVal: &t))
t = TestEnum.test2(20, 20, 20)
print("t = .test2(20, 20, 20)的內存狀況:",Mems.memStr(ofVal: &t))
t = TestEnum.other
print("t = .other的內存狀況: ",Mems.memStr(ofVal: &t))
print("Stop for debug")



*************************************運行結果
系統實際分配內存 32
實際使用的內存 25
內存對齊參數 8
t = .aaa的內存狀況:               0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000003
t = .test(10)的內存狀況:          0x000000000000000a 0x0000000000000000 0x0000000000000000 0x0000000000000000
t = .ccc的內存狀況:               0x0000000000000001 0x0000000000000000 0x0000000000000000 0x0000000000000003
t = .test3(16, 32)的內存狀況:     0x0000000000000010 0x0000000000000020 0x0000000000000000 0x0000000000000001
t = .test2(20, 20, 20)的內存狀況: 0x0000000000000014 0x0000000000000014 0x0000000000000014 0x0000000000000002
t = .other的內存狀況:             0x0000000000000002 0x0000000000000000 0x0000000000000000 0x0000000000000003
Stop for debug
Program ended with exit code: 0
複製代碼

從上面的調試,又挖掘了一點小細節: image

  • 對於有關聯值的成員case,它的case值會根據定義的順序,默認從0開始+1累加,
  • 其他全部不帶關聯值的成員case,它們的case值相同,並且都等於最後一個可關聯成員case 的值+1

關聯值 VS 原始值rawValue

以上咱們看清楚了簡單枚舉、關聯值枚舉、原始值枚舉在內存中分別是如何存儲的,能夠看出,枚舉的關聯值和原始值又如下區別:

  • 內存角度:關聯值是直接存儲在枚舉變量內存裏面的,而原始值則不是,由於原始值是經過xx.rawValue訪問的,所以它的值徹底不須要存儲,能夠在枚舉定義完以後經過方法提供給外部。
  • 使用角度:原始值必須在枚舉定義的時候肯定原始值類型,才能被使用 enum Direction : String/Int/... {...}。關聯值則必須在枚舉定義的時候,肯定好case所對應的關聯值類型
  • 賦值:關聯值只能在枚舉case被賦值給變量的時候進行賦值,由於同一個case每次被賦值給變量,都須要設定一個關聯值,所以也能夠說關聯值是能夠改變的,以下
enum Score {
    case points(Int)
    case grade(Character)
}
var score = Score.points(96)
score = .grade("A")
score = .grade("B") -->相同的case,不一樣的關聯值
複製代碼

而原始值,只能在枚舉定義的時候進行賦值,不賦值則系統會給定相應的默認值,也就是隻有一次機會能夠賦值,定義完枚舉以後,就沒有辦法能夠更改原始值了,示例以下

enum Grade: String {
    case perfect = "A"
    case great
    case good = "C"
    case bad = "D"
}
print(Grade.perfect.rawValue) --> A
print(Grade.great.rawValue) --> 定義時無賦值,系統默認爲case的名稱 great
print(Grade.good.rawValue) --> C
print(Grade.bad.rawValue) -> D
複製代碼

switch的實現原理(待續...)

相關文章
相關標籤/搜索