多鄰國團(tuán)隊的Swift代碼實踐
最近我們剛剛發(fā)布了一款新的基于Swift的應(yīng)用,當(dāng)時還被蘋果著重推薦了,目前它已經(jīng)獲得了相當(dāng)多的用戶。在這片文章里,我們想要分享一下這些經(jīng)驗,把我們對于這個新語言的看法呈現(xiàn)給大家,并且指出Swift中那些可以讓我們寫出更好程序的新特性。
這不是一篇Swift入門指南,這篇文章的受眾是那些對Swift并不是很熟悉,而且好奇Swift在真實的編程過程中是怎么樣子的開發(fā)者。我們會引用一些技術(shù)概念并且會在合適的地方提供關(guān)于它們的入門指南和文檔的鏈接。
首先,我們會簡單介紹一下這個新的應(yīng)用是做什么的和我們的主要目標(biāo)是什么。
新的應(yīng)用
你可能已經(jīng)很熟悉我們的主應(yīng)用Duolingo,一個非常受歡迎的語言學(xué)習(xí)應(yīng)用,它擁有超過6000萬的用戶(截止2014年12月),它也曾被蘋果評為2013年年度應(yīng)用。如果你想要學(xué)習(xí)一門新的語言,Duolinggo將是你在你的iPhone或者iPad上的首選應(yīng)用。
之后,我們發(fā)布了Duolingo Test Center(下文稱Test Center),這個應(yīng)用非常實用,它可以讓你測試你對一門語言的掌握情況。例如,如果你是一個外國人,并且想要在美國或者英國的大學(xué)里尋求一份工作,這些工作通常都會要求你有一些官方證書,來證明你可以熟練使用英語。此應(yīng)用的用戶可以通過一些測試來讓用戶確定自己的語言水平,同時為了防止作弊,會有真人來監(jiān)督測試。
這款應(yīng)用發(fā)布伊始就被蘋果在超過50個國家的APP Store“最佳新應(yīng)用”中推薦。
目標(biāo)
性能方面,Test Center對性能要求并不高。應(yīng)用中大部分都是一些靜態(tài)內(nèi)容和少量的控件。另外,為了防止作弊,測試的全過程會被錄像,基本Test Center就是這樣了。我們在使用Swift的過程中并沒有碰到任何性能問題,但是還是必須要注意一下性能。
對我們來說更重要的是應(yīng)用的穩(wěn)定性和健壯性。由于測試會持續(xù)大約20分鐘并且它們是收費的,所以在測試途中崩潰會造成相當(dāng)差的用戶體驗1。另外,一旦一個測試開始,你就必須完成它(就是說,用戶不能暫停或退出此應(yīng)用;這樣做是為了防止作弊)。所以,我們需要將崩潰的可能性最小化。
對Swift的一般看法
當(dāng)Swift剛發(fā)布時,許多人只是看了看它的語法就開始拿它和其它語言作比較、下結(jié)論。有些人說他們現(xiàn)在“不再需要忍受Objective-C的語法”,可以直接開始iOS開發(fā)了。老實說,這種看法是錯誤的。誰會在意語法(只要語法不是很變態(tài))?對于一門語言來說,除了語法還有很多更重要的東西,比如它可以讓你更容易地表達(dá)你的想法,還有不鼓勵不好的行為。
Swift比Objective-C或者任何其它語言都能給我們帶來更多的啟發(fā)。如果你在Twitter上關(guān)注了Swift的一些作者,你就會知道他們從其它地方拿來了很多非常好的概念,包括函數(shù)式編程,同時他們也在合適的地方摒棄了很多現(xiàn)存的(但是并不十分理想)的概念。
由于我們已經(jīng)習(xí)慣了用Objective-C 來編程,Swift對我們來說是一個不錯的的且友好的進(jìn)步。如果你本來用的語言是Haskell(或者similar),你可能會覺得Swift仍有進(jìn)步的空間。同時我們也很期待未來的Swift版本會帶來什么更多的改進(jìn)。
優(yōu)點
Swift支持了很多新特性,這些特性是開發(fā)者在其它語言的使用過程中已經(jīng)習(xí)慣了的,像是自定義操作符和函數(shù)重載。值類型(含有字面上的值的類型,例如Swift的結(jié)構(gòu)體)可以讓你更容易的理解代碼。
我們也非常喜歡使用Swift中更強大的靜態(tài)類型系統(tǒng),還有類型推斷。尤其是當(dāng)Objective-C中沒有泛型時,在Swift中我們終于有了類型安全的集合,而不是只能希望在NSArray中存儲的是某一種類型的對象。
接下來詳細(xì)得看一下我們在Swift中發(fā)現(xiàn)的十分實用的特性
沒有Exception
到現(xiàn)在為止,Swift中還沒有錯誤處理。我們并不知道是Swift的作者在設(shè)計這門語言時特意不加入錯誤處理,或者只是因為當(dāng)時時間不夠。不論如何,我們覺得沒有錯誤處理是一件非常好的事情,因為(沒有被處理的)exception讓代碼更難被讀懂(被良好處理的exception能讓代碼變得更清晰,讓開發(fā)者知道在哪里會發(fā)生exception,但是它們又顯得過于笨重了,反正Objective-C中就不支持exception處理)。
事實上,在我們最常碰到的應(yīng)用崩潰的原因中,第七個就是因為蘋果提供的一個方法拋出了exception(-[AVAssetWriterInputHelper markAsFinished])。這個方法并沒有被標(biāo)記為會拋出exception,在文檔中也沒有注明,所以在真正看到這個崩潰報告之前,我們完全不知道它的行為會是這樣,而那時有些用戶的應(yīng)用已經(jīng)崩潰了。
有經(jīng)驗的Cocoa開發(fā)者會知道,盡管Objective-C提供exception拋出和處理的機制,但它只在極少數(shù)情況下使用,而且這些情況經(jīng)常是一些不可恢復(fù)的情況(盡管有一些例子)。在這種情況下,更好的解決方案可能不是去獲取并處理這個exception,而是去改善代碼,使得這個exception根本不會被拋出。有些人可能會爭論說這樣exception好像變成是一個失敗斷言方法,但可能這個概念本來的設(shè)計目的就是這樣呢,那么在一個含有assert()和fatalError()的新語言中,為什么還要保留它呢?
通常,我們都想要避免自己忘記去處理一個錯誤,更理想的情況,我們想要在編譯時就發(fā)現(xiàn)所有的問題,而不是在我們的應(yīng)用已經(jīng)崩潰之后。Exception 只會讓這變得困難,所有我們在Swift中為什么還需要使用它呢?
Optional
Swift中有很多非常重要的基本概念,Optional(你可能知道這個和Haskell中的Maybe類型很像)便是其中之一。蘋果的文檔中這么寫道:
Optional是一個有兩個值的枚舉類型,None和Some(T),它們分別代表無值和有值。所有類型都可顯式地(或者隱式地轉(zhuǎn)換為)一個Optional類型。
同時,Swift提供了簡單方便的使用Optional類型的語法糖,例如在None的情況下可以使用nil,特殊的展開語法,操作符等等。另外,Optional鏈還允許你寫出簡單清晰的包含多Optional依賴的代碼。
那么我們怎么使用它呢?Optional是一個非常好的用來表示“值可能為空”的方法,你可以用它作為函數(shù)的返回值類型,來表示這個函數(shù)可能會不返回任何結(jié)果(只要你不好奇這究竟是為什么)
為什么這會比在Objective-C中給一個指針賦值為空更好呢?因為這樣編譯器(在編譯期)就能保證我們操作的是正確的類型。換句話說,在Swift中一個不是Optional類型的值永遠(yuǎn)不可能為空,另外,由于Swift中的Optional不僅僅是簡單的指針類型,所以他們的用處更廣泛。
這里是一個關(guān)于Optional使用的小例子:在Objective-C里,所有返回直指針類型的方法,比如對象初始化方法(例如-init),都可能會合法的返回nil(例如當(dāng)一個對象不能被初始化)。一個很明顯的例子就是+ (UIImage *)imageNamed:(NSString *)name,只通過看這個方法名,你并不能確定它會不會返回nil。
然而在Swift里你就可以。蘋果在Swift中引入了可失敗的初始化程序的概念,這樣就可以很方便地在類型的層面上表達(dá)一個方法不會返回nil。在Swift里,同樣的例子是這樣子的: init?(named name: String) -> UIImage,注意這里有個問號,這個問號表示如果標(biāo)識符為name的變量找不到時,init方法可能會返回nil。
我們在合適的地方大量的使用了這個特性(我們在試圖避免對Optional進(jìn)行顯式的拆包或者強制拆包)。如果一個表達(dá)式可能會返回nil(例如失敗時)而且我們不需要知道為什么,那么Optional便是很好的選擇。
Result
如果你有一個可能會失敗的函數(shù)調(diào)用,而且你想要知道為什么它會失敗,那么你可以使用Swift提供的Result(對于函數(shù)式編程的開發(fā)者而言,他就像Either的子類型),它會是一個既簡單又實用的選擇。
和Optional相似,Result使你可以在類型的層面上表示一個東西可能是一種類型的某個值,或者是一個NSError
像Optional一樣,Result也是一個簡單的枚舉類型,它有兩個枚舉值Success(T)和Failure(NSError)。正常情況下success枚舉值會包含你感興趣的正常的值,如果有錯誤,你會得到一個.Failure和一個描述性的NSError。
和Optional不同的是,Result不是Swift標(biāo)準(zhǔn)庫的一部分,也就是說,你必須自己定義它。(當(dāng)前階段,編譯器還缺少一些相關(guān)的特性,你需要找到一個變通方案。)
我們在我們的網(wǎng)絡(luò)通信、I/O、和代碼分析模塊的很多地方都用到了Result,這個方案要比老的NSError指針在函數(shù)里傳入傳出,或者通過一個completion塊來包含成功的值和錯誤指針(或者更復(fù)雜的布爾型返回值和NSError指針的一起使用的方案)要好太多了,
Result是一個相當(dāng)優(yōu)雅的解決方案,它能讓你寫出更好、更簡潔、更安全的代碼。在我們的應(yīng)用中,任何可能執(zhí)行失敗(非致命的失敗)的表達(dá)式都會返回一個Optional或者Result。
和Objective-C的互操作
與Objective-C的互操作是Swift設(shè)計時的一個很重要的考慮因素。如果蘋果僅僅是發(fā)布一個新的編程語言,然后想用Swift的實現(xiàn)完全代替之前的所有代碼庫是行不通的——至少現(xiàn)在還不行。另外,開發(fā)社區(qū)里還有大量的Objective-C的代碼,如果沒有與Objective-C不錯的互操作性,可能不會有人愿意去用Swift。
幸運的是,Swift和Objective-C之間的互操作相當(dāng)簡單,而且我們已經(jīng)在一個很小的范圍里進(jìn)行了一些實踐,效果還是不錯的。但是值得注意的是,有些Swift的概念(比如枚舉)在Objective-C中并不能直接使用。
例如,我們的應(yīng)用中有一個小的功能部件需要操作PDF文件,這個部件我們是用Swift來寫的,然后我們又想在主應(yīng)用中使用這個模塊,主應(yīng)用是用Objective-C寫的。哎,偏偏有一些方法使用了僅Swift中才有的特性,這就意味著這些方法不能在Objective-C中不能自動被橋接。為什么繞過這個問題,我們簡單地對Swift的方法做了一個包裝方法,這個方法可以在Objective-C中使用2。
當(dāng)然,在Swift中直接使用我們主應(yīng)用中已有的Objective-C代碼也是非常簡單的。如果想要這么做,你只要簡單地把那部分代碼從應(yīng)用中拿出來(或者更好,它本身就是一個單獨的模塊),然后通過一個橋接頭文件導(dǎo)入你的Swift代碼中。
缺點
盡管Swift相比Objective-C而言有了很多進(jìn)步,但是現(xiàn)在它還是有一些地方需要改進(jìn)的。例如,這門新語言缺少一些其他現(xiàn)代語言中常見的高可表達(dá)性。但是作為一個新的語言,可能這種情況會很快改變。
蘋果保障說會保證兼容性,但是還說他們可能會在合適的時候修改這門語言的一些特性(實際上,他們已經(jīng)這樣做過幾次了)。這就意味著在更新編譯器之后你可能必須去修改你的代碼,否則就不能編譯通過。我們知道這種事情會發(fā)生,并且也無所謂,幸運的是,對于我們現(xiàn)存的之前運行良好的代碼,“修復(fù)”它們往往不需要花太多的時間。
我們對Swift最不爽的——也是我們受挫的根源——可能并不是語言本身,而是與之配套的工具。在Xcode(蘋果的Objective-C和Swift的IDE)上使用Swift的體驗還不是很好。在我們開發(fā)的過程中,Xcode經(jīng)常會運行很卡或者直接崩潰。大部分時間里并沒有(或者很慢)代碼提示,基本上可以說沒有調(diào)試器,不穩(wěn)定而且不可信的語法高亮,編輯器很慢(一旦項目達(dá)到了一定的大小),還有沒有重構(gòu)工具。
另外,編譯器報的錯誤信息經(jīng)常難以理解,編譯器中也還有一些bug和缺失的特性(例如類型推斷經(jīng)常出錯)。
從我們開始使用到現(xiàn)在,Xcode已經(jīng)有了很大的進(jìn)步了,大部分都很好,只是有一些小地方破壞了編程體驗。我們希望蘋果能多一些關(guān)注并且不斷改進(jìn)這個開發(fā)工具。
一些數(shù)字
蘋果是在2014年6月的WWDC大會上發(fā)布Swift的,同年的7月底,我們啟動了Test Center,它是我們第一個只用Swift語言開發(fā)的應(yīng)用,之后我們在十一月中旬發(fā)布了它。開發(fā)到1.0版本耗費了3個月多一點的時間(一個程序員;Android版本和web版本那時已經(jīng)存在了,所以當(dāng)時我們確實已經(jīng)有了完整的后臺和設(shè)計)。
像我們之前說的,健壯性和穩(wěn)定性對我們而言非常重要,所以讓我們在這方面是怎么做的。
崩潰
在寫這篇文章時,Test Center已經(jīng)發(fā)布有大約兩個半月了,并且已經(jīng)有了相當(dāng)大的下載量和用戶量(可能要歸功于被蘋果推薦的原因)。
和其他任何第一版一樣,我們碰到了很多之前沒碰到過的問題,但是幸運的是,我們似乎并沒有忽略任何很重要的bug。到今天為止,test center的崩潰率在大約0.2%,好像還不錯嘛3。
如果仔細(xì)看一下崩潰組(由于同樣的原因造成的崩潰):崩潰組的第一名(造成了大約30%的崩潰)是由于外部的Objective-C庫。事實上,前五名中有四組是由于Objective-C的原因造成的(第五名是由于一個我們在最終的發(fā)布版本中忘了關(guān)掉的失敗斷言)。
還有一個值得注意的是,第七名是因為前面提到的那個蘋果提供的Objective-C函數(shù)中有時會拋出exception,而這點在文檔中并沒有體現(xiàn)(-[AVAssetWriterInputHelper markAsFinished])。
我們把這么低的崩潰率歸功于可靠的軟件架構(gòu)和我們對一些很好的編程原則的堅持,然而,Swift的優(yōu)良的設(shè)計也減低了很多bug產(chǎn)生的可能性,這對我們?nèi)?gòu)建我們的軟件架構(gòu)是很有幫助的。例如,使用Swift的類型系統(tǒng),很多的錯誤可以在編譯期被發(fā)現(xiàn),而不是在已發(fā)布產(chǎn)品運行時才被發(fā)現(xiàn)4。
編譯器性能
我們必須要問一個問題,對于一個像我們這種規(guī)模的項目,編譯器是怎么來編譯的。根據(jù)sloc的數(shù)據(jù),我們的項目中現(xiàn)在有10634行實際代碼(不包含空行和注釋等)。
清除Xcode的緩存,然后運行完time xcodebuild -configuration Release命令需要2分鐘,一次調(diào)試運行需要大約30秒的編譯時間。所有的測試都是在一個mid 2013 Retina MacBook Pro上做的。需要注意的是編譯xib也需要一定的時間,并不全是Swift5。
你可以明顯的感覺到Xcode會隨著你的項目的增長變得越來越慢,而且碰到這個問題的不僅僅是我們。循環(huán)時間(當(dāng)你在改動了代碼之后,從按下CMD+R,到應(yīng)用在模擬器里打開的時間)也比Objective-C要長。在一次簡單的測試中,在代碼中增加一行,要等14秒編譯,這個時間取決于在這行代碼中到底做了什么,而在Objective-C的項目中作相似的改動,只需要2、3秒。
當(dāng)然,這并不是復(fù)雜的編譯器基準(zhǔn)測試,所以可以有保留的看待這些數(shù)字。希望你至少能對現(xiàn)在的編譯器性能有一個大致的了解。
結(jié)論
對于Objective-C的長期開發(fā)者來說——尤其是那些對現(xiàn)代編程語言感興趣的——Swift是一個受歡迎的且激動人心的進(jìn)步,同時,由于(當(dāng)前的)開發(fā)工具的原因,它有時也可能會讓人倍感挫折。
我們已經(jīng)展示了(至少在我們這類的應(yīng)用中)Swift可以用來寫出穩(wěn)定的健壯的并且高容量的應(yīng)用。我們的主應(yīng)用Duolingo,也已經(jīng)使用了一部分Swift代碼,我們也計劃在將來更多的使用它。
那么為什么你會選擇Swift呢?只要你在開發(fā)大型項目時有保持更新的用戶(你只能支持iOS7以上)和耐心,Swift提供了一個新鮮的,良好結(jié)構(gòu)的編程語言選擇。我們真誠地推薦你試一下它,特別地,去理解一下蘋果想要推廣的這種編程哲學(xué)。
如果你正在使用Objective-C,那么轉(zhuǎn)換到Swift還是比較簡單并且直接的。你可以用和Objective-C一樣的編程方法來使用Swift。當(dāng)你使用到一些Swift中新的概念時,會很有趣。尤其現(xiàn)在好像有一種擁抱函數(shù)式編程的趨勢,我們認(rèn)為這挺好的。
如果你已經(jīng)有了一個基于Objective-C的應(yīng)用,你可能不想為了使用Swift而完全重寫整個應(yīng)用,但是你在增加模塊時可以考慮用Swift來實現(xiàn)。
如果時光倒流,你必須要重寫這個應(yīng)用,你還會用Swift嗎?會。