Enum
枚舉的基本用法
swift
中通過 enum
關鍵字來聲明一個枚舉
enum CXEnum {
case test_one
case test_two
case test_three
}
而在 C
或者 OC
中默認受整數支持,也就意味著下面的例子中: A
、B
、C
分別代表 0、 1、2。
typedef NS_ENUM(NSUInteger, CXEnum) {
A,
B,
C,
};
Swift
中的枚舉則更加靈活,并且不需給枚舉中的每一個成員都提供值。如果一個值(所謂“原 始”值)要被提供給每一個枚舉成員,那么這個值可以是字符串、字符、任意的整數值,或者是浮點類型。
enum Color : String {
case red = "Red"
case amber = "Amber"
case green = "Green"
}
enum CXEnum: Double {
case a = 10.0
case b= 20.0
case c = 30.0
case d = 40.0
}
這里其實是因為編譯器做了很多的操作,第一個就是隱士 RawValue
分配,是建立在 Swift
的類型推斷機制上的。
enum DayOfWeek: Int {
case mon, tue, wed, thu, fri = 10, sat, sun
}
這里從 mon
開始對應的值是從 0 開始的,但是也可以自己指定枚舉的值,當指定 fri = 10
之后,從 fri
開始 sat
、sun
就分別對應 11、12。
對于 DayOfWeek
,也可以指定為 String
類型,也可以指定枚舉的值,如下所示:
enum DayOfWeek: String {
case mon, tue, wed, thu, fri = "Hello world", sat, sun
}
這里輸出枚舉值的 RawValue
,當沒指定枚舉成員的原始值的時候 RawValue
的值跟枚舉成員值的字符串是一樣的, 這是編譯器默認分配的。
關聯值
enum Shape {
case circle(radius: Double)
case rectangle(width: Int, height: Int)
}
var circle = Shape.circle(radius: 15.0)
var rectangle = Shape.rectangle(width: 15, height: 7)
如果我們想用枚舉類型來表示更復雜的類型,例如圓形跟矩形,就可以如上,通過關聯值的形式進行實現,circle
代表圓形,可以為它關聯一個 Double
類型的 radius
來代表半徑。rectangle
代表矩形,可以為它關聯 Int
類型的 width
、height
來代表寬高。
模式匹配
enum Weak: String {
case MONDAY
case TUEDAY
case WEDDAY
case THUDAY
case FRIDAY
case SATDAY
case SUNDAY
}
let currentWeak: Weak = Weak.MONDAY
switch currentWeak {
case .MONDAY: print(Weak.MONDAY.rawValue)
case .TUEDAY: print(Weak.TUEDAY.rawValue)
case .WEDDAY: print(Weak.WEDDAY.rawValue)
case .THUDAY: print(Weak.THUDAY.rawValue)
case .FRIDAY: print(Weak.FRIDAY.rawValue)
case .SUNDAY: print(Weak.SUNDAY.rawValue)
case .SATDAY: print(Weak.SUNDAY.rawValue)
}
如上代碼所示,是窮盡了所有的枚舉成員值,如果不想匹配所有的 case
,可以使用 defalut
關鍵字。
switch currentWeak{
case .SATDAY, .SUNDAY: print("Happy Day")
default : print("SAD DAY")
}
如果我們要匹配關聯值的話,示例代碼如下:
enum Shape {
case circle(radius: Double)
case rectangle(width: Int, height: Int)
}
var shape = Shape.circle(radius: 15.0)
switch shape {
case let .circle(radious):
print("Circle radious:\(radious)")
case let .rectangle(width, height):
print("rectangle width:\(width),height\(height)")
}
當然還可以這么寫:
switch shape{
case .circle(let radious):
print("Circle radious:\(radious)")
case .rectangle(let width, var height):
height += 10
print("rectangle width:\(width),height\(height)")
}
跟類跟結構體一樣,枚舉中也可以添加異變方法(mutaing
)、屬性、擴展(extension
),也可以遵循協議。枚舉是值類型,存儲在棧區。
枚舉的大小
No-payload enums
接下來我們來討論一下枚舉占用的內存大小,這里我們區分幾種不同的情況,首先第一種就是
No-payload enums
,沒有關聯值。
enum Weak: String {
case MONDAY
case TUEDAY
case WEDDAY
case THUDAY
case FRIDAY
case SATDAY
case SUNDAY
}
大家可以看到這種枚舉類型類似我們在 C
語言中的枚舉,當前類型默認是 Int
類型,那么對于這一類的枚舉在內存中是如何布局?以及在內存中占用的大小是多少那?這里我們就可以直接使用 MemoryLayout
來測量一下當前枚舉內存大小。
可以看到這里我們測試出來的不管是 size
還是 stride
都是 1 ,這個地方我們也很好理解,當前的 enum
有幾個 case
? 是不是 8 個,在 Swift
中進行枚舉布局的時候一直是嘗試使用最少的空間來存儲 enum
,對于當前的 case
數量來說, UInt8
能夠表示 256 個 case
,也就意味著如果一個默認枚舉類型且沒有關聯值的 case
少于 256 ,當前枚舉類型的大小都是 1 字節,所以可以由 UInt8
的方式來存儲當前枚舉的值。當 case
大于 256 的時候存儲方式就會由 UInt8
升級為 UInt16
。
通過上面的打印我們可以直觀的看到,當前變量 a
、 b
、 c
這三個變量存儲的內容分別是 00
、01
、02
,分別相差 1 個字節, 這和我們上面說的布局理解是一致的。
Single-payload enums
No-payload enums
的布局比較簡單,我們也比較好理解,接下來我們來理解一下 Single-payload enums
的內存布局, 字面意思就是有一個成員負載的 enum
,比如下面這個例子:
這個時候可以看到枚舉的大小是 1 字節,下面我們將關聯值改為 Int
類型再來看一下:
這里我們可以看到,同樣是一個負載,只是因為關聯值類型不同,就會造成內存的大小不同。 這是因為在 Swift
中的 enum
中的 Single-payload enums
會使用負載類型中的額外空間來記錄沒有負載的 case
值。這句話該怎么理解?首先 Bool
類型是 1 字節,也就是 UInt8
,所以當前能表達 256 個 case
的情況,對于布爾類型來說,只需要使用低位的 0, 1 這兩種情況,其他 7 位剩余的空間就可以用來表示沒有負載的 case
值。對于 Int
類型的負載來說,其實系統是沒有辦法推算當前的負載所要使用的位數,Int
類型就是 8 字節,也就意味著當前 Int
類型的負載是沒有額外的剩余空間的,這個時候我們就需要額外開辟內存空間來去存儲我們的 case
值,也就是 8+1 =9
字節。
Mutil-payload enums
上面說完了 Single-payload enums
, 接下來我們說第三種情況 Mutil-payload enums
, 有多個負載的情況產生時,當前的 enum
是如何進行布局的呢?
這里我們可以看到當前內存存儲的分別是 00, 01, 40, 41, 80,81 , 這里在存儲當前的 case
時候會使用到 common spare bits
,什么意思?其實在上一個案例我們也講過了,首先 bool
類型需要 1 字節,也就是 8 位。
對于 bool
類型來說,我們存儲的無非就是 0 或 1 ,只需要用到 1 位,所以剩余的 7 位這里我們都統稱為 common spare bits
,對于當前的 case
數量來說我們完全可以把所有的情況放到 common spare bits
中,所以這里我們只需要 1 字節就可以存儲所有的內容了。
接下來我們來看一下 00、01、40、41、80、81
分別代表的是什么?首先 0、4、8
這里我們叫做 tag value
, 0, 1
這里我們就叫做 tag index
,至于這個 tag value
怎么來的, 目前在源碼中還沒有找到驗證,如果大家感興趣的的話也可以閱讀一下源碼中的 Enum.cpp
和 GenEnum.cpp
這兩個文件,找到了驗證大家可以一起交流。
當前一般來說,我們有多個負載的枚舉時,當前枚舉類型的大小取決于當前最大關聯值的大小。
我們來看一個例子
enum CXEnum{
case test_one(Bool)
case test_two(Int)
case test_three
case test_four
}
當前 CXEnum
的大小就等于 sizeof(Int) + sizeof(rawVlaue) = 9
,在比如下面這個例子:
enum CXEnum{
case test_one(Bool)
case test_two(Int, Int, Int)
case test_three
case test_four
}
當前大小就是 sizeof(Int) * 3 + sizeof(rawVlaue) = 25
。
最后這里有我們再來看下下面的案例:
enum CXEnum{
case test_one
}
對于當前的 CXEnum
只有一個 case
,我們不需要用任何東?來去區分當前的 case
, 所以當我們打印當前的 CXEnum
,可以發現大小是 0。
indirect 關鍵字
indirect enum BinaryTree<T> {
case empty
case node(left: BinaryTree, value: T, right: BinaryTree)
}
如上所示,我們也可以用枚舉來表示樹中的一個節點,不過需要用到 indirect
關鍵字,indirect
的意思是表示當前枚舉是引用類型,分配在堆空間。
enum BinaryTree<T> {
case empty
indirect case node(left: BinaryTree, value: T, right: BinaryTree)
}
我們也可以修改 indirect
關鍵字的位置,當 indirect
關鍵字放在 case
的前面的時候,表示只有是 node
這種 case
的時候枚舉才會是引用類型,為 empty
的時候依然是值類型。
Optional
認識可選值
之前我們在寫代碼的過程中早就接觸過可選值,比如我們在代碼這樣定義:
class LGTeacher{
var age: Int?
}
當前的 age 我們就稱之為可選值,當然可選值的寫法這兩者是等同的
var age: Int? = var age: Optional<Int>
那對于 Optional
的本質是什么?我們直接跳轉到源碼,打開 Optional.swift
文件,可以看到源碼中 Optional
的本質是枚舉。
@frozen
public enum Optional<Wrapped>: ExpressibleByNilLiteral {
case none
case some(Wrapped)
}
既然 Optional
的本質是枚舉,那么我們也可以仿照系統的實現制作一個自己的 Optional
enum MyOptional<Value> {
case some(Value)
case none
}
比如給定任意一個自然數,如果當前自然數是偶數返回,否則為 nil
,我們應該怎么表達這個案 例
enum MyOptional<Value> {
case some(Value)
case none
}
func getOddValue(_ value: Int) -> MyOptional<Int> {
if value % 2 == 0 {
return .some(value) }
else{
return .none
}
}
這個時候給定一個數組,我們想刪除數組中所有的偶數
var array = [1, 2, 3, 4, 5, 6]
for element in array {
let value = getOddValue(element)
array.remove(at: array.firstIndex(of: value))
}
這個時候編譯器就會檢查我們當前的 value
會發現他的類型和系統編譯器期望的類型不符,這個時候我們就能使用 MyOptional
來限制語法的安全性。
于此同時我們通過 enum
的模式匹配來取出對應的值
for element in array {
let value = getOddValue(element)
switch value {
case .some(let value):
array.remove(at: array.firstIndex(of: value)!)
case .none:
print("vlaue not exist") }
}
如果我們把上述的返回值更換一下,其實就和系統的 Optional
使用無異
func getOddValue(_ value: Int) -> Int? {
if value % 2 == 0 {
return .some(value)
}else{
return .none
}
}
這樣我們其實是利用當前編譯器的類型檢查來達到語法書寫層面的安全性。
當然如果每一個可選值都用模式匹配的方式來獲取值在代碼書寫上就比較繁瑣,我們還可以使 用 if let
的方式來進行可選值綁定
if let value = value{
array.remove(at: array.firstIndex(of: value)!)
}
除了使用 if let
來處理可選值之外,我們還可以使用 gurad let
來簡化我們的代碼,我們來看一個具體的案例
gurad let
和 if let
剛好相反,守護一定有值。如果沒有,直接返回。通常判斷是否有值之后,會做具體的邏輯實現,通常代碼多,如果用 if let
憑空多了一層分支,guard let
是降低分支層次的辦法。
可選鏈
我們都知道在 OC
中我們給一個 nil
對象發送消息什么也不會發生,Swift
中我們是沒有辦法向一個 nil
對象直接發送消息,但是借助可選鏈可以達到類似的效果。我們看下面兩段代碼
let str: String? = "abc"
let upperStr = str?.uppercased() // Optional<"ABC">
var str: String?
let upperStr = str?.uppercased() // nil
我們再來看下面這段代碼輸出什么
let str: String? = "kody"
let upperStr = str?.uppercased().lowercased()
同樣的可選鏈對于下標和函數調用也適用
var closure: ((Int) -> ())? closure?(1) // closure 為 nil 不執行
let dict = ["one": 1, "two": 2] dict?["one"] // Optional(1) dict?["three"] // nil
?? 運算符 (空合并運算符)
( a ?? b )
將對可選類型 a
進行空判斷,如果 a
包含一個值就進行解包,否則就返回 一個默認值 b
。
- 表達式
a
必須是Optional
類型 - 默認值
b
的類型必須要和a
存儲值的類型保持一致
運算符重載
在源碼中我們可以看到除了重載了 ??
運算符,Optional
類型還重載了 == , ?=
等運算符,實際開發中我們可以通過重載運算符簡化我們的表達式。
比如在開發中我們定義了一個二維向量,這個時候我們想對兩個向量進行基本的操作,那么我們就可以通過重載運算符來達到我們的目的
struct Vector {
let x: Int
let y: Int
}
extension Vector {
static func + (fistVector: Vector, secondVector: Vector) -> Vector {
return Vector(x: fistVector.x + secondVector.x, y: fistVector.y + seco
}
static prefix func - (vector: Vector) -> Vector {
return Vector(x: -vector.x, y: -vector.y)
}
static func - (fistVector: Vector, secondVector: Vector) -> Vector {
return fistVector + -secondVector
}
}
通過以上代碼我們可以對 Vector
進行相加、相減、或者取負。
更多關于自定義運算符的規則也可以看下官方文檔 自定義運算符。
隱士解析可選類型
隱式解析可選類型是可選類型的一種,使用的過程中和非可選類型無異。它們之間唯一 的區別是,隱式解析可選類型是你告訴對 Swift
編譯器,我在運行時訪問時,值不會為 nil
。
var age: Int?
var age1: Int!
age = nil
age1 = nil
其實日常開發中我們比較常?這種隱士解析可選類型
IBOutlet
類型是 Xcode
強制為可選類型的,因為它不是在初始化時賦值的,而是在加載視圖的時候。你可以把它設置為普通可選類型,但是如果這個視圖加載正確,它是不會為空的。
與可選值有關的高階函數
-
map
: 這個方法接受一個閉包,如果可選值有內容則調用這個閉包進行轉換
var dict = ["one": "1", "two": "2"]
let result = dict["one"].map{ Int($0) } // Optional(Optional(1))
上面的代碼中我們從字典中取出字符串 "1"
,并將其轉換為 Int
類型,但因為 String
轉換成 Int
不一定能成功,所以返回的是 Int?
類型,而且字典通過鍵不一定能取得到值,所以 map
返回的也是一個 Optional
,所以最后上述代碼 result
的類型為 Int??
類型。
那么如何把我們的雙重可選展平開來,這個時候我們就需要使用到 flatMap
。
-
flatMap
: 可以把結果展平成為單個可選值
var dict = ["one": "1", "two": "2"]
let result = dict["one"].flatMap{ Int($0) } // Optional(1)
注意,這個方法是作用在
Optioanl
上的作用在
Sequence
上的flatMap
方法在Swift4.1
中被更名為compactMap
,該方法可以將序列中的nil
過濾出去。
let array = ["1", "2", "3", nil]
let result = array.compactMap{ $0 } // ["1", "2", "3"]
let array = ["1", "2", "3", "four"]
let result = array.compactMap{ Int($0) } // [1, 2, 3]