Swift 四、Enum & Optional

Enum & Optional.png

一、Enum

1.1 枚舉的基本用法

Swift語(yǔ)言中使用enum關(guān)鍵字來(lái)進(jìn)行枚舉的創(chuàng)建。

///創(chuàng)建一個(gè)姓氏枚舉類型
enum Surname {
    case 趙
    case 錢
    case 孫
    case 李
}

上面的代碼創(chuàng)建了一個(gè)姓氏枚舉類型,這個(gè)枚舉類型中定義了4個(gè)枚舉值,分別是趙、錢、孫、李,上面的寫法將4個(gè)枚舉值分別在4個(gè)case語(yǔ)句中定義,開(kāi)發(fā)者也可以在1個(gè)case子句中完成多個(gè)枚舉值的定義,如

///創(chuàng)建一個(gè)姓氏枚舉類型
enum Surname {
    case 趙, 錢,孫,李
}

1.2 枚舉的原始值和相關(guān)值

枚舉的原始值特性可以將枚舉值與另一種數(shù)據(jù)類型進(jìn)行綁定,相關(guān)值則可以為枚舉值關(guān)聯(lián)一些其他數(shù)據(jù)。通過(guò)相關(guān)值,開(kāi)發(fā)者可以實(shí)現(xiàn)復(fù)雜的枚舉類型

1.2.1 枚舉的原始值

swift語(yǔ)言中的枚舉支持開(kāi)發(fā)者聲明一個(gè)原始值類型,并將某個(gè)已經(jīng)存在的類型的值與枚舉值進(jìn)行綁定,枚舉指定原始值類型的語(yǔ)法與繼承的語(yǔ)法有些類似,示例代碼如下:

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

如果開(kāi)發(fā)者要指定枚舉的原始值類型為Int類型,那么可以只設(shè)置第一個(gè)枚舉值的原始值,其后的枚舉值的原始值會(huì)在第一個(gè)枚舉值原始值的基礎(chǔ)上依次遞增,示例如下:

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

通過(guò)枚舉類型中的rawValue屬性來(lái)獲取枚舉的原始值,示例如下:

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

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

隱式RawValue分配是建立在swift的類型推斷機(jī)制上的。

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

我們當(dāng)前的系統(tǒng)已經(jīng)默認(rèn)給我們的每一個(gè)枚舉值分配了一個(gè)字符串,而這個(gè)字符串其實(shí)跟我們枚舉成員值的字符串是一致的。那么它到底是怎么做到的哪,我們?cè)?strong>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

這里就是在訪問(wèn)我們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 就是我們傳進(jìn)來(lái)的當(dāng)前枚舉成員值mon,接下來(lái)就是一個(gè)模式匹配,匹配我們當(dāng)前的成員值。匹配上了swift case #DayOfWeek.mon!enumelt: bb1代碼塊。

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

那么這個(gè)字符串 "mon" 是從哪里得到的哪?這樣的字符串其實(shí)就是一個(gè)字符串常量,而字符串常量存儲(chǔ)在哪里哪,我們把Mach-o文件拖到MachoView應(yīng)用來(lái)查看一下。


image.png

在枚舉變量初始化時(shí),開(kāi)發(fā)者可以使用枚舉類型加點(diǎn)語(yǔ)法的方式,如果這個(gè)枚舉有指定的原始值,也可以通過(guò)枚舉值的原始值來(lái)完成枚舉實(shí)例的構(gòu)造,示例如下:

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

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

需要注意,通過(guò)原始值進(jìn)行枚舉實(shí)例的構(gòu)造時(shí),是有可能構(gòu)造失敗的,因?yàn)殚_(kāi)發(fā)者傳入的原始值不一定會(huì)對(duì)應(yīng)某一個(gè)枚舉值。因此,這個(gè)方法實(shí)際上返回的是一個(gè)Optional類型的可選值,如果構(gòu)造失敗,則會(huì)返回nil。

1.2.1 枚舉的相關(guān)值

在定義枚舉值的時(shí)候,開(kāi)發(fā)者可以為其設(shè)置一個(gè)參數(shù)列表,這個(gè)參數(shù)列表被稱為枚舉的相關(guān)值。示例如下:

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

在創(chuàng)建相關(guān)值枚舉的時(shí)候,開(kāi)發(fā)者需要提供參數(shù)列表中所需要的參數(shù),示例如下:

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

1.3 模式匹配

在switch-case結(jié)構(gòu)語(yǔ)句中,匹配到枚舉后,可以通過(guò)參數(shù)捕獲的方式來(lái)獲取枚舉實(shí)例的相關(guān)值,這里捕獲到的相關(guān)值參數(shù)可以在開(kāi)發(fā)者的代碼中使用,示例如下:

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

///創(chuàng)建圓形,圓心為(0, 0),半徑為3
var circle = Shape.circle(center: (0, 0), radius: 3)
///創(chuàng)建矩形,中心點(diǎn)為(1, 1), 寬度為10,高度為15
var rect = Shape.rect(center: (1, 1), width: 10, height: 15)
///創(chuàng)建三角形,頂點(diǎn)為(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個(gè)頂點(diǎn)為:\(point1),\(point2),\(point3)")
    }
}

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

1.4 枚舉的大小

接下來(lái)我們來(lái)討論一下枚舉占用的內(nèi)存大小,這里我們區(qū)分幾種不同的情況,首先第一種就是 No-payload enums 。

///UInt8
/// 最多可存儲(chǔ)256個(gè)case
enum Week {
    
    case MONDAY
    
    case TUEDAY
    
    case WEDDAY
    
    case THUDAY
    
    case FRIDAY
    
    case SATDAY
    
    case SUNDAY
    
}

print(MemoryLayout<Week>.size)

打印結(jié)果是1字節(jié)。
可以看到這里我們測(cè)試出來(lái)的不管是 size 還是 stride 都是 1 ,這個(gè)地方我們也很好理解,當(dāng)前的 enum 有幾個(gè) case ? 是不是 8 個(gè)啊,在 Swift 中進(jìn)行枚舉布局的時(shí)候一直是嘗試使用最少的空間來(lái)存儲(chǔ) enum ,對(duì)于當(dāng)前的 case 數(shù)量來(lái)說(shuō), UInt8能夠表示 256 cases ,也就意味著如果一個(gè)默認(rèn)枚舉類型且沒(méi)有關(guān)聯(lián)值的 case 少于 256 ,當(dāng)前枚舉類型的大小都是 1 字節(jié)。

image.png

通過(guò)上面的打印我們可以直觀的看到,當(dāng)前變量 a , b , c這三個(gè)變量存儲(chǔ)的內(nèi)容分別 是 00, 01, 02 這和我們上面說(shuō)的布局理解是一致的。
No-payload enums 的布局比較簡(jiǎn)單,我們也比較好理解,接下來(lái)我們來(lái)理解一下 Single- payload enums 的內(nèi)存布局, 字面意思就是只有一個(gè)負(fù)載的 enum, 比如下面這個(gè)例子:

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

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

大家猜一下當(dāng)前的這個(gè)案例, enum 在內(nèi)存中的大小是多少?

1
1

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

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

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

結(jié)果是

9
16

這里我們就產(chǎn)生了疑問(wèn)了,為什么都是單個(gè)負(fù)載,但是當(dāng)前占用的大小卻不一致?
注意, Swift 中的 enum 中的 Single-payload enums 會(huì)使用負(fù)載類型中的額外空間來(lái)記錄沒(méi)有負(fù)載的 case 值。這句話該怎么理解?首先 Bool 類型1字節(jié),也就是 UInt8 ,所以當(dāng)前能表達(dá) 256 個(gè) case的情況,對(duì)于 Bool類型來(lái)說(shuō),只需要使用低位的 0, 1 這兩種情況,其 他剩余的空間就可以用來(lái)表示沒(méi)有負(fù)載的 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

枚舉單負(fù)載.png

可以看到,不同的 case 值確實(shí)是按照我們?cè)陂_(kāi)始得出來(lái)的那個(gè)結(jié)論進(jìn)行布局的。

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
枚舉單負(fù)載Int類型.png

對(duì)于 Int 類型的負(fù)載來(lái)說(shuō),其實(shí)系統(tǒng)是沒(méi)有辦法推算當(dāng)前的負(fù)載所要使用的位數(shù),也就意味著當(dāng)前 Int 類型的負(fù)載是沒(méi)有額外的剩余空間的,這個(gè)時(shí)候我們就需要額外開(kāi)辟內(nèi)存空間來(lái)去存儲(chǔ)我們的 case 值,也就是 8 + 1 = 9 字節(jié)。

上面說(shuō)完了 Single-payload enums , 接下來(lái)我們說(shuō)第三種情況 Mutil-payload enums , 有多個(gè)負(fù)載的情況產(chǎn)生時(shí),當(dāng)前的 enum是如何進(jìn)行布局的哪?

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

上面這個(gè)例子中,我們有兩個(gè) Bool 類型的負(fù)載,這個(gè)時(shí)候我們打印當(dāng)前的 enum 大小 發(fā)現(xiàn)其大小仍然為 1,這個(gè)時(shí)候我們來(lái)看一下內(nèi)存當(dāng)中的存儲(chǔ)情況。

枚舉雙負(fù)載Bool類型.png

這里我們可以看到當(dāng)前內(nèi)存存儲(chǔ)的分別是 00 41 80 80 81 00 , 這里在存儲(chǔ)當(dāng)前的 case 時(shí)候會(huì)使用到 common spare bits,什么意思?其實(shí)在上一個(gè)案例我們也講過(guò)了,首先 bool 類型需要 1 字節(jié),也就是 8 位。

WX20220114-100716@2x.png

接下來(lái)我們來(lái)看一下 00 41 80 80 81 00 分別代表的是什么?首先 0, 4, 8 這里我們叫 做 tag value , 0, 1 這里我們就做tag index
當(dāng)前一般來(lái)說(shuō),我們有多個(gè)負(fù)載的枚舉時(shí),當(dāng)前枚舉類型的大小取決于當(dāng)前最大關(guān)聯(lián)值的大小。

1.5 遞歸枚舉

遞歸枚舉其實(shí)就是使用遞歸的方式來(lái)進(jìn)行數(shù)據(jù)描述。使用indirect關(guān)鍵字修飾的枚舉值表示這個(gè)枚舉值是可遞歸的,即此枚舉值中的相關(guān)值可以使用其枚舉類型本身。

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枚舉來(lái)描述復(fù)合表達(dá)式( (5 + 5) * 2 - 8) / 2的代碼如下:

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

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

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

我們可以為這個(gè)四則表達(dá)式枚舉類型Expression實(shí)現(xiàn)一個(gè)函數(shù)來(lái)進(jìn)行運(yùn)算,在開(kāi)發(fā)中將描述與運(yùn)算結(jié)合,能夠編寫出十分優(yōu)美的代碼。處理遞歸枚舉通常會(huì)采用遞歸函數(shù),函數(shù)方法實(shí)現(xiàn)示例如下:

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 打印結(jié)果為6
expressionFunc(num: expFinal)

關(guān)于遞歸枚舉還有一點(diǎn)需要注意,如果一個(gè)枚舉中所有的枚舉值都是可遞歸的,開(kāi)發(fā)者可以直接將整個(gè)枚舉類型聲明為可遞歸的,示例如下:

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 認(rèn)識(shí)可選值

之前我們?cè)趯懘a的過(guò)程中早就接觸過(guò)可選值,比如我們?cè)诖a這樣定義:

class ZGTeacher {
    var age: Int?
}

當(dāng)前的 age 我們就稱之為可選值,那對(duì)于 Optional 的本質(zhì)是什么?我們直接跳轉(zhuǎn)到源碼,打開(kāi) Optional.swift 文件。

public enum Optional<Wrapped>: ExpressibleByNilLiteral {
  
  case none

  case some(Wrapped)
}

從源碼可以得知,Optional 的本質(zhì)是枚舉,那么我們也可以仿照系統(tǒng)的實(shí)現(xiàn)制作一個(gè)自己的 Optional。
比如給定任意一個(gè)自然數(shù),如果當(dāng)前自然數(shù)是偶數(shù)返回,否則為 nil,我們應(yīng)該怎么表達(dá)這個(gè)案例。

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

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

這個(gè)時(shí)候給定一個(gè)數(shù)組,我們想刪除數(shù)組中所有的偶數(shù)

截屏2022-01-14 11.49.10.png

這個(gè)時(shí)候編譯器就會(huì)檢查我們當(dāng)前的 value 會(huì)發(fā)現(xiàn)他的類型和系統(tǒng)編譯器期望的類型不符,這 個(gè)時(shí)候我們就能使用 MyOptional來(lái)限制語(yǔ)法的安全性。
于此同時(shí)我們通過(guò) enum 的模式匹配來(lái)取出對(duì)應(yīng)的值。

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")
    }
}

如果我們把上述的返回值更換一下,其實(shí)就和系統(tǒng)的 Optional使用無(wú)疑。

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

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

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

除了使用 if let 來(lái)處理可選值之外,我們還可以使用 gurad let 來(lái)簡(jiǎn)化我們的代碼。
gurad letif let 剛好相反,gurad let 守護(hù)一定有值。如果沒(méi)有,直接返回。 通常判斷是否有值之后,會(huì)做具體的邏輯實(shí)現(xiàn),通常代碼多 如果用 if let t 憑空多了一層分支, gurad let 是降低分支層次的辦法。

2.2 可選鏈

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

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

我們?cè)賮?lái)看下面這段代碼輸出什么?

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

同樣的可選鏈對(duì)于下標(biāo)和函數(shù)調(diào)用也適用。

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

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

2.3 ?? 運(yùn)算符 (空合并運(yùn)算符)

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

  • 表達(dá)式 a 必須是 Optional 類型
  • 默認(rèn)值 b 的類型必須要和 a 存儲(chǔ)值的類型保持一致。
var q: Int? = 8
var value: Int
value = q ?? 0

print(value)

2.4 運(yùn)算符重載與自定義

2.4.1 運(yùn)算符重載

讀者在認(rèn)識(shí)重載運(yùn)算符之前,首先應(yīng)該清楚重載的概念。重載的概念最初是針對(duì)函數(shù)的,對(duì)同一個(gè)函數(shù)名設(shè)置不同的參數(shù)類型以實(shí)現(xiàn)不同的功能被稱為函數(shù)的重載。
下面我們自定義一個(gè)圓形的類,通過(guò)重載加號(hào)運(yùn)算符+來(lái)實(shí)現(xiàn)對(duì)圓形類實(shí)例的相加操作。
設(shè)計(jì)圓形類如下,其中有兩個(gè)屬性,分別表示圓形半徑與圓心:

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

定義兩個(gè)Circle實(shí)例進(jìn)行相加操作時(shí)應(yīng)執(zhí)行這樣的運(yùn)算:兩個(gè)Circle實(shí)例相加返回一個(gè)新的Circle實(shí)例,并且這個(gè)新的Circle實(shí)例的圓心為兩個(gè)操作數(shù)Circle實(shí)例半徑的和,重載加法運(yùn)算符如下:

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

比如在開(kāi)發(fā)中我們定義了一個(gè)二維向量,這個(gè)時(shí)候我們想對(duì)兩個(gè)向量進(jìn)行基本的操作,那么我們就可以通過(guò)重載運(yùn)算符來(lái)達(dá)到我們的目的。

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 自定義運(yùn)算符

開(kāi)發(fā)者可以自定義不存在的運(yùn)算符來(lái)實(shí)現(xiàn)特殊的需求。
自定義運(yùn)算符分為兩個(gè)步驟,首先開(kāi)發(fā)者需要對(duì)要定義的運(yùn)算符進(jìn)行聲明。在聲明運(yùn)算符的結(jié)構(gòu)中,prefix的作用是聲明運(yùn)算符的類型,可以使用prefix關(guān)鍵字將其聲明為前綴運(yùn)算符,使用infix關(guān)鍵字將其聲明為中綴運(yùn)算符,使用postfix關(guān)鍵字將其聲明為后運(yùn)算符。
在進(jìn)行運(yùn)算符的實(shí)現(xiàn)時(shí),后綴和前綴運(yùn)算符只能有一個(gè)參數(shù),參數(shù)在func關(guān)鍵字前需要表明要實(shí)現(xiàn)的運(yùn)算符類型,而中綴運(yùn)算符需要有兩個(gè)參數(shù)且func關(guān)鍵字前不需要額外標(biāo)明,示例如下:

///自定義前綴運(yùn)算符
prefix operator ++

///進(jìn)行自定義運(yùn)算符實(shí)現(xiàn)
prefix func ++(num: Int) -> Int {
    return num + 1
}

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

///自定義中綴運(yùn)算符
infix operator ++

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

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

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

提示
前綴運(yùn)算符是指只有一個(gè)操作數(shù)且在使用運(yùn)算符進(jìn)行運(yùn)算時(shí),運(yùn)算符需要出現(xiàn)在操作數(shù)的前面;
中綴運(yùn)算符需要有兩個(gè)操作數(shù),且在進(jìn)行運(yùn)算時(shí)運(yùn)算符需要出現(xiàn)在兩個(gè)操作數(shù)的中間;
后綴運(yùn)算符只能有一個(gè)操作數(shù),在運(yùn)算時(shí)后綴運(yùn)算符需要出現(xiàn)在操作數(shù)的后面。

2.4.3 運(yùn)算符的優(yōu)先級(jí)與結(jié)合性

任何運(yùn)算符都有默認(rèn)的優(yōu)先級(jí),開(kāi)發(fā)者自定義的運(yùn)算符也是如此,優(yōu)先級(jí)越高的運(yùn)算符越優(yōu)先執(zhí)行。對(duì)于結(jié)合性而言,由于前綴運(yùn)算符與后綴運(yùn)算符都只有一個(gè)操作數(shù),因此它只對(duì)中綴運(yùn)算符有意義。
在重載運(yùn)算符操作中,并不會(huì)改變?cè)\(yùn)算符的結(jié)合性和優(yōu)先級(jí),但對(duì)于自定義運(yùn)算符,開(kāi)發(fā)者可以設(shè)置其結(jié)合性與優(yōu)先級(jí),示例如下:

precedencegroup customGroup {
    higherThan: AdditionPrecedence///優(yōu)先級(jí)比加法運(yùn)算符的優(yōu)先級(jí)高
    lowerThan: MultiplicationPrecedence///優(yōu)先級(jí)比乘法運(yùn)算符的優(yōu)先級(jí)低
    assignment: true///設(shè)置執(zhí)行可選鏈操作時(shí)的優(yōu)先級(jí)
    associativity: left///定義結(jié)合性
}
///定義中綴運(yùn)算符,其屬性由customGroup描述
infix operator +++: customGroup

當(dāng)系統(tǒng)內(nèi)置的優(yōu)先級(jí)組不能滿足我們的要求時(shí),即可使用precedencegroup關(guān)鍵字來(lái)自定義優(yōu)先級(jí)組。

2.5 隱式解析可選類型

隱式解析可選類型是可選類型的一種,使用的過(guò)程中和非可選類型無(wú)異。它們之間唯一的區(qū)別是,隱式解析可選類型是你告訴Swift 編譯器,我在運(yùn)行時(shí)訪問(wèn)時(shí),值不會(huì)為 nil。

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

其實(shí)日常開(kāi)發(fā)中我們比較常見(jiàn)這種隱士解析可選類型。


截屏2022-01-14 15.56.47.png

IBOutlet類型是Xcode強(qiáng)制為可選類型的,因?yàn)樗皇窃诔跏蓟瘯r(shí)賦值的,而是在加載視圖的時(shí)候。你可以把它設(shè)置為普通可選類型,但是如果這個(gè)視圖加載正確,它是不會(huì)為空的。

2.6 與可選值有關(guān)的高階函數(shù)

  • map : 這個(gè)方法接受一個(gè)閉包,如果可選值有內(nèi)容則調(diào)用這個(gè)閉包進(jìn)行轉(zhuǎn)換。
var dict = ["one": "1", "two": "2"]
let result = dict["one"].map{ Int($0) }/// Optional(Optional(1))

上面的代碼中我們從字典中取出字符串”1”,并將其轉(zhuǎn)換為Int類型,但因?yàn)?strong>String轉(zhuǎn)換成 Int不一定能成功,所以返回的是Int?類型,而且字典通過(guò)鍵不一定能取得到值,所以map 返回的也是一個(gè)Optional,所以最后上述代碼result的類型為Int??類型。
那么如果要把我們的雙重可選展平開(kāi)來(lái),這個(gè)時(shí)候我們就需要使用到

  • flatMap: 可以把結(jié)果展平成為單個(gè)可選值
var dict = ["one": "1", "two": "2"]
let result = dict["one"].flatMap{ Int($0) } /// Optional(1)
  • 注意,這個(gè)方法是作用在Optioanl的方法,而不是作用在Sequence上的。
  • 作用在Sequence上的flatMap方法在Swift4.1中被更名為compactMap,該方法可以將序列中的nil過(guò)濾出去。
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,類的類型,僅遵守類的協(xié)議。
class ZGTeacher {
    var age = 18
}

var t = ZGTeacher()
var t1: AnyObject = t
var t2: AnyObject = ZGTeacher.self
  • T.self: T是實(shí)例對(duì)象,當(dāng)前T.self返回的就是他本身;如果 T 是類,當(dāng)前 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

通過(guò)打印我們可以得知,當(dāng)前t3存儲(chǔ)的是我們的metadata,也就是元類型。

  • self:
class ZGTeacher {
    var age = 18
    func test() {
        ///當(dāng)前實(shí)例對(duì)象
        print(self)
    }
    static func test1() {
        ///self 是 ZGTeacher 這個(gè)類型本身
        print(self)
    }
}
  • Self: Self 類型不是特定類型,?是讓您?便地引?當(dāng)前類型,??需重復(fù)或知道該類型的名稱。 在協(xié)議聲明或協(xié)議成員聲明中,Self 類型是指最終符合協(xié)議的類型。可作為?法的返回類型, 作為只讀下標(biāo)的返回類型,作為只讀計(jì)算屬性的類型。
class ZGTeacher {
    static let age = 18
    func test() -> Self {
        ///當(dāng)前實(shí)例對(duì)象
        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: 代表任意類的實(shí)例,包括 funcation 類型或者 Optional 類型。

    截屏2022-01-14 17.27.57.png

  • AnyClass: 代表任意實(shí)例的類型。

class ZGTeacher {
    var age = 18
}
var t: AnyClass = ZGTeacher.self
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容