《Pro Swift》 第四章:函數(shù)(Functions)

當(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), SwiftCore 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ù):separatorterminator疯溺,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ù)(左邊和右邊分別是lhsrhs),并返回一個布爾值钩蚊,該值報告這兩個數(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, 2x23x3修赞。

*操作符已經(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)存在虐杯,所以重要的是lhsrhs參數(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)與這個操作符一起使用時囤采,可以推斷24是雙精度值:

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,然后得到覆蓋 110 的范圍 见秽。在默認(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ù)需要做兩件事:

  1. 計算一個新的區(qū)間,從右邊的整數(shù)到左邊區(qū)間的最高點踪危,然后反轉(zhuǎn)這個區(qū)間洗贰。
  2. 將左邊的區(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ā)生任何變化吉嚣。相反,它返回一個閉包蹬铺,該閉包將counter1 并打印出它的新值尝哆。它不調(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ù)時彤敛,你將看到counter1 与帆。

讓事情變得加倍有趣的是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ù)书释,并返回truefalse翘贮。然后閉包檢查名稱是否具有前綴 “ 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幻件,然后是13 (得到總數(shù): 4 )拨黔,然后是45 (9),然后是 97 (16)绰沥,然后是 169篱蝇,最終得到 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)建傻铣,例如,如果要使用 GCDasyncAfter()方法在一段時間的延遲之后調(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刑桑、throwrethrow氯质、運算符重載、三元運算符和@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)為~=是使用操作符重載來整理日常語法的一個很好的例子博敬。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市峰尝,隨后出現(xiàn)的幾起案子偏窝,更是在濱河造成了極大的恐慌,老刑警劉巖武学,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件祭往,死亡現(xiàn)場離奇詭異,居然都是意外死亡火窒,警方通過查閱死者的電腦和手機硼补,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來熏矿,“玉大人括勺,你說我怎么就攤上這事∏” “怎么了疾捍?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長栏妖。 經(jīng)常有香客問我乱豆,道長,這世上最難降的妖魔是什么吊趾? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任宛裕,我火速辦了婚禮瑟啃,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘揩尸。我一直安慰自己蛹屿,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布岩榆。 她就那樣靜靜地躺著错负,像睡著了一般。 火紅的嫁衣襯著肌膚如雪勇边。 梳的紋絲不亂的頭發(fā)上犹撒,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天,我揣著相機與錄音粒褒,去河邊找鬼识颊。 笑死,一個胖子當(dāng)著我的面吹牛奕坟,可吹牛的內(nèi)容都是我干的祥款。 我是一名探鬼主播,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼月杉,長吁一口氣:“原來是場噩夢啊……” “哼刃跛!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起沙合,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎跌帐,沒想到半個月后首懈,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡谨敛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年究履,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片脸狸。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡最仑,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出炊甲,到底是詐尸還是另有隱情泥彤,我是刑警寧澤,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布卿啡,位于F島的核電站吟吝,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏颈娜。R本人自食惡果不足惜剑逃,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一浙宜、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蛹磺,春花似錦粟瞬、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至鳖轰,卻和暖如春清酥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蕴侣。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工焰轻, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人昆雀。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓辱志,卻偏偏與公主長得像,于是被迫代替她去往敵國和親狞膘。 傳聞我的和親對象是個殘疾皇子揩懒,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,685評論 2 360

推薦閱讀更多精彩內(nèi)容