當(dāng)編寫代碼在兩個數(shù)字之間進行插值時闹获,很容易默認(rèn)為線性插值。然而,在兩個值之間平穩(wěn)過渡通常會更好雹顺。所以我的建議是避免步進,并使用函數(shù)(如smooterstep()
)進行插值:
func smootherStep(value: CGFloat) -> CGFloat {
let x = value < 0 ? 0 : value > 1 ? 1 : value
return ((x) * (x) * (x) * ((x) * ((x) * 6 - 15) + 10))
}
—— Simon Gladman (@flexmonkey), Swift 版 Core Image 的作者
可變參數(shù)函數(shù)(Variadic functions)
可變參數(shù)函數(shù)是具有不確定性的函數(shù)芬位,這是一種奇特的說法无拗,就是說它們接受的參數(shù)和發(fā)送的參數(shù)一樣多。在一些基本函數(shù)(甚至print()
)中都使用了這種方法昧碉,以使代碼編寫更簡單、更安全揽惹。
讓我們使用print()
被饿,因為它是一個你很熟悉的函數(shù)。你習(xí)慣看到這樣的代碼:
print("I'm Commander Shepard and this is my favorite book")
但是print()
是一個可變參數(shù)函數(shù)搪搏,這意味著您可以傳遞任意數(shù)量的要打印的內(nèi)容:
print(1, 2, 3, 4, 5, 6)
這將與為每個數(shù)字調(diào)用一次print()
產(chǎn)生不同的輸出:使用一次性調(diào)用將在一行中打印所有數(shù)字狭握,而使用多次調(diào)用將逐行打印數(shù)字。
一旦添加了可選的額外參數(shù):separator
和terminator
疯溺,print()
的可變參數(shù)特性將變得更加有用论颅。第一個參數(shù)在傳遞的每個值之間放置一個字符串,第二個參數(shù)在打印完所有值后放置一個字符串囱嫩。例如恃疯,這將打印 “1、2墨闲、3今妄、4、5鸳碧、6 ! ”:
print(1, 2, 3, 4, 5, 6, separator: ", ", terminator: "!")
這就是如何調(diào)用可變參數(shù)函數(shù)《芰郏現(xiàn)在我們來談?wù)勅绾沃谱魉鼈儯艺J(rèn)為你會發(fā)現(xiàn)這在 Swift 中相當(dāng)巧妙瞻离。
考慮下面的代碼:
func add(numbers: [Int]) -> Int {
var total = 0
for number in numbers {
total += number
}
return total
}
add(numbers: [1, 2, 3, 4, 5])
該函數(shù)接受一個整數(shù)數(shù)組腾仅,然后將每個數(shù)字相加,得到一個總數(shù)套利。有更有效的方法可以做到這一點推励,但這不是本章的重點!
要使該函數(shù)擁有可變參數(shù),即它接受任何數(shù)量的單個整數(shù)而不是單個數(shù)組日裙,需要進行兩次更改吹艇。首先,我們需要將參數(shù)寫成Int...
昂拂,而不是寫成[int]
受神。其次,我們不需要這樣調(diào)用add(numbers: [1, 2, 3, 4, 5])
格侯,而是應(yīng)該這樣調(diào)用add(numbers: 1, 2, 3, 4, 5)
鼻听。
就是這樣财著。最后的代碼是這樣的:
func add(numbers: Int...) -> Int {
var total = 0
for number in numbers {
total += number
}
return total
}
add(numbers: 1, 2, 3, 4, 5)
你可以將可變參數(shù)放在函數(shù)的參數(shù)列表中的任何位置,但是每個函數(shù)只能有一個可變參數(shù)撑碴。
操作符重載(Operator overloading)
這是一個人們既愛又恨的話題撑教。操作符重載是實現(xiàn)你自己的操作符甚至調(diào)整現(xiàn)有操作符(如+
或 *
)的能力。
使用操作符重載的主要原因是它提供了非常清晰醉拓、自然和富有表現(xiàn)力的代碼伟姐。你已經(jīng)理解了 5 + 5 = 10 ,因為你了解基礎(chǔ)數(shù)學(xué)亿卤,所以允許 myShoppingList + yourShoppingList 是一個邏輯擴展愤兵,即將兩個自定義結(jié)構(gòu)相加。
操作符重載有幾個缺點排吴。首先秆乳,它的含義可能是不透明的:如果我說henrytheeight + AnneBoleyn
,結(jié)果是一對幸福的夫婦(暫時!)钻哩、一個未來伊麗莎白女王( Queen Elizabeth )形狀的嬰兒屹堰,還是某個四肢相連的人類?
其次,它沒有做任何方法不能做的事情:HenryTheEighth.marry(AnneBoleyn)
也會有同樣的結(jié)果街氢,而且明顯更清晰扯键。第三,它隱藏了復(fù)雜性:5 + 5 是一個微不足道的操作阳仔,但是 Person + Person 可能涉及到安排一個儀式忧陪、找到一件婚紗等等。
第四近范,可能也是最嚴(yán)重的嘶摊,操作符重載可能會產(chǎn)生意想不到的結(jié)果,特別是因為你可以不受懲罰地調(diào)整現(xiàn)有的操作符评矩。
基礎(chǔ)操作符(The basics of operators)
為了演示操作符重載是多么令人困惑叶堆,我想先給出一個重載==
操作符的基本示例〕舛牛考慮以下代碼:
if 4 == 4 {
print("Match!")
} else {
print("No match!")
}
就像你想得那樣虱颗,它會打印 “Match! ” 因為 4 總是等于 4。還是……蔗喂?
進入操作符重載忘渔。只需三行代碼,我們就可以對幾乎所有應(yīng)用程序造成嚴(yán)重?fù)p害:
func ==(lhs: Int, rhs: Int) -> Bool {
return false
}
if 4 == 4 {
print("Match!")
} else {
print("No match!")
}
當(dāng)代碼運行時缰儿,它將輸出 “ No match ! ”畦粮,因為我們重載了==
操作符,所以它總是返回false
。正如你所看到的宣赔,函數(shù)的名稱是操作符本身预麸,即func ==
,所以你要修改的內(nèi)容非常清楚儒将。你還可以看到吏祸,這個函數(shù)期望接收兩個整數(shù)(左邊和右邊分別是lhs
和rhs
),并返回一個布爾值钩蚊,該值報告這兩個數(shù)字是否相等贡翘。
除了完成實際工作的函數(shù)外,操作符還具有優(yōu)先級和關(guān)聯(lián)性两疚,這兩者都會影響操作的結(jié)果床估。當(dāng)多個運算符一起使用而沒有括號時,Swift 首先使用優(yōu)先級最高的運算符——你可能學(xué)習(xí)過PEMDAS(括號诱渤、指數(shù)、乘除谈况、加減)勺美、BODMAS 或類似的運算符,取決于你在哪里上學(xué)碑韵。如果僅憑優(yōu)先級不足以決定操作的順序赡茸,則使用結(jié)合律。
Swift 允許你控制優(yōu)先級和關(guān)聯(lián)性∽N牛現(xiàn)在讓我們嘗試一個實驗:下面操作的結(jié)果是什么占卧?
let i = 5 * 10 + 1
根據(jù) PEMDAS ,應(yīng)該首先執(zhí)行乘法(5 * 10 = 50 )联喘,然后執(zhí)行加法(50 + 1 = 51 )华蜒,因此結(jié)果是 51 。這個優(yōu)先級被直接寫入了 Swift ——以下是來自 Swift 標(biāo)準(zhǔn)庫的確切代碼:
precedencegroup AdditionPrecedence {
associativity: left
higherThan: RangeFormationPrecedence
}
precedencegroup MultiplicationPrecedence {
associativity: left
higherThan: AdditionPrecedence
}
infix operator * : MultiplicationPrecedence
infix operator + : AdditionPrecedence
infix operator - : AdditionPrecedence
這將聲明兩個操作符優(yōu)先組豁遭,然后聲明 *
叭喜、+
和 -
操作符位于這些組中。你可以看到蓖谢,MultiplicationPrecedence
被標(biāo)記為高于AdditionPrecedence
捂蕴,這就是 *
在 +
之前被計算的原因。
這三個操作符被稱為中綴操作符闪幽,因為它們被放在兩個操作數(shù)中啥辨,即 5 + 5 ,而不是像!
這樣的前綴操作符盯腌,例如:!loggedIn
溉知。
Swift 允許我們通過將現(xiàn)有操作符分配給新的組來重新定義它們的優(yōu)先級。如果需要,可以創(chuàng)建自己的優(yōu)先組着倾,或者重用現(xiàn)有的優(yōu)先組拾酝。
在上面的代碼中,你可以看到順序是乘法優(yōu)先級(用于*
卡者、/
蒿囤、%
和更多),然后是加法優(yōu)先級(用于+
崇决、-
材诽、|
和更多),然后是范圍優(yōu)先級(用于...
和 ..<
恒傻。)
在我們的小算術(shù)中脸侥,我們可以通過像這樣重寫*
運算符來引起各種奇怪的行為:
infix operator * : RangeFormationPrecedence
這就重新定義了*
的優(yōu)先級比+
低,這意味著這段代碼現(xiàn)在將返回55
:
let i = 5 * 10 + 1
這是與之前相同的代碼行盈厘,但現(xiàn)在將執(zhí)行加法(10 + 1 = 11)睁枕,然后乘法(5 * 11) 得到 55。
當(dāng)兩個操作符具有相同的優(yōu)先級時沸手,就會發(fā)揮結(jié)合律的作用外遇。例如,考慮以下問題:
let i = 10 - 5 - 1
再看看 Swift 自己的代碼是如何聲明 AdditionPrecedence 組的契吉,-
運算符屬于這個組:
precedencegroup AdditionPrecedence {
associativity: left
higherThan: RangeFormationPrecedence
}
如你所見跳仿,它被定義為具有左結(jié)合性,這意味著 10 - 5 - 1 被執(zhí)行為 (10 - 5) - 1捐晶,而不是 10 - (5 - 1)菲语。
這種差別很細(xì)微,但很重要:除非我們改變它惑灵,否則 10 - 5 - 1 將得到 4 山上。當(dāng)然,如果你想造成一點破壞泣棋,你可以這樣做:
precedencegroup AdditionPrecedence {
associativity: right
higherThan: RangeFormationPrecedence
}
infix operator - : AdditionPrecedence
let i = 10 - 5 - 1
這將修改現(xiàn)有的加法優(yōu)先組胶哲,然后隨著改變被更新,式子將被解釋為 10 - (5 - 1)潭辈,即結(jié)果等于 6鸯屿。
添加到現(xiàn)有操作符(Adding to an existing operator)
現(xiàn)在你已經(jīng)了解了操作符的工作原理,讓我們修改*
操作符把敢,使它可以像這樣對整數(shù)數(shù)組進行乘法操作:
let result = [1, 2, 3] * [1, 2, 3]
完成之后寄摆,將返回一個包含[1,4,9]
的新數(shù)組,即1x1
, 2x2
和3x3
修赞。
*
操作符已經(jīng)存在婶恼,所以我們不需要聲明它桑阶。相反,我們只需要創(chuàng)建一個新的func *
勾邦,它接受我們的新數(shù)據(jù)類型蚣录。這個函數(shù)將創(chuàng)建一個新數(shù)組,該數(shù)組由所提供的兩個數(shù)組中的每一項相乘組成眷篇。這是代碼:
func *(lhs: [Int], rhs: [Int]) -> [Int] {
guard lhs.count == rhs.count else { return lhs }
var result = [Int]()
for (index, int) in lhs.enumerated() {
result.append(int * rhs[index])
}
return result
}
注意萎河,我在開頭添加了一個guard
,以確保兩個數(shù)組包含相同數(shù)量的項蕉饼。
因為*
操作符已經(jīng)存在虐杯,所以重要的是lhs
和rhs
參數(shù),它們都是整數(shù)數(shù)組:當(dāng)兩個整數(shù)數(shù)組相乘時昧港,這些參數(shù)確保選擇這個新函數(shù)擎椰。
添加一個新的操作符(Adding a new operator)
當(dāng)你添加一個新的操作符時,你需要提供足夠的 Swift 信息來使用它创肥。至少需要指定新操作符的位置(前綴达舒、后綴或中綴),但如果不指定優(yōu)先級或關(guān)聯(lián)性 Swift 將提供默認(rèn)值叹侄,使其成為低優(yōu)先級休弃、非關(guān)聯(lián)操作符。
讓我們添加一個新的操作符**
圈膏,它返回一個值的冪。也就是說篙骡,2 ** 4 應(yīng)該等于 2 * 2 * 2 * 2 稽坤,即 16。我們將使用pow()
函數(shù)糯俗,所以你需要導(dǎo)入Foundation
框架:
import Foundation
一旦完成尿褪,我們需要告訴 Swift **
將是一個中綴操作符,因為我們將在其左側(cè)有一個操作數(shù)得湘,在其右側(cè)有另一個操作數(shù):
infix operator **
它沒有指定優(yōu)先級或關(guān)聯(lián)杖玲,因此將使用默認(rèn)值。
最后淘正,新的**
函數(shù)本身摆马。我已經(jīng)讓它接受雙精度值以獲得最大的靈活性,Swift
足夠聰明鸿吆,當(dāng)與這個操作符一起使用時囤采,可以推斷2
和4
是雙精度值:
func **(lhs: Double, rhs: Double) -> Double {
return pow(lhs, rhs)
}
如你所見,由于pow()
惩淳,函數(shù)本身非常簡單蕉毯。自己試試:
let result = 2 ** 4
到目前為止,一切順利。然而代虾,像這樣的表達(dá)是行不通的:
let result = 4 ** 3 ** 2
事實上进肯,甚至像這樣的東西也不會奏效:
let result = 2 ** 3 + 2
這是因為我們使用的是默認(rèn)優(yōu)先級和結(jié)合性。為了解決這個問題棉磨,我們需要決定與其他操作符相比**
應(yīng)該排在什么位置江掩,為此,你可以返回到 PEMDAS (它是 E !)含蓉,或者查看其他語言的功能频敛。例如,Haskell 把它放在乘法和除法之前馅扣,在PEMDAS 之后斟赚。Haskell還聲明冪運算右結(jié)合性,這意味著 4 ** 3 ** 2 將被解析為 *4 *(3 ** 2) 差油。
我們可以使我們自己的**
操作符的行為相同的方式拗军,修改其聲明如下:
precedencegroup ExponentiationPrecedence {
higherThan: MultiplicationPrecedence
associativity: right
}
infix operator **: ExponentiationPrecedence
有了這個更改,你現(xiàn)在可以在同一個表達(dá)式中使用**
兩次蓄喇,還可以將它與其他操作符組合使用——這樣做會更好!
修改現(xiàn)有的操作符(Modifying an existing operator)
現(xiàn)在來看一些更復(fù)雜的東西:修改現(xiàn)有的操作符发侵。我選擇了一個稍微復(fù)雜一點的例子,因為如果你能看到我在這里解決它妆偏,我希望它能幫助你解決你自己的操作符重載問題刃鳄。
我要修改的運算符是...
,它已經(jīng)作為閉區(qū)間運算符存在钱骂。所以叔锐,你可以寫1...10
,然后得到覆蓋 1 到10 的范圍 见秽。在默認(rèn)情況下這是一個中綴操作符愉烙,范圍的低端在左側(cè),高端在右側(cè)解取,但我要修改它步责,以便它還接受左側(cè)的范圍和右側(cè)的另一個整數(shù),如下所示:
let range = 1...10...1
當(dāng)代碼運行時禀苦,它將返回一個數(shù)組蔓肯,其中包含數(shù)字1、2伦忠、3省核、4、5昆码、6气忠、6邻储、7、8旧噪、9吨娜、10、9淘钟、8宦赠、7、6米母、5勾扭、4、3铁瞒、2妙色、1
——它先遞增再遞減。這是可能的慧耍,因為運算符出現(xiàn)了兩次:第一次它將看到1...10
身辨,這是一個閉合范圍運算符,第二次它將看到CountableClosedRange<Int>...1
芍碧,這將是我們的新操作煌珊。在此函數(shù)中,CountableClosedRange<Int>
是左側(cè)操作數(shù)泌豆,而Int 1
是右側(cè)操作數(shù)定庵。
新...
函數(shù)需要做兩件事:
- 計算一個新的區(qū)間,從右邊的整數(shù)到左邊區(qū)間的最高點踪危,然后反轉(zhuǎn)這個區(qū)間洗贰。
- 將左邊的區(qū)間追加到新創(chuàng)建的遞減區(qū)間,并作為函數(shù)的結(jié)果返回該區(qū)間陨倡。
在代碼中,它看起來是這樣的:
func ...(lhs: CountableClosedRange<Int>, rhs: Int) -> [Int] {
let downwards = (rhs ..< lhs.upperBound).reversed()
return Array(lhs) + downwards
}
如果你嘗試使用該代碼许布,你將看到它無法工作—至少目前還不能兴革。要知道為什么,看看Swift對...
操作符的定義:
infix operator ... : RangeFormationPrecedence
precedencegroup RangeFormationPrecedence {
higherThan: CastingPrecedence
}
現(xiàn)在再來看看我們的代碼:
let range = 1...10...1
你可以看到我們用到了...
操作符兩次蜜唾,這意味著 Swift 需要知道我們想要(1...10)...1
還是1...(10...1)
杂曲。正如你在上面看到的贡茅,Swift 的定義的...
操作符沒有提到它的結(jié)合律屿良,所以 Swift 不知道在這種情況下該怎么做。所以刀崖,就目前情況來看颖榜,我們的新操作符只能處理這樣的代碼:
let range = (1...10)...1
如果我們想要相同的行為而不需要用戶添加括號棚饵,我們需要告訴 Swift ...
操作符有左結(jié)合性煤裙,像這樣:
precedencegroup RangeFormationPrecedence {
associativity: left
higherThan: CastingPrecedence
}
infix operator ... : RangeFormationPrecedence
就是這樣:現(xiàn)在代碼在沒有括號的情況下可以正常工作,并且我們有了一個有用的新操作符噪漾。不要忘記硼砰,在 Playground ,你的代碼順序很重要——你的最終代碼應(yīng)該是這樣的:
precedencegroup RangeFormationPrecedence {
associativity: left
higherThan: CastingPrecedence
}
infix operator ... : RangeFormationPrecedence
func ...(lhs: CountableClosedRange<Int>, rhs: Int) -> [Int] {
let downwards = (rhs ..< lhs.upperBound).reversed()
return Array(lhs) + downwards
}
let range = 1...10...1
print(range)
閉包(Closures)
和元組一樣欣硼,閉包在 Swift 中是特有的:全局函數(shù)是閉包题翰,嵌套函數(shù)是閉包,sort()
和map()
等函數(shù)方法接受閉包诈胜,惰性屬性使用閉包豹障,這只是冰山一角。在你的 Swift 開發(fā)職業(yè)生涯中焦匈,你將需要使用閉包血公,如果你想晉升到高級開發(fā)職位,那么你也需要輕松地創(chuàng)建閉包括授。
我知道有些人對閉包有不同尋常的理解坞笙,所以讓我們從一個簡單的定義開始:閉包是一段代碼,可以像變量一樣傳遞和存儲荚虚,它還能夠捕獲它使用的任何值薛夜。這種捕獲確實使閉包難以理解,所以我們稍后再討論它版述。
創(chuàng)建簡單的閉包(Creating simple closures)
讓我們創(chuàng)建一個簡單的閉包來讓事情運行起來:
let greetPerson = {
print("Hello there!")
}
它創(chuàng)建一個名為greetPerson
的閉包梯澜,然后可以像函數(shù)一樣使用:
greetPerson()
因為閉包是第一類數(shù)據(jù)類型——也就是說,就像整數(shù)渴析、字符串和其他類型一樣——所以你可以復(fù)制它們并將它們用作其他函數(shù)的參數(shù)晚伙。以下是實際復(fù)制:
let greetCopy = greetPerson
greetCopy()
復(fù)制閉包時,請記住閉包是引用類型——這兩個“副本”實際上指向同一個共享閉包俭茧。
要將閉包作為參數(shù)傳遞給函數(shù)咆疗,請指定閉包自己的參數(shù)列表并將返回值作為其數(shù)據(jù)類型。也就是說母债,你不需要編寫param: String
午磁,而是編寫類似param: () -> Void
這樣的東西來接受沒有參數(shù)且沒有返回值的閉包。是的毡们,-> Void
是必需的迅皇,否則param:()
將意味著一個空元組。
如果我們想將greetPerson
閉包傳遞給一個函數(shù)并在那里調(diào)用它衙熔,我們將使用如下代碼:
func runSomeClosure(_ closure: () -> Void) {
closure()
}
runSomeClosure(greetPerson)
為什么需要閉包登颓?在那個例子中不是,但是如果我們想在 5 秒后調(diào)用閉包呢红氯?或者我們只是想偶爾調(diào)用它框咙?或者是否滿足某些條件咕痛?這就是閉包變得有用的地方:它們是一些功能,你的應(yīng)用程序可以將它們存儲起來扁耐,以便以后需要時使用暇检。
閉包開始變得混亂的地方是當(dāng)它們接受自己的參數(shù)時,部分原因是它們的參數(shù)列表放在一個不尋常的位置婉称,還因為這些閉包的類型語法可能看起來非晨槠停混亂!
首先:如何使閉包接受參數(shù)。要做到這一點王暗,請在閉包的括號內(nèi)寫入?yún)?shù)列表悔据,然后輸入關(guān)鍵字in
:
let greetPerson = { (name: String) in
print("Hello, \(name)!")
}
greetPerson("Taylor")
如果需要,還可以在這里指定捕獲列表俗壹。這是最常用的科汗,以避免self
引用循環(huán),通過使它unowned
绷雏,像這樣的:
let greetPerson = { (name: String) [unowned self] in
print("Hello, \(name)!")
}
greetPerson("Taylor")
現(xiàn)在头滔,討論如何使用閉包將參數(shù)傳遞給函數(shù)。這很復(fù)雜涎显,有兩個原因:1)它可能看起來像一個冒號和括號的海洋坤检,2)調(diào)用約定根據(jù)你做的事情而變化。
讓我們回到runSomeClosure()
函數(shù)期吓。為了讓它接受一個參數(shù)——一個本身接受一個參數(shù)的閉包——我們需要這樣定義它:
func runSomeClosure(_ closure: (String) -> Void)
閉包是一個函數(shù)早歇,它接受一個字符串,但什么也不返回讨勤。這是一個新的功能:
let greetPerson = { (name: String) in
print("Hello, \(name)!")
}
func runSomeClosure(_ closure: (String) -> Void) {
closure("Taylor")
}
runSomeClosure(greetPerson)
閉包捕獲(Closure capturing)
我已經(jīng)討論了閉包是如何作為引用類型的箭跳,它對捕獲的值有巧妙的含義:當(dāng)兩個變量指向同一個閉包時,它們都使用相同的捕獲數(shù)據(jù)潭千。
讓我們從基礎(chǔ)開始:當(dāng)一個閉包引用一個值時谱姓,它需要確保該值在運行閉包時仍然存在。這看起來像是閉包在復(fù)制數(shù)據(jù)刨晴,但實際上它比這更微妙逝段。這個過程稱為捕獲,它允許閉包引用和修改它引用的值割捅,即使原始值不再存在。
區(qū)別很重要:如果閉包復(fù)制了它的值帚桩,那么就會應(yīng)用值類型語義亿驾,并且閉包內(nèi)的值類型的任何更改都將發(fā)生在一個惟一的副本上,不會影響原來的調(diào)用方账嚎。相反莫瞬,閉包捕獲數(shù)據(jù)儡蔓。
我知道這一切聽起來都是假設(shè),所以讓我給你一個實際的例子:
func testCapture() -> () -> Void {
var counter = 0
return {
counter += 1
print("Counter is now \(counter)")
}
}
let greetPerson = testCapture()
greetPerson()
greetPerson()
greetPerson()
let greetCopy = greetPerson
greetCopy()
greetPerson()
greetCopy()
這段代碼聲明了一個名為testCapture()
的函數(shù)疼邀,該函數(shù)的返回值為()-> Void
喂江,即它返回一個不接受任何參數(shù)且什么也不返回的函數(shù)。在testCapture()
中旁振,我創(chuàng)建了一個名為counter
的新變量获询,初始值為0
。但是拐袜,函數(shù)內(nèi)的變量沒有發(fā)生任何變化吉嚣。相反,它返回一個閉包蹬铺,該閉包將counter
加 1 并打印出它的新值尝哆。它不調(diào)用那個閉包,它只返回它甜攀。
有趣的地方是函數(shù)之后:greetPerson
被設(shè)置為testCapture()
返回的函數(shù)秋泄,它被調(diào)用了三次。該閉包引用了在testCapture()
中創(chuàng)建的counter
值规阀,現(xiàn)在顯然超出了范圍恒序,因為該函數(shù)已經(jīng)完成。因此姥敛,Swift 捕捉到了這個值:這個閉包現(xiàn)在有了自己對counter
的獨立引用奸焙,可以在調(diào)用它時使用。每次調(diào)用greetPerson()
函數(shù)時彤敛,你將看到counter
加 1 与帆。
讓事情變得加倍有趣的是greetCopy
。這就是我所說的閉包是引用墨榄,并且使用相同的捕獲數(shù)據(jù)玄糟。當(dāng)調(diào)用greetCopy()
時,它將增加與greetPerson
相同的counter
值袄秩,因為它們都指向相同的捕獲數(shù)據(jù)阵翎。這意味著在一次又一次地調(diào)用閉包時counter
值將從 1 增加到 6。這個怪癖我已經(jīng)講過兩次了之剧,所以如果它傷害了你的大腦郭卫,不要擔(dān)心:它不會再被覆蓋了!
閉包簡寫語法(Closure shorthand syntax)
在討論更高級的內(nèi)容之前,我想快速地全面介紹一下閉包簡寫語法背稼,這樣我們就完全處于同一種思路贰军。當(dāng)你把一個內(nèi)聯(lián)閉包傳遞給一個函數(shù)時,Swift 有幾種技術(shù)蟹肘,所以你不需要寫太多的代碼词疼。
為了給你提供一個好例子俯树,我將使用數(shù)組的filter()
方法,它接受一個帶有一個字符串參數(shù)的閉包贰盗,如果該字符串應(yīng)該在一個新的數(shù)組中许饿,則返回true
。下面的代碼過濾一個數(shù)組舵盈,這樣我們就得到了一個新的數(shù)組陋率,每個人的名字都以Michael
開頭:
let names = ["Michael Jackson", "Taylor Swift", "Michael Caine", "Adele Adkins", "Michael Jordan"]
let result1 = names.filter({ (name: String) -> Bool in
if name.hasPrefix("Michael") {
return true
} else {
return false
}
})
print(result1.count)
從中可以看出filter()
希望接收一個閉包,該閉包接受一個名為name
的字符串參數(shù)书释,并返回true
或false
翘贮。然后閉包檢查名稱是否具有前綴 “ Michael ” 并返回一個值。
Swift 知道傳遞給filter()
的閉包必須接受一個字符串并返回一個布爾值爆惧,所以我們可以刪除它狸页,只使用一個變量的名稱,該變量將用于對每個條目進行過濾:
let result2 = names.filter({ name in
if name.hasPrefix("Michael") {
return true
} else {
return false
}
})
接下來扯再,我們可以直接返回hasPrefix()
的結(jié)果芍耘,如下:
let result3 = names.filter({ name in
return name.hasPrefix("Michael")
})
尾隨閉包允許我們刪除一組括號,這總是受歡迎的:
let result4 = names.filter { name in
return name.hasPrefix("Michael")
}
因為我們的閉包只有一個表達(dá)式——即現(xiàn)在我們已經(jīng)刪除了很多代碼熄阻,它只做一件事——我們甚至不再需要return
關(guān)鍵字斋竞。Swift 知道我們的閉包必須返回一個布爾值,因為我們只有一行代碼秃殉,Swift 知道它必須是返回值的那一行坝初。代碼現(xiàn)在看起來是這樣的:
let result4 = names.filter { name in
return name.hasPrefix("Michael")
}
許多人在此止步,理由很充分:下一步開始可能會相當(dāng)混亂钾军。你看鳄袍,當(dāng)這個閉包被調(diào)用時,Swift 會自動創(chuàng)建匿名參數(shù)名吏恭,這些匿名參數(shù)名由一個美元符號和一個從 0 開始計數(shù)的數(shù)字組成拗小。$0, $1, $2,以此類推樱哼。你不允許在自己的代碼中使用這樣的名稱哀九,所以這些名稱很容易脫穎而出!
這些簡寫參數(shù)名映射到閉包接受的參數(shù)。在本例中搅幅,這意味著name
可用為$0
阅束。不能混合顯式參數(shù)和匿名參數(shù):要么聲明入?yún)⒘斜恚词褂?$0 系列茄唐。這兩者做的是完全一樣的:
let result6 = names.filter { name in
name.hasPrefix("Michael")
}
let result7 = names.filter {
$0.hasPrefix("Michael")
}
注意到在使用匿名時必須刪除name in
部分嗎息裸?是的,這意味著更少的輸入,但同時你也放棄了一點可讀性界牡。我喜歡在我自己的代碼中使用簡寫名稱,但是只有在需要時才應(yīng)該使用它們漾抬。
如果你選擇使用簡寫名稱宿亡,通常會將整個方法調(diào)用放在一行上,如下所示:
let result8 = names.filter { $0.hasPrefix("Michael") }
當(dāng)你將其與原始閉包的大小進行比較時纳令,你必須承認(rèn)這是一個很大的改進!
函數(shù)作為閉包(Functions as closures)
Swift 確實模糊了函數(shù)挽荠、方法、操作符和閉包之間的界限平绩,這非常棒圈匆,因為它向你隱藏了所有編譯器的復(fù)雜性,并讓開發(fā)人員做我們最擅長的事情:制作出色的應(yīng)用程序捏雌。這種模糊的行為一開始很難理解跃赚,在日常編碼中更難使用,但是我想向你展示兩個示例性湿,我希望它們能展示 Swift 是多么聰明纬傲。
我的第一個例子是這樣的:給定一個名為 words
的字符串?dāng)?shù)組,如何查明這些單詞是否存在于名為 input
的字符串中? 一種可能的解決方案是將input
分解為它自己的數(shù)組肤频,然后遍歷兩個數(shù)組以尋找匹配項叹括。但是 Swift 給了我們一個更好的解決方案:如果導(dǎo)入 Foundation 框架, String 會得到一個名為contains()
的方法,該方法接受另一個字符串并返回一個布爾值宵荒。因此汁雷,這段代碼將返回true
:
let input = "My favorite album is Fearless"
input.contains("album")
String 數(shù)組還有兩個contains()
方法:一個方法直接指定一個元素(在我們的例子中是字符串),另一個方法使用where
參數(shù)接受閉包报咳。該閉包需要接受一個字符串并返回一個布爾值侠讯,如下所示:
words.contains { (str) -> Bool in
return true
}
Swift 編譯器的出色設(shè)計讓我們把這兩件事放在一起:即使字符串的contains()
是一個來自 NSString 的基礎(chǔ)方法,我們也可以將它傳遞到數(shù)組的contains(where:)
中少孝,而不是傳遞閉包继低。所以,整個代碼變成這樣:
import Foundation
let words = ["1989", "Fearless", "Red"]
let input = "My favorite album is Fearless"
words.contains(where: input.contains)
最后一行是關(guān)鍵稍走。contains(where:)
將對數(shù)組中的每個元素調(diào)用一次閉包袁翁,直到找到一個返回true
的元素。傳入input.contains
意味著 Swift 將調(diào)用 input.contains("1989")
并返回 false
婿脸,然后它將調(diào)用input.contains("Fearless")
并返回true
——然后停止粱胜。因為contains()
具有與contains(where:)
所期望的(接受一個字符串并返回一個布爾值)完全相同的簽名,所以這就像一個魔咒狐树。
我的第二個例子使用了數(shù)組的reduce()
方法:提供一個初始值焙压,然后給它一個函數(shù)來應(yīng)用于數(shù)組中的每一項。每次調(diào)用該函數(shù)時,都會給你兩個參數(shù):調(diào)用該函數(shù)時的前一個值(這將是初始值)和要使用的當(dāng)前值涯曲。
為了演示這一點野哭,下面是一個調(diào)用reduce()
對一個整型數(shù)組來計算它們的和的例子:
let numbers = [1, 3, 5, 7, 9]
numbers.reduce(0) { (int1, int2) -> Int in
return int1 + int2
}
當(dāng)代碼運行時,它將初始值和 1 相加得到 1幻件,然后是1和 3 (得到總數(shù): 4 )拨黔,然后是4 和 5 (9),然后是 9 和 7 (16)绰沥,然后是 16 和 9篱蝇,最終得到 25 。
這種方法非常好徽曲,但 Swift 有一個更簡單零截、更有效的解決方案:
let numbers = [1, 3, 5, 7, 9]
let result = numbers.reduce(0, +)
當(dāng)你思考它的時候,+
是一個接受兩個整數(shù)并返回它們的和的函數(shù)秃臣,所以我們可以移除整個閉包并用一個操作符替換它涧衙。
逃逸閉包(Escaping closures)
當(dāng)你把一個閉包傳遞給一個函數(shù)時,Swift 默認(rèn)認(rèn)為它是不可逃逸的甜刻。這意味著閉包必須立即在函數(shù)內(nèi)部使用绍撞,并且不能存儲起來供以后使用。如果你試圖在函數(shù)返回后使用閉包得院,Swift 編譯器將拒絕構(gòu)建傻铣,例如,如果要使用 GCD 的 asyncAfter()
方法在一段時間的延遲之后調(diào)用它祥绞。
這對于許多類型的函數(shù)都非常有用非洲,比如sort()
,在這些函數(shù)中蜕径,你可以確定閉包將在方法中使用两踏,然后就再也不會使用閉包了。sort()
方法接受非逃逸閉包作為其惟一的參數(shù)兜喻,因為sort()
不會嘗試存儲該閉包的副本供以后使用——它會立即使用閉包梦染,然后結(jié)束。
另一方面朴皆,逃逸閉包是在方法返回后調(diào)用的閉包帕识。它們存在于許多需要異步調(diào)用閉包的地方。例如遂铡,可能會給你一個閉包肮疗,該閉包只應(yīng)該在用戶做出選擇時調(diào)用。你可以將該閉包存儲起來扒接,提示用戶作出決定伪货,然后在準(zhǔn)備好用戶的選擇后調(diào)用閉包们衙。
逃逸閉包和非逃逸閉包之間的區(qū)別可能聽起來很小,但這很重要碱呼,因為閉包是引用類型蒙挑。一旦 Swift 知道函數(shù)一旦完成就不會使用閉包——它是非逃逸的——它就不需要擔(dān)心引用計數(shù),因此它可以節(jié)省一些工作愚臀。因此脆荷,非逃逸閉包速度更快,并且是 Swift 的默認(rèn)閉包懊悯。也就是說,除非另外指定梦皮,否則所有閉包參數(shù)都被認(rèn)為是非逃逸的炭分。
如果希望指定逃逸閉包,需要使用@escaping
關(guān)鍵字剑肯。最好的方法是在需要的時候演示一個場景捧毛。考慮下面的代碼:
var queuedClosures: [() -> Void] = []
func queueClosure(_ closure: () -> Void) {
queuedClosures.append(closure)
}
queueClosure({ print("Running closure 1") })
queueClosure({ print("Running closure 2") })
queueClosure({ print("Running closure 3") })
這將創(chuàng)建要運行的閉包數(shù)組和接受要排隊的閉包的函數(shù)让网。該函數(shù)除了將它被賦予的閉包追加到隊列閉包數(shù)組之外呀忧,什么都不做。最后溃睹,它使用三個簡單的閉包調(diào)用queueClosure()
三次而账,每個閉包打印一條消息。
為了完成這段代碼因篇,我們只需要創(chuàng)建一個名為executequeuedclosure()
的新方法泞辐,它遍歷隊列并執(zhí)行每個閉包:
func executeQueuedClosures() {
for closure in queuedClosures {
closure()
}
}
executeQueuedClosures()
讓我們更仔細(xì)地研究queueClosure()
方法:
func queueClosure(_ closure: () -> Void) {
queuedClosures.append(closure)
}
它只接受一個參數(shù),這是一個沒有參數(shù)或返回值的閉包竞滓。然后將該閉包添加到queuedclosure
數(shù)組中咐吼。這意味著我們傳入的閉包可以稍后使用,在本例中商佑,當(dāng)調(diào)用executequeuedclosure()
函數(shù)時使用锯茄。
因為閉包可以稍后調(diào)用,Swift 認(rèn)為它們是逃逸閉包茶没,所以它將拒絕構(gòu)建這段代碼肌幽。請記住,出于性能考慮礁叔,非逃逸閉包是默認(rèn)的牍颈,所以我們需要顯式地添加@escape
關(guān)鍵字,以明確我們的意圖:
func queueClosure(_ closure: @escaping () -> Void) {
queuedClosures.append(closure)
}
所以:如果你寫了一個函數(shù)琅关,它會立即調(diào)用閉包煮岁,然后不再使用它讥蔽,它在默認(rèn)情況下是非逃逸的,你可以忘記它画机。但是冶伞,如果你打算存儲閉包供以后使用,則需要
@escape
關(guān)鍵字步氏。
自動閉包(@autoclosure)
@autoclosure
屬性類似于@escaping
响禽,因為你將它應(yīng)用于函數(shù)的閉包參數(shù),但是它的使用要少得多荚醒。嗯芋类,不,嚴(yán)格來說不是這樣的:調(diào)用使用@autoclosure
的函數(shù)是很常見的界阁,但是用它編寫函數(shù)則不常見侯繁。
當(dāng)你使用此屬性時,它會根據(jù)傳入的表達(dá)式自動創(chuàng)建閉包泡躯。當(dāng)你調(diào)用使用此屬性的函數(shù)時贮竟,你編寫的代碼不是閉包,當(dāng)它會變成閉包较剃,這可能有點令人困惑——甚至官方的 Swift 參考指南也警告說咕别,過度使用自動閉包會使代碼更難理解。
為了幫助你理解它是如何工作的写穴,這里有一個簡單的例子:
func printTest(_ result: () -> Void) {
print("Before")
result()
print("After")
}
printTest( { print("Hello") } )
該代碼創(chuàng)建了printTest()
方法惰拱,該方法接受閉包并調(diào)用它。如你所見啊送,print(“Hello”)
位于一個閉包中弓颈,該閉包在 “ Before ” 和 “ After ” 之間調(diào)用,因此最終的輸出是 “ Before ”删掀、“ Hello ”和 “ After ”翔冀。
如果我們使用@autoclosure
,它將允許我們重寫代碼printTest()
調(diào)用披泪,這樣它就不需要大括號纤子,如下所示:
func printTest(_ result: @autoclosure () -> Void) {
print("Before")
result()
print("After")
}
printTest(print("Hello"))
由于@autoclosure
,這兩段代碼產(chǎn)生了相同的結(jié)果款票。在第二個代碼示例中控硼,print("Hello")
不會立即執(zhí)行,因為它被包裝在一個閉包中艾少,以便稍后執(zhí)行卡乾。
這種行為看起來很簡單:所有這些工作只是刪除了一對大括號,使代碼更難理解缚够。但是幔妨,有一個特定的地方需要使用它們:assert()
鹦赎。這是一個 Swift 函數(shù),用于檢查條件是否為真误堡,如果不為真古话,則會導(dǎo)致應(yīng)用程序停止。
這聽起來可能非常極端:為什么你希望你的應(yīng)用程序崩潰锁施?顯然陪踩,你不會這樣做,但是在測試應(yīng)用程序時悉抵,添加assert()
調(diào)用有助于確保代碼的行為符合預(yù)期肩狂。你真正想要的是,你的斷言在 debug 模式下是活動的姥饰,而在 release 模式下是禁用的婚温,這正是assert()
的工作方式。
請看下面三個例子:
assert(1 == 1, "Maths failure!")
assert(1 == 2, "Maths failure!")
assert(myReallySlowMethod() == false, "The slow method returned false!")
第一個例子返回true
媳否,所以什么也不會發(fā)生。第二個將返回false
荆秦,因此應(yīng)用程序?qū)⑼V估榻摺5谌齻€例子是assert()
的強大功能:因為它使用@autoclosure
將代碼封裝在閉包中,所以 Swift 編譯器在 release 模式下不會運行閉包步绸。這意味著你可以在調(diào)試時獲得所有斷言的安全性掺逼,而不需要在 release 模式中付出任何性能代價。
你可能有興趣知道瓤介,自動閉包還用于處理&&
和||
操作符吕喘。以下是在官方編譯器中找到&&
完整的 Swift 源代碼:
public static func && (lhs: Bool, rhs: @autoclosure () throws -> Bool) rethrows -> Bool {
return lhs ? try rhs() : false
}
是的,它包含try/catch
刑桑、throw
和rethrow
氯质、運算符重載、三元運算符和@autoclosure
祠斧,所有這些都在一個小函數(shù)中闻察。盡管如此,我還是希望你能夠理解代碼的全部功能:如果lhs
為真琢锋,則返回rhs()
的結(jié)果辕漂,否則返回false
。這是實際的短路評估:如果lhs
代碼已經(jīng)返回false
, Swift 不需要運行rhs
閉包吴超。
關(guān)于@autoclosure
的最后一件事:如果你想要進行逃逸閉包钉嘹,你應(yīng)該將這兩個屬性組合起來。例如鲸阻,我們可以像這樣重寫前面的queueClosure()
函數(shù):
func queueClosure(_ closure: @autoclosure @escaping () -> Void) {
queuedClosures.append(closure)
}
queueClosure(print("Running closure 1"))
提醒:小心使用自動閉包跋涣。它們會使代碼更難理解缨睡,所以不要僅僅因為想避免鍵入一些花括號就使用它們。
~=操作符(The ~= operator)
我知道有一個喜歡的運算符聽起來很奇怪仆潮,但是我確實喜歡宏蛉,它是~=
。我喜歡它性置,因為它簡單拾并。我愛它,即使它不是真的需要鹏浅。我甚至喜歡它的形狀——只要看看它的美麗就行了嗅义!所以我希望你能原諒我花了幾分鐘時間給你看這個。
我已經(jīng)對兩個簡單的符號流口水了:這到底是做什么的隐砸?我很高興你這么問! ~=
是模式匹配操作符之碗,它允許你這樣編寫代碼:
let range = 1...100
let i = 42
if range ~= i {
print("Match!")
}
正如我所說,不需要這個操作符季希,因為你可以使用區(qū)間內(nèi)置的contains()
方法編寫代碼褪那。但是,它確實比contains()
有一點語法上的優(yōu)勢式塌,因為它不需要額外的一組括號:
let test1 = (1...100).contains(42)
let test2 = 1...100 ~= 42
我認(rèn)為~=
是使用操作符重載來整理日常語法的一個很好的例子博敬。