[ 本文運(yùn)行環(huán)境:Xcode12_beta_6 (Swift 5.3) ]
多尾隨閉包(Multiple Trailing Closures)
尾隨閉包在開發(fā)中隨處可見:
// 定義:
open class func animate(withDuration duration: TimeInterval, animations: @escaping () -> Void)
// 調(diào)用
UIView.animate(withDuration: 0.3) {
// 各種動畫
}
在 Swift5.3 之前,當(dāng)有多個尾隨閉包時寫法是這樣的:
// 定義
open class func animate(withDuration duration: TimeInterval, animations: @escaping () -> Void, completion: ((Bool) -> Void)? = nil)
// 調(diào)用
UIView.animate(withDuration: 0.3, animations: {
// 各種動畫
}) { (finish) in
// ???
}
可以發(fā)現(xiàn),這里的多尾隨閉包有個缺點(diǎn):調(diào)用時最后一個閉包沒有名字。這樣造成的后果是代碼可讀性差,對于不熟悉調(diào)用方法的開發(fā)者來說,就得去看方法的定義,運(yùn)氣差點(diǎn)接手了個毫無規(guī)范的前輩留下的自定義方法,沒寫注釋,參數(shù)名再整個a、b、c,那還得去看方法實(shí)現(xiàn)。
蘋果很早就注意到了這個問題,但卻只是在規(guī)范中建議在調(diào)用含有多個閉包參數(shù)的方法中避免使用尾隨閉包。意思是建議使用其他方式實(shí)現(xiàn)多尾隨閉包的結(jié)構(gòu)。
這很不 "Swift"。
好在 Swift5.3 中對尾隨閉包進(jìn)行了優(yōu)化,調(diào)用多尾隨閉包時最后一個閉包將會顯示出參數(shù)名:
UIView.animate(withDuration: 0.3) {
// 各種動畫
} completion: { (finish) in
// completion - 動畫結(jié)束
}
老夫這強(qiáng)迫癥終于... "等等,第一個閉包名呢?"
與之前相比,Swift 在 5.3 中默認(rèn)不顯示第一個閉包參數(shù)名,后面的閉包均顯示出了參數(shù)名。對此,蘋果的說法為:這樣的寫法無傷大雅,因?yàn)楹卸辔搽S閉包的方法一般第一個為最主要的閉包,其他閉包都是可選的。這意思是默認(rèn)所有開發(fā)者都有良好的開發(fā)規(guī)范,如方法名,參數(shù)名等等。
對于晚期強(qiáng)迫癥有個好消息,第一個尾隨閉包的參數(shù)名是可選的,開發(fā)者可以自己加上名稱,以 UIView.animte 為例:
// 單個閉包
UIView.animate(withDuration: 0.3, animations: {() in
// ...
})
// 多個閉包
UIView.animate(withDuration: 0.3, animations: { () in
// ...
}, completion: { (finish) in
// ...
})
但為了加上第一個閉包的參數(shù),幾乎將默認(rèn)的調(diào)用結(jié)構(gòu)重寫一遍,這樣的代價是否值得?是否可以使用一個中間閉包來包含所有尾隨閉包?又或者自定義一個鏈?zhǔn)秸Z法的擴(kuò)展鏈接所有尾隨閉包?
中間閉包增大了方法的復(fù)雜性,而鏈?zhǔn)秸{(diào)用擴(kuò)展來連接所有閉包是對尾隨閉包的二次封裝,SE-0279 中的設(shè)計(jì)原理部分可以看到多尾隨閉包底層的實(shí)現(xiàn)機(jī)制,僅為了一個閉包名進(jìn)行二次封裝是否值得?實(shí)現(xiàn)出的效果風(fēng)格也異于系統(tǒng)。
擴(kuò)展:向后搜索匹配與向前搜索匹配
對于多尾隨閉包,5.3之前和之后,一個是匿名閉包放最末尾,一個是匿名閉包在最前。 當(dāng)匿名閉包與剩余未匹配參數(shù)不是1對1的關(guān)系時 Swift 又是如何匹配匿名閉包的?
例如下面代碼,以下誰將持有閉包?
func test(a: () -> Int = { 1 }, b: Any? = nil) {}
test { 2 }
// a 與 b 誰將持有傳入的閉包參數(shù)?
test 函數(shù)有 a 和 b 兩個參數(shù),兩者均擁有默認(rèn)參數(shù)。運(yùn)行后發(fā)現(xiàn) b 持有了傳入的閉包。為什么會這樣?
在 SE-0279 的設(shè)計(jì)原理中提到,Swift 匹配多尾隨閉包最開始使用的是 “backwards scan”(向后掃描/逆向掃描)來匹配匿名閉包。
通過例子了解下向后掃描/逆向掃描是個什么東西:
typealias RJBlock = ()->Int
func test(a: RJBlock? = nil, b: RJBlock? = nil, c:RJBlock? = nil, d:RJBlock? = nil) {
print(a,b,c,d)
}
test(a: {1}, b: {2}, c: {3}, d: {4}) // 1
test(b: {100}) // 2
test{ 100 } // 3
test(a: nil) { 100 } // 4
test(b: nil) { 100 } // 5
test {200} c: { 100 } // 6
持有結(jié)果如下:
- 傳入了所有閉包且指定了對應(yīng)的參數(shù),運(yùn)行正常。
- 只傳入了一個閉包并指定其為b的參數(shù),其余參數(shù)使用默認(rèn)值,運(yùn)行正常。
- 傳入了一個匿名閉包,從運(yùn)行結(jié)果來看,其被
d所持有 - 傳入了一個閉包給予
a與一個尾隨匿名閉包,匿名閉包被d所持有 - 傳入了一個閉包給予
b與一個尾隨匿名閉包, 匿名閉包被d所持有 - 傳入了一個指定參數(shù)的尾隨閉包
c,第一個參數(shù)為匿名閉包,匿名閉包被a所持有
測試的過程中可以看到3,4,5用例均顯示了警告信息:
Backward matching of the unlabeled trailing closure is deprecated; label the argument with 'XXX' to suppress this warning
提示我們不推薦使用未標(biāo)記的尾隨閉包來進(jìn)行“向后匹配”。前面的舉例是因?yàn)?b參數(shù)為Any類型,所以編譯器未提示警告。
多尾隨閉包中匿名閉包匹配是通過對參數(shù)執(zhí)行向后掃描完成的。使用標(biāo)簽匹配所有帶標(biāo)簽的尾隨閉包,然后從匹配到的最后一個標(biāo)記參數(shù)對匿名尾隨閉包執(zhí)行掃描。所以,在4,5用例中,指定了部分參數(shù)名,尾隨閉包從最后一個參數(shù)開始“向后掃描”,而d就是第一個被掃描的參數(shù),掃描類型結(jié)果匹配,所以匿名的尾隨閉包就賦值給了d。
等等···在第六個用例,指定了參數(shù)c,逆向搜索不應(yīng)該是從d開始么,就算是從C開始,也應(yīng)該是b啊,怎么匿名閉包賦值給了a?還是因?yàn)槟涿]包非末尾閉包,匹配方式有所不同?這與"向后掃描"本身缺陷有關(guān)。
在 SE-0286中,蘋果注意到了多尾隨閉包“向后掃描匹配”所衍生的問題:
向后掃描匹配規(guī)則使得使用尾部閉包(尤其是多個尾隨閉包)編寫良好的 API 變得困難。
文檔中蘋果舉了以下例子:
class func animate(
withDuration duration: TimeInterval,
animations: @escaping () -> Void,
completion: ((Bool) -> Void)? = nil
)
UIView.animate(withDuration: 0.3) {
self.view.alpha = 0
}
使用“向后搜索匹配”傳入的匿名閉包將被completion持有,這明顯不是我們想要的結(jié)果,為解決“向后搜索匹配”的問題,,蘋果提出了“向前搜索匹配”了。上面的例子在“向前搜索匹配”下等價于:
UIView.animate(withDuration: 0.3, animations: {
self.view.alpha = 0
})
使用“向前搜索匹配”時,匿名閉包將從未匹配的參數(shù)從前向后進(jìn)行匹配,用例中匹配到的第一個參數(shù)為animations。
接下來我們嘗試一下參數(shù)默認(rèn)值對匹配的影響:
// 刪除參數(shù)b的默認(rèn)值,匿名函數(shù)將優(yōu)先匹配 b
func test(a: RJBlock? = nil, b: RJBlock?, c:RJBlock? = nil, d:RJBlock? = nil) {}
無論是向前搜索匹配還是向后搜索匹配都會優(yōu)先匹配第一個找到的無默認(rèn)值的同類型參數(shù),當(dāng)全部都有默認(rèn)值時則匹配第一個類型相匹配的參數(shù)(此處可以擴(kuò)展閉包與方法類型的匹配)。那么何時使用向前匹配,何時又使用向后匹配?
對此,SE-0286 有這樣一段描述:
“Swift會使用兩種方法進(jìn)行匹配,如果兩者都成功了,由于兼容性考慮將首選向后掃描匹配”
”向后匹配將會在 Swift6 中移除“
總結(jié)
- Swift 在 5.3 版本中,加入了顯示末尾閉包的參數(shù)名,默認(rèn)隱藏了首個尾隨閉包的參數(shù)名。
- 在部分多尾隨閉包的場景下,不同Swift版本下將會有不同的匹配結(jié)果。
- 向后匹配將會在 Swift6 中移除,在多個匿名閉包的地方要留意使用了
向后搜索匹配的情況。 - 與其總讓 Swift 去采用
向前/向后匹配,不如老老實(shí)實(shí)按規(guī)范把參數(shù)名寫上。