七個(gè)Swift中的陷阱以及避免方法
Swift正在完成一個(gè)驚人的壯舉,它正在改變我們?cè)谔O果設(shè)備上編程的方式,引入了很多現(xiàn)代范例,例如:函數(shù)式編程和相比于OC這種純面向?qū)ο笳Z(yǔ)言更豐富的類型檢查。
Swift語(yǔ)言希望通過(guò)采用安全的編程模式去幫助開(kāi)發(fā)者避免bug。然而這也會(huì)不可避免的產(chǎn)生一些人造的陷阱,他們會(huì)在編譯器不報(bào)錯(cuò)的情況下引入一些Bug。這些陷阱有的已經(jīng)在Swift book中提到,有一些還沒(méi)有。這里有七個(gè)我在去年遇到的陷阱,它們涉及Swift協(xié)議擴(kuò)展、可選鏈和函數(shù)式編程。
協(xié)議擴(kuò)展:強(qiáng)大但是需要謹(jǐn)慎使用
一個(gè)Swift類可以去繼承另一個(gè)類,這種能力是強(qiáng)大的。繼承將使類之間的特定關(guān)系更加清晰,并且支持細(xì)粒度代碼分享。但是,Swift中如果不是引用類型的話(如:結(jié)構(gòu)體、枚舉),就不能具有繼承關(guān)系。然而,一個(gè)值類型可以繼承協(xié)議,同時(shí)協(xié)議可以繼承另一個(gè)協(xié)議。雖然協(xié)議除了類型信息外不能包含其他代碼,但是協(xié)議擴(kuò)展(protocol extension)可以包含代碼。照這種方式,我們可以用繼承樹來(lái)實(shí)現(xiàn)代碼的分享共用,樹的葉子是值類型(結(jié)構(gòu)體或枚舉類),樹的內(nèi)部和根是協(xié)議和與他們對(duì)應(yīng)的擴(kuò)展。
但是Swift協(xié)議擴(kuò)展的實(shí)現(xiàn)依然是一片新的、未開(kāi)發(fā)的領(lǐng)域,尚存在一些問(wèn)題。代碼并不總是按照我們期望的那樣執(zhí)行。因?yàn)檫@些問(wèn)題出現(xiàn)在值類型(結(jié)構(gòu)體與枚舉)與協(xié)議組合使用的場(chǎng)景下,我們將使用類與協(xié)議組合使用的例子去說(shuō)明這種場(chǎng)景下不存在陷阱。當(dāng)我們重新改為使用值類型和協(xié)議的時(shí)候?qū)?huì)發(fā)生令人驚奇的事。
開(kāi)始介紹我們的例子:classy pizza
假設(shè)這里有使用兩種不同谷物制作的三種Pizza:
- enum Grain { case Wheat, Corn }
- class NewYorkPizza { let crustGrain: Grain = .Wheat }
- class ChicagoPizza { let crustGrain: Grain = .Wheat }
- class CornmealPizza { let crustGrain: Grain = .Corn }
我們可以通過(guò)crustGrain屬性取得披薩所對(duì)應(yīng)的原料
- NewYorkPizza().crustGrain // returns Wheat
- ChicagoPizza().crustGrain // returns Wheat
- CornmealPizza().crustGrain // returns Corn
因?yàn)榇蠖鄶?shù)的Pizza是用小麥(wheat)做的,這些公共代碼可以放進(jìn)一個(gè)超類中作為默認(rèn)執(zhí)行的代碼。
- enum Grain { case Wheat, Corn }
- class Pizza {
- var crustGrain: Grain { return .Wheat }
- // other common pizza behavior
- }
- class NewYorkPizza: Pizza {}
- class ChicagoPizza: Pizza {}
這些默認(rèn)的代碼可以被重載去處理其它的情況(用玉米制作)
- class CornmealPizza: Pizza {
- override var crustGain: Grain { return .Corn }
- }
哎呀!這代碼是錯(cuò)的,并且很幸運(yùn)的是編譯器發(fā)現(xiàn)了這些錯(cuò)誤。你能發(fā)現(xiàn)這個(gè)錯(cuò)誤么?我們?cè)诘诙€(gè)crustGain中少寫了r。Swift通過(guò)顯式的標(biāo)注override避免這種錯(cuò)誤。比如在這個(gè)例子中,我們用到了override,但是拼寫錯(cuò)誤的"crustGain"其實(shí)并沒(méi)有重寫任何屬性,下面是修改后的代碼:
- class CornmealPizza: Pizza {
- override var crustGrain: Grain { return .Corn }
- }
現(xiàn)在它可以通過(guò)編譯并成功運(yùn)行:
- NewYorkPizza().crustGrain // returns Wheat
- ChicagoPizza().crustGrain // returns Wheat
- CornmealPizza().crustGrain // returns Corn
同時(shí)Pizza超類允許我們的代碼在不知道Pizza具體類型的時(shí)候去操作pizzas。我們可以聲明一個(gè)Pizza類型的變量。
- var pie: Pizza
但是通用類型Pizza仍然可以去得到特定類型的信息。
- pie = NewYorkPizza(); pie.crustGrain // returns Wheat
- pie = ChicagoPizza(); pie.crustGrain // returns Wheat
- pie = CornmealPizza(); pie.crustGrain // returns Corn
Swift的引用類型在這個(gè)Demo中工作的很好。但是如果這個(gè)程序涉及到并發(fā)性、競(jìng)爭(zhēng)條件,我們可以使用值類型來(lái)避免這些。讓我們來(lái)試一下值類型的Pizza吧!
這里和上面一樣簡(jiǎn)單,只需要把class修改為struct即可:
- enum Grain { case Wheat, Corn }
- struct NewYorkPizza { let crustGrain: Grain = .Wheat }
- struct ChicagoPizza { let crustGrain: Grain = .Wheat }
- struct CornmealPizza { let crustGrain: Grain = .Corn }
執(zhí)行
- NewYorkPizza() .crustGrain // returns Wheat
- ChicagoPizza() .crustGrain // returns Wheat
- CornmealPizza() .crustGrain // returns Corn
當(dāng)我們使用引用類型的時(shí)候,我們通過(guò)一個(gè)超類Pizza來(lái)達(dá)到目的。但是對(duì)于值類型將要求一個(gè)協(xié)議和一個(gè)協(xié)議擴(kuò)展來(lái)合作完成。
- protocol Pizza {}
- extension Pizza { var crustGrain: Grain { return .Wheat } }
- struct NewYorkPizza: Pizza { }
- struct ChicagoPizza: Pizza { }
- struct CornmealPizza: Pizza { let crustGain: Grain = .Corn }
這段代碼可以通過(guò)編譯,我們來(lái)測(cè)試一下:
- NewYorkPizza().crustGrain // returns Wheat
- ChicagoPizza().crustGrain // returns Wheat
- CornmealPizza().crustGrain // returns Wheat What?!
對(duì)于執(zhí)行結(jié)果,我們想說(shuō)cornmeal pizza并不是Wheat制作的,返回結(jié)果出現(xiàn)錯(cuò)誤!哎呀!我把
- struct CornmealPizza: Pizza { let crustGain: Grain = .Corn }
中的 crustGrain寫成了crustGain,再一次忘記了r,但是對(duì)于值類型這里沒(méi)有override關(guān)鍵字去幫助編譯器去發(fā)現(xiàn)我們的錯(cuò)誤。沒(méi)有編譯器的幫助,我們不得不更加小心的編寫代碼。
在協(xié)議擴(kuò)展中重寫協(xié)議中的屬性時(shí)要仔細(xì)核對(duì)
ok,我們把這個(gè)拼寫錯(cuò)誤改正過(guò)來(lái):
- struct CornmealPizza: Pizza { let crustGrain: Grain = .Corn }
重新執(zhí)行
- NewYorkPizza().crustGrain // returns Wheat
- ChicagoPizza().crustGrain // returns Wheat
- CornmealPizza().crustGrain // returns Corn Hooray!
為了在討論P(yáng)izza的時(shí)候不需要擔(dān)心到底是New York, Chicago, 還是 cornmeal,我們可以使用Pizza協(xié)議作為變量的類型。
- var pie: Pizza
這個(gè)變量能夠在不同種類的Pizza中去使用
- pie = NewYorkPizza(); pie.crustGrain // returns Wheat
- pie = ChicagoPizza(); pie.crustGrain // returns Wheat
- pie = CornmealPizza(); pie.crustGrain // returns Wheat Not again?!
為什么這個(gè)程序顯示cornmeal pizza 包含wheat?Swift編譯代碼的時(shí)候忽略了變量的目前實(shí)際值。代碼只能夠使用編譯時(shí)期的知道的信息,并不知道運(yùn)行時(shí)期的具體信息。程序中可以在編譯時(shí)期得到的信息是pie是pizza類型,pizza協(xié)議擴(kuò)展返回wheat,所以在結(jié)構(gòu)體CornmealPizza中的重寫起不到任何作用。雖然編譯器本能夠在使用靜態(tài)調(diào)度替換動(dòng)態(tài)調(diào)度時(shí),為潛在的錯(cuò)誤提出警告,但它實(shí)際上并沒(méi)有這么做。這里的粗心將帶來(lái)巨大的陷阱。
在這種情況下,Swift提供一種解決方案,除了在協(xié)議擴(kuò)展中(extension)定義crustGrain屬性之外,還可以在協(xié)議中聲明。
- protocol Pizza { var crustGrain: Grain { get } }
- extension Pizza { var crustGrain: Grain { return .Wheat } }
在協(xié)議內(nèi)聲明變量并在協(xié)議拓展中定義,這樣會(huì)告訴編譯器關(guān)注變量pie運(yùn)行時(shí)的值。
在協(xié)議中一個(gè)屬性的聲明有兩種不同的含義,靜態(tài)還是動(dòng)態(tài)調(diào)度,取決于是否這個(gè)屬性在協(xié)議擴(kuò)展中定義。
補(bǔ)充了協(xié)議中變量的聲明后,代碼可以正常運(yùn)行了:
- pie = NewYorkPizza(); pie.crustGrain // returns Wheat
- pie = ChicagoPizza(); pie.crustGrain // returns Wheat
- pie = CornmealPizza(); pie.crustGrain // returns Corn Whew!
在協(xié)議擴(kuò)展中定義的每一個(gè)屬性,需要在協(xié)議中進(jìn)行聲明。
然而這個(gè)設(shè)法避免陷阱的方式并不總是有效的。
導(dǎo)入的協(xié)議不能夠完全擴(kuò)展。
框架(庫(kù))可以使一個(gè)程序?qū)虢涌谌ナ褂?,而不必包含相關(guān)實(shí)現(xiàn)。例如蘋果提供給我們提供了需要框架,實(shí)現(xiàn)了用戶體驗(yàn)、系統(tǒng)設(shè)施和其他功能。Swift的擴(kuò)展允許程序向?qū)氲念?、結(jié)構(gòu)體、枚舉和協(xié)議中添加自己的屬性(這里的屬性并不是存儲(chǔ)屬性)。通過(guò)協(xié)議拓展添加的屬性,就好像它原來(lái)就在協(xié)議中一樣。但實(shí)際上定義在協(xié)議拓展中的屬性并非一等公民,因?yàn)橥ㄟ^(guò)協(xié)議拓展無(wú)法添加屬性的聲明。
我們首先實(shí)現(xiàn)一個(gè)框架,這個(gè)框架定義了Pizza協(xié)議和具體的類型
- // PizzaFramework:
- public protocol Pizza { }
- public struct NewYorkPizza: Pizza { public init() {} }
- public struct ChicagoPizza: Pizza { public init() {} }
- public struct CornmealPizza: Pizza { public init() {} }
導(dǎo)入框架并且擴(kuò)展Pizza
- import PizzaFramework
- public enum Grain { case Wheat, Corn }
- extension Pizza { var crustGrain: Grain { return .Wheat } }
- extension CornmealPizza { var crustGrain: Grain { return .Corn } }
和以前一樣,靜態(tài)調(diào)度產(chǎn)生一個(gè)錯(cuò)誤的答案
- var pie: Pizza = CornmealPizza()
- pie.crustGrain // returns Wheat Wrong!
這個(gè)是因?yàn)椋ㄅc剛才的解釋一樣)這個(gè)crustGrain屬性并沒(méi)有在協(xié)議中聲明,而是只是在擴(kuò)展中定義。然而,我們沒(méi)有辦法對(duì)框架的代碼進(jìn)行修改,因此也就不能解決這個(gè)問(wèn)題。因此,想要通過(guò)擴(kuò)展增加其他框架的協(xié)議屬性是不安全的。
不要對(duì)導(dǎo)入的協(xié)議進(jìn)行擴(kuò)展,新增可能需要?jiǎng)討B(tài)調(diào)度的屬性
正像剛才描述的那樣,框架與協(xié)議擴(kuò)展之間的交互,限制了協(xié)議擴(kuò)展的效用,但是框架并不是唯一的限制因素,同樣,類型約束也不利于協(xié)議擴(kuò)展。
Attributes in restricted protocol extensions: declaration is no longer enough
回顧一下此前Pizza的例子:
- enum Grain { case Wheat, Corn }
- protocol Pizza { var crustGrain: Grain { get } }
- extension Pizza { var crustGrain: Grain { return .Wheat } }
- struct NewYorkPizza: Pizza { }
- struct ChicagoPizza: Pizza { }
- struct CornmealPizza: Pizza { let crustGrain: Grain = .Corn }
讓我們用Pizza做一頓飯。不幸的是,并不是每頓飯都會(huì)吃pizza,所以我們使用一個(gè)通用的Meal結(jié)構(gòu)體來(lái)適應(yīng)各種情況。我們只需要傳入一個(gè)參數(shù)就可以確定進(jìn)餐的具體類型。
- struct Meal: MealProtocol {
- let mainDish: MainDishOfMeal
- }
結(jié)構(gòu)體Meal繼承自MealProtocol協(xié)議,它可以測(cè)試meal是否包含谷蛋白。
- protocol MealProtocol {
- typealias MainDish_OfMealProtocol
- var mainDish: MainDish_OfMealProtocol {get}
- var isGlutenFree: Bool {get}
- }
為了避免中毒,代碼中使用了默認(rèn)值(不含有谷蛋白)
- extension MealProtocol {
- var isGlutenFree: Bool { return false }
- }
Swift中的 Where提供了一種方式去表達(dá)約束性協(xié)議擴(kuò)展。當(dāng)主菜是pizza的時(shí)候,我們知道pizza有scrustGrain屬性,我們就可以訪問(wèn)這個(gè)屬性。如果沒(méi)where這里的限制,我們?cè)诓皇荘izza的情況下訪問(wèn)scrustGrain是不安全的。
- extension MealProtocol where MainDish_OfMealProtocol: Pizza {
- var isGlutenFree: Bool { return mainDish.crustGrain == .Corn }
- }
一個(gè)帶有Where的擴(kuò)展叫做約束性擴(kuò)展。
讓我們做一份美味的cornmeal Pizza
- let meal: Meal = Meal(mainDish: CornmealPizza())
結(jié)果:
- meal.isGlutenFree // returns false
- // 根據(jù)協(xié)議拓展,理論上應(yīng)該返回true
正像我們?cè)谇懊嫘」?jié)演示的那樣,當(dāng)發(fā)生動(dòng)態(tài)調(diào)度的時(shí)候,我們應(yīng)該在協(xié)議中聲明,并且在協(xié)議擴(kuò)展中進(jìn)行定義。但是約束性擴(kuò)展的定義總是靜態(tài)調(diào)度的。為了防止由于意外的靜態(tài)調(diào)度而引起的bug:
如果一個(gè)新的屬性需要?jiǎng)討B(tài)調(diào)度,避免使用約束性協(xié)議擴(kuò)展。
使用可選鏈賦值和副作用
Swift可以通過(guò)靜態(tài)地檢查變量是否為nil來(lái)避免錯(cuò)誤,并使用一種方便的縮略表達(dá)式,可選鏈,用于忽略可能出現(xiàn)的nil。這一點(diǎn)也正是Objective-C的默認(rèn)行為。
不幸的是,如果可選鏈中被賦值的引用有可能為空,就可能導(dǎo)致錯(cuò)誤,考慮下面這段代碼,Holder中存放一個(gè)整數(shù):
- class Holder {
- var x = 0
- }
- var n = 1
- var h: Holder? = nil
- h?.x = n++
在這段代碼的***一行中,我們把n++賦值給h的屬性。除了賦值以外,變量n還會(huì)自增,我們稱此為副作用。
變量n最終的值會(huì)取決于h是否為nil。如果h不為nil,那么賦值語(yǔ)句執(zhí)行,n++也會(huì)執(zhí)行。但如果h為nil,不僅賦值語(yǔ)句不會(huì)執(zhí)行,n++也不會(huì)執(zhí)行。為了避免沒(méi)有發(fā)生副作用導(dǎo)致的令人驚訝的結(jié)果,我們應(yīng)該:
避免把一個(gè)有副作用的表達(dá)式的結(jié)果通過(guò)可選鏈賦值給等號(hào)左邊的變量
函數(shù)編程陷阱
由于Swift的支持,函數(shù)式編程的優(yōu)點(diǎn)得以被帶入蘋果的生態(tài)圈中。Swift中的函數(shù)和閉包都是一等公民,不僅方便易用而且功能強(qiáng)大。不幸的是,其中也有一些我們需要小心避免的陷阱。
比如,inout參數(shù)會(huì)在閉包中默默的失效。
Swift的inout參數(shù)允許函數(shù)接受一個(gè)參數(shù)并直接對(duì)參數(shù)賦值,Swift的閉包支持在執(zhí)行過(guò)程中引用被捕獲的函數(shù)。這些特性有助于我們寫出優(yōu)雅易讀的代碼,所以你也許會(huì)把它們結(jié)合起來(lái)使用,但這種結(jié)合有可能會(huì)導(dǎo)致問(wèn)題。
我們重寫crustGrain屬性來(lái)說(shuō)明inout參數(shù)的使用,為簡(jiǎn)單起見(jiàn),開(kāi)始時(shí)先不使用閉包:
- enum Grain {
- case Wheat, Corn
- }
- struct CornmealPizza {
- func setCrustGrain(inout grain: Grain) {
- grain = .Corn
- }
- }
為了測(cè)試這個(gè)函數(shù),我們給它傳一個(gè)變量作為參數(shù)。函數(shù)返回后,這個(gè)變量的值應(yīng)該從Wheat變成了Corn:
- let pizza = CornmealPizza()
- var grain: Grain = .Wheat
- pizza.setCrustGrain(&grain)
- grain // returns Corn
現(xiàn)在我們嘗試在函數(shù)中返回閉包,然后在閉包中設(shè)置參數(shù)的值:
- struct CornmealPizza {
- func getCrustGrainSetter() -> (inout grain: Grain) -> Void {
- return { (inout grain: Grain) in
- grain = .Corn
- }
- }
- }
使用這個(gè)閉包只需要多一次調(diào)用:
- var grain: Grain = .Wheat
- let pizza = CornmealPizza()
- let aClosure = pizza.getCrustGrainSetter()
- grain // returns Wheat (We have not run the closure yet)
- aClosure(grain: &grain)
- grain // returns Corn
到目前為止一切正常,但如果我們直接把參數(shù)傳進(jìn)getCrustGrainSetter函數(shù)而不是閉包呢?
- struct CornmealPizza {
- func getCrustGrainSetter(inout grain: Grain) -> () -> Void {
- return { grain = .Corn }
- }
- }
然后再試一次:
- var grain: Grain = .Wheat
- let pizza = CornmealPizza()
- let aClosure = pizza.getCrustGrainSetter(&grain)
- print(grain) // returns Wheat (We have not run the closure yet)
- aClosure()
- print(grain) // returns Wheat What?!?
inout參數(shù)在傳入閉包的作用域外時(shí)會(huì)失效,所以:
避免在閉包中使用in-out參數(shù)
這個(gè)問(wèn)題在Swift文檔中提到過(guò),但還有一個(gè)與之相關(guān)的問(wèn)題值得注意,這與創(chuàng)建的閉包的等價(jià)方法:柯里化有關(guān)。
在使用柯里化技術(shù)時(shí),inout參數(shù)顯得前后矛盾。
在一個(gè)創(chuàng)建并返回閉包的函數(shù)中,Swift為函數(shù)的類型和主體提供了一種簡(jiǎn)潔的語(yǔ)法。盡管這種柯里化看上去僅是一種縮略表達(dá)式,但它與inout參數(shù)結(jié)合使用時(shí)卻會(huì)給人們帶來(lái)一些驚訝。為了說(shuō)明這一點(diǎn),我們用柯里化語(yǔ)法實(shí)現(xiàn)上面那個(gè)例子。函數(shù)沒(méi)有聲明為返回一個(gè)閉包,而是在***個(gè)參數(shù)列表后加上了第二個(gè)參數(shù)列表,然后在函數(shù)體內(nèi)省略了顯式的閉包創(chuàng)建:
- struct CornmealPizza {
- func getCrustGrainSetterWithCurry(inout grain: Grain)() -> Void {
- grain = .Corn
- }
- }
和顯式創(chuàng)建閉包時(shí)一樣,我們調(diào)用這個(gè)函數(shù)然后返回一個(gè)閉包:
- var grain: Grain = .Wheat
- let pizza = CornmealPizza()
- let aClosure = pizza.getCrustGrainSetterWithCurry(&grain)
在上面的例子中,閉包被顯式創(chuàng)建但沒(méi)能成功為inout參數(shù)賦值,但這次就成功了:
- aClosure()
- grain // returns Corn
這說(shuō)明在柯里化函數(shù)中,inout參數(shù)可以正常使用,但是顯式的創(chuàng)建閉包時(shí)就不行了。
避免在柯里化函數(shù)中使用inout參數(shù),因?yàn)槿绻愫髞?lái)將柯里化改為顯式的創(chuàng)建閉包,這段代碼就會(huì)產(chǎn)生錯(cuò)誤
總結(jié):七個(gè)避免
-
在協(xié)議擴(kuò)展中重寫協(xié)議中的屬性時(shí)要仔細(xì)核對(duì)
-
在協(xié)議擴(kuò)展中定義的每一個(gè)屬性,需要在協(xié)議中進(jìn)行聲明
-
不要對(duì)導(dǎo)入的第三方協(xié)議進(jìn)行屬性擴(kuò)展,那樣可能需要?jiǎng)討B(tài)調(diào)度
-
如果一個(gè)新的屬性需要?jiǎng)討B(tài)調(diào)度,避免使用約束性協(xié)議擴(kuò)展
-
避免把一個(gè)有副作用的表達(dá)式的結(jié)果通過(guò)可選鏈賦值給等號(hào)左邊的變量
-
避免在閉包中使用inout參數(shù)
-
避免在柯里化函數(shù)中使用inout參數(shù),因?yàn)槿绻愫髞?lái)將柯里化改為顯式的創(chuàng)建閉包,這段代碼就會(huì)產(chǎn)生錯(cuò)誤