太長(zhǎng)不讀版
被動(dòng)態(tài)語(yǔ)言寵壞的前端在初次開(kāi)發(fā) Native 應(yīng)用的時(shí)候婆瓜,容易掉進(jìn)以下幾個(gè)坑:
- 害怕程序崩潰
- 滿篇 Optional
正文
首先說(shuō)一下我司的背景快集。我司大部分開(kāi)發(fā)人員是網(wǎng)頁(yè)前端開(kāi)發(fā)和 Java 后端開(kāi)發(fā)。公司最近搞了一個(gè) iOS 應(yīng)用開(kāi)發(fā)廉白,使用了蘋果的新開(kāi)發(fā)語(yǔ)言 Swift个初。
經(jīng)過(guò)了一段時(shí)間的開(kāi)發(fā),我發(fā)現(xiàn)前端開(kāi)發(fā)者在初次開(kāi)發(fā)原生應(yīng)用猴蹂,特別是使用靜態(tài)語(yǔ)言時(shí)容易走入幾個(gè)誤區(qū)院溺。
害怕程序崩潰
一個(gè)非常有趣的現(xiàn)象是,我發(fā)現(xiàn)很多開(kāi)發(fā)者非常害怕程序崩潰掉磅轻,因此在代碼中加入了大量的防御性代碼珍逸。
其實(shí)這個(gè)東西見(jiàn)仁見(jiàn)智,但是個(gè)人認(rèn)為聋溜,在函數(shù)遇到與預(yù)期不符的狀況的時(shí)候谆膳,優(yōu)雅地崩潰掉比為了不崩潰而隱藏錯(cuò)誤重要得多。
舉一個(gè)非常簡(jiǎn)單的例子:某個(gè)成員函數(shù)需要使用某個(gè)成員撮躁,而這個(gè)成員在這個(gè)類初始化的時(shí)候并不一定存在漱病。
很多人會(huì)把代碼寫成這樣:
class MyObject {
var somethingToBeSetOutside: ImportData?
func foo() {
guard let sth = somethingToBeSetOutside else {
return
}
// use 'sth' the import data
...
...
...
}
}
這看起來(lái)并沒(méi)有什么問(wèn)題。但是如果將來(lái)某個(gè)時(shí)候,外部的調(diào)用者偶然沒(méi)有設(shè)置 somethingToBeSetOutside 這個(gè)成員的值杨帽,那么 foo 里面很大一部分代碼都將不會(huì)被調(diào)用漓穿。而且這個(gè) bug 將會(huì)非常難以查找。
那么這個(gè)問(wèn)題怎么解決呢注盈?其實(shí)在“古老”的 C/C++ 里面晃危,有一種簡(jiǎn)單的處理方式,那就是 assert ——調(diào)用者如果想要調(diào)用某個(gè)函數(shù)老客,那么傳入的參數(shù)必須滿足指定的契約山害。
例子:
class MyObject {
var somethingToBeSetOutside: ImportData!
func foo() {
assert(somethingToBeSetOutside != nil)
// use the import data
...
...
...
}
}
這個(gè)例子看起來(lái)和前面的代碼非常類似,僅有兩個(gè)區(qū)別:
- somethingToBeSetOutside 的類型是 ImportData!
- 加入斷言判空沿量,如果不滿足條件就讓程序崩潰
我們把類型修改為 ImportData! 的目的是為了 foo 下面的代碼能夠直接訪問(wèn)這個(gè) somethingToBeSetOutside 的內(nèi)容浪慌。而加入斷言的目的,是給這個(gè)函數(shù)加入了一個(gè)契約的概念朴则,就是說(shuō)我函數(shù)不負(fù)責(zé)檢查你傳入的參數(shù)权纤,你要調(diào)用我,那你得負(fù)責(zé)輸入的參數(shù)符合我的規(guī)則乌妒。
那么有人可能會(huì)說(shuō)了汹想,程序編譯成 release 以后,就不會(huì)做 assert 檢查了撤蚊,那你這程序交付給客戶的時(shí)候崩潰了掉了怎么辦古掏?
其實(shí)這很簡(jiǎn)單,這個(gè) assert 的作用是在開(kāi)發(fā)的時(shí)候防呆侦啸,在 debug 時(shí)候給開(kāi)發(fā)人員提示錯(cuò)誤原因槽唾,而不是用來(lái)保證 release 代碼正常工作的。只要應(yīng)用程序經(jīng)過(guò)了適當(dāng)?shù)臏y(cè)試光涂,基本上所有錯(cuò)誤調(diào)用的地方都能在上線前被找出來(lái)庞萍。
另外,如果遇到了數(shù)據(jù)真正是可能沒(méi)有的情況忘闻,代碼也不應(yīng)該這樣寫了钝计。
滿篇的 Optional
先上代碼:
struct APIReturnedPassenger {
let firstName: String?
let middleName: String?
let lastName: String?
}
因?yàn)椴荒艽_認(rèn) API 返回的數(shù)據(jù)是否包含了 Passenger 所有的信息,有些開(kāi)發(fā)人員就把結(jié)構(gòu)上的很多成員做成了 Optional 的齐佳。
這看起來(lái)似乎很好私恬。但是,如果我把這個(gè)翻譯成 C 的結(jié)構(gòu):
struct APIReturnedPassenger
{
char *firstName;
char *middleName;
char *lastName;
};
看出什么沒(méi)有炼吴?這似乎就是 C 語(yǔ)言被新人詬病的指針滿天飛本鸣!
這種例子非常好解決。
首先把結(jié)構(gòu)設(shè)計(jì)成下面這樣:
struct APIReturnedPassenger {
let firstName: String
let middleName: String
let lastName: String
}
然后在接收 API 返回內(nèi)容的地方缺厉,對(duì)返回的信息做驗(yàn)證即可永高。
如果 API 返回的信息不滿足我們預(yù)先設(shè)定好的格式,直接提示用戶服務(wù)器出現(xiàn)了故障或者使用預(yù)先設(shè)定的默認(rèn)值提针,后面的代碼寫起來(lái)不要舒服太多命爬。
再看一個(gè)例子:
假設(shè)這是一個(gè)點(diǎn)餐的系統(tǒng),客戶可以選擇不要甜點(diǎn)辐脖,一份甜點(diǎn)饲宛,或者兩份甜點(diǎn)∈燃郏恐怕下面這種代碼是很常見(jiàn)的:
func setDessert(firstDessert: Dessert?, secondDessert: Dessert?) {
...
}
可以想象艇抠,后面處理甜點(diǎn)的代碼將是大災(zāi)難,到處都需要判空久锥,各種復(fù)雜的邏輯處理家淤。
其實(shí) Swift 提供了一種機(jī)制,在處理類似的問(wèn)題上非常輕松愉快瑟由。
enum DessertType {
case NoDessert,
case OneDessert(Dessert),
case FullDessert(Dessert, Dessert)
}
func setDessert(dessert: DessertType) {
...
}
程序這樣寫以后絮重,我們可以發(fā)現(xiàn)完全不需要處理各種奇葩的邏輯了。
總結(jié)
使用靜態(tài)語(yǔ)言的原生開(kāi)發(fā)和使用動(dòng)態(tài)語(yǔ)言的前端開(kāi)發(fā)在開(kāi)發(fā)理念上有一些不一樣的地方歹苦。不同領(lǐng)域的開(kāi)發(fā)人員確實(shí)有不同的習(xí)慣青伤。
希望這篇簡(jiǎn)單的文章能夠幫助一些轉(zhuǎn)到原生開(kāi)發(fā)的開(kāi)發(fā)人員,我就很滿足了殴瘦。