閉包方法的學習

anchoriteFili發表於2018-07-19
相關連結

swift中使用@noescape的正確姿勢

文件閉包

閉包可以捕獲和儲存其在上下文中任意常量和變數的引用。被稱為包裹常量和變數。Swift會為你管理在捕獲過程中涉及到的所有記憶體操作。

在函式章節中介紹的全域性和巢狀函式也是特殊的閉包,閉包採取如下三種形式之一:

  • 全域性函式式一個有名字但不會捕獲任何值的閉包
  • 巢狀函式是一個有名字並可以捕獲其封閉函式域內值得閉包
  • 閉包表示式是一個利用輕量級語法所寫的可以捕獲其上下文中變數和常量值的匿名閉包

Swift的閉包表示式有用簡潔的風格,並鼓勵在常見場景中進行語法優化,主要優化如下:

  • 利用上下文推斷引數和返回值型別
  • 隱式返回單表示式閉包,即單表示式閉包可以省略return關鍵字
  • 引數名稱縮寫
  • 尾隨閉包語法

閉包表示式

sorted方法
func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}

let names = ["C","A","E","D","B"]
//        let reversedNames = names.sorted(by: {$0 > $1})
   // 兩者相同
let reversedNames = names.sorted(by: backward)
print("\(reversedNames)")
閉包表示式語法
let reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
   return s1 > s2
})

// 也可以寫一行
let reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 })
根據上下文推斷型別

因為所有的型別都可以被正確推斷,返回箭頭(->)和圍繞在引數周圍的括號也可以被省略

let reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 })
單表示式閉包隱式返回

單行表示式閉包可以通過省略return關鍵字來隱式返回單行表示式的結果

let reversedNames = names.sorted(by: { s1, s2 in s1 > s2 })
引數名稱所寫

Swift自動為內聯閉包提供了引數名稱所寫功能,你可以直接通過$0$1,$2來順序呼叫閉包的引數,依次類推,如果你在閉包表示式中使用引數名稱所寫,你可以在閉包定義中省略引數列表,並且對應引數名稱縮寫的型別會通過函式型別進行推斷。in關鍵字也同樣可以被省略,因此此時閉包表示式完全由閉包函式體構成:

let reversedNames = names.sorted(by: { $0 > $1 })
運算子方法
let reversedNames = names.sorted(by: >)
尾隨閉包法

如果你需要一個很長的閉包表示式作為最後一個引數傳遞給函式,可以使用尾隨閉包來增強函式的可讀性。尾隨閉包是一個書寫在函式括號之後的閉包表示式,函式支援將其作為最後一個引數呼叫。在使用尾隨閉包時,你不用寫出它的引數標籤。

         func someFunctionThatTakesAClosure(closure: (_ str: String) -> String) {
            // 函式體部分
            print("可以判斷是誰調取的:\(closure("給你一"))")
         }

         // 以下是不使用尾隨閉包進行函式呼叫
        someFunctionThatTakesAClosure(closure: { str in
            // 閉包主體部分
            print("返回二 \(str)")
            return "返回二"

        })

        // 以下是使用尾隨閉包進行函式呼叫
        someFunctionThatTakesAClosure() { str in

            // 閉包主體部分
            print(str)
            return "返回三"
        }

        // sorted(by:) 也可以使用這樣寫
        let reversedNames = names.sorted() { $0 > $1 }

如果閉包表示式是函式或方法的唯一引數,則當你使用尾隨閉包的使用,甚至可以把()去掉

let reversedNames = names.sorted { $0 > $1 }

當閉包非常的長以至於不能在一行中進行書寫時,尾隨閉包變的非常有用。舉例來說,Swift的 Array型別有一個 map(_:)方法,這個方法獲取一個閉包表示式作為其唯一引數。該閉包函式會為陣列中的每一個元素呼叫一次,並返回該元素所對映的值。具體的對映方式和返回值型別由閉包來指定。

當提供給陣列的閉包應用於每個陣列元素後,map(_:) 方法將返回一個新的陣列,陣列中包含了與原陣列中的元素一一對應的對映後的值

        let strings = numbers.map { (number) -> String in
            var number = number
            var output = ""

            repeat {
                output = digitNames[number % 10]! + output
                number /= 10
            } while number > 0

            return output
        }
        print(strings)

map(_:) 為陣列中每一個元素呼叫了一次閉包表示式。你不需要指定閉包的輸入引數的 number 的型別,因為可以通過要對映的資料型別進行推斷

值捕獲

注意

如果你將閉包賦值給一個類例項的屬性,並且該閉包通過訪問該例項或其成員而捕獲了該例項,你將在閉包和該例項間建立一個迴圈強引用。Swift使用捕獲列表來打破這種迴圈強引用。


   func makeIncrement(forIncrement amount: Int) -> () -> Int {
        var runningTotal = 0

        func incrementer() -> Int {

            runningTotal += amount
            return runningTotal
        }
        return incrementer
    }

如果我們單獨考慮巢狀函式 incrementer() 我們會發現它有些不同尋常:

func incrementer() -> Int {
   runningTotal += amount
   return runningTotal
}

incrementer() 函式並沒有任何引數,但是在函式體內訪問了runningTotalamount 變數。這是因為它從外圍函式捕獲了 runningTotalamount 變數的引用。捕獲引用保證了 runningTotalamount 變數在呼叫完 makeIncrementer 後不會消失,並且保證在下次執行 incrementer 函式式, runningTotal 依舊存在。

呼叫此函式方法

   func makeIncrement(forIncrement amount: Int) -> () -> Int {
        var runningTotal = 0

        func incrementer() -> Int {

            runningTotal += amount
            return runningTotal
        }
        print("runningTotal ****** \(runningTotal)")
        return incrementer
    }

    let incrementByTen = makeIncrement(forIncrement: 10)

    print(incrementByTen())
    print(incrementByTen())
    print(incrementByTen())
    print(incrementByTen())

    /*
    10
    20
    30
    40
    */

該例子定義了一個叫做 incrementByTen 的常量,該常量指向一個每次呼叫會將 runningTotal 變數增加 10incrementer 函式。如果從新建立一個 incrementer, 它會有屬於自己的引用,指向一個全新、獨立的 `runningTotal變數:

閉包是引用型別

上面的例子中,incrementBySevenincrementByTen 都是常量,但是這些常量指向的閉包仍然可以增加其不活動額變數的值。這是因為函式和閉包都是引用型別。
無論你將函式或閉包賦值給一個常量還是變數,你實際上都是講常量或變數的值設定為對應函式或閉包的引用。上面的例子中,指向閉包的引用 incrementByTen 而並非閉包內容本省。
這也意味著如果你講閉包賦值給了兩個不同的常量或變數,兩個值都會指向同一個閉包:

let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// 返回的值為50
逃逸閉包

當一個閉包作為引數傳到一個函式中,但是這個閉包在函式返回之後才被執行,我們稱該閉包從函式中逃逸。當你定義接受閉包作為引數的函式時,你可以在引數名之前標註 @escaping,用來指明這個閉包是允許“逃逸”出這個函式的。
一種能使閉包“逃逸”出函式的方法是,將這個閉包儲存在一個函式外部定義的變數中。舉個例子,很多啟動非同步操作的函式接受一個閉包引數作為completion handler。這類函式會在非同步操作開始之後立刻返回,但是閉包直到非同步操作開始之後立刻返回,但是閉包直到非同步操作結束後才會被呼叫。在這種情況下,閉包需要“逃逸”出函式,因為閉包需要在函式返回之後被呼叫。

   var completionHandlers: [() -> Void] = []
   func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
      completionHandlers.append(completionHandler)
   }

someFunctionWithEscapingClosure(_:) 函式接受一個閉包作為引數,該閉包被新增到一個函式外定義的陣列中。如果你不將這個引數標記為 @escaping,就會得到一個編譯錯誤。
將一個閉包標記為 @escaping 意味著你必須在閉包中顯式地引用 self

var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}

func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()
}


class someClass {
    var x = 10
    func doSomthing() {
        someFunctionWithEscapingClosure {
            self.x = 100
            print("調取了1")
        }

        someFunctionWithNonescapingClosure {
            x = 200
            print("調取了2")
        }
    }
}

let instance = someClass()

instance.doSomthing()
print(instance.x)

// 將函式中的閉包方法在函式外調取,相當於block異地調
completionHandlers.first?()
print(instance.x)

// 結果
調取了2
200
調取了1
100
自動閉包

自動閉包是一種自動建立的閉包,用於包裝傳遞給函式作為引數的表示式。這種閉包不接受任何引數,當它被呼叫的時候,會返回被包裝在其中的表示式的值。這種遍歷語法讓你能夠省略閉包的花括號,用一個普通的表示式來代替顯示的閉包。
自動閉包讓你能夠延遲求值,因為知道你呼叫這個閉包,程式碼段才會被執行。延遲求值對於那些有副作用(Side Effect)和高計算成本的程式碼來說是很有益處的,因為它能控制程式碼的執行時機。

var customersInLine = ["C","A","E","B","D","F"]
print(customersInLine.count)

// 此處只是一個簡單的函式賦值,並不會執行移除
let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)

print("現在,移除第一個元素 \(customerProvider())")

print(customersInLine.count)

儘管在閉包的程式碼中,customersInLin的第一個元素被移除了,不過在閉包在呼叫之前,這個元素是不會被移除的。如果這個閉包永遠不被呼叫,那麼在閉包裡面的表示式將永遠不會執行,這意味著列表中的元素永遠不會被執行。

將閉包作為引數傳遞給函式時,你能獲得同樣的延時求值行為。


// 自定義的一個自己需要的符合要求的函式式
func test() -> String {
    return "我很好"
}

// 在此處固定閉包函式型別
func serve(customer customerProvider: () -> String) {
    print("此處調取方法 \(customerProvider())")
}

// autoclosure的使用對比
func serveOne(customer customerProvider: @autoclosure () -> String ) {
    print("調取閉包方法:\(customerProvider())")
}

var customersInLine = ["C","A","E","B","D","F"]
print(customersInLine.count)

// 此處只是一個簡單的函式賦值,並不會執行移除
let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)

print("現在,移除第一個元素 \(customerProvider())")
print(customersInLine.count)

// 直接在自身處理閉包
serve { () -> String in
    // 這種是自定義
    return "你好啊"
}

// 在外部調取閉包
serve(customer: test)

// 這種是將固定型別的放進去
serve(customer: { customersInLine.remove(at: 0) })
// 加了autoclosure,相當於去掉了外部括號,不太好看出是自動閉包
serveOne(customer: customersInLine.remove(at: 0))

/**結果**/
6
6
現在,移除第一個元素 C
5
此處調取方法 你好啊
此處調取方法 我很好
此處調取方法 A
調取閉包方法:E

通過將引數標記為 @autoclosure 來接收一個自動閉包。現在你可以將該函式當做接受 String 型別引數(而非閉包)的函式來呼叫。customerProvider 引數將自動轉化為一個閉包,因為該引數被標記了 @autoclosure 特性。

// autoclosure
func serveOne(customer customerProvider: @autoclosure () -> String ) {
    print("調取閉包方法:\(customerProvider())")
}

serveOne(customer: customersInLine.remove(at: 0))

注意:

過度使用 autoclosures 會讓你的程式碼變得難以理解。上下文和函式名應該能夠清晰地表明求值是被延遲執行的。

如果你想讓一個自動閉包可以“逃逸”,則應該同時使用 @autoclosure@escaping 屬性。

var customerProviders: [() -> String] = []

func collectCustomerProviders(_ customerProveder: @autoclosure @escaping () -> String) {
    customerProviders.append(customerProveder)
}

var customersInLine = ["C","A","E","B","D","F"]

collectCustomerProviders(customersInLine.remove(at: 0))
collectCustomerProviders(customersInLine.remove(at: 0))

for customerProvider in customerProviders {

    print("自動閉包:\(customerProvider())")
}

相關文章