Swift閉包

jopen 8年前發布 | 11K 次閱讀 閉包 iOS開發 Apple Swift開發

前言

閉包是自包含的功能代碼塊,可以在代碼中使用或者用來作為參數傳值。 在Swift中的閉包與C、OC中的blocks和其它編程語言(如Python)中的lambdas類似。 閉包可以捕獲和存儲上下文中定義的的任何常量和變量的引用。這就是所謂的變量和變量的自封閉,因此命名為”閉包“。

Swift還會處理所有捕獲的引用的內存管理,全局函數和嵌套函數其實就是特殊的閉包。閉包的形式主要有:

  1. 全局函數都是閉包,是特殊的閉包,有名字但不能捕獲任何值。
  2. 嵌套函數都是閉包,且有名字,也能捕獲封閉函數內的值。
  3. 閉包表達式都是無名閉包,使用輕量級語法,可以根據上下文環境捕獲值。

Swift中的閉包有很多優化的地方:

  • 根據上下文推斷參數和返回值類型
  • 從單行表達式閉包中隱式返回(也就是閉包體只有一行代碼,可以省略return)
  • 可以使用簡化參數名,如$0, $1(從0開始,表示第i個參數…)
  • 提供了尾隨閉包語法(Trailing closure syntax)

Swift版:2.1Xcode: 7.2

sort函數(Standard Library Functions)

Swift標準庫提供了名為sort的方法,會根據您提供的用于排序的閉包函數將已知類型數組中的值進行排序。一旦排序完成,sort(_:)方法會返回一個與原數組大小相同,包含同類型元素且元素已正確排序的新數組。原數組不會被sort(_:)方法修改。

下面用Swift標準庫中的sort方法來一步步簡化閉包寫法:

 
// sort函數需要兩個參數
// 參數一:數組
// 參數二:一個閉包:帶有兩個參數,這兩個參數類型與數組中的元素類型相同,返回值是Bool
varnames = ["Swift", "Arial", "Soga", "Donary"]
 

第一種方式:使用函數

 
funcbackwards(firstString: String,secondString: String) -> Bool {
    return firstString > secondString // 升序排序
}
 
// 這里第二個參數,傳了一個函數
// reversed is equal to ["Swift", "Soga", "Donary", "Arial"]
varreversed = sort(nams, backwards)
 

第二種方式:使用閉包方式

完整閉包寫法是在花括號內有參數列表和返回值,用關鍵字in表明閉包體的開始。閉包的語法為:

 
{ (parameters) -> returnTypein
    statements
}
 

其中 parameters 是參數列表, ->returnType 是返回值類型, in 是閉包體的開始。

完整閉包寫法

完整的閉包寫法是不帶任何缺省的:

 
// (firstString: String, secondString: String) 閉包參數列表
// -> Bool 指明閉包返回值類型是Bool
// in關鍵字表明閉包體的開始
reversed = sort(names, { (firstString: String, secondString: String) -> Bool in
    return firstString > secondString
})
 

簡化一

這里可以進一步簡化寫法,因為閉包代碼比較短,可以寫到一行上:

 
reversed = sort(names, { (firstString: String, secondString: String) -> Bool in return firstString > secondString})
 

簡化二

下面再進一步簡化寫法 :根據環境上下文自動推斷出類型

參數列表都沒有指明類型,也沒有指明返回值類型,這是因為swift可以根據上下文推測出:

 
// firstString和secondString的類型會是names數組元素的類型,而返回值類型會根據return語句結果得到
reversed = sort(names, { firstString, secondStringin return firstString > secondString})
 

簡化三

再進一步簡化:隱式返回(單行語句閉包)

因為閉包體只有一行代碼,可以省略return:

 
reversed = sort(names, { firstString, secondStringin firstString > secondString})
 

簡化四

再進一步簡化:使用簡化參數名($i,i=0,1,2…從0開始的)

 
// Swift會推斷出閉包需要兩個參數,類型與names數組元素相同
reversed = sort(names, { $0 > $1 }) 
 

最簡單的一種寫法:使用操作符,它是符號函數,也需要函數:

 
reversed = sort(names, >)  
 

尾隨閉包(Trailing Closures)

如果函數需要一個閉包參數作為參數,且這個參數是最后一個參數,而這個閉包表達式又很長時,使用尾隨閉包是很有用的。尾隨閉包可以放在函數參數列表外,也就是括號外。如果函數只有一個參數,那么可以把括號()省略掉,后面直接跟著閉包。

我們來看看數組提供的 map 方法,它需要一個參數,而且這個參數是一個閉包,那么它就屬于尾隨閉包了:

 
// Array的方法map()就需要一個閉包作為參數
letstrings = numbers.map { // map函數后面的()可以省略掉
  (varnumber) -> String in
  varoutput = ""
  while number > 0 {
    output = String(number % 10) + output
    number /= 10
  }
  return output
}
 

捕獲值(Capturing Values)

閉包可以根據環境上下文捕獲到定義的常量和變量。閉包可以引用和修改這些捕獲到的常量和變量,就算在原來的范圍內定義為常量或者變量已經不再存在。在Swift中閉包的最簡單形式是嵌套函數:

 
funcincrement(#amount: Int) -> (() -> Int) {
  vartotal = 0
  funcincrementAmount() -> Int {
    total += amount // total是外部函數體內的變量,這里是可以捕獲到的
    return total
  }
  return incrementAmount // 返回的是一個嵌套函數(閉包)
}  
    
// 閉包是引用類型,所以incrementByTen聲明為常量也可以修改total
letincrementByTen = increment(amount: 10) 
incrementByTen() // return 10,incrementByTen是一個閉包
 
// 這里是沒有改變對increment的引用,所以會保存之前的值
incrementByTen() // return 20  
incrementByTen() // return 30  
 
letincrementByOne = increment(amount: 1)
incrementByOne() // return 1
incrementByOne() // return 2    
incrementByTen() // return 40
incrementByOne() // return 3
 

閉包是引用類型(Closures Are Reference Types)

函數或閉包賦值給一個常量還是變量,實際上都是將常量或變量的值設置為對應函數或閉包的引用,也就是說將閉包賦值給了兩個不同的常量或變量,兩個值都會指向同一個閉包:

 
// 將函數或者閉包賦值給常量或者變量,都是對同一個函數或者閉包的引用
letalsoIncrementByTen = incrementByTen
alsoIncrementByTen()
 
// 此時,alsoIncrementByTen和anotherIncrementByTen都是同一個函數或者閉包的引用
letanotherIncrementByTen = alsoIncrementByTen
anotherIncrementByTen()
 

非逃逸閉包(Nonescaping Closures)

當一個閉包作為參數傳到一個函數中,但是這個閉包在函數返回之后才被執行,我們稱該閉包從函數中逃逸。當你定義接受閉包作為參數的函數時,在參數名之前標注 @noescape ,用來指明這個閉包是不允許“逃逸”出這個函數的。將閉包標注 @noescape 能使編譯器知道這個閉包的生命周期。

標注為 @noescape ,那么閉包只能在函數體中被執行,不能脫離函數體執行,所以編譯器可以明確知道運行時的上下文,從而可以進行一些優化。

我們看看swift標準庫函數 sort(_:) ,其定義就添加了 @noescape ,表示這個閉包不能逃逸,而只有在函數內使用:

 
publicfuncsort(@noescape isOrderedBefore: 
              (Self.Generator.Element, Self.Generator.Element) -> Bool) 
              -> [Self.Generator.Element]
 

函數前面添加了 @noescape ,是因為這個函數可以確保在排序結束之后就沒用了。

逃逸閉包(Escaping Closures)

既然有非逃逸閉包,自然也需要逃逸閉包,因為在開發中通常需要將所傳過來的閉包存儲下來,以便在真正需要回調的時候調用。

那么,所謂逃逸閉包就是使函數調用返回后,依然可以使用這個閉包。想想這么一種場景:A界面傳過來一個閉包,在B界面執行C操作時,才回調該閉包,以便A可以得到反饋。這就是所謂的反向傳值。這里傳閉包是在A界面初始化的時候,這時候閉包并不會調用,而是在B界面執行C操作時才真正地調用,因此這時候需要將閉包逃逸,才能在后續可以繼續使用。

要使閉包出了函數還可以繼續使用,那么我們可以將閉包引用賦值給外部變量。比如,A界面傳過來的閉包可以在B界面通過屬性引用A所傳過來的閉包,那么在B執行C操作時,就可以通過屬性引用A所傳過來的閉包,然后回調之。

以下只是一個例子,隨手寫出,不代表運行能通過:

 
let b = BController(callback: {
  // Do what you need to do
} 
self.navigationController?.pushViewController(b,animated: true)
 
// B界面需要存儲下來這個閉包
varcallback: (() ->Void)?
 
init(callback: () ->Void) {
  // 存儲下來
  self.callback = callback
}
 
// 執行C操作
if letblock = self.callback {
  block()
}
 
 

自動閉包(Autoclosures)

自動閉包是一種自動創建的閉包,用于包裝傳遞給函數作為參數的表達式。這種閉包不接受任何參數,當它被調用的時候,會返回被包裝在其中的表達式的值。這種便利語法讓你能夠用一個普通的表達式來代替顯式的閉包,從而省略閉包的花括號。

有聲明屬性的時候,有一種屬性叫自動計算屬性,它其實就使用了自動閉包。動閉包讓你能夠延遲求值,因為代碼段不會被執行直到你調用這個閉包。延遲求值對于那些有副作用和代價昂貴的代碼來說是很有益處的,因為你能控制代碼什么時候執行。

下面這段代碼其實就是使用了自動閉包,因為它不需要參數,其實只需要實現體:

 
funcfind(closure: () ->String) {
  print(closure())
}
 
// { "找不到啊找不到" }會被編譯器自動轉化為@autoclosure
find({ "找不到啊找不到" })
 

不過我們也可以手動聲明為 @autoclosure ,它主要用在延遲執行代碼段。過度使用 @autoclosure 會讓你的代碼變得難以理解。上下文和函數名應該能夠清晰地表明求值是被延遲執行的。

@autoclosure 特性暗含了 @noescape 特性,這個特性在非逃逸閉包一節中有描述。如果你想讓這個閉包可以“逃逸”,則應該使用 @autoclosure(escaping) 特性.

 
// customersInLine is ["Barry", "Daniella"]
// 存儲閉包的數組
varcustomerProviders: [() -> String] = []
 
// @autoclosure(escaping)來聲明自動閉包可逃逸
funccollectCustomerProviders(@autoclosure(escaping)customerProvider: () -> String) {
    customerProviders.append(customerProvider)
}
 
// 只是添加自動閉包到數組中,并沒有執行
collectCustomerProviders(customersInLine.removeAtIndex(0))
collectCustomerProviders(customersInLine.removeAtIndex(0))
 
print("Collected \(customerProviders.count) closures.")
// prints "Collected 2 closures."
 
// 這個時候才是遍歷數組來獲取閉包,并執行代碼段
forcustomerProviderin customerProviders {
    print("Now serving \(customerProvider())!")
}
// prints "Now serving Barry!"
// prints "Now serving Daniella!"
 

寫在最后

本篇博文是筆者在學習Swift 2.1的過程中記錄下來的,可能有些翻譯不到位,還請指出。另外,所有例子都是筆者練習寫的,若有不合理之處,還望指出。

學習一門語言最好的方法不是看萬遍書,而是動手操作、動手練習。如果大家喜歡,可以關注哦,盡量2-3天整理一篇Swift 2.1的文章。這里所寫的是基礎知識,如果您已經是大神,還請繞路!

關注我

如果在使用過程中遇到問題,或者想要與我交流,可加入有問必答 QQ群: 324400294

關注微信公眾號: iOSDevShares

關注新浪微博賬號:標哥Jacky

支持并捐助

如果您覺得文章對您很有幫助,希望得到您的支持。您的捐肋將會給予我最大的鼓勵,感謝您的支持!

支付寶捐助 微信捐助

來自: http://www.henishuo.com/closures-of-swift/

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!