iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 3
0
自我挑戰組

iOS 新手開發的大小事系列 第 3

Day 3: [Swift] 閉包 (Closures) -2

  • 分享至 

  • xImage
  •  

前情提要

昨天的文章,提到了閉包的概念,以及閉包的使用可以讓程式變得更加有彈性,接著介紹尾隨閉包,是另一種表達閉包的方式,一開始不太熟悉這些語法,導致在閱讀程式碼時會有障礙,藉著紀錄也加深自己對這些用法的了解。今天是介紹閉包的其他特性和種類:

  • 捕獲值
  • 逃逸閉包
  • 自動閉包

捕獲值 (Capturing Values)

閉包可以從其定義的上下文中捕獲 (capture) 常數及變數,可以在閉包內參考或修改這些常數及變數,即使原本定義這些常數和變數的區域已經不存在。
在 Swift 中,巢狀函式是最簡單形式可以捕獲值的閉包。一個巢狀函式可以捕獲任何其外部的函式參數,也可以捕獲定義在巢狀函式外部的任何常數及變數。

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    
    return incrementer
}

上述程式中可以看到,巢狀函式內部存取了的 runningTotal 變數及 amount 參數,是因為它從外部函式捕獲了這兩個變數的參考。而這個捕獲參考會讓 runningTotalamount 在呼叫完 makeIncrementer 函式後不會消失,並且下次呼叫incrementer 函式時,runningTotal 仍會存在。

以下即是例子:

let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30

由上述例子可以知道呼叫三次相同函數,會不停地做累加。
以下在重新傳入不同參數後再重新呼叫同一參數,捕獲值仍存在,並再次累加:

let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// returns a value of 7

incrementByTen()
// returns a value of 40

逃逸閉包 (Escaping Closures)

當閉包為傳入函式的參數,而此參數在函式外部必須被使用的時候,就得使用逃逸閉包。

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

以上程式碼如果不加 @escaping 會發生錯誤,因為有 @escaping ,在函式 someFunctionWithEscapingClosure 執行結束後,其閉包參數仍會被外部的參數 completionHandlers 所使用,所以就必須使用逃逸閉包將閉包傳出調用。


自動閉包 (Autoclosure)

自動閉包 (autoclosure) 是一個自動創建的閉包,用於包裝成表達式作為參數傳遞給函數。它不接受任何參數,當它被調用時,它返回包含在其中的表達式的值。這種語法方便使我們可以通過編寫普通表達式省略函數參數周圍的大括號而不是顯式閉包。

自動閉包允許延遲評估,因為在我們調用閉包之前,內部代碼不會運行。延遲評估對於具有副作用或計算成本高昂的代碼非常有用,因為它可以讓我們控制何時評估該代碼。下面的代碼顯示了如何延遲評估。

var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// Prints "5"

let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// Prints "5"

print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// Prints "4"

雖然 customersInLine 陣列中的第一個元素已在必包內被移除了,但如果從未執行這段代碼,則陣列實際並不會移除元素。
特別注意一點,customerProvider 的型別不是字串,而是沒有參數的函式並回傳字串。

當我們將閉包當作參數傳入函式中,我們一樣會得到相同的延遲評估行為。以下為範例:

// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// Prints "Now serving Alex!"

serve(customer:) 函式使用一個明確回傳顧客名稱的閉包。以下例子使用 @autoclosure,現在我們可以呼叫這個函數如同其參數為一個字串,而非一個閉包。而參數會自動地轉換成閉包,因為 customerProvider 已標註 @autoclosure 屬性。
可比較上下例子的不同之處:

// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// Prints "Now serving Ewa!"

如果我們同時希望自動閉包能逃逸,則需同時標註 @autoclosure@escaping 屬性。

// customersInLine is ["Barry", "Daniella"]
var customerProviders: [() -> String] = []
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
    customerProviders.append(customerProvider)
}
collectCustomerProviders(customersInLine.remove(at: 0))
collectCustomerProviders(customersInLine.remove(at: 0))

print("Collected \(customerProviders.count) closures.")
// Prints "Collected 2 closures."
for customerProvider in customerProviders {
    print("Now serving \(customerProvider())!")
}
// Prints "Now serving Barry!"
// Prints "Now serving Daniella!"

以上的代碼,並非使用閉包當作參數傳入 customerProvider 函式 collectCustomerProviders(_:) 將閉包附加到 customerProvider 陣列中,而此陣列是宣告在函數之外,代表此閉包可在函式回傳後被呼叫,所以需使用 @escaping,才能允許 customerProvider 的參數逃逸到函式的範圍外。


上一篇
Day 2: [Swift] 閉包 (Closures) -1
下一篇
Day 4: [Swift] 列舉 (Enumerations) -1
系列文
iOS 新手開發的大小事30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言