枚舉
為一組相關的值定義了一個共同的類型
,使你可以在你的代碼中以類型安全
的方式來使用這些值。
我們熟悉 的C 語言
,枚舉會為一組整型值
分配相關聯的名稱。Swift
中的枚舉更加靈活,不必給每一個枚舉成員提供一個值。如果給枚舉成員提供一個值(稱為原始值
),則該值的類型可以是字符串
、字符
,或是一個整型值
或浮點數
。
此外,枚舉成員可以指定任意類型的關聯值
存儲到枚舉成員中,就像其他語言中的聯合體(unions
)和變體(variants)
。你可以在一個枚舉中定義一組相關的枚舉
成員,每一個枚舉成員都可以有適當類型的關聯值
。
在 Swift
中,枚舉類型是一等(first-class)類型
。它們采用了很多在傳統上只被類(class)所支持的特性,例如計算屬性(computed properties
),用于提供枚舉值的附加信息;實例方法(instance methods)
,用于提供和枚舉值相關聯的功能;枚舉也可以定義構造函數(initializers)
來提供一個初始值;可以在原始實現的基礎上擴展
它們的功能;還可以遵循協議(protocols)
來提供標準的功能。
一、枚舉語法
Swift
中,枚舉支持Int
、Double
、String
等基礎類型,也有默認枚舉值
(String
類型默認枚舉值為case
的key
名稱,Int
、Double
數值型默認枚舉值為0
開始,+1
遞增。
代碼:
// 寫法一
// 不需要逗號隔開
enum Weak1 {
case MON
case TUE
case WED
case THU
case FRI
case SAT
case SUN
}
// 寫法二
// 也可以直接一個case,然后使用逗號隔開
enum Weak2 {
case MON, TUE, WED, THU, FRI, SAT, SUN
}
// 定義一個枚舉變量
var w: Weak1 = .MON
/*
String類型的enum
- =左邊的值是枚舉值,例如 MON
- =右邊的值在swift中稱為 RawValue(原始值),例如 "MON"
- 兩者的關系為:case 枚舉值 = 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"
}
如果不想寫枚舉值后的字符串,也可以使用隱式RawValue
分配。
enum Week: String{
case MON, TUE, WED, THU, FRI, SAT, SUN
}
var w = Week.MON.rawValue
print(w)
//打印結果:MON
1.2 枚舉的訪問
那么Swift是如何獲取rawValue的值
,我們可以通過SIL
文件分析。
SIL命令:
swiftc -emit-sil main.swift |xcrun swift-demangle >> ./main.sil && open main.sil
SIL文件
enum Week : String {
case MON, TUE, WED, THU, FRI, SAT, SUN
init?(rawValue: String) //默認添加了一個可選類型的init方法
typealias RawValue = String //給枚舉值的類型,通過typealias取了一個別名RawValue
var rawValue: String { get } //增加一個計算屬性rawValue,用于獲取枚舉值的原始值
}
main
函數流程:
rawValue
的getter
方法:bb8
代碼段:總結:
rawValue
的底層就是調用的getter
方法,getter
方法中構造了字符串
,但是這個字符串的值(例如“MON”)從哪里取出的呢?他們在編譯期
已經確定了,我們查看Mach-O
,在TEXT的__cstring段
就能看到。
1.3 case枚舉值 & rawValue原始值
代碼:
//輸出 case枚舉值
print(Week.MON)
//輸出 rawValue
print(Week.MON.rawValue)
//打印結果:MON MON
從結果來看是沒有什么區別的,輸出值都一樣,但他們本質是不一樣的。
第一個,輸出的case枚舉值
;第二個,是通過rawValue訪問的rawValue的get方法
。
下面這種寫法,編譯器就會報錯:
1.4 枚舉的init
枚舉的init
會在什么時候調用,我們通過斷點看一下:
①,不設置
Condition
,進不到方法里面;②原生代碼第一個斷點,進入的是getter
方法,進不去init
。即,
enum
中init方法
的調用是通過枚舉.init(rawValue:)
或者枚舉(rawValue:)
觸發的。
繼續:
print(Week.init(rawValue: "MON"))
print(Week.init(rawValue: "Hello"))
//打印結果:
//Optional(_6_EnumTest.Week.MON)
//nil
第一個輸出的可選值
,第二個輸出的是nil
。表示,沒有找到對應的case枚舉值
。
分析SIL文件中的Week.init
方法,主要有以下幾步:
1、在init
方法中是將所有enum
的字符串從Mach-O
文件中取出,依次放入數組
中;
2、放完后,然后調用_findStringSwitchCase
方法進行匹配。
如下:
index_addr:表示獲取當前數組中的第n個元素值的地址,然后再把構建好的字符串放到當前地址中
struct_extract:表示取出當前的Int值,Int類型在系統中也是結構體
cond_br:表示比較的表達式,即分支條件跳轉
- 如果匹配成功,則構建一個 .some的Optional 返回
- 如果匹配不成功,則繼續匹配,知道最后還是沒有匹配上,則構建一個.none的Optional返回
_findStringSwitchCase
在swift-source
中,接收兩個參數,分別是 數組
+ 需要匹配的String
。①遍歷數組
,如果匹配則返回對應的index
;②如果不匹配
,則返回-1
。
所以,這也是為什么一個打印可選值
,一個打印nil
的原因。
1.5枚舉的遍歷
CaseIterable協議
通常用于沒有關聯值的枚舉
,用來訪問所有的枚舉值,只需要對應的枚舉遵守該協議即可,然后通過allCases
獲取所有枚舉值,如下:
// Double類型
enum Week1: Double, CaseIterable {
case Mon,Tue, Wed, Thu, Fri, Sat, Sun
}
Week1.allCases.forEach { print($0.rawValue)}
// String類型
enum Week2: String {
case Mon,Tue, Wed, Thu, Fri, Sat, Sun
}
extension Week2: CaseIterable {}
Week2.allCases.forEach { print($0.rawValue)}
二、關聯值枚舉、模式匹配、屬性方法
如果希望用枚舉表示復雜的含義,關聯更多的信息,就需要使用關聯值
了。
他與普通類型的枚舉
不同
:沒有rawValue,沒有rawValue的getter方法;沒有初始化init方法
。
//注:當使用了關聯值后,就沒有RawValue了
//因為:case可以用一組值來表示,而rawValue是單個的值
enum Shape{
//case枚舉值后括號內的就是關聯值,如 radius
case circle(radius: Double)
case rectangle(width: Int, height: Int)
}
//創建
var circle = Shape.circle(radius: 10.0)
//重新分配
circle = Shape.rectangle(width: 10, height: 10)
模式匹配
enum
中的模式匹配其實就是匹配case枚舉值
,根據枚舉類型,分為2種:
1、簡單類型
的枚舉的模式匹配;
2、自定義類型
的枚舉(關聯值)的模式匹配。
簡單enum的模式匹配
注:swift中的enum模式匹配需要將所有情況都列舉,或者使用default
表示默認情況,否則會報錯
enum Week: String{
case MON
case TUE
case WED
case THU
case FRI
case SAT
case SUN
}
var current: Week?
switch current {
case .MON: print(Week.MON.rawValue)
case .TUE: print(Week.MON.rawValue)
default:print("unknow day")
}
//打印結果:unknow day
關聯值類型的模式匹配
關聯值類型
的模式匹配
有兩種方式:1、switch - case, 匹配所有case;2、if - case, 匹配單個case
。
switch - case
定義關聯值枚舉:
enum Shape{
case circle(radius: Double)
case rectangle(width: Int, height: Int)
}
可以let var
修飾關聯值的入參:
let shape = Shape.circle(radius: 10.0)
switch shape{
//相當于將10.0賦值給了聲明的radius常量
case let .circle(radius):
print("circle radius: \(radius)")
case .rectangle(let width, var height):
height += 1
print("rectangle width: \(width) height: \(height)")
}
- 通過
if-case
匹配單個case
,如下:
let circle = Shape.circle(radius: 10)
//匹配單個case
if case let Shape.circle(radius) = circle {
print("circle radius: \(radius)")
}
- 如果我們只關心
不同case的相同關聯值(即關心不同case的某一個值)
,需要使用同一個參數
。
例如,案例中的x
,如果分別使用x、y
, 編譯器會報錯:
enum Shape{
case circle(radius: Double)
case rectangle(width: Double, height: Double)
case square(width: Double, height: Double)
}
let shape = Shape.circle(radius: 10)
switch shape{
case let .circle(x), let .square(20, x):
print(x)
default:
break
}
也可以使用通配符_(表示匹配一切)
的方式:
let shape = Shape.rectangle(width: 10, height:20)
switch shape{
case let .rectangle(x, _), let .square(_, x):
print("x = \(x)")
default:
break
}
注:枚舉使用過程中不關心某一個關聯值
,可以使用通配符_
表示。OC只能調用swift中Int類型
的枚舉。
屬性 & 函數
enum中可以包含計算屬性
、類型屬性
,不能包含存儲屬性
。
enum Direct: Int {
case up
case down
case left
case right
// 計算型屬性
var description: String{
switch self {
case .up:
return "這是上面"
default:
return "這是\(self)"
}
}
//存儲屬性:編譯器報錯,Enums must not contain stored properties
//var radius: Double
//類型屬性 - 是一個全局變量
static let height = 20.0
// 函數
func printSelf() {
print(description)
}
mutating func nextDay(){
if self == .up{
self = Direct(rawValue: 1)!
}else{
self = Direct(rawValue: self.rawValue+1)!
}
}
}
Direct.down.printSelf()
//打印結果:這是down
var direct = Direct.left;
direct.nextDay();
direct.printSelf()
//打印結果:這是right
為什么
struct
中可以放存儲屬性
,而enum不可以?
struct
中可以包含存儲屬性,是因為其大小就是存儲屬性的大小
。而enum
是不一樣的(請查閱后文的enum大小講解),enum枚舉的大小是取決于case的個數的
,如果沒有超過255,enum的大小就是1字節(8位)
可以在enum中定義實例方法
、static修飾的方法
:
enum Week: Int{
case MON, TUE, WED, THU, FRI, SAT, SUN
mutating func nextDay(){
if self == .SUN{
self = Week(rawValue: 0)!
}else{
self = Week(rawValue: self.rawValue+1)!
}
}
}
<!--使用-->
var w = Week.MON
w.nextDay()
print(w)
三、枚舉的嵌套
枚舉的嵌套主要用于以下場景:
1、枚舉嵌套枚舉
:一個復雜枚舉是由一個或多個枚舉組成;
2、結構體嵌套枚舉
:enum是不對外公開的,即是私有
的。
3.1 enum嵌套enum
枚舉嵌套枚舉,改動上面的例子:
enum CombineDirect{
//枚舉中嵌套的枚舉
enum BaseDirect{
case up
case down
case left
case right
}
//通過內部枚舉組合的枚舉值
case leftUp(baseDIrect1: BaseDirect, baseDirect2: BaseDirect)
case leftDown(baseDIrect1: BaseDirect, baseDirect2: BaseDirect)
case rightUp(baseDIrect1: BaseDirect, baseDirect2: BaseDirect)
case rightDown(baseDIrect1: BaseDirect, baseDirect2: BaseDirect)
}
//使用
let leftUp = CombineDirect.leftUp(baseDIrect1: CombineDirect.BaseDirect.left, baseDirect2: CombineDirect.BaseDirect.up)
結構體嵌套枚舉
//結構體嵌套枚舉
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 .up, .down:
print("up, down")
}
}
}
枚舉的遞歸:indirect
遞歸枚舉
是一種枚舉類型,它有一個或多個枚舉成員使用該枚舉類型的實例作為關聯值。使用遞歸枚舉時,編譯器會插入一個間接層。你可以在枚舉成員前加上 indirect
來表示該成員可遞歸。
//一、用枚舉表示鏈表結構
enum List<T>{
case end
//表示case使是引用來存儲
indirect case node(T, next: List<T>)
}
//二、也可以將indirect放在enum前
//表示整個enum是用引用來存儲
indirect enum List<T>{
case end
case node(T, next: List<T>)
}
第一種寫法,如果沒有關鍵字indirect
,編譯報錯。原因是使用該枚舉時,enum的大小需要case來確定,而case的大小又需要使用到enum大小。所以無法計算enmu的大小,于是報錯!
根據編譯器提示,需要使用關鍵字indirect
,意思就是將該枚舉標記位遞歸,同時也支持標記單個case。
enum
內存大小:
enum List<T>{
case end
indirect case node(T, next: List<T>)
}
print(MemoryLayout<Int>.size)
print(MemoryLayout<List<Int>>.size)
print(MemoryLayout<List<Int>>.stride)
print(MemoryLayout<String>.size)
print(MemoryLayout<List<String>>.size)
print(MemoryLayout<List<String>>.stride)
//打印結果:
//8 8 8
//16 8 8
發現Int
、String
都是8。為什么?
下面通過LLDB分析查看一下:
如果是end,此時存儲的是case值,為0
,而case為node時存儲的是引用地址
。所以,
indirect
關鍵字其實就是通知編譯器,我當前的enum是遞歸的,大小是不確定的,需要分配一塊堆區的內存空間
,用來存放enum。
四、swift和OC混編enum
在swift中,enum
非常強大,而在OC
中,enum
僅僅只是一個整數值
。
因此,OC調用Swift枚舉,必須具備2個條件:①、用@objc關鍵字標記enum
;②、當前enum應該是Int類型
。
// Swift中定義枚舉
@objc enum Weak: Int{
case MON, TUE, WED, THU, FRI, SAT, SUN
}
// OC使用
- (void)test{
Weak mon = WeakMON;
}
Swift
使用OC
枚舉,OC
中的枚舉會自動轉換成swift中的enum
。
// OC定義1:NS_ENUM
NS_ENUM(NSInteger, ENUM_OC_TYPE){
Value1,
Value2
};
// OC定義2:typedef enum
typedef enum {
Num1,
Num2
}OCEnumType;
// swift使用
//1、將OC頭文件導入橋接文件xxx-Bridging-Header.h
#import "xxx.h"
//2、使用
let ocEnum1 = ENUM_OC_TYPE.Value1
let ocEnum2 = OCEnumType.init(0)
print("\(ocEnum1) + \(ocEnum2.rawValue)")
//打印結果:ENUM_OC_TYPE + 0
OC自動轉換成swift方式:上圖可知,通過
typedef enum
定義的enum
,在swift
中變成了一個結構體
,并遵循了兩個協議:Equatable
和 RawRepresentable
。OC使用Swift中String類型的枚舉方式
@objc enum Weak: Int{
case MON, TUE, WED
var val: String?{
switch self {
case .MON:
return "MON"
case .TUE:
return "TUE"
case .WED:
return "WED"
default:
return nil
}
}
}
// OC中使用
Weak mon = WeakMON;
// swift中使用
let weak = Weak.MON.val
即:swift中的enum成Int整型;enum再聲明一個變量/方法,用于返回固定的字符串,用于在swift中使用。
五、Enum內存大小
我們主要分析兩個函數的區別:
-
size
:實際占用內存大小; -
stride
:系統分配的內存大小。
5.1 普通enum
一個case
的情況:
enum Weak {
case MON
//case TUE
}
print(MemoryLayout<Weak>.size)
print(MemoryLayout<Weak>.stride)
//打印結果:0 1
再加一個case:
enum Weak {
case MON
case TUE
}
print(MemoryLayout<Weak>.size)
print(MemoryLayout<Weak>.stride)
//打印結果:1 1
繼續增加多個case:
enum Weak {
case MON
case TUE
case WED
case THU
case FRI
case SAT
case SUN
}
print(MemoryLayout<Weak>.size)
print(MemoryLayout<Weak>.stride)
//打印結果:1 1
以上可以看出,當case個數為1時,枚舉size為0
,個數>=2時,size的大小始終是1
,即,說明enum就是以1字節存儲在內存中的
。why?
讀取內存可以看出,
case都是1字節大小
,1個字節是8個byte,那么有255種
排列組合(0x00000000 - 0x11111111
)。所以,
當case為1個的時候,size的大小是0(二進制是0x0)
;case數<=255時,size都是1,是UInt8類型
。超過255個時,會自動擴容,size和stride都會增加
。
總結:
1、如果enum
中有原始值,即rawValue
,其大小取決于case的多少
,如果沒有超過UInt8
即255,則就是1字節
存儲case,Int
標識的其實就是RawValue
的值。
2、當只有一個case
的情況下,size是0
,表示這個enum是沒有意義
的。
3、當有兩個及以上case
時,如果沒有超過255,則case的步長是1字節
;如果超過,則UInt8->UInt16...
,以此類推。
5.2 關聯值的enum
自定義類型的枚舉,即關聯值
類型,size
和stride
值的變化,如下:
enum Shape{
case circle(radius: Double)
case rectangle(width: Double, height: Double)
}
print(MemoryLayout<Shape>.size)
print(MemoryLayout<Shape>.stride)
//打印結果:17 24
分析:
-
case
(枚舉值)size
是1
; - 枚舉是
共用內存
,有關聯值時,取最大值
;
2.1circle
的參數是double
類型,內存size
是8
;
2.2rectangle
的參數是兩個double
類型,內存size
是16
; - 取
最大值
,為16
。即,內存size
為:16(最大關聯值大小) + 1(case枚舉值)= 17
。 -
內存對齊
,所以stride為24
。
斷點讀取內存驗證一下:image.png
5.3 enum嵌套enum
enum CombineDirect{
enum BaseDirect{
case up, down, left, right
}
case leftUp(baseDirect1: BaseDirect, baseDirect2: BaseDirect)
case rightUp(baseDirect1: BaseDirect, baseDirect2: BaseDirect)
case leftDown(baseDirect1: BaseDirect, baseDirect2: BaseDirect)
case rightDown(baseDirect1: BaseDirect, baseDirect2: BaseDirect)
}
print(MemoryLayout<CombineDirect>.size)
print(MemoryLayout<CombineDirect>.stride)
//打印結果:2 2
說明:enum嵌套enum
同具有關聯值的enum是一樣的,同樣取決于關聯值的大小,其內存大小是最大關聯值的大小
。
定義一個變量,觀察一下內存:
var combine = CombineDirect.leftDown(baseDirect1: .left, baseDirect2: .down)
總結,有待進一步驗證:
enum嵌套enum同樣取決于最大case的關聯值大小;
當嵌套enum的case只有2個時,case在內存中的存儲是0、8;
當嵌套enum的case大于2,小于等于4時,case在內存中的存儲是 0、4、8、12;
當嵌套enum的case大于4時,case在內存中的存儲是從0、1、2...以此類推。
5.4 結構體嵌套enum
struct Skill {
enum KeyType{
case up
case down
case left
case right
}
}
print(MemoryLayout<Skill>.size)
print(MemoryLayout<Skill>.stride)
//打印結果:0 1
如果只嵌套了enum,沒有聲明變量。size的大小取決于成員變量,但是struct中目前沒有屬性,所以size是1
。
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 .up, .down:
print("up, down")
}
}
}
print(MemoryLayout<Skill>.size)
print(MemoryLayout<Skill>.stride)
//打印結果:1 1
結構體的大小計算,跟函數無關,所以只看成員變量key的大小
,key是枚舉Skill類型,大小為1,所以結構體大小為1
。
如果在添加一個成員變量:
struct Skill {
enum KeyType{
case up
case down
case left
case right
}
let key: KeyType //1字節
var height: UInt8 //1字節
func launchSkill(){
switch key {
case .left, .right:
print("left, right")
case .up, .down:
print("up, down")
}
}
}
print(MemoryLayout<Skill>.size)
print(MemoryLayout<Skill>.stride)
//打印結果:2 2
在添加一個Int屬性:
struct Skill {
enum KeyType{
case up
case down
case left
case right
}
let key: KeyType //1字節
var height: UInt8 //1字節
var width: Int //8字節
func launchSkill(){
switch key {
case .left, .right:
print("left, right")
case .up, .down:
print("up, down")
}
}
}
print(MemoryLayout<Skill>.size)
print(MemoryLayout<Skill>.stride)
//打印結果:16 16
//打印結果2:如果把Int屬性放在最前面,則為,10 16
為什么屬性位置不一樣
,打印結果不一樣,也是因為內存對齊
。內存對齊規則如下:
/**
數據成員的對齊規則可以理解為min(m, n) 的公式
m,表示當前成員的開始位置;n,表示當前成員所需要的位數。
如果滿足條件 m 整除 n (即 m % n == 0), n 從 m 位置開始存儲;
反之繼續檢查 m+1 能否整除 n, 直到可以整除, 從而就確定了當前成員的開始位置。
*/
struct Mystruct4{
int a; //4字節 min(0,4)--- (0,1,2,3)
struct Mystruct5{ //從4開始,存儲開始位置必須是最大的整數倍(最大成員為8),min(4,8)不符合 4,5,6,7,8 -- min(8,8)滿足,從8開始存儲
double b; //8字節 min(8,8) --- (8,9,10,11,12,13,14,15)
short c; //1字節,從16開始,min(16,1) -- (16,17)
}Mystruct5;
}Mystruct4;
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"%lu %lu", sizeof(Mystruct4), sizeof(Mystruct4.Mystruct5));
//打印結果:24 16
NSLog(@"Hello, World!");
}
return 0;
}
結構體嵌套了enum總結:
1、如果沒有聲明變量,此時的size是0,stride是1
;
2、如果結構體中沒有其他屬性,只有枚舉變量
,那么結構體的大小就是枚舉的大小
,即size為1
;
3、如果結構體中還有其他屬性,則按照OC
中的結構體內存對齊原則
進行分析。
內存對齊 & 字節對齊 區分
內存對齊
:iOS中是8字節
對齊,蘋果實際分配采用16字節
對齊,這種只會在分配對象
時出現。
字節對齊
:存儲屬性的位置必須是偶地址,即OC內存對齊中的min(m,n)
,其中m表示存儲的位置
,n表示屬性的大小
,需要滿足位置m整除n
時,才能從該位置存放屬性。簡單來說,就是必須在自身的倍數位置
開始。
外部調用對象時,對象是服從內存對齊
。
單純從結構上說,結構內部服從最大字節對齊
。即,枚舉size為1的情況。
總結:
一、枚舉定義:
1、enum
中使用rawValue
的本質是調用get方法
,即在get方法中從Mach-O
對應地址中取出字符串
并返回的操作;
2、enum
中init方法
的調用是通過枚舉.init(rawValue:)
或者枚舉(rawValue:)
觸發的;
3、沒有關聯值的enum
,如果希望獲取所有枚舉值,需要遵循CaseIterable協議
,然后通過枚舉名.allCase
的方式獲取;
4、case枚舉值
和rawValue原始值
的關系:case 枚舉值 = rawValue原始值
;
5、具有關聯值
的枚舉,可以稱為三無
enum,因為沒有別名RawValue、init、計算屬性rawValue
;
6、enum
的模式匹配
方式,主要有兩種:switch-case
/if-case
7、enum可以嵌套enum
,也可以在結構體
中嵌套enum,表示該enum是struct私有
的;
8、enum中還可以包含計算屬性
、類型屬性
,但是不能包含存儲屬性
;
9、enum中可以定義實例
、static修飾的方法
。
二、枚舉內存:
1、普通enum的內存大小一般是1字節
,如果只有一個case
,則為0
,表示沒有意義,如果case個數超過255
,則枚舉值的類型由UInt8->UInt16->UInt32...;
2、具有關聯值的enum
大小,取決于最大case的內存大小+case的大小(1字節)
;
3、enum嵌套
enum同樣取決于最大case的關聯值大小
;
4、結構體嵌套
enum,如果沒有屬性,則size為0
,如果只有enum屬性
,size為1
,如果還有其他屬性
,則按照OC中內存對齊
原則進行計算。
參考:枚舉教程