Swift 四、Enum & Optional

Enum & Optional.png

一、Enum

1.1 枚舉的基本用法

Swift語言中使用enum關鍵字來進行枚舉的創建。

///創建一個姓氏枚舉類型
enum Surname {
    case 趙
    case 錢
    case 孫
    case 李
}

上面的代碼創建了一個姓氏枚舉類型,這個枚舉類型中定義了4個枚舉值,分別是趙、錢、孫、李,上面的寫法將4個枚舉值分別在4個case語句中定義,開發者也可以在1個case子句中完成多個枚舉值的定義,如

///創建一個姓氏枚舉類型
enum Surname {
    case 趙, 錢,孫,李
}

1.2 枚舉的原始值和相關值

枚舉的原始值特性可以將枚舉值與另一種數據類型進行綁定,相關值則可以為枚舉值關聯一些其他數據。通過相關值,開發者可以實現復雜的枚舉類型

1.2.1 枚舉的原始值

swift語言中的枚舉支持開發者聲明一個原始值類型,并將某個已經存在的類型的值與枚舉值進行綁定,枚舉指定原始值類型的語法與繼承的語法有些類似,示例代碼如下:

///為枚舉類型指定一個原始值類型
enum CharEnum: Character {
    ///通過賦值的方式為枚舉值設置一個原始值
    case a = "a"
    case b = "b"
    case c = "c"
    case d = "d"
}

如果開發者要指定枚舉的原始值類型為Int類型,那么可以只設置第一個枚舉值的原始值,其后的枚舉值的原始值會在第一個枚舉值原始值的基礎上依次遞增,示例如下:

enum IntEnum: Int {
    ///第一個枚舉值的原始值設置為1
    case a = 1
    ///默認原始值為2
    case b
    ///默認原始值為3
    case c
    ///默認原始值為4
    case d
}

通過枚舉類型中的rawValue屬性來獲取枚舉的原始值,示例如下:

enum CharEnum: Character {
    ///通過賦值的方式為枚舉值設置一個原始值
    case a = "a"
    case b = "b"
    case c = "c"
    case d = "d"
}

var char = CharEnum.a
var value = char.rawValue
print(value)

隱式RawValue分配是建立在swift的類型推斷機制上的

enum DayOfWeek: String {
    case mon, tue, wed, thu, fri = "Hello World", sat, sun
}
print(DayOfWeek.mon.rawValue)
print(DayOfWeek.fri.rawValue)
print(DayOfWeek.sat.rawValue)

lldb打印輸出

mon
Hello World
sat

我們當前的系統已經默認給我們的每一個枚舉值分配了一個字符串,而這個字符串其實跟我們枚舉成員值的字符串是一致的。那么它到底是怎么做到的哪,我們在SIL文件看一下。

enum DayOfWeek : String {
  case mon, tue, wed, thu, fri, sat, sun
  init?(rawValue: String)
  typealias RawValue = String
  var rawValue: String { get }
}
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  alloc_global @$s4main1xSSvp                     // id: %2
  %3 = global_addr @$s4main1xSSvp : $*String      // user: %8
  %4 = metatype $@thin DayOfWeek.Type
  %5 = enum $DayOfWeek, #DayOfWeek.mon!enumelt    // user: %7
  // function_ref DayOfWeek.rawValue.getter
  %6 = function_ref @$s4main9DayOfWeekO8rawValueSSvg : $@convention(method) (DayOfWeek) -> @owned String // user: %7
  %7 = apply %6(%5) : $@convention(method) (DayOfWeek) -> @owned String // user: %8
  store %7 to %3 : $*String                       // id: %8
  %9 = integer_literal $Builtin.Int32, 0          // user: %10
  %10 = struct $Int32 (%9 : $Builtin.Int32)       // user: %11
  return %10 : $Int32                             // id: %11
} 

image.png

這里就是在訪問我們rawValuegetter方法。

sil hidden @$s4main9DayOfWeekO8rawValueSSvg : $@convention(method) (DayOfWeek) -> @owned String {
// %0 "self"                                      // users: %2, %1
bb0(%0 : $DayOfWeek):
  debug_value %0 : $DayOfWeek, let, name "self", argno 1 // id: %1
  switch_enum %0 : $DayOfWeek, case #DayOfWeek.mon!enumelt: bb1, case #DayOfWeek.tue!enumelt: bb2, case #DayOfWeek.wed!enumelt: bb3, case #DayOfWeek.thu!enumelt: bb4, case #DayOfWeek.fri!enumelt: bb5, case #DayOfWeek.sat!enumelt: bb6, case #DayOfWeek.sun!enumelt: bb7 // id: %2

switch_enum %0 就是我們傳進來的當前枚舉成員值mon,接下來就是一個模式匹配,匹配我們當前的成員值。匹配上了swift case #DayOfWeek.mon!enumelt: bb1代碼塊。

bb1:                                              // Preds: bb0
  %3 = string_literal utf8 "mon"                  // user: %8

那么這個字符串 "mon" 是從哪里得到的哪?這樣的字符串其實就是一個字符串常量,而字符串常量存儲在哪里哪,我們把Mach-o文件拖到MachoView應用來查看一下。


image.png

在枚舉變量初始化時,開發者可以使用枚舉類型加點語法的方式,如果這個枚舉有指定的原始值,也可以通過枚舉值的原始值來完成枚舉實例的構造,示例如下:

enum IntEnum: Int {
    ///第一個枚舉值的原始值設置為1
    case a = 1
    ///默認原始值為2
    case b
    ///默認原始值為3
    case c
    ///默認原始值為4
    case d
}

///通過原始值構造枚舉變量a
var intEnum = IntEnum(rawValue: 1)

需要注意,通過原始值進行枚舉實例的構造時,是有可能構造失敗的,因為開發者傳入的原始值不一定會對應某一個枚舉值。因此,這個方法實際上返回的是一個Optional類型的可選值,如果構造失敗,則會返回nil。

1.2.1 枚舉的相關值

在定義枚舉值的時候,開發者可以為其設置一個參數列表,這個參數列表被稱為枚舉的相關值。示例如下:

///定義形狀枚舉
enum Shape {
    ///圓形,設置圓心和半徑為相關值
    case circle(center: (Double, Double), radius: Double)
    ///矩形,設置中心,寬高為相關值
    case rect(center: (Double, Double), width: Double, height: Double)
    ///三角形,設置三個頂點為相關值
    case triangle(point1: (Double, Double), point2: (Double, Double), point3: (Double, Double))
}

在創建相關值枚舉的時候,開發者需要提供參數列表中所需要的參數,示例如下:

省略......
///創建圓形,圓心為(0, 0),半徑為3
var circle = Shape.circle(center: (0, 0), radius: 3)
///創建矩形,中心點為(1, 1), 寬度為10,高度為15
var rect = Shape.rect(center: (1, 1), width: 10, height: 15)
///創建三角形,頂點為(2, 2), (3, 3), (2, 5)
var triangle = Shape.triangle(point1: (2, 2), point2: (3, 3), point3: (2, 5))

1.3 模式匹配

在switch-case結構語句中,匹配到枚舉后,可以通過參數捕獲的方式來獲取枚舉實例的相關值,這里捕獲到的相關值參數可以在開發者的代碼中使用,示例如下:

///定義形狀枚舉
enum Shape {
    ///圓形,設置圓心和半徑為相關值
    case circle(center: (Double, Double), radius: Double)
    ///矩形,設置中心,寬高為相關值
    case rect(center: (Double, Double), width: Double, height: Double)
    ///三角形,設置三個頂點為相關值
    case triangle(point1: (Double, Double), point2: (Double, Double), point3: (Double, Double))
}

///創建圓形,圓心為(0, 0),半徑為3
var circle = Shape.circle(center: (0, 0), radius: 3)
///創建矩形,中心點為(1, 1), 寬度為10,高度為15
var rect = Shape.rect(center: (1, 1), width: 10, height: 15)
///創建三角形,頂點為(2, 2), (3, 3), (2, 5)
var triangle = Shape.triangle(point1: (2, 2), point2: (3, 3), point3: (2, 5))

func ShapeFunc(param: Shape) {
    switch param {
    case let .circle(center, radius):
        print("此圓的圓心為:\(center),半徑為:\(radius)")
    case let .rect(center, width, height):
        print("此矩形的中心為:\(center),寬為:\(width),高為:\(height)")
    case let .triangle(point1, point2, point3):
        print("此三角形的3個頂點為:\(point1),\(point2),\(point3)")
    }
}

ShapeFunc(param: circle)
ShapeFunc(param: rect)
ShapeFunc(param: triangle)

1.4 枚舉的大小

接下來我們來討論一下枚舉占用的內存大小,這里我們區分幾種不同的情況,首先第一種就是 No-payload enums

///UInt8
/// 最多可存儲256個case
enum Week {
    
    case MONDAY
    
    case TUEDAY
    
    case WEDDAY
    
    case THUDAY
    
    case FRIDAY
    
    case SATDAY
    
    case SUNDAY
    
}

print(MemoryLayout<Week>.size)

打印結果是1字節。
可以看到這里我們測試出來的不管是 size 還是 stride 都是 1 ,這個地方我們也很好理解,當前的 enum 有幾個 case ? 是不是 8 個啊,在 Swift 中進行枚舉布局的時候一直是嘗試使用最少的空間來存儲 enum ,對于當前的 case 數量來說, UInt8能夠表示 256 cases ,也就意味著如果一個默認枚舉類型且沒有關聯值的 case 少于 256 ,當前枚舉類型的大小都是 1 字節。

image.png

通過上面的打印我們可以直觀的看到,當前變量 a , b , c這三個變量存儲的內容分別 是 00, 01, 02 這和我們上面說的布局理解是一致的。
No-payload enums 的布局比較簡單,我們也比較好理解,接下來我們來理解一下 Single- payload enums 的內存布局, 字面意思就是只有一個負載的 enum, 比如下面這個例子:

enum ZGEnum {
    
    case test_one(Bool)
    case test_two
    case test_three
    case test_four
    
}

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

大家猜一下當前的這個案例, enum 在內存中的大小是多少?

1
1

如果我把當前的案例換一下,換成如下的案例,那么當前 enum 占用的大小又是多少?

enum ZGEnum {
    
    case test_one(Int)
    case test_two
    case test_three
    case test_four
    
}

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

結果是

9
16

這里我們就產生了疑問了,為什么都是單個負載,但是當前占用的大小卻不一致?
注意, Swift 中的 enum 中的 Single-payload enums 會使用負載類型中的額外空間來記錄沒有負載的 case 值。這句話該怎么理解?首先 Bool 類型1字節,也就是 UInt8 ,所以當前能表達 256 個 case的情況,對于 Bool類型來說,只需要使用低位的 0, 1 這兩種情況,其 他剩余的空間就可以用來表示沒有負載的 case 值。

enum ZGEnum {
    
    case test_one(Bool)
    case test_two
    case test_three
    case test_four
    
}

var a = ZGEnum.test_one(false)
var b = ZGEnum.test_one(true)
var c = ZGEnum.test_two
var d = ZGEnum.test_three
var f = ZGEnum.test_four

枚舉單負載.png

可以看到,不同的 case 值確實是按照我們在開始得出來的那個結論進行布局的。

enum ZGEnum {
    
    case test_one(Int)
    case test_two
    case test_three
    case test_four
    
}

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

var a = ZGEnum.test_one(10)
var b = ZGEnum.test_one(20)
var c = ZGEnum.test_two
var d = ZGEnum.test_three
var f = ZGEnum.test_four
枚舉單負載Int類型.png

對于 Int 類型的負載來說,其實系統是沒有辦法推算當前的負載所要使用的位數,也就意味著當前 Int 類型的負載是沒有額外的剩余空間的,這個時候我們就需要額外開辟內存空間來去存儲我們的 case 值,也就是 8 + 1 = 9 字節。

上面說完了 Single-payload enums , 接下來我們說第三種情況 Mutil-payload enums , 有多個負載的情況產生時,當前的 enum是如何進行布局的哪?

enum ZGEnum {
    
    case test_one(Bool)
    case test_two(Bool)
    case test_three
    case test_four
    
}

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

var a = ZGEnum.test_one(false)
var b = ZGEnum.test_two(true)
var c = ZGEnum.test_three
var d = ZGEnum.test_three
var f = ZGEnum.test_four
1
1

上面這個例子中,我們有兩個 Bool 類型的負載,這個時候我們打印當前的 enum 大小 發現其大小仍然為 1,這個時候我們來看一下內存當中的存儲情況。

枚舉雙負載Bool類型.png

這里我們可以看到當前內存存儲的分別是 00 41 80 80 81 00 , 這里在存儲當前的 case 時候會使用到 common spare bits,什么意思?其實在上一個案例我們也講過了,首先 bool 類型需要 1 字節,也就是 8 位。

WX20220114-100716@2x.png

接下來我們來看一下 00 41 80 80 81 00 分別代表的是什么?首先 0, 4, 8 這里我們叫 做 tag value0, 1 這里我們就做tag index
當前一般來說,我們有多個負載的枚舉時,當前枚舉類型的大小取決于當前最大關聯值的大小。

1.5 遞歸枚舉

遞歸枚舉其實就是使用遞歸的方式來進行數據描述。使用indirect關鍵字修飾的枚舉值表示這個枚舉值是可遞歸的,即此枚舉值中的相關值可以使用其枚舉類型本身。

enum Expression {
    case num(num: Int)
    indirect case add(num1: Expression, num2: Expression)
    indirect case sub(num1: Expression, num2: Expression)
    indirect case mul(num1: Expression, num2: Expression)
    indirect case div(num1: Expression, num2: Expression)
}

使用Expression枚舉來描述復合表達式( (5 + 5) * 2 - 8) / 2的代碼如下:

///單值 5
var num5 = Expression.num(num: 5)
/// 表達式 5 + 5
var exp1 = Expression.add(num1: num5, num2: num5)
///單值 2
var num2 = Expression.num(num: 2)
///表達式 (5 + 5) * 2
var exp2 = Expression.mul(num1: exp1, num2: num2)
///單值 8
var num8 = Expression.num(num: 8)

///表達式 (5 + 5) * 2 - 8
var exp3 = Expression.sub(num1: exp2, num2: num8)

///表達式 ((5 + 5) * 2 - 8) / 2
var expFinal = Expression.div(num1: exp3, num2: num2)

我們可以為這個四則表達式枚舉類型Expression實現一個函數來進行運算,在開發中將描述與運算結合,能夠編寫出十分優美的代碼。處理遞歸枚舉通常會采用遞歸函數,函數方法實現示例如下:

func expressionFunc(num: Expression) -> Int {
    switch num {
    case let .num(num):
        return num
    case let .add(num1, num2):
        return expressionFunc(num: num1) + expressionFunc(num: num2)
    case let .sub(num1, num2):
        return expressionFunc(num: num1) - expressionFunc(num: num2)
    case let .mul(num1, num2):
        return expressionFunc(num: num1) * expressionFunc(num: num2)
    case let .div(num1, num2):
        return expressionFunc(num: num1) / expressionFunc(num: num2)
        
    }
}
///((5 + 5) * 2 - 8) / 2 打印結果為6
expressionFunc(num: expFinal)

關于遞歸枚舉還有一點需要注意,如果一個枚舉中所有的枚舉值都是可遞歸的,開發者可以直接將整個枚舉類型聲明為可遞歸的,示例如下:

indirect enum Expression {
    case num(num: Int)
    case add(num1: Expression, num2: Expression)
    case sub(num1: Expression, num2: Expression)
    case mul(num1: Expression, num2: Expression)
    case div(num1: Expression, num2: Expression)
}

二、Optional

2.1 認識可選值

之前我們在寫代碼的過程中早就接觸過可選值,比如我們在代碼這樣定義:

class ZGTeacher {
    var age: Int?
}

當前的 age 我們就稱之為可選值,那對于 Optional 的本質是什么?我們直接跳轉到源碼,打開 Optional.swift 文件。

public enum Optional<Wrapped>: ExpressibleByNilLiteral {
  
  case none

  case some(Wrapped)
}

從源碼可以得知,Optional 的本質是枚舉,那么我們也可以仿照系統的實現制作一個自己的 Optional。
比如給定任意一個自然數,如果當前自然數是偶數返回,否則為 nil,我們應該怎么表達這個案例。

enum MyOptional<Value> {
    case none
    case some(Value)
}

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

這個時候給定一個數組,我們想刪除數組中所有的偶數

截屏2022-01-14 11.49.10.png

這個時候編譯器就會檢查我們當前的 value 會發現他的類型和系統編譯器期望的類型不符,這 個時候我們就能使用 MyOptional來限制語法的安全性。
于此同時我們通過 enum 的模式匹配來取出對應的值。

for index in array {
    let value = getOddValue(index)
    switch value {

    case .some(let v):
        array.remove(at: array.firstIndex(of: v)!)
    case .none :
        print("value not exit")
    }
}

如果我們把上述的返回值更換一下,其實就和系統的 Optional使用無疑。

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

這樣我們其實是利用當前編譯器的類型檢查來達到語法書寫層面的安全性。
當然如果每一個可選值都用模式匹配的方式來獲取值在代碼書寫上就比較繁瑣,我們還可以使 用 if let 的方式來進行可選值綁定。

case .some(let v):
        array.remove(at: array.firstIndex(of: v)!)

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

2.2 可選鏈

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

let str: String? = "abc"
let upperStr = str?.uppercased() // Optional<"ABC">
var str1: String?
let upperStr1 = str1?.uppercased() // nil

我們再來看下面這段代碼輸出什么?

let str: String? = "zhang"
let upperStr = str?.uppercased().lowercased()

同樣的可選鏈對于下標和函數調用也適用。

var closure: ((Int) -> ())?
/// closure 為 nil 不執行
closure?(1)

let dict = ["one": 1, "two": 2]
///Optional(1)
var one = dict["one"]
///nil
var three = dict["three"]
print(one, three)

2.3 ?? 運算符 (空合并運算符)

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

  • 表達式 a 必須是 Optional 類型
  • 默認值 b 的類型必須要和 a 存儲值的類型保持一致。
var q: Int? = 8
var value: Int
value = q ?? 0

print(value)

2.4 運算符重載與自定義

2.4.1 運算符重載

讀者在認識重載運算符之前,首先應該清楚重載的概念。重載的概念最初是針對函數的,對同一個函數名設置不同的參數類型以實現不同的功能被稱為函數的重載。
下面我們自定義一個圓形的類,通過重載加號運算符+來實現對圓形類實例的相加操作。
設計圓形類如下,其中有兩個屬性,分別表示圓形半徑與圓心:

class Circle {
    var center: (Double, Double)
    var radius: Double
    init(center: (Double, Double), radius: Double) {
        self.center = center
        self.radius = radius
    }
}

定義兩個Circle實例進行相加操作時應執行這樣的運算:兩個Circle實例相加返回一個新的Circle實例,并且這個新的Circle實例的圓心為兩個操作數Circle實例半徑的和,重載加法運算符如下:

func +(param1: Circle, param2: Circle) -> Circle {
    return Circle(center: param1.center, radius: param1.radius + param2.radius)
}

比如在開發中我們定義了一個二維向量,這個時候我們想對兩個向量進行基本的操作,那么我們就可以通過重載運算符來達到我們的目的。

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 u = -z
print(u)

輸出打印

Vector(x: 30, y: 50)
Vector(x: -30, y: -50)

2.4.2 自定義運算符

開發者可以自定義不存在的運算符來實現特殊的需求。
自定義運算符分為兩個步驟,首先開發者需要對要定義的運算符進行聲明。在聲明運算符的結構中,prefix的作用是聲明運算符的類型,可以使用prefix關鍵字將其聲明為前綴運算符,使用infix關鍵字將其聲明為中綴運算符,使用postfix關鍵字將其聲明為后運算符。
在進行運算符的實現時,后綴和前綴運算符只能有一個參數,參數在func關鍵字前需要表明要實現的運算符類型,而中綴運算符需要有兩個參數且func關鍵字前不需要額外標明,示例如下:

///自定義前綴運算符
prefix operator ++

///進行自定義運算符實現
prefix func ++(num: Int) -> Int {
    return num + 1
}

var a = ++5
///將返回6
print(a)

///自定義中綴運算符
infix operator ++

func ++(num1: Int, num2: Int) -> Int {
    return num1 * num1 + num2 * num2
}
var b = 5 ++ 4
///將返回41
print(b)
///自定義后綴運算符
postfix operator ++

postfix func ++(num: Int) -> Int {
    return num + num
}

var c = 5++
///將返回10
print(c)

提示
前綴運算符是指只有一個操作數且在使用運算符進行運算時,運算符需要出現在操作數的前面;
中綴運算符需要有兩個操作數,且在進行運算時運算符需要出現在兩個操作數的中間;
后綴運算符只能有一個操作數,在運算時后綴運算符需要出現在操作數的后面。

2.4.3 運算符的優先級與結合性

任何運算符都有默認的優先級,開發者自定義的運算符也是如此,優先級越高的運算符越優先執行。對于結合性而言,由于前綴運算符與后綴運算符都只有一個操作數,因此它只對中綴運算符有意義。
在重載運算符操作中,并不會改變原運算符的結合性和優先級,但對于自定義運算符,開發者可以設置其結合性與優先級,示例如下:

precedencegroup customGroup {
    higherThan: AdditionPrecedence///優先級比加法運算符的優先級高
    lowerThan: MultiplicationPrecedence///優先級比乘法運算符的優先級低
    assignment: true///設置執行可選鏈操作時的優先級
    associativity: left///定義結合性
}
///定義中綴運算符,其屬性由customGroup描述
infix operator +++: customGroup

當系統內置的優先級組不能滿足我們的要求時,即可使用precedencegroup關鍵字來自定義優先級組。

2.5 隱式解析可選類型

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

var age: Int?
var age1: Int!
age = nil
age1 = nil
截屏2022-01-14 15.55.32.png

其實日常開發中我們比較常見這種隱士解析可選類型。


截屏2022-01-14 15.56.47.png

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

2.6 與可選值有關的高階函數

  • 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: 可以把結果展平成為單個可選值
var dict = ["one": "1", "two": "2"]
let result = dict["one"].flatMap{ Int($0) } /// Optional(1)
  • 注意,這個方法是作用在Optioanl的方法,而不是作用在Sequence上的。
  • 作用在Sequence上的flatMap方法在Swift4.1中被更名為compactMap,該方法可以將序列中的nil過濾出去。
let array = ["1", "2", "3", nil]
let result = array.compactMap{ $0 } // ["1", "2", "3"]

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

2.7 元類型、AnyClass、Self (self)

  • AnyObject: 代表任意類的 instance,類的類型,僅遵守類的協議。
class ZGTeacher {
    var age = 18
}

var t = ZGTeacher()
var t1: AnyObject = t
var t2: AnyObject = ZGTeacher.self
  • T.self: T是實例對象,當前T.self返回的就是他本身;如果 T 是類,當前 T.self 返回的就是元類型。
class ZGTeacher {
    var age = 18
}

var t = ZGTeacher()

var t1 = t.self
var t2 = t.self.self
var t3 = ZGTeacher.self

lldb打印一下

(lldb) po t1
<ZGTeacher: 0x101c28fc0>

(lldb) po t2
<ZGTeacher: 0x101c28fc0>

(lldb) po t3
ZGTest.ZGTeacher
class ZGTeacher {
    var age = 18
}

var t = ZGTeacher()

var t1 = t.self

var t2 = t.self.self

var t3 = ZGTeacher.self

WX20220114-170747@2x.png

通過打印我們可以得知,當前t3存儲的是我們的metadata,也就是元類型。

  • self:
class ZGTeacher {
    var age = 18
    func test() {
        ///當前實例對象
        print(self)
    }
    static func test1() {
        ///self 是 ZGTeacher 這個類型本身
        print(self)
    }
}
  • Self: Self 類型不是特定類型,?是讓您?便地引?當前類型,??需重復或知道該類型的名稱。 在協議聲明或協議成員聲明中,Self 類型是指最終符合協議的類型。可作為?法的返回類型, 作為只讀下標的返回類型,作為只讀計算屬性的類型。
class ZGTeacher {
    static let age = 18
    func test() -> Self {
        ///當前實例對象
        return self
    }
}

class ZGPerson {
    static let age = 0
    let age1 = age
    var age2 = age
    lazy var age3 = Self.age
}

protocol MyProtocol {
    func get() -> Self
}
  • Any: 代表任意類的實例,包括 funcation 類型或者 Optional 類型。

    截屏2022-01-14 17.27.57.png

  • AnyClass: 代表任意實例的類型。

class ZGTeacher {
    var age = 18
}
var t: AnyClass = ZGTeacher.self
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。