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