Swift 中的多尾隨閉包(Multiple Trailing Closures)

[ 本文運(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、bc,那還得去看方法實(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ù)有 ab 兩個參數(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é)果如下:

  1. 傳入了所有閉包且指定了對應(yīng)的參數(shù),運(yùn)行正常。
  2. 只傳入了一個閉包并指定其為b的參數(shù),其余參數(shù)使用默認(rèn)值,運(yùn)行正常。
  3. 傳入了一個匿名閉包,從運(yùn)行結(jié)果來看,其被 d 所持有
  4. 傳入了一個閉包給予 a 與一個尾隨匿名閉包,匿名閉包被 d 所持有
  5. 傳入了一個閉包給予 b 與一個尾隨匿名閉包, 匿名閉包被 d 所持有
  6. 傳入了一個指定參數(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é)

  1. Swift 在 5.3 版本中,加入了顯示末尾閉包的參數(shù)名,默認(rèn)隱藏了首個尾隨閉包的參數(shù)名。
  2. 在部分多尾隨閉包的場景下,不同Swift版本下將會有不同的匹配結(jié)果。
  3. 向后匹配將會在 Swift6 中移除,在多個匿名閉包的地方要留意使用了向后搜索匹配的情況。
  4. 與其總讓 Swift 去采用向前/向后匹配,不如老老實(shí)實(shí)按規(guī)范把參數(shù)名寫上。

最新更新以及更多的深入剖析與嘗試請查看官方文檔SE-0279SE-0286

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

友情鏈接更多精彩內(nèi)容