Swift-27個關鍵字,助力開發(篇幅有點長)

SunshineBrother發表於2020-04-01

Swift-27個關鍵字,助力開發(篇幅有點長)

1、柯里化(Currying)

Swift 裡可以將方法進行柯里化 (Currying),也就是把接受多個引數的方法變換成接受第一個引數的方法,並且返回接受餘下的引數並且返回結果的新方法

func add(_ v1:Int,_ v2:Int) -> Int {
    return v1 + v2
}
print(add(1, 2))


//柯里化(Currying)
func add(_ v:Int) -> (Int) -> Int {
    return {$0 + v}
}
print(add(1)(2))

複製程式碼

2、mutating

Swift 的 protocol 不僅可以被 class型別實現,也適用於 struct 和 enum

Swift 的 mutating 關鍵字修飾方法是為了能在該方法中修改 struct 或是 enum 的變數,所以如果你沒在協議方法裡寫 ``mutating 的話,別人如果用 struct 或者 enum 來實現這個協議的話,就不能在方法裡改變自己的變數了

在使用 class 來實現帶有 mutating的方法的協議時,具體實現的前面是不需要加 mutating修飾的,因為 class 可以隨意更改自己的成員變數。所以說在協議裡用 mutating修飾方法,對於 class 的實現是完全透明,可以當作不存在的

protocol Vehicle {
   var numberOfWheels:Int{get}
   mutating func changeNumberOfWheels()
}

struct MyCar:Vehicle {
   var numberOfWheels: Int = 4
   
   mutating func changeNumberOfWheels() {
       numberOfWheels = 4
   }
}

class Cars: Vehicle {
   var numberOfWheels: Int = 0
   func changeNumberOfWheels() {
       numberOfWheels = 2
   }
}
複製程式碼

3、Sequence

Sequence 是一系列相同型別的值的集合,並且提供對這些值的迭代能力。

迭代一個Sequence最常見的方式就是 for-in 迴圈

let animals = ["Antelope", "Butterfly", "Camel", "Dolphin"]
for animal in animals {
   print(animal)
}
複製程式碼

Sequence 協議的定義

protocol Sequence {
   associatedtype Iterator: IteratorProtocol
   func makeIterator() -> Iterator
}
複製程式碼

Sequence 協議只有一個必須實現的方法 makeIterator()

makeIterator() 需要返回一個 Iterator,它是一個 IteratorProtocol 型別。

也就是說只要提供一個Iterator 就能實現一個 Sequence,那麼 Iterator 又是什麼呢?

Iterator Iterator 在 Swift 3.1 標準庫中即為 IteratorProtocol,它用來為 Sequence 提供迭代能力。對於 Sequence,我們可以用 for-in 來迭代其中的元素,其實 for-in 的背後是 IteratorProtocol 在起作用

IteratorProtocol 的定義如下:

public protocol IteratorProtocol {
   associatedtype Element
   public mutating func next() -> Self.Element?
}
複製程式碼

對於這個for...in迴圈

 let animals = ["Antelope", "Butterfly", "Camel", "Dolphin"]
for animal in animals {
   print(animal)
}
複製程式碼

實際上編譯器會把以上程式碼轉換成下面的程式碼

var animalIterator = animals.makeIterator()
while let animal = animalIterator.next() {
    print(animal)
}
複製程式碼
  • 1、獲取到 animals 陣列的 Iterator
  • 2、在一個 while 迴圈中,通過 Iterator 不斷獲取下一個元素,並對元素進行操作
  • 3、當 next() 返回 nil 時,退出迴圈

實現一個逆序

//我們先實現一個IteratorProtocol協議型別
class ReverseIterator<T>: IteratorProtocol {
   typealias Element = T
   var array: [Element]
   var currentIndex = 0

   init(array: [Element]) {
       self.array = array
       currentIndex = array.count - 1
   }
   func next() -> Element? {
       if currentIndex < 0{
           return nil
       }
       else {
           let element = array[currentIndex]
           currentIndex -= 1
           return element
       }
   }
}

// 然後我們來定義 Sequence
struct ReverseSequence<T>:Sequence {
   var array:[T]
   init (array: [T]) {
       self.array = array
   }
   typealias Iterator = ReverseIterator<T>
   func makeIterator() -> ReverseIterator<T> {
       return ReverseIterator(array: self.array)
   }
}

for item in ReverseSequence(array: animals){
   print(item)
}

複製程式碼

參考:Swift 中的 Sequence(一)

4、元組(Tuple)

元組是swift程式語言中唯一的一種複合型別,他可以將指定有限個數的任何型別一次整理為一個物件,元組中的每一種型別都可以是任何的結構體、列舉或類型別,甚至也可以是一個元組以及空元組。

比如交換輸入,普通程式設計師亙古以來可能都是這麼寫的

func swapMel1<T>(a:inout T, b:inout T) {
    let temp = a
    a = b
    b = temp
}
複製程式碼

但是要是使用多元組的話,我們可以不使用額外空間就完成交換,一下子就達到了文藝程式設計師的寫法

func swapMel2<T>(a:inout T, b:inout T) {
   (a,b) = (b,a)
}
複製程式碼

5、自動閉包(@autoclosure)

自動閉包是一種自動建立的用來把作為實際引數傳遞給函式的表示式打包的閉包。它不接受任何實際引數,並且當它被呼叫時,它會返回內部打包的表示式的值

這個語法的好處在於通過寫普通表示式代替顯式閉包而使你省略包圍函式形式引數的括號

func getFirstPositive1(_ v1:Int, _ v2:Int) -> Int {
    return v1 > 0 ? v1 : v2
}
getFirstPositive1(1, 2)


func getFirstPositive2(_ v1:Int, _ v2:() -> Int) -> Int {
    return v1 > 0 ? v1 : v2()
}
getFirstPositive2(1, 2) //這個報錯
getFirstPositive2(1, {2})

func getFirstPositive3(_ v1:Int, _ v2:@autoclosure () -> Int) -> Int {
    return v1 > 0 ? v1 : v2()
}
getFirstPositive3(1, 2)
複製程式碼
  • @autoclosure會自動的將2封裝為{2}

  • @autoclosure只支援() -> T的格式引數

??

在 Swift 中,有一個非常有用的操作符,可以用來快速地對 nil 進行條件判斷,那就是 ??。這個操作符可以判斷輸入並在當左側的值是非 nil 的 Optional 值時返回其 value,當左側是 nil 時返回右側的值

??就是一個@autoclosure

public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T) rethrows -> T
複製程式碼

我們來猜測一下??的實現

func ??<T>(optional: T?, defaultValue: @autoclosure () -> T) -> T {
   switch optional {
       case .Some(let value):
           return value
       case .None:
           return defaultValue()
       }
}
複製程式碼

可能你會有疑問,為什麼這裡要使用autoclosure,直接接受T 作為引數並返回不行麼,為何要用 () -> T 這樣的形式包裝一遍,豈不是畫蛇添足?其實這正是 autoclosure 的一個最值得稱讚的地方。如果我們直接使用T,那麼就意味著在 ?? 操作符真正取值之前,我們就必須準備好一個預設值傳入到這個方法中,一般來說這不會有很大問題,但是如果這個預設值是通過一系列複雜計算得到的話,可能會成為浪費 -- 因為其實如果optional 不是 nil 的話,我們實際上是完全沒有用到這個預設值,而會直接返回optional 解包後的值的。這樣的開銷是完全可以避免的,方法就是將預設值的計算推遲到 optional 判定為 nil 之後

在 Swift 中,其實 &&|| 這兩個操作符裡也用到了 @autoclosure

public static func && (lhs: Bool, rhs: @autoclosure () throws -> Bool) rethrows -> Bool

public static func || (lhs: Bool, rhs: @autoclosure () throws -> Bool) rethrows -> Bool
複製程式碼

6、逃逸閉包(@escaping )與非逃逸閉包(@noescaping)

逃逸閉包(@escaping )

當閉包作為一個實際引數傳遞給一個函式的時候,我們就說這個閉包逃逸了,因為它是在函式返回之後呼叫的。當你宣告一個接受閉包作為形式引數的函式時,你可以在形式引數前寫 @escaping 來明確閉包是允許逃逸的。

閉包可以逃逸的一種方法是被儲存在定義於函式外的變數裡。比如說,很多函式接收閉包實際引數來作為啟動非同步任務的回撥。函式在啟動任務後返回,但是閉包要直到任務完成——閉包需要逃逸,以便於稍後呼叫

例如:當網路請求結束後呼叫的閉包。發起請求後過了一段時間後這個閉包才執行,並不一定是在函式作用域內執行的

override func viewDidLoad() {
        super.viewDidLoad()
         
        getData { (data) in
            print("閉包返回結果:\(data)")
        }
    }

    func getData(closure:@escaping (Any) -> Void) {
        print("函式開始執行--\(Thread.current)")
        DispatchQueue.global().async {
            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()+2, execute: {
                print("執行了閉包---\(Thread.current)")
                closure("345")
            })
        }
        print("函式執行結束---\(Thread.current)")
    }
複製程式碼

Swift-27個關鍵字,助力開發(篇幅有點長)

從結果可以看出,逃逸閉包的生命週期是長於函式的。

逃逸閉包的生命週期:

  • 1、閉包作為引數傳遞給函式;
  • 2、退出函式;
  • 3、閉包被呼叫,閉包生命週期結束

即逃逸閉包的生命週期長於函式,函式退出的時候,逃逸閉包的引用仍被其他物件持有,不會在函式結束時釋放。

非逃逸閉包(@noescaping)

一個接受閉包作為引數的函式, 閉包是在這個函式結束前內被呼叫。

    override func viewDidLoad() {
        super.viewDidLoad()
         
        handleData { (data) in
            print("閉包返回結果:\(data)")
        }
    }

    func handleData(closure:(Any) -> Void) {
        print("函式開始執行--\(Thread.current)")
        print("執行了閉包---\(Thread.current)")
        closure("123")
        print("函式執行結束---\(Thread.current)")
    }
複製程式碼

Swift-27個關鍵字,助力開發(篇幅有點長)

為什麼要分逃逸閉包和非逃逸閉包

為了管理記憶體,閉包會強引用它捕獲的所有物件,比如你在閉包中訪問了當前控制器的屬性、函式,編譯器會要求你在閉包中顯示 self 的引用,這樣閉包會持有當前物件,容易導致迴圈引用。

非逃逸閉包不會產生迴圈引用,它會在函式作用域內釋放,編譯器可以保證在函式結束時閉包會釋放它捕獲的所有物件;使用非逃逸閉包的另一個好處是編譯器可以應用更多強有力的效能優化,例如,當明確了一個閉包的生命週期的話,就可以省去一些保留(retain)和釋放(release)的呼叫;此外非逃逸閉包它的上下文的記憶體可以儲存在棧上而不是堆上。

7、操作符

與 Objective-C 不同,Swift 支援過載操作符這樣的特性,最常見的使用方式可能就是定義一些簡便的計算了

系統操作符

比如我們需要一個表示二維向量的資料結構

struct Vector2D {
   var x:CGFloat = 0
   var y:CGFloat = 0
}
複製程式碼

一個很簡單的需求是兩個 Vector2D 相加

let v1 = Vector2D(x: 2.0, y: 3.0)
let v2 = Vector2D(x: 1.0, y: 4.0)
let v3 = Vector2D(x: v1.x + v2.x, y: v1.y + v2.y)  
複製程式碼

如果只做一次的話似乎還好,但是一般情況我們會進行很多這種操作。這樣的話,我們可能更願意定義一個 Vector2D 相加的操作,來讓程式碼簡化清晰

func +(left:Vector2D,right:Vector2D) -> Vector2D {
   Vector2D(x: left.x + right.x, y: left.y + right.y)
}
let v3 = v1 + v2
複製程式碼

8、自定義操作符

在Swift語言中,常見的操作符有+、-、*、/、>、<、==、&&、||等等,如果不喜歡,你也可以定義自己喜歡的操作符。

  • precedencegroup:定義操作符的優先順序
  • associativity:操作符的結合律
  • higherThanlowerThan:運算子的優先順序
  • prefix、infix、postfix:字首、中綴、字尾運算子

中綴

/// 定義優先順序組
precedencegroup MyPrecedence {
    // higherThan: AdditionPrecedence   // 優先順序,比加法運算高
    lowerThan: AdditionPrecedence       // 優先順序, 比加法運算低
    associativity: none                 // 結合方向:left, right or none
    assignment: false                   // true=賦值運算子,false=非賦值運算子
}

infix operator +++: MyPrecedence        // 繼承 MyPrecedence 優先順序組
// infix operator +++: AdditionPrecedence // 也可以直接繼承加法優先順序組(AdditionPrecedence)或其他優先順序組
func +++(left: Int, right: Int) -> Int {
    return left+right*2
}
 
print(2+++3) // 8
複製程式碼

字首

prefix operator ==+
prefix func ==+(left: Int) -> Int {
   
   return left*2
}
print(==+2) // 4
複製程式碼

字尾

postfix operator +==
postfix func +==(right: Int) -> Int {
   
   return right*3
}
print(2+==) // 6
複製程式碼

9、inout:輸入輸出引數

可變形式引數只能在函式的內部做改變。如果你想函式能夠修改一個形式引數的值,而且你想這些改變在函式結束之後依然生效,那麼就需要將形式引數定義為輸入輸出形式引數。

在形式引數定義開始的時候在前邊新增一個 inout關鍵字可以定義一個輸入輸出形式引數。輸入輸出形式引數有一個能輸入給函式的值,函式能對其進行修改,還能輸出到函式外邊替換原來的值。

你只能把變數作為輸入輸出形式引數的實際引數。你不能用常量或者字面量作為實際引數,因為常量和字面量不能修改。在將變數作為實際引數傳遞給輸入輸出形式引數的時候,直接在它前邊新增一個和符號 (&) 來明確可以被函式修改。

var b = 10
func test(a:inout Int) {
   a = 20
}
test(a: &b)
print(b) //20
複製程式碼

可以用inout定義一個輸入輸出引數:可以在函式內部修改外部實參的值

  • 1、不可變引數不能標記為inout
  • 2、inout引數不能有預設值
  • 3、inout引數只能傳入可以被多次賦值的
  • 4、inout引數的本質就是地址傳遞

10、下標

下標相信大家都很熟悉了,在絕大多數語言中使用下標來讀寫類似陣列或者是字典這樣的資料結構的做法,似乎已經是業界標準。在 Swift 中,Array 和 Dictionary 當然也實現了下標讀寫

var arr = [1,2,3]
arr[2]            // 3
arr[2] = 4        // arr = [1,2,4]

var dic = ["cat":"meow", "goat":"mie"]
dic["cat"]          // {Some "meow"}
dic["cat"] = "miao" // dic = ["cat":"miao", "goat":"mie"]  
複製程式碼

作為一門代表了先進生產力的語言,Swift 是允許我們自定義下標的,我們找到Array已經支援的下標型別

subscript (index: Int) -> T
subscript (subRange: Range<Int>) -> Slice<T>
複製程式碼

我們發現如果我們想要取出0、2、4下標值,我們需要迴圈列舉。

其實這裡有一個更好的做法,比如可以實現一個接受陣列作為下標輸入的讀取方法

extension Array {
   subscript(input: [Int]) -> ArraySlice<Element> {
       get {
           var result = ArraySlice<Element>()
           for i in input {
               assert(i < self.count, "Index out of range")
               result.append(self[i])
           }
           return result
       }
       set {
           for (index,i) in input.enumerated() {
               assert(i < self.count, "Index out of range")
               self[i] = newValue[index]
           }
       }
   }
}

var arr = ["a","b","c","d","z"]
print(arr[[0,3]]) //["a", "d"]
複製程式碼

11、巢狀函式

我們可以把函式當成引數或者變數來使用,函式內部巢狀函式

func forward(_ forward:Bool) -> (Int) -> Int {
    
    func next(_ input:Int) -> Int {
        input + 1
    }

    func previous(_ input:Int) -> Int {
        input - 1
    }
    
    return forward ? next : previous
}
複製程式碼

12、名稱空間

Objective-C 一個一直以來令人詬病的地方就是沒有名稱空間,在應用開發時,所有的程式碼和引用的靜態庫最終都會被編譯到同一個域和二進位制中。這樣的後果是一旦我們有重複的類名的話,就會導致編譯時的衝突和失敗。為了避免這種事情的發生,Objective-C 的型別一般都會加上兩到三個字母的字首,比如 Apple 保留的 NS 和 UI 字首,各個系統框架的字首 SK (StoreKit),CG (CoreGraphic) 等。Objective-C 社群的大部分開發者也遵守了這個約定,一般都會將自己名字縮寫作為字首,把類庫命名為 AFNetworking 或者 MBProgressHUD 這樣。這種做法可以解決部分問題,至少我們在直接引用不同人的庫時衝突的概率大大降低了,但是字首並不意味著不會衝突,有時候我們確實還是會遇到即使使用字首也仍然相同的情況。另外一種情況是可能你想使用的兩個不同的庫,分別在它們裡面引用了另一個相同的很流行的第三方庫,而又沒有更改名字。在你分別使用“這兩個庫中的一個時是沒有問題的,但是一旦你將這兩個庫同時加到你的專案中的話,這個大家共用的第三方庫就會和自己發生衝突了。

在 Swift 中,由於可以使用名稱空間了,即使是名字相同的型別,只要是來自不同的名稱空間的話,都是可以和平共處的。和 C# 這樣的顯式在檔案中指定名稱空間的做法不同,Swift 的名稱空間是基於 module 而不是在程式碼中顯式地指明,每個 module 代表了 Swift 中的一個名稱空間。也就是說,同一個 target 裡的型別名稱還是不能相同的。在我們進行 app 開發時,預設新增到 app 的主 target 的內容都是處於同一個名稱空間中的,我們可以通過建立 Cocoa (Touch) Framework 的 target 的方法來新建一個 module,這樣我們就可以在兩個不同的 target 中新增同樣名字的型別了

13、typealias別名

我們可以給一個複雜的難以理解的型別起一個別名,方便我們使用和理解

按照swift標準庫的定義Void就是一個空元組

public typealias Void = ()
複製程式碼

我們知道swift中沒有byte、short、Long型別,如果我們想要這樣的型別,就可以用typealias實現

typealias Byte = Int8
typealias Short = Int16
typealias Long = Int64
複製程式碼

我們還可以給函式起一個別名

typealias IntFn = (Int,Int) -> Int
func difference(v1:Int,v2:Int) -> Int {
   v1 - v2
}
let fn:IntFn = difference
print(fn(2,1))  //1
複製程式碼

我們還可以給元組起別名

typealias Date = (year:Int,month:Int,day:Int)
func test(_ date:Date) {
   print(date.year)
}
test((2019,10,30))
複製程式碼

14、associatedtype

我們在 Swift 協議中可以定義屬性和方法,並要求滿足這個協議的型別實現它們:

protocol Food { }

protocol Animal {
   func eat(_ food: Food)
}

struct Meat: Food { }
struct Grass: Food { }
複製程式碼
struct Tiger: Animal {
   func eat(_ food: Food) {

   }
}
複製程式碼

因為老虎並不吃素,所以在 Tiger 的 eat 中,我們很可能需要進行一些轉換工作才能使用 meat

associatedtype 宣告中可以使用冒號來指定型別滿足某個協議

protocol Animal {
   associatedtype F: Food
   func eat(_ food: F)
}

struct Tiger: Animal {
   func eat(_ food: Meat) {
       print("eat \(meat)")
   }
}

struct Sheep: Animal {
   func eat(_ food: Grass) {
       print("eat \(food)")
   }
} 
複製程式碼

不過在新增associatedtype 後,Animal 協議就不能被當作獨立的型別使用了。試想我們有一個函式來判斷某個動物是否危險:

func isDangerous(animal: Animal) -> Bool {
   if animal is Tiger {
       return true
   } else {
       return false
   }
}
複製程式碼

會報錯

Protocol 'Animal' can only be used as a generic constraint because it has Self or associated type requirements

這是因為 Swift 需要在編譯時確定所有型別,這裡因為 Animal 包含了一個不確定的型別,所以隨著 Animal 本身型別的變化,其中的F 將無法確定 (試想一下如果在這個函式內部呼叫 eat的情形,你將無法指定 eat 引數的型別)。在一個協議加入了像是 associatedtype 或者 Self 的約束後,它將只能被用為泛型約束,而不能作為獨立型別的佔位使用,也失去了動態派發的特性。也就是說,這種情況下,我們需要將函式改寫為泛型

func isDangerous<T: Animal>(animal: T) -> Bool {
   if animal is Tiger {
       return true
   } else {
       return false
   }
}

isDangerous(animal: Tiger()) // true
複製程式碼

15、可變引數

一個可變形式引數可以接受零或者多個特定型別的值,可變引數必須是同一型別的。當呼叫函式的時候你可以利用可變形式引數來宣告形式引數可以被傳入值的數量是可變的。可以通過在形式引數的型別名稱後邊插入三個點符號(...)來書寫可變形式引數。

func sum(_ numbers:Int...) -> Int{
   var total = 0
   for item in numbers {
       total += item
   }
   return total
}
複製程式碼

Swift自帶的print函式

/// - Parameters:
///   - items: Zero or more items to print.
///   - separator: A string to print between each item. The default is a single
///     space (`" "`).
///   - terminator: The string to print after all items have been printed. The
///     default is a newline (`"\n"`).
public func print(_ items: Any..., separator: String = " ", terminator: String = "\n")
複製程式碼
  • 1、第一個引數:就是需要列印的值,是一個可變引數
  • 2、第二個引數:兩個列印值連線的地方,預設是空格
  • 3、第三個引數:結尾預設是\n換行

16、初始化

初始化是為類、結構體或者列舉準備例項的過程。這個過需要給例項裡的每一個儲存屬性設定一個初始值並且在新例項可以使用之前執行任何其他所必須的配置或初始化

你通過定義初始化器來實現這個初始化過程,它更像是一個用來建立特定型別新例項的特殊的方法。不同於 Objective-C 的初始化器,Swift 初始化器不返回值。這些初始化器主要的角色就是確保在第一次使用之前某型別的新例項能夠正確初始化。

類有兩種初始化器

  • 1、指定初始化器(designated initializer)
  • 2、便捷初始化器(convenience initializer)
class Person {
   var age: Int
   var name: String
   
   //指定初始化器
   init(age:Int, name:String) {
       self.age = age
       self.name = name
   }
   //便捷初始化器
   convenience init(age:Int){
       self.init(age:age,name:"")
   }
}
複製程式碼
  • 1、每一個類至少有一個指定初始化器,指定初始化器是類的最主要初始化器
  • 2、預設初始化器總是類的指定初始化器
  • 3、類偏向於少量指定初始化器,一個類通常就只有一個指定初始化器

初始化器的相互呼叫規則

  • 1、指定初始化器必須從他的直系父類呼叫指定初始化器
  • 2、便捷初始化器必須從相同的類裡呼叫另一個初始化器
  • 3、便捷初始化器最終必須呼叫一個指定初始化器

17、Static & Class

Swift 中表示 “型別範圍作用域” 這一概念有兩個不同的關鍵字,它們分別是 staticclass。這兩個關鍵字確實都表達了這個意思

非class的型別上下文中,我們統一使用 static 來描述型別作用域

18、default 引數

Swift 的方法是支援預設引數的,也就是說在宣告方法時,可以給某個引數指定一個預設使用的值。在呼叫該方法時要是傳入了這個引數,則使用傳入的值,如果缺少這個輸入引數,那麼直接使用設定的預設值進行呼叫

func test(a:String = "1",b:String,c:String = "3"){}
複製程式碼

19、匹配模式

什麼是模式 模式是用於匹配的規則,比如switchcase、捕獲錯誤的catchif\guard\while\for語句

Swift中模式有

  • 1、萬用字元模式(Wildcard Pattern)
  • 2、識別符號模式(Identifier Pattern)
  • 3、值繫結模式(Value-Binding Pattern)
  • 4、元祖模式(Tuple Pattern)
  • 5、列舉Case模式(Enumeration Case Pattern)
  • 6、可選模式(Optional Pattern)
  • 7、型別轉換模式(Type-Casting Pattern)
  • 8、表示式模式(Expression Pattern)

萬用字元模式(Wildcard Pattern)

  • _ 匹配任何值
  • _?匹配非nil值
enum Life {
    case human(name:String,age:Int?)
    case animal(name:String,age:Int?)
}

func check(_ life:Life){
    switch life {
    case .human(let name,_):
        print("human:",name)
    case .animal(let name,_?):
        print("animal",name)
    default:
        print("other")
        break
    }
}

check(.human(name: "小明", age: 20)) //human: 小明
check(.human(name: "小紅", age: nil))//human: 小紅
check(.animal(name: "dog", age: 5))//animal dog
check(.animal(name: "cat", age: nil))//other
複製程式碼

識別符號模式(Identifier Pattern)

就是給對應的變數常亮賦值

let a = 10
let b = "text"
複製程式碼

值繫結模式(Value-Binding Pattern)

let point = (2,3)
switch point {
case (let x,let y):
    print("x:\(x)  y:\(y)")
}
//x:2  y:3

複製程式碼

元祖模式(Tuple Pattern)

匹配任何元祖

let point = [(0,0),(1,0),(2,0)]
for (x,_) in point{
    print(x)
}
//0
//1
//2
複製程式碼

列舉Case模式(Enumeration Case Pattern)

if case語句等價於一個caseswitch語句,簡化了一些判斷語句

let age = 2
//if
if age >= 0 && age <= 9 {
    print("[0,9]")
}
//列舉模式
if case 0...9 = age{
    print("[0,9]")
}
複製程式碼
let ages:[Int?] = [2,3,nil,5]
for case nil in ages{
    print("有nil值")
}
複製程式碼

可選模式(Optional Pattern)

let ages:[Int?] = [nil,2,3,nil]
for case let age? in ages{
    print(age)
}
// 2
//3
複製程式碼

等價於

let ages:[Int?] = [nil,2,3,nil]
for item in ages{
    if let age = item {
        print(age)
    }
}

複製程式碼

型別轉換模式(Type-Casting Pattern)

class Animal {
    func eat() {
        print(type(of: self),"eat")
    }
}

class Dog: Animal {
    func run() {
        print(type(of: self),"run")
    }
}

class Cat: Animal {
    func jump() {
        print(type(of: self),"jump")
    }
}

func check(_ animal:Animal) {
    switch animal {
    case let dog as Dog:
        dog.run()
        dog.eat()
    case is Cat:
        animal.eat()
    default:
        break
    }
}
check(Dog())
//Dog run
//Dog eat
check(Cat())
//Cat eat
複製程式碼

表示式模式(Expression Pattern)

可以通過過載運算子,自定義表示式模式的匹配規則

struct Student {
    var score = 0, name = ""
    static func ~=(pattern:Int,value:Student) -> Bool{
        value.score >= pattern
    }
    
    static func ~=(pattern:ClosedRange<Int>,value:Student) -> Bool{
        pattern.contains(value.score)
    }
    static func ~=(pattern:Range<Int>,value:Student) -> Bool{
        pattern.contains(value.score)
    }
    
}
var stu = Student(score: 81, name: "tom")
switch stu{
case 100:print(">=100")
case 90:print(">=90")
case 80..<90:print("[80,90]")
case 60...79:print("[60,79]")
default:break
}
複製程式碼
extension String{
    static func ~=(pattern:(String) -> Bool,value:String) ->Bool{
        pattern(value)
    }
}

func hasPrefix(_ prefix:String) ->((String) -> Bool){{$0.hasPrefix(prefix)}}
func hasSuffix(_ prefix:String) ->((String) -> Bool){{$0.hasSuffix(prefix)}}



var str = "jack"
switch str {
case hasPrefix("j"),hasSuffix("k"):
    print("以j開頭,或者以k結尾")
default:
    break
}
複製程式碼

20、... 和 ..<

Range 操作符,用來簡單地指定一個從 X 開始連續計數到 Y 的範圍

我們可以仔細看看 Swift 中對著兩個操作符的定義

/// Forms a closed range that contains both `minimum` and `maximum`.
func ...<Pos : ForwardIndexType>(minimum: Pos, maximum: Pos)
        -> Range<Pos>

/// Forms a closed range that contains both `start` and `end`.
/// Requres: `start <= end`
func ...<Pos : ForwardIndexType where Pos : Comparable>(start: Pos, end: Pos)
        -> Range<Pos>


/// Forms a half-open range that contains `minimum`, but not
/// `maximum`.
func ..<<Pos : ForwardIndexType>(minimum: Pos, maximum: Pos)
        -> Range<Pos>

/// Forms a half-open range that contains `start`, but not
/// `end`.  Requires: `start <= end`
func ..<<Pos : ForwardIndexType where Pos : Comparable>(start: Pos, end: Pos)
        -> Range<Pos>


/// Returns a closed interval from `start` through `end`
func ...<T : Comparable>(start: T, end: T) -> ClosedInterval<T>
  
  
 /// Returns a half-open interval from `start` to `end`
func ..<<T : Comparable>(start: T, end: T) -> HalfOpenInterval<T>  
複製程式碼

不難發現,其實這幾個方法都是支援泛型的。除了我們常用的輸入 Int 或者 Double,返回一個 Range 以外,這個操作符還有一個接受 Comparable 的輸入,並返回 ClosedInterval 或 HalfOpenInterval 的過載

比如想確認一個單詞裡的全部字元都是小寫英文字母的話,可以這麼做

let test = "helLo"
let interval = "a"..."z"
for c in test {
    if !interval.contains(String(c)) {
        print("\(c) 不是小寫字母")
    }
}

// 輸出
// L 不是小寫字母
複製程式碼

21、AnyClass,元型別和 .self

在 Swift 中能夠表示 “任意” 這個概念的除了 Any 和 AnyObject 以外,還有一個 AnyClassAnyClass 在 Swift 中被一個 typealias 所定義

public typealias AnyClass = AnyObject.Type
複製程式碼

通過 AnyObject.Type 這種方式所得到是一個元型別 (Meta)。在宣告時我們總是在型別的名稱後面加上 .Type,比如 A.Type 代表的是 A 這個型別的型別。也就是說,我們可以宣告一個元型別來儲存 A 這個型別本身,而在從A中取出其型別時,我們需要使用到 .self

其實在 Swift 中,.self 可以用在型別後面取得型別本身,也可以用在某個例項後面取得這個例項本身。前一種方法可以用來獲得一個表示該型別的值,這在某些時候會很有用;而後者因為拿到的例項本身

class A {
    class func method() {
        print("Hello")
    }
}

let typeA: A.Type = A.self
typeA.method()

// 或者
let anyClass: AnyClass = A.self
(anyClass as! A.Type).method() 
複製程式碼

也許你會問,這樣做有什麼意義呢,我們難道不是可以直接使用 A.method() 來呼叫麼?沒錯,對於單個獨立的型別來說我們完全沒有必要關心它的元型別,但是元型別或者超程式設計的概念可以變得非常靈活和強大,這在我們編寫某些框架性的程式碼時會非常方便。比如我們想要傳遞一些型別的時候,就不需要不斷地去改動程式碼了。在下面的這個例子中雖然我們是用程式碼宣告的方式獲取了 MusicViewController 和 AlbumViewController 的元型別,但是其實這一步驟完全可以通過讀入配置檔案之類的方式來完成的。而在將這些元型別存入陣列並且傳遞給別的方法來進行配置這一點上,元型別程式設計就很難被替代了

class MusicViewController: UIViewController {

}

class AlbumViewController: UIViewController {

}

let usingVCTypes: [AnyClass] = [MusicViewController.self,
    AlbumViewController.self]

func setupViewControllers(_ vcTypes: [AnyClass]) {
    for vcType in vcTypes { 
            if vcType is UIViewController.Type {
            let vc = (vcType as! UIViewController.Type).init()
            print(vc)
        }

    }
}

setupViewControllers(usingVCTypes) 
複製程式碼

這麼一來,我們完全可以搭好框架,然後用 DSL 的方式進行配置,就可以在不觸及 Swift 編碼的情況下,很簡單地完成一系列複雜操作了

22、動態型別

Swift 中我們雖然可以通過 dynamicType 來獲取一個物件的動態型別 (也就是執行時的實際型別,而非程式碼指定或編譯器看到的型別)。但是在使用中,Swift 現在卻是不支援多方法的,也就是說,不能根據物件在動態時的型別進行合適的過載方法呼叫

class Pet {}
class Dog:Pet {}
class Cat:Pet {}
 
func eat(_ pet:Pet) {
    print("pet eat")
}

func eat(_ dog:Dog) {
    print("dog eat")
}

func eat(_ cat:Cat) {
    print("cat eat")
}
 
func eats(_ pet:Pet,_ cat:Cat) {
    eat(pet)
    eat(cat)
}
eats(Dog(), Cat())
//pet eat
//cat eat
複製程式碼

我們在列印Dog型別資訊的時候,並沒有被用來在執行時選擇合適的func eat(_ dog:Dog) {}方法,而是被忽略了,並採用了編譯期間決定的Pet方法。

因為 Swift 預設情況下是不採用動態派發的,因此方法的呼叫只能在編譯時決定

要想繞過這個限制,我們可能需要進行通過對輸入型別做判斷和轉換

func eats(_ pet:Pet,_ cat:Cat) {
    if let aCat = pet as? Cat {
        eat(aCat)
    }else if let aDog = pet as? Cat{
        eat(aDog)
    }
    eat(cat)
}
複製程式碼

23、屬性

在Swift中所宣告的屬性包括

  • 1、儲存屬性:儲存屬性將會在記憶體中實際分配地址進行屬性的儲存
  • 2、計算屬性:計算屬性則不包括儲存,只是提供setget方法

儲存屬性

我們可以在儲存屬性中提供了willSetdidSet兩種屬性觀察方法

class Person {
    var age:Int = 0{
        willSet{
            print("即將將年齡從\(age)設定為\(newValue)")
        }
        didSet{
            print("已經將年齡從\(oldValue)設定為\(age)")
        }
    }
}

let p = Person()
p.age = 10
//即將將年齡從0設定為10
//已經將年齡從0設定為10

複製程式碼

willSetdidSet中我們分別可以使用newValueoldValue 來獲取將要設定的和已經設定的值。

初始化方法對屬性的設定,以及在 willSet 和 didSet 中對屬性的再次設定都不會再次觸發屬性觀察的呼叫

計算屬性

在 Swift 中所宣告的屬性包括儲存屬性計算屬性兩種。其中儲存屬性將會在記憶體中實際分配地址對屬性進行儲存,而計算屬性則不包括背後的儲存,只是提供setget 兩種方法。在同一個型別中,屬性觀察和計算屬性是不能同時共存的。也就是說,想在一個屬性定義中同時出現 setwillSetdidSet 是一件辦不到的事情。

計算屬性中我們可以通過改寫 set 中的內容來達到和willSetdidSet 同樣的屬性觀察的目的。如果我們無法改動這個類,又想要通過屬性觀察做一些事情的話,可能就需要子類化這個類,並且重寫它的屬性

重寫的屬性並不知道父類屬性的具體實現情況,而只從父類屬性中繼承名字和型別,因此在子類的過載屬性中我們是可以對父類的屬性任意地新增屬性觀察的,而不用在意父類中到底是儲存屬性還是計算屬性

class A {
    var number:Int {
        get{
            print("get")
            return 1
        }
        set{
            print("set")
        }
    }
}

class B:A {
    override var number: Int{
        willSet{
            print("willSet")
        }
        didSet{
            print("didSet")
        }
    }
}

let b = B()
b.number = 10

get
willSet
set
didSet
複製程式碼

set 和對應的屬性觀察的呼叫都在我們的預想之中。這裡要注意的是 get 首先被呼叫了一次。這是因為我們實現了 didSetdidSet 中會用到 oldValue,而這個值需要在整個 set 動作之前進行獲取並儲存待用,否則將無法確保正確性。如果我們不實現 didSet 的話,這次 get操作也將不存在。

24、lazy修飾符

延時載入或者說延時初始化是很常用的優化方法,在構建和生成新的物件的時候,記憶體分配會在執行時耗費不少時間,如果有一些物件的屬性和內容非常複雜的話,這個時間更是不可忽略。另外,有些情況下我們並不會立即用到一個物件的所有屬性,而預設情況下初始化時,那些在特定環境下不被使用的儲存屬性,也一樣要被初始化和賦值,也是一種浪費

swift提供了一個關鍵字lazy

class A {
    lazy var str:String = {
        let str = "Hello"
        print("首次訪問的時候輸出")
        return str
    }()
}

let a = A()
print(a.str)
複製程式碼

為了簡化,我們如果不需要做什麼額外工作的話,也可以對這個 lazy 的屬性直接寫賦值語句

lazy var str1 = "word"
複製程式碼

25、Reflection 和 Mirror

Objective-C 中我們不太會經常提及到 “反射” 這樣的詞語,因為 Objective-C 的執行時比一般的反射還要靈活和強大。可能很多讀者已經習以為常的像是通過字串生成類或者 selector,並且進而生成物件或者呼叫方法等,其實都是反射的具體的表現。而在 Swift 中其實就算拋開 Objective-C 的執行時的部分,在純 Swift 範疇內也存在有反射相關的一些內容,只不過相對來說功能要弱得多。

Swift提供了Mirror型別來做對映的事情

struct Person {
    let name:String
    let age:Int
}

let xiaoming = Person(name: "小明", age: 10)
let r = Mirror(reflecting: xiaoming)

print("xiaoming是\(r.displayStyle)")
print("屬性個數:\(r.children.count)")

for child in r.children{
    print("屬性名:\(child.label) 值:\(child.value)")
}


xiaoming是Optional(Swift.Mirror.DisplayStyle.struct)
屬性個數:2
屬性名:Optional("name") 值:小明
屬性名:Optional("age") 值:10
複製程式碼

通過 Mirror 初始化得到的結果中包含的元素的描述都被集合在children 屬性下

public typealias Child = (label: String?, value: Any)
public typealias Children = AnyCollection<Mirror.Type.Child> 
複製程式碼

如果覺得一個個列印太過於麻煩,我們也可以簡單地使用 dump 方法來通過獲取一個物件的映象並進行標準輸出的方式將其輸出出來

print(dump(xiaoming))

▿ 反射.Person
  - name: "小明"
  - age: 10
Person(name: "小明", age: 10)
複製程式碼

常見的應用場景是類似對 Swift 型別的物件做像 Objective-C 中 KVC 那樣的 valueForKey: 的取值。通過比較取到的屬性的名字和我們想要取得的 key 值就行了,非常簡單

func valueFrom(_ object: Any, key: String) -> Any? {
    let mirror = Mirror(reflecting: object)
    for child in mirror.children {
        let (targetKey, targetMirror) = (child.label, child.value)
        if key == targetKey {
            return targetMirror
        }
    }
    return nil
}


if let name = valueFrom(xiaoming, key: "name") as? String {
    print("通過 key 得到值: \(name)")
}

通過 key 得到值: 小明
複製程式碼

在現在的版本中,Swift 的反射特性並不是非常強大,我們只能對屬性進行讀取,還不能對其設定。 另外需要特別注意的是,雖然理論上將反射特性應用在實際的 app 製作中是可行的,但是這一套機制設計的最初目的是用於 REPL 環境和 Playground 中進行輸出的。所以我們最好遵守 Apple 的這一設定,只在 REPL 和 Playground 中用它來對一個物件進行深層次的探索,而避免將它用在 app 製作中 -- 因為你永遠不知道什麼時候它們就會失效或者被大幅改動

26、可選值(optional)

我們點選進入官方文件,可以看到optional是一個列舉

enum Optional<Wrapped> : ExpressibleByNilLiteral {
    case none
    case some(Wrapped)
    public init(_ some: Wrapped)
}
複製程式碼

我們來看看下面這兩個值是否相同anotherString literalString

var string:String? = "string"
var anotherString:String?? = string
var literalString:String?? = "string"

print(anotherString)
print(literalString)
print(anotherString == literalString)

//Optional(Optional("string"))
//Optional(Optional("string"))
//true
複製程式碼
  • 1、var anotherString:String?? = string的寫法其實就是Optional.some(string)
  • 2、var literalString:String?? = "string"的寫法是Optional.some(Optional.some("string"))

兩者的返回值是一樣的,所以兩者相等

下面兩個物件是否相等呢?

var aNil:String? = nil
var anoterNil:String?? = aNil
var literalNil:String?? = nil
print(anoterNil)
print(literalNil)
print(anoterNil == literalNil)
//Optional(nil)
//nil
//false
複製程式碼
  • 1、anoterNil是可選項裡面包含一個可選項值為nil的可選項
  • 2、literalNil是可選擇值為nil的可選項

我們可以使用fr v -R來列印具體資訊

1

27、匹配模式

什麼是模式 模式是用於匹配的規則,比如switchcase、捕獲錯誤的catchif\guard\while\for語句

Swift中模式有

  • 1、萬用字元模式(Wildcard Pattern)
  • 2、識別符號模式(Identifier Pattern)
  • 3、值繫結模式(Value-Binding Pattern)
  • 4、元祖模式(Tuple Pattern)
  • 5、列舉Case模式(Enumeration Case Pattern)
  • 6、可選模式(Optional Pattern)
  • 7、型別轉換模式(Type-Casting Pattern)
  • 8、表示式模式(Expression Pattern)

萬用字元模式(Wildcard Pattern)

  • _ 匹配任何值
  • _?匹配非nil值
enum Life {
    case human(name:String,age:Int?)
    case animal(name:String,age:Int?)
}

func check(_ life:Life){
    switch life {
    case .human(let name,_):
        print("human:",name)
    case .animal(let name,_?):
        print("animal",name)
    default:
        print("other")
        break
    }
}

check(.human(name: "小明", age: 20)) //human: 小明
check(.human(name: "小紅", age: nil))//human: 小紅
check(.animal(name: "dog", age: 5))//animal dog
check(.animal(name: "cat", age: nil))//other
複製程式碼

識別符號模式(Identifier Pattern)

就是給對應的變數常亮賦值

let a = 10
let b = "text"
複製程式碼

值繫結模式(Value-Binding Pattern)

let point = (2,3)
switch point {
case (let x,let y):
    print("x:\(x)  y:\(y)")
}
//x:2  y:3

複製程式碼

元祖模式(Tuple Pattern)

匹配任何元祖

let point = [(0,0),(1,0),(2,0)]
for (x,_) in point{
    print(x)
}
//0
//1
//2
複製程式碼

列舉Case模式(Enumeration Case Pattern)

if case語句等價於一個caseswitch語句,簡化了一些判斷語句

let age = 2
//if
if age >= 0 && age <= 9 {
    print("[0,9]")
}
//列舉模式
if case 0...9 = age{
    print("[0,9]")
}
複製程式碼
let ages:[Int?] = [2,3,nil,5]
for case nil in ages{
    print("有nil值")
}
複製程式碼

可選模式(Optional Pattern)

let ages:[Int?] = [nil,2,3,nil]
for case let age? in ages{
    print(age)
}
// 2
//3
複製程式碼

等價於

let ages:[Int?] = [nil,2,3,nil]
for item in ages{
    if let age = item {
        print(age)
    }
}

複製程式碼

型別轉換模式(Type-Casting Pattern)

class Animal {
    func eat() {
        print(type(of: self),"eat")
    }
}

class Dog: Animal {
    func run() {
        print(type(of: self),"run")
    }
}

class Cat: Animal {
    func jump() {
        print(type(of: self),"jump")
    }
}

func check(_ animal:Animal) {
    switch animal {
    case let dog as Dog:
        dog.run()
        dog.eat()
    case is Cat:
        animal.eat()
    default:
        break
    }
}
check(Dog())
//Dog run
//Dog eat
check(Cat())
//Cat eat
複製程式碼

表示式模式(Expression Pattern)

可以通過過載運算子,自定義表示式模式的匹配規則

struct Student {
    var score = 0, name = ""
    static func ~=(pattern:Int,value:Student) -> Bool{
        value.score >= pattern
    }
    
    static func ~=(pattern:ClosedRange<Int>,value:Student) -> Bool{
        pattern.contains(value.score)
    }
    static func ~=(pattern:Range<Int>,value:Student) -> Bool{
        pattern.contains(value.score)
    }
    
}
var stu = Student(score: 81, name: "tom")
switch stu{
case 100:print(">=100")
case 90:print(">=90")
case 80..<90:print("[80,90]")
case 60...79:print("[60,79]")
default:break
}
複製程式碼
extension String{
    static func ~=(pattern:(String) -> Bool,value:String) ->Bool{
        pattern(value)
    }
}

func hasPrefix(_ prefix:String) ->((String) -> Bool){{$0.hasPrefix(prefix)}}
func hasSuffix(_ prefix:String) ->((String) -> Bool){{$0.hasSuffix(prefix)}}



var str = "jack"
switch str {
case hasPrefix("j"),hasSuffix("k"):
    print("以j開頭,或者以k結尾")
default:
    break
}
複製程式碼

總結

  • 1、文章中程式碼的demo地址
  • 2、文章是讀王巍 (onevcat). “Swifter - Swift 必備 Tips (第四版)總結所得

相關文章