Swift 中 Enum 及 Optional 介紹

Enum

枚舉的基本用法

swift 中通過 enum 關鍵字來聲明一個枚舉

enum CXEnum { 
  case test_one
  case test_two
  case test_three 
}

而在 C 或者 OC 中默認受整數支持,也就意味著下面的例子中: ABC 分別代表 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 開始 satsun 就分別對應 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 類型的 widthheight 來代表寬高。

模式匹配

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

通過上面的打印我們可以直觀的看到,當前變量 abc 這三個變量存儲的內容分別是 000102,分別相差 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 value0, 1 這里我們就叫做 tag index,至于這個 tag value 怎么來的, 目前在源碼中還沒有找到驗證,如果大家感興趣的的話也可以閱讀一下源碼中的 Enum.cppGenEnum.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 letif 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]
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容