什么是閉包?閉包運算式又是什么?
一、閉包運算式(Closure Expression)
在Swift中,可以通過func定義一個函式,也可以通過閉包運算式定義一個函式,
1.1. 閉包運算式的格式
{
(引數串列) -> 回傳值型別 in
函式體代碼
}
1.2. 閉包運算式和函式的比較
定義一個普通的函式:
func sum(_ v1: Int, _ v2: Int) -> Int { v2 + v2 }
let result = sum(10, 20)
print(result)
// 輸出:30
定義閉包:
var sum = {
(v1: Int, v2: Int) -> Int in
return v1 + v2
}
let result = sum(10, 20)
print(result)
// 輸出:30
1.3. 閉包運算式的簡寫
定義函式:
func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
print(fn(v1, v2))
}
要想使用exec函式,則必須傳入兩個Int型別的引數和一個回傳Int型別的函式,然后exec內部執行了傳入的函式,
func sum(_ a: Int, _ b: Int) -> Int {
return a + b
}
exec(v1: 10, v2: 20, fn: sum)
// 輸出:30
按照以往的知識,我們需要定義一個函式,然后把函式傳給exec就行了,其實我們也可以使用閉包運算式,
exec(v1: 10, v2: 20, fn: {
(v1: Int, v2: Int) -> Int in
return v1 + v2
})
// 輸出:30
上面的閉包運算式還可以簡寫:
1.3.1. 簡寫一
- 省略引數型別和回傳值;
- 編譯器會自動推斷閉包運算式中引數型別和回傳值型別,
exec(v1: 10, v2: 20, fn: {
v1, v2 in return v1 + v2
})
// 輸出:30
1.3.2. 簡寫二
如果函式的回傳值是一個單一運算式,可以省略return,
exec(v1: 10, v2: 20, fn: {
v1, v2 in v1 + v2
})
// 輸出:30
1.3.3. 簡寫三
如果閉包運算式不想寫引數,可以使用美元符$序號代替,序號從0開始,代表引數位置,
exec(v1: 10, v2: 20, fn: { $0 + $1 })
// 輸出:30
1.3.4. 簡寫四(不建議)
簡單的閉包運算式還可以直接使用運算子代替,
exec(v1: 10, v2: 20, fn: +)
// 輸出:30
二、尾隨閉包
2.1. 特點一(最后一個實參)
如果將一個很長的閉包運算式作為函式的最后一個實參,使用尾隨閉包可以增強函式的可讀性,
尾隨閉包是一個被書寫在函式呼叫括號外面(后面)的閉包運算式,
以呼叫上面的exec函式為例:
exec(v1: 10, v2: 20) {
$0 + $1
}
// 輸出:30
2.2. 特點二(唯一實參)
如果閉包運算式是函式的唯一實參,而且使用了尾隨閉包的語法,那就不需要在函式名后邊寫圓括號,
定義函式:
func exec(fn: (Int, Int) -> Int) {
print(fn(10, 20))
}
呼叫方式:
// 方式一:
exec(fn: { $0 + $1 })
// 方式二:
exec() { $0 + $1 }
// 方式三:
exec { $0 + $1 }
/*
輸出:
30
30
30
*/
三、閉包(Closure)
閉包和閉包運算式嚴格意義上來講并不是同一個概念,
一個函式和它所捕獲的變數/常量環境組合起來,稱為閉包,
- 一般指定義在函式內部的函式;
- 一般它捕獲的是外層函式的區域變數/常量,
示例代碼:
typealias Fn = (Int) -> Int
func getFn() -> Fn {
var num = 0
func plus(_ i: Int) -> Int {
num += 1
return num
}
return plus
}
var fn = getFn()
print(fn(1))
print(fn(2))
print(fn(3))
print(fn(4))
/*
輸出:
1
3
6
10
*/
為什么var num = 0作為區域變數還能一直累加?不是應該在函式執行完成后就被釋放了么?我們通過匯編一探究竟,
3.1. 匯編分析閉包
3.1.1. 如果內部函式沒有捕獲外部變數:


通過分析可以看到,函式回傳的是一個地址,也就是變數fn里面存放的是函式地址,
3.1.2. 如果內部函式捕獲外部變數:


匯編代碼就變得復雜一點了,并且出現了swift_allocObject關鍵字,也就意味著在堆空間申請了一塊記憶體,記憶體存放的是num的值,每次呼叫fn,訪問的num都是同一塊記憶體地址,所以才會出現區域變數也能一直累加的效果,
3.1.3. 證明swift_allocObject存放的是num:
第一步:源代碼斷點:

第二步:查看swift_allocObject回傳的地址:

第三步:查看rax地址存放的初始化值:


第四步:執行fn(1)后:

第五步:執行fn(2)后:

結論: 內部函式一旦捕獲了外部的區域變數,要想持續使用這個變數,就需要延遲變數的生命周期,所以在堆空間分配一塊記憶體來存放區域變數的值,
思考:為什么可以訪問同一塊記憶體空間?
var fn = getFn()fn占用16個位元組,前8個位元組存放回傳的函式地址(plus的封裝),后8個位元組存放堆空間(num)的地址,如果var fn2 = getFn(),fn1和fn2前8個位元組可能相同,不同的是后面的8個位元組,
3.2. 閉包和類的比較
可以把閉包想象成是一個類的實體物件,
- 記憶體在堆空間;
- 捕獲的區域變數/常量就是物件的成員(存盤屬性);
- 組成閉包的函式就是類內部定義的方法,
class Closure {
var num = 0
func plus(_ i: Int) -> Int {
num += i
return num
}
}
var cs = Closure()
print(cs.plus(1))
print(cs.plus(2))
print(cs.plus(3))
print(cs.plus(4))
/*
輸出:
1
3
6
10
*/
四、自動閉包
示例代碼:
// 如果第一個數大于0,回傳第一個數,否則回傳第二個數
func getFirst(_ v1: Int, _ v2: Int) -> Int {
return v1 > 0 ? v1 : v2
}
getFirst(10, 20) // 10
getFirst(-2, 20) // 20
getFirst(0, -4) // -4
把上面的代碼修改如下:
func getNumber() -> Int {
print("getNumber")
let a = 10
let b = 10
return a + b
}
let result1 = getFirst(10, getNumber())
print(result1)
/*
輸出:
getNumber
10
*/
let result2 = getFirst(-1, getNumber())
print(result2)
/*
輸出:
getNumber
20
*/
分析:不管第一個數是否大于0,都會執行第二個引數傳入的函式,這樣整體有點浪費(性能/空間),我們可以嘗試把函式第二個入參型別修改為函式型別,
優化代碼:
typealias VoidFunc = () -> Int
func getFirst(_ v1: Int, _ v2: VoidFunc) -> Int {
print("getFirst")
return v1 > 0 ? v1 : v2()
}
func getNumber() -> Int {
print("getNumber")
let a = 10
let b = 10
return a + b
}
getFirst(10, getNumber)
/*
輸出:
getFirst
*/
getFirst(-1, getNumber)
/*
輸出:
getFirst
getNumber
*/
結果:只有需要的時候才會執行對應的代碼,
但是,如果這樣修改后,每次都需要傳入一個函式會有點麻煩,Swift提供了自動閉包功能,可以把普通變數自動包裹成閉包,這樣就能滿足上面代碼的所有的功能了,
關鍵字: @autoclosure
用法:在函式前面加上@autoclosure關鍵字即可,
自動閉包代碼:
typealias VoidFunc = () -> Int
func getFirst(_ v1: Int, _ v2: @autoclosure VoidFunc) -> Int {
print("getFirst")
return v1 > 0 ? v1 : v2()
}
getFirst(10, 20) // 10
getFirst(-1, 10) // 10
自動閉包特點:
@autoclosure會將普通引數(例如,20)封裝成閉包{ 引數 }(例如,{ 20 });@autoclosure只支持() -> T(無參有回傳值)格式的引數;@autoclosure并非只支持最后一個引數,和位置沒有任何關系;- 有
@autoclosure、無@autoclosure,構成函式多載; - 為了避免與期望沖突,使用了有
@autoclosure的地方最好明確注釋清楚:這個值會被延遲執行(有可能不執行),
延伸: 空合并運算子??使用了@autoclosure技術,
public func ?? <t>(optional: T?, defaultValue: @autoclosure () throws -> T?) rethrows -> T?
五、應用
通過陣列的排序看下閉包運算式是如何使用的,
定義函式:
var arr = [20, 52, 19, 3, 80, 72]
3.1. 系統排序
在Swift中,Array為開發者提供了sort()排序函式,開發者可以直接使用,
arr.sort()
print(arr)
// 輸出:[3, 19, 20, 52, 72, 80]
3.2. 自定義排序
sort()是升序的,如果要降序呢?我們可以使用另外一個函式進行自定義排序,
Array提供的函式:
func sort(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows
可以看到,該函式讓傳入一個閉包運算式,使用規則如下:
- 回傳true:第一個元素排在第二個元素前面;
- 回傳false:第一個元素排在第二個元素后面,
呼叫方式一(普通函式):
func compare(i1: Int, i2: Int) -> Bool {
return i1 > i2
}
arr.sort(by: compare)
print(arr)
// 輸出:[80, 72, 52, 20, 19, 3]
呼叫方式二(閉包運算式):
arr.sort(by: {
(i1: Int, i2: Int) -> Bool in
return i1 > i2
})
arr.sort(by: { i1, i2 in return i1 > i2 })
arr.sort(by: { i1, i2 in i1 > i2 })
arr.sort(by: { $0 > $1 })
arr.sort(by: >)
arr.sort() { $0 > $1 }
arr.sort { $0 > $1 }
// 輸出:[80, 72, 52, 20, 19, 3]
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/286146.html
標籤:iOS
下一篇:有關Git基礎操作的學習
