文章總結(jié)翻譯自:Seven Swift Snares & How to Avoid Them
Swift正在完成一個(gè)驚人的壯舉,它正在改變我們?cè)谔O果設(shè)備上編程的方式画舌,引入了很多現(xiàn)代范例,例如:函數(shù)式編程和相比于OC這種純面向?qū)ο笳Z言更豐富的類型檢查已慢。
Swift語言希望通過采用安全的編程模式去幫助開發(fā)者避免bug曲聂。然而這也會(huì)不可避免的產(chǎn)生一些人造的陷阱,他們會(huì)在編譯器不報(bào)錯(cuò)的情況下引入一些Bug佑惠。這些陷阱有的已經(jīng)在Swift book中提到朋腋,有一些還沒有。這里有七個(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
)可以包含代碼审胚。照這種方式匈勋,我們可以用繼承樹來實(shí)現(xiàn)代碼的分享共用,樹的葉子是值類型(結(jié)構(gòu)體或枚舉類)膳叨,樹的內(nèi)部和根是協(xié)議和與他們對(duì)應(yīng)的擴(kuò)展洽洁。
但是Swift協(xié)議擴(kuò)展的實(shí)現(xiàn)依然是一片新的、未開發(fā)的領(lǐng)域菲嘴,尚存在一些問題饿自。代碼并不總是按照我們期望的那樣執(zhí)行。因?yàn)檫@些問題出現(xiàn)在值類型(結(jié)構(gòu)體與枚舉)與協(xié)議組合使用的場景下龄坪,我們將使用類與協(xié)議組合使用的例子去說明這種場景下不存在陷阱昭雌。當(dāng)我們重新改為使用值類型和協(xié)議的時(shí)候?qū)?huì)發(fā)生令人驚奇的事。
開始介紹我們的例子: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 }
我們可以通過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通過顯式的標(biāo)注override
避免這種錯(cuò)誤间聊。比如在這個(gè)例子中,我們用到了override
抵拘,但是拼寫錯(cuò)誤的"crustGain"其實(shí)并沒有重寫任何屬性哎榴,下面是修改后的代碼:
class CornmealPizza: Pizza {
override var crustGrain: Grain { return .Corn }
}
現(xiàn)在它可以通過編譯并成功運(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ā)性充尉、競爭條件飘言,我們可以使用值類型來避免這些。讓我們來試一下值類型的Pizza吧驼侠!
這里和上面一樣簡單姿鸿,只需要把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í)候谆吴,我們通過一個(gè)超類Pizza來達(dá)到目的。但是對(duì)于值類型將要求一個(gè)協(xié)議和一個(gè)協(xié)議擴(kuò)展來合作完成苛预。
protocol Pizza {}
extension Pizza { var crustGrain: Grain { return .Wheat } }
struct NewYorkPizza: Pizza { }
struct ChicagoPizza: Pizza { }
struct CornmealPizza: Pizza { let crustGain: Grain = .Corn }
這段代碼可以通過編譯句狼,我們來測試一下:
NewYorkPizza().crustGrain // returns Wheat
ChicagoPizza().crustGrain // returns Wheat
CornmealPizza().crustGrain // returns Wheat What?!
對(duì)于執(zhí)行結(jié)果,我們想說cornmeal pizza
并不是Wheat
制作的热某,返回結(jié)果出現(xiàn)錯(cuò)誤腻菇!哎呀!我把
struct CornmealPizza: Pizza { let crustGain: Grain = .Corn }
中的 crustGrain
寫成了crustGain
昔馋,再一次忘記了r
筹吐,但是對(duì)于值類型這里沒有override
關(guān)鍵字去幫助編譯器去發(fā)現(xiàn)我們的錯(cuò)誤。沒有編譯器的幫助绒极,我們不得不更加小心的編寫代碼骏令。
?? 在協(xié)議擴(kuò)展中重寫協(xié)議中的屬性時(shí)要仔細(xì)核對(duì)
ok,我們把這個(gè)拼寫錯(cuò)誤改正過來:
struct CornmealPizza: Pizza { let crustGrain: Grain = .Corn }
重新執(zhí)行
NewYorkPizza().crustGrain // returns Wheat
ChicagoPizza().crustGrain // returns Wheat
CornmealPizza().crustGrain // returns Corn Hooray!
為了在討論Pizza
的時(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í)際上并沒有這么做。這里的粗心將帶來巨大的陷阱脓钾。
在這種情況下售睹,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ò)展昼牛。
框架(庫)可以使一個(gè)程序?qū)虢涌谌ナ褂檬跷停槐匕嚓P(guān)實(shí)現(xiàn)。例如蘋果提供給我們提供了需要框架贰健,實(shí)現(xiàn)了用戶體驗(yàn)、系統(tǒng)設(shè)施和其他功能恬汁。Swift的擴(kuò)展允許程序向?qū)氲念惲娲弧⒔Y(jié)構(gòu)體、枚舉和協(xié)議中添加自己的屬性(這里的屬性并不是存儲(chǔ)屬性)氓侧。通過協(xié)議拓展添加的屬性脊另,就好像它原來就在協(xié)議中一樣。但實(shí)際上定義在協(xié)議拓展中的屬性并非一等公民约巷,因?yàn)橥ㄟ^協(xié)議拓展無法添加屬性的聲明偎痛。
我們首先實(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
屬性并沒有在協(xié)議中聲明踩麦,而是只是在擴(kuò)展中定義。然而氓癌,我們沒有辦法對(duì)框架的代碼進(jìn)行修改谓谦,因此也就不能解決這個(gè)問題。因此贪婉,想要通過擴(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)體來適應(yīng)各種情況巩螃。我們只需要傳入一個(gè)參數(shù)就可以確定進(jìn)餐的具體類型演怎。
struct Meal: MealProtocol {
let mainDish: MainDishOfMeal
}
結(jié)構(gòu)體Meal
繼承自MealProtocol
協(xié)議,它可以測試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
屬性歹叮,我們就可以訪問這個(gè)屬性跑杭。如果沒where
這里的限制,我們?cè)诓皇?code>Pizza的情況下訪問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可以通過靜態(tài)地檢查變量是否為nil
來避免錯(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腥泥,那么賦值語句執(zhí)行匾南,n++
也會(huì)執(zhí)行。但如果h為nil蛔外,不僅賦值語句不會(huì)執(zhí)行蛆楞,n++
也不會(huì)執(zhí)行。為了避免沒有發(fā)生副作用導(dǎo)致的令人驚訝的結(jié)果夹厌,我們應(yīng)該:
?? 避免把一個(gè)有副作用的表達(dá)式的結(jié)果通過可選鏈賦值給等號(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í)行過程中引用被捕獲的函數(shù)肄方。這些特性有助于我們寫出優(yōu)雅易讀的代碼冰垄,所以你也許會(huì)把它們結(jié)合起來使用,但這種結(jié)合有可能會(huì)導(dǎo)致問題权她。
我們重寫crustGrain
屬性來說明inout參數(shù)的使用播演,為簡單起見,開始時(shí)先不使用閉包:
enum Grain {
case Wheat, Corn
}
struct CornmealPizza {
func setCrustGrain(inout grain: Grain) {
grain = .Corn
}
}
為了測試這個(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ì)失效尼啡,所以:
?? 避免在閉包中使用inout參數(shù)
這個(gè)問題在Swift文檔中提到過,但還有一個(gè)與之相關(guān)的問題值得注意询微,這與創(chuàng)建的閉包的等價(jià)方法:柯里化有關(guān)崖瞭。
在使用柯里化技術(shù)時(shí),inout參數(shù)顯得前后矛盾撑毛。
在一個(gè)創(chuàng)建并返回閉包的函數(shù)中书聚,Swift為函數(shù)的類型和主體提供了一種簡潔的語法。盡管這種柯里化看上去僅是一種縮略表達(dá)式藻雌,但它與inout參數(shù)結(jié)合使用時(shí)卻會(huì)給人們帶來一些驚訝雌续。為了說明這一點(diǎn),我們用柯里化語法實(shí)現(xiàn)上面那個(gè)例子胯杭。函數(shù)沒有聲明為返回一個(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)建但沒能成功為inout參數(shù)賦值,但這次就成功了:
aClosure()
grain // returns Corn
這說明在柯里化函數(shù)中居暖,inout參數(shù)可以正常使用顽频,但是顯式的創(chuàng)建閉包時(shí)就不行了。
?? 避免在柯里化函數(shù)中使用inout參數(shù)膝但,因?yàn)槿绻愫髞韺⒖吕锘臑轱@式的創(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é)果通過可選鏈賦值給等號(hào)左邊的變量
- 避免在閉包中使用inout參數(shù)
- 避免在柯里化函數(shù)中使用inout參數(shù)丑孩,因?yàn)槿绻愫髞韺⒖吕锘臑轱@式的創(chuàng)建閉包,這段代碼就會(huì)產(chǎn)生錯(cuò)誤