Swift探索(五): Enum & Optional

一:枚舉(Enum)

1. 枚舉的基本用法

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

enum LJLEnum {
    case test_one
    case test_two
    case test_three
}

Swift 中的枚舉則更加靈活,并且不需給枚舉中的每一個成員都提供值。如果一個值(所謂“原始”值)要被提供給每一個枚舉成員,那么這個值可以是字符串、字符、任意的整數值,或者是浮點類型。

enum Color: String {
    case red = "Red"
    case blue = "Blue"
    case green = "Grren"
}

enum LJLEnum: Double {
    case a = 10.0
    case b = 22.2
    case c = 33.3
    case d = 44.4
}

2. 原始值 RawValue

隱士 RawValue 是建立在 Swift 的類型判斷機制上的

enum LJLEnum: Int {
    case one, two, three = 10, four, five
}
print(LJLEnum.one.rawValue)
print(LJLEnum.two.rawValue)
print(LJLEnum.three.rawValue)
print(LJLEnum.four.rawValue)
print(LJLEnum.five.rawValue)
// 打印結果
0
1
10
11
12

可以看到 RawValue 原始值跟 OC 一樣都是從 0 、 12 開始,當指定值時,后面的枚舉值的 RawValue 會在當前值的基礎上進行累加操作,因此 fourfive 的值為 1112。

將枚舉類型改成 String 類型

enum LJLEnum: String {
    case one, two, three = "Hello World", four, five
}
print(LJLEnum.one.rawValue)
print(LJLEnum.two.rawValue)
print(LJLEnum.three.rawValue)
print(LJLEnum.four.rawValue)
print(LJLEnum.five.rawValue)

// 打印結果
one
two
Hello World
four
five

可以看出系統已經默認給了每一個枚舉成員分配了一個字符串,并且該字符串與枚舉成員值的字符串一致。
將上述代碼簡化一下,通過 LLDB 命令 swiftc xxx.swift -emit-sil 編譯成 sil 文件

enum LJLEnum: String {
    case one, two = "Hello World", three
}
var x = LJLEnum.one.rawValue

sil 文件代碼

// 枚舉的聲明
enum LJLEnum : String {
  case one, two, three
  init?(rawValue: String)
  typealias RawValue = String
  var rawValue: String { get }
}

// LJLEnum.rawValue.getter
sil hidden @$s4main7LJLEnumO8rawValueSSvg : $@convention(method) (LJLEnum) -> @owned String {
// %0 "self"                                      // users: %2, %1
bb0(%0 : $LJLEnum):
  debug_value %0 : $LJLEnum, let, name "self", argno 1 // id: %1
  switch_enum %0 : $LJLEnum, case #LJLEnum.one!enumelt: bb1, case #LJLEnum.two!enumelt: bb2, case #LJLEnum.three!enumelt: bb3 // id: %2

bb1:                                              // Preds: bb0
  %3 = string_literal utf8 "one"                  // user: %8
  %4 = integer_literal $Builtin.Word, 3           // user: %8
  %5 = integer_literal $Builtin.Int1, -1          // user: %8
  %6 = metatype $@thin String.Type                // user: %8
  // function_ref String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
  %7 = function_ref @$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %8
  %8 = apply %7(%3, %4, %5, %6) : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %9
  br bb4(%8 : $String)                            // id: %9

bb2:                                              // Preds: bb0
  %10 = string_literal utf8 "Hello World"         // user: %15
  %11 = integer_literal $Builtin.Word, 11         // user: %15
  %12 = integer_literal $Builtin.Int1, -1         // user: %15
  %13 = metatype $@thin String.Type               // user: %15
  // function_ref String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
  %14 = function_ref @$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %15
  %15 = apply %14(%10, %11, %12, %13) : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %16
  br bb4(%15 : $String)                           // id: %16

bb3:                                              // Preds: bb0
  %17 = string_literal utf8 "three"               // user: %22
  %18 = integer_literal $Builtin.Word, 5          // user: %22
  %19 = integer_literal $Builtin.Int1, -1         // user: %22
  %20 = metatype $@thin String.Type               // user: %22
  // function_ref String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
  %21 = function_ref @$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %22
  %22 = apply %21(%17, %18, %19, %20) : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %23
  br bb4(%22 : $String)                           // id: %23

// %24                                            // user: %25
bb4(%24 : $String):                               // Preds: bb3 bb2 bb1
  return %24 : $String                            // id: %25
} // end sil function '$s4main7LJLEnumO8rawValueSSvg'

我們可以看到 rawValue.getter 函數的調用,根據傳進來的枚舉成員值,通過模式匹配的方式走到不同的代碼分支,在不同的代碼分支中把不同的字符串給到當前對應的代碼分支返回值。

3. 關聯值

用枚舉表達更復雜的情況,

enum Shape {
    case circle(radius: Double)
    case rectangle(width: Int, height: Int)
}

var circle = Shape.circle(radius: 10.0)
var square = Shape.rectangle(width: 5, height: 5)

4. 模式匹配

enum Week: String {
    case MONDAY
    case TUEDAY
    case WEDDAY
    case THUDAY
    case FRIDAY
    case SATDAY
    case SUNDAY
}

通過 Switch 關鍵字進行匹配

let currentWeek: Week
switch currentWeek {
    case .MONDAY: print(Week.MONDAY.rawValue)
    case .TUEDAY: print(Week.TUEDAY.rawValue)
    case .WEDDAY: print(Week.WEDDAY.rawValue)
    case .THUDAY: print(Week.THUDAY.rawValue)
    case .FRIDAY: print(Week.FRIDAY.rawValue)
    case .SUNDAY: print(Week.SUNDAY.rawValue)
    case .SATDAY: print(Week.SUNDAY.rawValue)
}

如果不想匹配所有的 case ,使用 defalut 關鍵字

let currentWeek: Week = Week.MONDAY
switch currentWeak {
    case .SATDAY, .SUNDAY: print("Happy Day")
    default : print("SAD DAY")
}

匹配關聯值

enum Shape {
    case circle(radius: Double)
    case rectangle(width: Int, height: Int)
}

let shape = Shape.circle(radius: 10.0)

// 方式一:
switch shape {
    case let .circle(radius):
        print("Circle radius:\(radius)")
    case let .rectangle(width, height):
        print("rectangle width:\(width), height\(height)")
}

// 方式二:
switch shape {
    case .circle(let radius):
        print("Circle radius:\(radius)")
    case .rectangle(let width, let height):
        print("rectangle width:\(width), height\(height)")
}

5. 枚舉的大小

5.1 沒有關聯值枚舉(No-payload enums)
enum Week: String {
    case MONDAY
    case TUEDAY
    case WEDDAY
    case THUDAY
    case FRIDAY
    case SATDAY
    case SUNDAY
}

print(MemoryLayout<Week>.size)
print(MemoryLayout<Week>.stride)

// 打印結果
1
1

可以看出不管是大小( size ) 還是步長 stride 都是 1 ,在 Swift 中進行枚舉布局的時候一直是嘗試使用最少的空間來存儲 enum ,對于當前的 case 數量來說, UInt8 能夠表示 256 cases ,也就意味著如果一個默認枚舉類型且沒有關聯值的 case 少于 256 ,當前枚舉類型的大小都是 1 字節。

No-payload enum.png

通過上面的打印我們可以看到,當前變量 abc 三個變量的地址相差 1 位,并且存儲的內容分別是 000102 ,這與我們上面說的布局理解是一致的

5.2 單個關聯值枚舉(Single-payload enums)
enum LJLBoolEnum {
    case one(Bool)
    case two
    case three
    case four
}

enum LJLIntEnum {
    case one(Int)
    case two
    case three
    case four
}

print("BoolEnum.Size:\(MemoryLayout<LJLBoolEnum>.size)")
print("BoolEnum.stride:\(MemoryLayout<LJLBoolEnum>.stride)")

print("IntEnum.Size:\(MemoryLayout<LJLIntEnum>.size)")
print("IntEnum.stride:\(MemoryLayout<LJLIntEnum>.stride)")
打印結果.png

Swift 中的 enum 中的 Single-payload enums 會使用負載類型中的額外空間來記錄沒有負載的 case 值。比如這里的 BoolEnum ,首先 Bool 類型是 1 字節,也就是 UInt8 ,所以當前能表達 256case 的情況,對于 Bool 類型來說,只需要使用低位的 0 , 1 這兩種情況,其他剩余的空間就可以用來表示沒有負載的 case 值。

LJLBoolEnum.png

可以看到不同的 case 值確實是按照我們在開始得出來的那個結論進行布局的。
對于 Int 類型的負載來說,其實系統是沒有辦法推算當前的負載所要使用的位數,也就意味著當前 Int 類型的負載是沒有額外的剩余空間的,這個時候我們就需要額外開辟內存空間來去存儲我們的 case 值,也就是 8 + 1 = 9 字節。
LJLIntEnum.png

可以看出變量 a 、 b 、 cd 、 e 的地址相差 16 位,這和上面打印的步長信息相一致。

5.3 多個關聯值枚舉(Mutil-payload enums)
enum LJLDoubleBoolEnum {
    case one(Bool)
    case two(Bool)
    case three
    case four
}

enum LJLDoubleIntEnum {
    case one(Int)
    case two(Int)
    case three
    case four
}

print("DoubleBoolEnum.Size:\(MemoryLayout<LJLDoubleBoolEnum>.size)")
print("DoubleBoolEnum.stride:\(MemoryLayout<LJLDoubleBoolEnum>.stride)")

print("DoubleIntEnum.Size:\(MemoryLayout<LJLDoubleIntEnum>.size)")
print("DoubleIntEnum.stride:\(MemoryLayout<LJLDoubleIntEnum>.stride)")

打印結果

打印結果.png

我們可以看到兩個 Bool 關聯值的枚舉的大小為 1,根據上面單個關聯值枚舉的所述不難理解。對于兩個 Int 關聯值的枚舉的大小為 9,是因為創建一個枚舉值時有且只有一個關聯值,但是還需要 1 個字節去存儲其他 case 枚舉值( threefour ) 所以當前只需要 8 字節 + 1 字節
LJLDoubleBoolEnum.png

我們可以看到當前內存存儲的分別是 00 、0140 、 41 、 80 、 81 ,這里在存儲當前的 case 時候會使用到 common spare bits,首先 bool 類型需要 1 字節,也就是 8 位,對于 bool 類型來說,我們存儲的無非就是 01 ,只需要用到 1 位,所以剩余的 7 位,這里我們都統稱為 common spare bits ,對于當前的 case 數量來說我們完全可以把所有的情況放到 common spare bits 所以這里只需要 1 字節就可以存儲所有的內容了。
對于 00 、 01 、 4041 、 80 、 81 ,其中 0 、 4 、 8 稱之為 tag value01 稱之為 tag index。

一般來說,有多個負載的枚舉時,當前枚舉類型的大小取決于當前最大關聯值的大小。

enum LJLEnum{
    case one(Bool)
    case two(Int)
    case three
    case four
}

print("LJLEnum.Size:\(MemoryLayout<LJLEnum>.size)")

// 打印結果
LJLEnum.Size:9

當前 LJLEnum 的大小就等于 sizeof(Int) + sizeof(rawVlaue) = 9

enum LJLEnum{
    case one(Bool)
    case two(Int, Bool, Int)
    case three
    case four
}

print("LJLEnum.Size:\(MemoryLayout<LJLEnum>.size)")
// 打印結果 
LJLEnum.Size:24

這里為什么不是 sizeof(Int) * 2 + sizeof(rawVlaue) = 17 呢?對于 two (Int, Bool, Int) 類型的由于字節對齊的原因所以它的存儲大小為 8 * 3 = 24 ,又由于中間的 Bool 實際值占用 1 個字節因此中間 8 字節還有剩余控件去存儲其他的 case 值。所以這里是 24 字節。那么將 bool 放在后面呢?我們試一試

enum LJLEnum{
    case one(Bool)
    case two(Int, Int, Bool)
    case three
    case four
}

print("LJLEnum.Size:\(MemoryLayout<LJLEnum>.size)")
print("LJLEnum.Stride:\(MemoryLayout<LJLEnum>.stride)")

打印結果.png

可以看到大小為 17,其實也不難理解,bool 放后面那么它的 1 字節就能放下其他的 case,那為什么這里不字節對齊呢?其實是因為最后會因為步長 24 的原因會對這里進行補齊。

注意對于只有一個 case 的枚舉,不需要用任何東?來去區分當前的 case ,所以它的大小是 0。

enum LJLEnum{
    case one
}
print("LJLEnum.Size:\(MemoryLayout<LJLEnum>.size)")

// 打印結果
LJLEnum.Size:0

6. 遞歸枚舉

遞歸枚舉是一種枚舉類型,它有一個或多個枚舉成員使用該枚舉類型的實例作為關聯值。使用遞歸枚舉時,編譯器會插入一個間接層。你可以在枚舉成員前加上 indirect 來表示該成員可遞歸。

enum BinaryTree<T> {
    case empty
    indirect case node(left: BinaryTree, value: T, right: BinaryTree)
}

var node = BinaryTree<Int>.node(left: BinaryTree<Int>.empty, value: 10, right: BinaryTree<Int>.empty)

也可以在枚舉類型開頭加上 indirect 關鍵字來表明它的所有成員都是可遞歸的。

indirect enum BinaryTree<T> {
    case empty
    case node(left: BinaryTree, value: T, right: BinaryTree)
}

var node = BinaryTree<Int>.node(left: BinaryTree<Int>.empty, value: 10, right: BinaryTree<Int>.empty)

在匯編中查看

匯編中查看.png

可以發現這里調用了 swift_allocObject , 在之前的文章 Swift探索(一): 類與結構體(上) 中我們就探討過, swift_allocObject 就是在堆空間中分配內存空間。對于 indirect 關鍵字放在 enum 前面也就意味著當前這個 enum 的大小都是用引用類型在堆空間中存儲。當 indirect 關鍵字放在 case 前面,那么就只有這個 case note 是存儲在堆空間中,其中 case empty 是存儲在 __DATA.__common(存儲沒有初始化過的符號聲明)的 section

二:可選值(Optional)

1. 什么是可選值

class Person {
    var age: Int?
    var name: Optional<String> = nil
}

agename 我們就稱之為可選值

2.可選值的本質

swift源碼.png

Swift 源碼中可以發現可選值實際上就是一個枚舉,并且有兩個 case 一個 none ,一個 some

3. 可選值的基本使用

func getOddValue(_ value: Int) -> 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)
    switch value {
        case .some(let value):
            array.remove(at: array.firstIndex(of: value)!)
        case .none:
            print("vlaue not exist")
    }
}

如果每一個可選值都用模式匹配的方式來獲取值在代碼書寫上就比較繁瑣,還可以使用 if let 的方式來進行可選值綁定

func getOddValue(_ value: Int) -> Int? {
    if value % 2 == 0 {
        return .some(value)
    } else {
        return .none
    }
}

var array = [1, 2, 3, 4, 5, 6]
for element in array {
    if let value = getOddValue(element) {
        array.remove(at: array.firstIndex(of: value)!)
    }
}

除了使用 if let 還可以使用 gurad let,和 if let 剛好相反,gurad let 守護一定有值。如果沒有,直接返回。 通常判斷是否有值之后,會做具體的邏輯實現,通常代碼多如果用 if let 憑空多了一層分支, gurad let 是降低分支層次的辦法

var name: String?
var age: Int?
var height: Double?

func testIfLet() {
    if let name1 = name {
        if let age1 = age {
            if let height1 = height {
                print("姓名: \(name1), 年齡:\(age1), 身高:\(height1)cm")
            } else {
                print("height 為空")
            }
        } else {
            print("age 為空")
        }
    } else {
        print("name 為空")
    }
}

func testGuardLet() {
    guard let name1 = name else {
        print("name 為空")
        return 
    }
    
    guard let age1 = age else {
        print("age 為空")
        return
    }

    guard let height1 = height else {
        print("height 為空")
        return
    }

    print("姓名: \(name1), 年齡:\(age1), 身高:\(height1)cm")
}

2. 可選鏈

OC 中我們給一個 nil 對象發送消息什么也不會發生,但是在 Swift 中是沒有辦 法向一個 nil 對象直接發送消息,但是借助可選鏈可以達到類似的效果。

let str: String? = "abc"
let upperStr = str?.uppercased() 
print(upperStr)

var str2: String?
let upperStr2 = str2?.uppercased()
print(upperStr2)

// 打印結果
Optional("ABC")
nil

同意可選鏈對數組和喜愛

var closure: ((Int) -> ())?
print(closure?(1)) // closure 為 nil 不執行
let dict = ["one": 1, "two": 2]
print(dict["one"]) // Optional(1)
print(dict["three"]) // nil

// 打印結果
nil
Optional(1)
nil

3. ?? 運算符(空合并運算符)

( a ?? b ) 將對可選類型 a 進行空判斷,如果 a 包含一個值就進行解包,否則就返回一個默認值 b 。

  • 表達式 a 必須是 Optional 類型
  • 默認值 b 的類型必須要和 a 存儲值的類型保持一致
var age: Int? = 10
var x = age ?? 0
print(x)

// 打印結果
10
?? 空運算符源碼.png

4. 運算符重載

源碼中我們可以看到除了重載了 ?? 運算符, 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 + secondVector.y)
    }
    static prefix func - (vector: Vector) -> Vector {
        return Vector(x: -vector.x, y: -vector.y)
    }
    static func - (fistVector: Vector, secondVector: Vector) -> Vector {
        return fistVector + -secondVector
    }
}

var x = Vector(x: 10, y: 20)
var y = Vector(x: 20, y: 30)
var z = x + y
print(z)

var w = -z
print(w)

// 打印結果
Vector(x: 30, y: 50)
Vector(x: -30, y: -50)

根據官方文檔創建一個自定義運算符 已有運算符

infix operator **: AdditionPrecedence
// 運算組名稱: LJLPrecedence 優先級低于: AdditionPrecedence 結合方式: 左結合
precedencegroup LJLPrecedence {
    lowerThan: AdditionPrecedence
    associativity: left
}

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 + secondVector.y)
    }
    static prefix func - (vector: Vector) -> Vector {
        return Vector(x: -vector.x, y: -vector.y)
    }
    static func - (fistVector: Vector, secondVector: Vector) -> Vector {
        return fistVector + -secondVector
    }
    
    static func ** (fistVector: Vector, secondVector: Vector) -> Vector {
        return Vector(x: fistVector.x * secondVector.x, y: fistVector.y * secondVector.y)
    }

}

var x = Vector(x: 10, y: 20)
var y = Vector(x: 20, y: 30)
var z = x ** y
var w = x + y ** x
print(z)
print(w)

// 打印結果
Vector(x: 200, y: 600)
Vector(x: 300, y: 1000)

5. 隱士解析可選類型

隱式解析可選類型是可選類型的一種,使用的過程中和非可選類型無異。它們之間唯一的區別是,隱式解析可選類型是你告訴對 Swift 編譯器,在運行時訪問時,值不會為 nil。

var age: Int
var age1: Int!
var age2: Int?

let x = age1 % 2
let y = age2 % 2

隱式解析.png

其中 age1 不用做解包的操作,編譯器已經做了

@IBOutlet weak var btn: UIButton!

IBOutlet 類型是 Xcode 強制為可選類型的,因為它不是在初始化時賦值的,而是在加載視圖的時候。可以把它設置為普通可選類型,但是如果這個視圖加載正確,它是不會為空的。

6. 可選值有關的高階函數

  • map :這個方法接受一個閉包,如果可選值有內容則調用這個閉包進行轉換
var dict = ["one": "1", "two": "2"]
let result = dict["one"].map{ Int($0) } 
print(result)

// 打印結果
Optional(Optional(1))

上面的代碼中我們從字典中取出字符串 1 ,并將其轉換為 Int 類型,但因為 String 轉換成 Int 不一定能成功,所以返回的是 Int? 類型,而且字典通過鍵不一定能取得到值,所以 map 返回的也是一個 Optional ,所以最后上述代碼 result 的類型為 Int?? 類型。

  • flatMap :可以把結果展平成為單個可選值
var dict = ["one": "1", "two": "2"]
let result = dict["one"].flatMap{ Int($0) } // Optional(1)
print(result)

// 打印結果
Optional(1)

注意這個方法是作用在 Optional 的方法,而不是作用在 Sequence 上的
作用在 Sequence 上的 flatMap 方法在 Swift4.1 中被更名為 compactMap ,該方法可以將序列中的 nil 過濾出去

let array = ["1", "2", "3", nil]
let result = array.compactMap{ $0 } // ["1", "2", "3"]
print(result)
let array1 = ["1", "2", "3", "four"]
let result1 = array1.compactMap{ Int($0) } // [1, 2, 3]
print(result1)

// 打印結果
["1", "2", "3"]
[1, 2, 3]
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容