Swift教程之閉包詳解_Swift

來源:互聯網
上載者:User

閉包(Closures)是獨立的函數代碼塊,能在代碼中傳遞及使用。Swift中的閉包與C和Objective-C中的代碼塊及其它程式設計語言中的匿名函數相似。

閉包可以在內容相關的範圍內捕獲、儲存任何被定義的常量和變數引用。因這些常量和變數的封閉性,而命名為“閉包(Closures)”。Swift能夠對所有你所能捕獲到的引用進行記憶體管理。

NOTE

假如你對“捕獲(capturing)”不熟悉,請不要擔心,具體可以參考Capturing Values(捕獲值)。

全域函數和嵌套函數已在 Functions(函數)中介紹過,實際上這些都是特殊的閉包函數
全域函數都是閉包,特點是有函數名但沒有捕獲任何值。
嵌套函數都是閉包,特點是有函數名,並且可以在它封閉的函數中捕獲值。
閉包運算式都是閉包,特點是沒有函數名,可以使用輕量的文法在它所圍繞的上下文中捕獲值。
Swift的閉包運算式有著乾淨,清晰的風格,並常見情況下對於鼓勵簡短、整潔的文法做出最佳化。這些最佳化包括:
推理參數及傳回值類型源自上下文
隱式返回源於單一運算式閉包
簡約參數名
尾隨閉包文法

1、閉包運算式

嵌套函數已經在Nested Functions(嵌套函數)中有所介紹,是種方便命名和定義自包含代碼塊的一種方式,然而,有時候在編寫簡短函數式的構造器時非常有用,它不需要完整的函式宣告及函數名,尤其是在你需要調用一個或多個參數的函數時。
閉包運算式是一種編寫內聯閉包的方式,它簡潔、緊湊。閉包運算式提供了數種語義最佳化,為的是以最簡單的形式編程而不需要大量的聲明或意圖。以下以同一個sort函數進行幾次改進,每次函數都更加簡潔,以此說明閉包運算式的最佳化。

Sort函數

Swift的標準函數庫提供了一個名為sort的函數,它通過基於輸出類型排序的閉包函數,給已知類型的數組資料的值排序。一旦完成排序工作,會返回一個同先前數組相同大小,相同資料類型,並且的新數組,並且這個數組的元素都在正確排好序的位置上。
The closure expression examples below use the sort function to sort an array of String values in reverse alphabetical order. Here's the initial array to be sorted:
以下的閉包運算式通過sort函數將String值按字母順序進行排序作說明,這是待排序的初始化數組。

複製代碼 代碼如下:

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

sort函數需要兩個參數:

一個已知實值型別的數組
一個接收兩個參數的閉包函數,這兩個參數的資料類型都同於數組元素。並且
返回一個Bool表明是否第一個參數應排在第二個參數前或後。

這個例子是一組排序的字串值,因此需要排序的封閉類型的函數(字串,字串)-> Bool。

構造排序閉包的一種方式是書寫一個符合其類型要求的普通函數:backwards,並將其傳回值作為 sort 函數的第二個參數傳入:

複製代碼 代碼如下:

func backwards(s1: String, s2: String) -> Bool {
return s1 > s2
}
var reversed = sort(names, backwards)
// reversed is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

如果backwards函數參數 s1 大於 s2,則返回true值,表示在新的數組排序中 s1 應該出現在 s2 前。 字元中的 “大於” 表示 “按照字母順序後出現”。 這意味著字母 “B” 大於字母 “A”, 字串 “Tom” 大於字串 “Tim”。 其將進行字母逆序排序,”Barry” 將會排在 “Alex” 之後,以此類推。

但這是一個相當冗長的方式,本質上只是做了一個簡單的單運算式函數 :(a > b)。 下面的例子中,我們利用閉合運算式可以相比上面的例子更效率的構造一個內聯排序閉包。

閉包運算式文法

閉合運算式文法具有以下一般構造形式:

複製代碼 代碼如下:
 
{ (parameters) -> return type in
statements
}

閉包運算式文法可以使用常量參數、變數參數和 inout 類型作為參數,但皆不可提供預設值。 如果你需要使用一個可變的參數,可將可變參數放在最後,元群組類型也可以作為參數和傳回值使用。

下面的例子展示了上面的 backwards 函數對應的閉包運算式建構函式代碼

複製代碼 代碼如下:
 
reversed = sort(names, { (s1: String, s2: String) -> Bool in
return s1 > s2
})

需要注意的是聲明內聯閉包的參數和傳回值類型與 backwards 函數型別宣告相同。 在這兩種方式中,都寫成了 (s1: String, s2: String) -> Bool類型。 然而在內聯閉包運算式中,函數和傳回值類型都寫在大括弧內,而不是大括弧外。

閉包的函數體部分由關鍵字 in 引入。 該關鍵字表示閉包的參數和傳回值類型定義已經完成,閉包函數體即將開始。

因為這個閉包的函數體非常簡約短所以完全可以將上面的backwards函數縮寫成一行連貫的代碼

複製代碼 代碼如下:

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

可以看出 sort 函數的整體調用保持不變,還是一對圓括弧包含兩個參數變成了內聯閉包形式、只不過第二個參數的值變成了。而其中一個參數現在變成了內聯閉包 (相比於 backwards 版本的代碼)。

根據上下文推斷類型

因為排序閉包是作為函數的參數進行傳入的,Swift可以推斷其參數和傳回值的類型。 sort 期望第二個參數是類型為 (String, String) -> Bool 的函數,因此實際上 String, String 和 Bool 類型並不需要作為閉包運算式定義中的一部分。 因為所有的類型都可以被正確推斷,返回箭頭 (->) 和 圍繞在參數周圍的括弧也可以被省略:

複製代碼 代碼如下:
 
reversed = sort(names, { s1, s2 in return s1 > s2 } )

實際情況下,通過構造內聯閉包運算式的閉包作為函數的參數傳遞給函數時,都可以判斷出閉包的參數和傳回值類型,這意味著您幾乎不需要利用完整格式構造任何內聯閉包。

同樣,如果你希望避免閱讀函數時可能存在的歧義, 你可以直接明確參數的類型。

這個排序函數例子,閉包的目的是很明確的,即排序被替換,而且對讀者來說可以安全的假設閉包可能會使用字串值,因為它正協助一個字串數組進行排序。

單行運算式閉包可以省略 return

單行運算式閉包可以通過隱藏 return 關鍵字來隱式返回單行運算式的結果,如上版本的例子可以改寫為:

複製代碼 代碼如下:
 
reversed = sort(names, { s1, s2 in s1 > s2 } )

在這個例子中,sort 函數的第二個參數函數類型明確了閉包必須返回一個 Bool 類型值。 因為閉包函數體只包含了一個單一運算式 (s1 > s2),該運算式返回 Bool 類型值,因此這裡沒有歧義,return關鍵字可以省略。

參數名簡寫

Swift 自動為內嵌函式提供了參數名稱簡寫功能,可以直接通過 $0,$1,$2等名字來引用閉包的參數值。

如果在閉包運算式中使用參數名稱簡寫,可以在閉包參數列表中省略對其的定義,並且對應參數名稱簡寫的類型會通過函數類型進行推斷。 in 關鍵字也同樣可以被省略,因為此時閉包運算式完全由閉包函數體構成:

複製代碼 代碼如下:
 
reversed = sort(names, { $0 > $1 } )

在這個例子中,$0 和 $1 表示閉包中第一個和第二個 String 類型的參數。

運算子函數
運算子函數實際上是一個更短的方式構造以上的運算式。

複製代碼 代碼如下:
 
reversed = sort(names, >)

更多關於運算子運算式的內容請查看Operator Functions 。

2、Trailing 閉包

如果您需要將一個很長的閉包運算式作為最後一個參數傳遞給函數,可以使用 trailing 閉包來增強函數的可讀性。

Trailing 閉包是一個書寫在函數括弧之外(之後)的閉包運算式,函數支援將其作為最後一個參數調用。

複製代碼 代碼如下:

func someFunctionThatTakesAClosure(closure: () -> ()) {
// function body goes here
}
// here's how you call this function without using a trailing closure:
someFunctionThatTakesAClosure({
// closure's body goes here
})
// here's how you call this function with a trailing closure instead:
someFunctionThatTakesAClosure() {
// trailing closure's body goes here
}

注意

如果函數只需要閉包運算式一個參數,當您使用 trailing 閉包時,您甚至可以把 () 省略掉。

在上例中作為 sort 函數參數的字串排序閉包可以改寫為:

複製代碼 代碼如下:
 
reversed = sort(names) { $0 > $1 }

當閉包非常長以至於不能在一行中進行書寫時,Trailing 閉包就變得非常有用。 舉例來說,Swift 的 Array 類型有一個 map 方法,其擷取一個閉包運算式作為其唯一參數。數組中的每一個元素調用一次該閉包函數,並返回該元素所映射的值(也可以是不同類型的值)。 具體的映射方式和傳回值類型由閉包來指定。

當提供給數組閉包函數後,map 方法將返回一個新的數組,數組中包含了與原數組一一對應的映射後的值。

下例介紹了如何在 map 方法中使用 trailing 閉包將 Int 類型數組 [16,58,510] 轉換為包含對應 String 類型的數組 ["OneSix", "FiveEight", "FiveOneZero"]:

複製代碼 代碼如下:
 
let digitNames = [
0: "Zero", 1: "One", 2: "Two", 3: "Three", 4: "Four",
5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
]
let numbers = [16, 58, 510]

上面的代碼建立了整數數字到他們的英文名字之間映射字典。 同時定義了一個準備轉換為字串的整型數組。

你現在可以通過傳遞一個 trailing 閉包給 numbers 的 map 方法來建立對應的字串版本數組。需要注意的時調用 numbers.map不需要在 map 後麵包含任何括弧,因為只需要傳遞閉包運算式這一個參數,並且該閉包運算式參數通過 trailing 方式進行撰寫:

複製代碼 代碼如下:

let strings = numbers.map {
(var number) -> String in
var output = ""
while number > 0 {
output = digitNames[number % 10]! + output
number /= 10
}
return output
}
// strings is inferred to be of type String[]
// its value is ["OneSix", "FiveEight", "FiveOneZero"]

map 在數組中為每一個元素調用了閉包運算式。您不需要指定閉包的輸入參數 number 的類型,因為可以通過要映射的數群組類型進行推斷。

閉包 number 參數被聲明為一個變數參數 (變數的具體描述請參看Constant and Variable Parameters),因此可以在閉包函數體內對其進行修改。閉包運算式制定了傳回值類型為 String,以表明儲存映射值的新數群組類型為 String。

閉包運算式在每次被調用的時候建立了一個字串並返回。其使用求餘運算子 (number % 10) 計算最後一位元字並利用digitNames 字典擷取所映射的字串。

注意:

字典 digitNames 下標後跟著一個歎號 (!),因為字典下標返回一個可選值 (optional value),表明即使該 key不存在也不會尋找失敗。 在上例中,它保證了 number % 10 可以總是作為一個 digitNames 字典的有效下標 key。 因此歎號可以用於強展開 (force-unwrap) 儲存在可選下標項中的 String 類型值。

從 digitNames 字典中擷取的字串被添加到輸出的前部,逆序建立了一個字串版本的數字。 (在運算式 number % 10中,如果number為16,則返回6,58返回8,510返回0)。

number 變數之後除以10。 因為其是整數,在計算過程中未除盡部分被忽略。 因此 16變成了1,58變成了5,510變成了51。

整個過程重複進行,直到 number /= 10 為0,這時閉包會將字串輸出,而map函數則會將字串添加到所映射的數組中。

上例中 trailing 閉包文法在函數後整潔封裝了具體的閉包功能,而不再需要將整個閉包包裹在 map 函數的括弧內。

3、擷取值

閉包可以在其定義的範圍內捕捉(引用/得到)常量和變數,閉包可以引用和修改這些值,即使定義的常量和變數已經不複存在了依然可以修改和引用。牛逼吧、

在Swift中最簡單形式是一個嵌套函數,寫在另一個函數的方法裡面。嵌套函數可以捕獲任何外部函數的參數,也可以捕獲任何常量和變數在外部函數的定義。

看下面這個例子,一個函數方法為makeIncrementor、這是一個嵌套函數,在這個函數體內嵌套了另一個函數方法:incrementor,在這個incrementor函數體內有兩個參數: runningTotal和amount,實際運作時傳進所需的兩個參數後,incrementor函數每次被調用時都會返回一個runningTotal值提供給外部的makeIncrementor使用:

複製代碼 代碼如下:

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

而函數makeincrementor的傳回型別值我們可以通過函數名後面的()-> int得知返回的是一個Int類型的值。如需想學習瞭解更多地函數傳回型別,可以參考: Function Types as Return Types.(超連結跳轉)

我們可以看見makeincrementor這個函數體內首先定義了一個整型變數:runningtotal,初始值為 0 ,而incrementor()函數最終啟動並執行出來的傳回值會賦值給這個整型變數。

makeincrementor函數()中向外部拋出了一個forIncrement參數供外部穿參進來、一旦有值進入函數體內會被函數執行個體化替代為amount,而amount會被傳遞進內嵌的incrementor函數體中與整型常量runningTotal相加得到一個新的runningTotal並返回。而我們這個主函數要返回的值是Int類型,runningTotal直接作為最終值被返回出去、makeincrementor函數()執行完畢。

makeincrementor函數()在其內部又定義了一個新的函數體incrementor,作用就是將外部傳遞過來的值amount 傳進incrementor函數中與整形常量runningTotal相加得到一個新的runningTotal,

單獨的看incrementor函數、你會發現這個函數不尋常:

複製代碼 代碼如下:

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

因為incrementor函數沒有任何的參數,但是在它的函數方法體內卻指向runningTotal和amount,顯而易見、這是incrementor函數擷取了外部函數的值amount,incrementor不能去修改它但是卻可以和體內的runningTotal相加得出新的runningTotal值返回出去。

不過,由於runningtotal每次被調用時都會相加改變一次實際值,相應地incrementor函數被調用時會去載入最新的runningtotal值,而不再是第一次初始化的0.並且需要保證每次runningTotal的值在makeIncrementor函數體內不會丟失直到函數完全載入完畢。要能確保在函數體內下一次引用時上一次的值依然還在。

注意
Swift中需要明確知道什麼時候該引用什麼時候該賦值,在incrementor函數中你不需要註解amount 和runningTotal。Swift還負責處理當函數不在需要runningTotal的時候,記憶體應該如何去管理。

這裡有一個例子makeIncrementor函數:

複製代碼 代碼如下:

let incrementByTen = makeIncrementor(forIncrement: 10)

4、參考型別閉包

在上面的例子中,incrementBySeven和incrementByTen是常量,但是這些常量在閉包的狀態下依然可以被修改。為何?很簡單,因為函數和閉包是參考型別。
當你指定一個函數或一個閉包常量/變數時、實際上是在設定該常量或變數是否為一個引用函數。在上面的例子中,它是閉合的選擇,incrementByTen指的是恒定的,而不是封閉件本身的內容。
這也意味著,如果你分配一個封閉兩種不同的常量或變數,這兩個常量或變數將引用同一個閉包:

複製代碼 代碼如下:
 
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// returns a value of 50

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.