Swift進階 - 個人總結(jié)

Swift進階 - 個人總結(jié)

本章內(nèi)容來自于喵神翻譯的Swift進階累奈,有興趣的同學(xué)可以閱讀原書斥铺,更加詳細彻桃!

本章內(nèi)容來自于喵神翻譯的Swift進階,有興趣的同學(xué)可以閱讀原書晾蜘,更加詳細邻眷!

本章內(nèi)容來自于喵神翻譯的Swift進階,有興趣的同學(xué)可以閱讀原書剔交,更加詳細肆饶!

1.guard的設(shè)計理念

什么是 guard

與if語句相同的是,guard也是基于一個表達式的布爾值去判斷一段代碼是否該被執(zhí)行岖常。

與if語句不同的是驯镊,guard語句判斷其后的表達式布爾值為false時,才會執(zhí)行之后代碼塊里的代碼竭鞍,如果為true板惑,則跳過整個guard語句,繼續(xù)執(zhí)行下面的代碼偎快;而且guard語句只會有一個代碼塊冯乘,不像if語句可以if else多個代碼塊。

可以把guard近似的看做是Assert

使用guard語法晒夹,可以先對每個條件逐一做檢查裆馒,如果不符合條件判斷就退出(或者進行其他某些操作)。

當(dāng)使用guard語句進行可選項綁定時丐怯,綁定的常量喷好、變量也能在外層作用域中使用。

設(shè)計理念

增強程序可讀性响逢。保持代碼美觀绒窑。

很多情況下我們會遇到if金字塔( if 太多,首行縮進太嚴重舔亭,大塊代碼都被擠到一塊兒)些膨,尤其是在閉包里面使用 if 嵌套對于可讀性來說簡直是災(zāi)難。guard這時可以幫我們解套钦铺。

guard語句特別適合用來提前退出

2.為什么引入值類型

值類型存儲在棧上订雾,引用類型存儲在堆上,效率比較高

使用值類型矛洞,有可能在編譯器就把問題暴露出來洼哎。比如

// 我們都知道let是常量(常量的值是不允許改變的)烫映,引用類型和值類型的let在邏輯上是有本質(zhì)不同的。letarr1=NSMutableArray.init(array: ["123","123","456"], copyItems:false)arr1.removeObject(at:0)//以上編譯不會報錯噩峦,由于arr1是引用類型锭沟,存儲的是指針,指針不變就沒問題识补,對指向的對象沒有限制letarr2=["123","123","123"]arr2.remove(at:0)//以上編譯會報錯族淮,由于arr2是值類型復(fù)制代碼

值類型,每個實例保持一份數(shù)據(jù)拷貝凭涂,就不會出現(xiàn)一個實例的值在其他位置被修改的情況祝辣。

3.值類型和引用類型的區(qū)別

源地址:swift的值類型和引用類型

值類型(Value Type)

值類型,即每個實例保持一份數(shù)據(jù)拷貝切油。

在 Swift 中蝙斜,典型的有 struct,enum澎胡,以及 tuple 都是值類型孕荠。而平時使用的?Int,?Double滤馍,F(xiàn)loat岛琼,String底循,Array巢株,Dictionary,Set?其實都是用結(jié)構(gòu)體實現(xiàn)的熙涤,也是值類型阁苞。

Swift 中,值類型的賦值為深拷貝(Deep Copy)祠挫,值語義(Value Semantics)即新對象和源對象是獨立的那槽,當(dāng)改變新對象的屬性,源對象不會受到影響等舔,反之同理骚灸。

如果聲明一個值類型的常量,那么就意味著該常量是不可變的(無論內(nèi)部數(shù)據(jù)為?var/let)慌植。

引用類型(Reference Type)

引用類型甚牲,即所有實例共享一份數(shù)據(jù)拷貝。

在 Swift 中蝶柿,class 和閉包是引用類型丈钙。引用類型的賦值是淺拷貝(Shallow Copy),引用語義(Reference Semantics)即新對象和源對象的變量名不同交汤,但其引用(指向的內(nèi)存空間)是一樣的雏赦,因此當(dāng)使用新對象操作其內(nèi)部數(shù)據(jù)時,源對象的內(nèi)部數(shù)據(jù)也會受到影響。

如果聲明一個引用類型的常量星岗,那么就意味著該常量的引用不能改變(即不能被同類型變量賦值)填大,但指向的內(nèi)存中所存儲的變量是可以改變的。

函數(shù)傳參

在 Swift 中俏橘,函數(shù)的參數(shù)默認為常量栋盹,即在函數(shù)體內(nèi)只能訪問參數(shù),而不能修改參數(shù)值敷矫。具體來說:

值類型作為參數(shù)傳入時例获,函數(shù)體內(nèi)部不能修改其值

引用類型作為參數(shù)傳入時,函數(shù)體內(nèi)部不能修改其指向的內(nèi)存地址曹仗,但是可以修改其內(nèi)部的變量值

但是如果要改變參數(shù)值或引用榨汤,那么就可以在函數(shù)體內(nèi)部直接聲明同名變量,并把原有變量賦值于新變量怎茫,那么這個新的變量就可以更改其值或引用收壕。

當(dāng)值類型的變量作為參數(shù)被傳入函數(shù)時,相當(dāng)于創(chuàng)建了新的常量并初始化為傳入的變量值轨蛤,該參數(shù)的作用域及生命周期僅存在于函數(shù)體內(nèi)蜜宪。

當(dāng)引用類型的變量作為參數(shù)被傳入函數(shù)時,相當(dāng)于創(chuàng)建了新的常量并初始化為傳入的變量引用祥山,當(dāng)函數(shù)體內(nèi)操作參數(shù)指向的數(shù)據(jù)圃验,函數(shù)體外也受到了影響。

嵌套類型

值類型嵌套值類型

值類型嵌套值類型時缝呕,賦值時創(chuàng)建了新的變量澳窑,兩者是獨立的,嵌套的值類型變量也會創(chuàng)建新的變量供常,這兩者也是獨立的摊聋。

值類型嵌套引用類型

值類型嵌套引用類型時,賦值時創(chuàng)建了新的變量栈暇,兩者是獨立的麻裁,但嵌套的引用類型指向的是同一塊內(nèi)存空間,當(dāng)改變值類型內(nèi)部嵌套的引用類型變量值時(除了重新初始化)源祈,其他對象的該屬性也會隨之改變煎源。

引用類型嵌套值類型

引用類型嵌套值類型時,賦值時創(chuàng)建了新的變量新博,但是新變量和源變量指向同一塊內(nèi)存薪夕,因此改變源變量的內(nèi)部值,會影響到其他變量的值赫悄。

引用類型嵌套引用類型

引用類型嵌套引用類型時原献,賦值時創(chuàng)建了新的變量馏慨,但是新變量和源變量指向同一塊內(nèi)存,內(nèi)部引用類型變量也指向同一塊內(nèi)存地址姑隅,改變引用類型嵌套的引用類型的值写隶,也會影響到其他變量的值。

擴展

我們經(jīng)常會處理一些需要有明確的生命周期的對象讲仰,我們會去初始化這樣的對象慕趴,改變它,最后摧毀它鄙陡。舉個例子冕房,一個文件句柄 (file handle) 就有著清晰的生命周期:我們會打開它,對其進行一些操作趁矾,然后在使用結(jié)束后我們需要把它關(guān)閉耙册。如果我們想要打開兩個擁有不同屬性的文件句柄,我們就需要保證它們是獨立的毫捣。想要比較兩個文件句柄详拙,我們可以檢查它們是否指向著同樣的內(nèi)存地址。因為我們對地址進行比較蔓同,所以文件句柄最好是由引用類型來進行實現(xiàn)饶辙。這也正是 Foundation 框架中 FileHandle 類所做的事情。

其他一些類型并不需要生命周期斑粱。比如一個 URL 在創(chuàng)建后就不會再被更改弃揽。更重要的是,它在被摧毀時并不需要進行額外的操作 (對比文件句柄珊佣,在摧毀時你需要將其關(guān)閉)蹋宦。當(dāng)我們比較兩個 URL 變量時,我們并不關(guān)心它們是否指向內(nèi)存中的同一地址咒锻,我們所比較的是它們是否指向同樣的 URL。因為我們通過它們的屬性來比較 URL守屉,我們將其稱為值惑艇。在 Objective-C 里,我們用 NSURL 來實現(xiàn)一個不可變的對象拇泛。不過在 Swift 中對應(yīng)的 URL 卻是一個結(jié)構(gòu)體滨巴。

軟件中擁有生命周期的對象非常多 — 比如文件句柄,通知中心俺叭,網(wǎng)絡(luò)接口恭取,數(shù)據(jù)庫連接,view controller 都是很好的例子熄守。對于這些類型蜈垮,我們想在初始化和銷毀的時候進行特定的操作耗跛。在對它們進行比較的時候,我們也不是去比較它們的屬性攒发,而是檢查兩者的內(nèi)存地址是否一樣调塌。所有這些類型的實現(xiàn)都使用了對象,它們?nèi)际且妙愋汀?/p>

在大多數(shù)軟件里值類型也扮演著重要的角色惠猿。URL羔砾,二進制數(shù)據(jù),日期偶妖,錯誤姜凄,字符串,通知以及數(shù)字等趾访,這些類型只通過它們的屬性來定義檀葛。當(dāng)對它們進行比較的時候,我們不關(guān)心內(nèi)存地址腹缩。所有這些類型都可以使用結(jié)構(gòu)體來實現(xiàn)屿聋。

值永遠不會改變,它們具有不可變的特性藏鹊。這 (在絕大多數(shù)情況下) 是一件好事润讥,因為使用不變的數(shù)據(jù)可以讓代碼更容易被理解。不可變性也讓代碼天然地具有線程安全的特性盘寡,因為不能改變的東西是可以在線程之間安全地共享的楚殿。

Swift 中,結(jié)構(gòu)體是用來構(gòu)建值類型的竿痰。結(jié)構(gòu)體不能通過引用來進行比較脆粥,你只能通過它們的屬性來比較兩個結(jié)構(gòu)體。雖然我們可以用 var 來在結(jié)構(gòu)體中聲明可變的變量屬性影涉,但是這個可變性只體現(xiàn)在變量本身上变隔,而不是指里面的值。改變一個結(jié)構(gòu)體變量的屬性蟹倾,在概念上來說匣缘,和為整個變量賦值一個全新的結(jié)構(gòu)體是等價的。我們總是使用一個新的結(jié)構(gòu)體鲜棠,并設(shè)置被改變的屬性值肌厨,然后用它替代原來的結(jié)構(gòu)體。

結(jié)構(gòu)體只有一個持有者豁陆。比如柑爸,當(dāng)我們將結(jié)構(gòu)體變量傳遞給一個函數(shù)時,函數(shù)將接收到結(jié)構(gòu)體的復(fù)制盒音,它也只能改變它自己的這份復(fù)制表鳍。這叫做值語義?(value semantics)馅而,有時候也被叫做復(fù)制語義。而對于對象來說进胯,它們是通過傳遞引用來工作的用爪,因此類對象會擁有很多持有者,這被叫做引用語義?(reference semantics)胁镐。

因為結(jié)構(gòu)體只有一個持有者偎血,所以它不可能造成引用循環(huán)。而對于類和函數(shù)這樣的引用類型盯漂,我們需要特別小心颇玷,避免造成引用循環(huán)的問題。

值總是需要復(fù)制這件事情聽來可能有點低效就缆,不過帖渠,編譯器可以幫助我們進行優(yōu)化,以避免很多不必要的復(fù)制操作竭宰。因為結(jié)構(gòu)體非晨战迹基礎(chǔ)和簡單,所以這是可能的切揭。結(jié)構(gòu)體復(fù)制的時候發(fā)生的是按照字節(jié)進行的淺復(fù)制狞甚。除非結(jié)構(gòu)體中含有類,否則復(fù)制時都不需要考慮其中屬性的引用計數(shù)廓旬。當(dāng)使用 let 來聲明結(jié)構(gòu)體時哼审,編譯器可以確定之后這個結(jié)構(gòu)體的任何一個字節(jié)都不會被改變。另外孕豹,和 C++ 中類似的值類型不同涩盾,開發(fā)者沒有辦法知道和干預(yù)何時會發(fā)生結(jié)構(gòu)體的復(fù)制。這些簡化給了編譯器更多的可能性励背,來排除那些不必要的復(fù)制春霍,或者使用傳遞引用而非值的方式來優(yōu)化一個常量結(jié)構(gòu)體。

編譯器所做的對于值類型的復(fù)制優(yōu)化和值語義類型的寫時復(fù)制行為并不是一回事兒椅野。寫時復(fù)制必須由開發(fā)者來實現(xiàn)终畅,想要實現(xiàn)寫時復(fù)制,你需要檢測所包含的類是否有共享的引用竟闪。

和自動移除不必要的值類型復(fù)制不同,寫時復(fù)制是需要自己實現(xiàn)的杖狼。不過編譯器會移除那些不必要的“無效”淺復(fù)制炼蛤,以及像是數(shù)組這樣的類型中的代碼會執(zhí)行“智能的”寫時復(fù)制,兩者互為補充蝶涩,都是對值類型的優(yōu)化理朋。我們接下來很快就會看到如何實現(xiàn)你自己的寫時復(fù)制機制的例子絮识。

如果你的結(jié)構(gòu)體只由其他結(jié)構(gòu)體組成,那編譯器可以確保不可變性嗽上。同樣地次舌,當(dāng)使用結(jié)構(gòu)體時,編譯器也可以生成非呈薹撸快的代碼彼念。舉個例子,對一個只含有結(jié)構(gòu)體的數(shù)組進行操作的效率浅萧,通常要比對一個含有對象的數(shù)組進行操作的效率高得多逐沙。這是因為結(jié)構(gòu)體通常要更直接:值是直接存儲在數(shù)組的內(nèi)存中的。而對象的數(shù)組中包含的只是對象的引用洼畅。最后吩案,在很多情況下,編譯器可以將結(jié)構(gòu)體放到棧上帝簇,而不用放在堆里徘郭。

當(dāng)和 Cocoa 以及 Objective-C 交互時,我們可能通常都需要類丧肴。比如在實現(xiàn)一個 table view 的代理時残揉,我們除了使用類以外別無它選。Apple 的很多框架都重度依賴于子類闪湾,不過在某些問題領(lǐng)域冲甘,我們?nèi)匀荒軇?chuàng)建一個對象為值的類。舉個例子途样,在 Core Image 框架里江醇,CIImage 對象是不可變的:它們代表了一個永不變化的圖像。

有些時候何暇,決定你的新類型應(yīng)該是結(jié)構(gòu)體還是類并不容易陶夜。兩者表現(xiàn)得不太一樣,知曉其中的區(qū)別將有助于作出決定裆站。

4.類方法 .class 和 .static的區(qū)別

在Swift中 static 和 class 都表示“類型范圍作用域”的關(guān)鍵字条辟。在所有類型中(class、static宏胯、enum)中羽嫡,我們可以使用 .static 來描述類型作用域,.class 是專門用于修飾class類型的肩袍。

.static 可以修飾屬性(計算杭棵、存儲)和方法,而且所修飾的屬性和方法不可以被子類重寫氛赐。

.class 可以修飾方法和計算屬性魂爪,但是不能修飾存儲屬性先舷,而且所修飾的屬性和方法是可以被子類重寫的,重寫的時候可以使用 .class 修飾滓侍,也可以使用 .static 修飾蒋川。

在 Protocol 中定義類方法的時候,推薦使用 .static撩笆,因為它是通用的捺球。

5.Swift語言的優(yōu)勢

Swift代碼更好寫

從Objective-C中遷移來的API寫法更簡潔,更易于閱讀和維護浇衬。類型推斷使代碼更簡潔懒构,更健壯,去掉了引用頭文件并提供名稱空間耘擂。內(nèi)存自動管理胆剧,不需要鍵入分號。

類型推斷(從此不用在定義時就顯示的給出變量類型醉冤,編譯器可以靠上下文進行推斷)

引入了命名空間秩霍,從此不用再import其他文件

告別MRC,全面使用ARC

結(jié)構(gòu)體可添加方法蚁阳、支持拓展铃绒、支持協(xié)議

函數(shù)支持可選參數(shù),支持多返回值螺捐,支持函數(shù)入?yún)⒌咝瑸楹瘮?shù)式編程提供了強大支持。

可嵌套可添加方法可傳參的枚舉

支持泛型

多種編程范式支持

可選鏈?zhǔn)秸{(diào)用

支持面向協(xié)議編程定血,復(fù)用性更強

Swift更安全

Swift的nil和OC的nil不是同樣的概念赔癌,在OC中,nil是一個指向不存在對象的指針澜沟。在Swift中灾票,nil并不是一個指針,而是代表一個特定類型值不存在茫虽。不光是對象刊苍,基本類型、結(jié)構(gòu)體濒析、枚舉都可以被設(shè)置為nil

OC允許變量為nil正什,而nil并不明確的代表沒有,而代表指向空對象的指針号杏,它還是個指針埠忘,而空指針無法很明確的表示不存在。而Swift提出了可選(Optional)的概念馒索,從此莹妒,變量只有存在和不存在兩種狀態(tài)、方法調(diào)用因此也只存在調(diào)了和沒調(diào)兩種情況绰上,去除了OC對于nil指針的種種不確定性旨怠。有了存在和不存在,就可以很明確的指出合法和非法蜈块,例如對于存在的對象一定可以調(diào)用它的方法鉴腻、不存在的就一定不去調(diào)用了;String到Int類型轉(zhuǎn)換成功就一定會返回一個特定類型的對象百揭,失敗就一定返回不存在爽哎。而這一強大特性又可以被用在各種類型上,在很大程度上提高了語言的嚴謹性器一。

Swift是個類型安全的語言课锌,類型安全的語言需要開發(fā)者清楚每個變量的類型。如果你需要一個String類型的參數(shù)祈秕,就絕不能錯誤地將Int傳遞給它渺贤。正因為Swift是類型安全的,它在編譯期對你的代碼進行類型檢測并指出錯誤请毛,可以今早發(fā)現(xiàn)代碼中的類型錯誤志鞍。

除了“可選”的概念之外,類型安全也是Swift安全性的一大體現(xiàn)方仿。在OC中固棚,如果將一個CGFloat賦值給NSInteger夭织,并不會使代碼編譯不過徐块,但是冥冥之中就損失了精度。在之后的計算中就有可能因為精度問題而導(dǎo)致Bug扑馁,而這些Bug有時候是不好被發(fā)現(xiàn)的鳍征。在Swift中黍翎,不同類型之間的賦值都需要經(jīng)過類型轉(zhuǎn)換,否則編譯器會報錯艳丛。在寫類型轉(zhuǎn)換代碼的同時匣掸,正是你考慮精度的時刻,這樣的設(shè)計最開始會讓你覺得Swift好煩人氮双、好不智能碰酝,就像個老太太一樣嘮嘮叨叨,但就是這樣的機制保證了類型安全戴差、使得語言更加嚴謹送爸,代碼運行結(jié)果更加可預(yù)測。

Swift更快

Swift性能分析

Swift開源

Swift跨平臺

6.結(jié)構(gòu)體和類

結(jié)構(gòu)體和類的區(qū)別

結(jié)構(gòu)體 (和枚舉) 是值類型,而類是引用類型袭厂。在設(shè)計結(jié)構(gòu)體時墨吓,我們可以要求編譯器保證不可變性。而對于類來說纹磺,我們就得自己來確保這件事情帖烘。

使用類,我們可以通過繼承來共享代碼橄杨。而結(jié)構(gòu)體 (以及枚舉) 是不能被繼承的秘症。想要在不同的結(jié)構(gòu)體或者枚舉之間共享代碼,我們需要使用不同的技術(shù)式矫,比如像是組合乡摹、泛型以及協(xié)議擴展等。

內(nèi)存的管理方式有所不同采转。結(jié)構(gòu)體可以被直接持有及訪問聪廉,但是類的實例只能通過引用來間接地訪問。結(jié)構(gòu)體不會被引用氏义,但是會被復(fù)制锄列。也就是說,結(jié)構(gòu)體的持有者是唯一的惯悠,但是類卻能有很多個持有者邻邮。

結(jié)構(gòu)體和類的使用時機

當(dāng)我們需要一個簡單不需要繼承、不多變的數(shù)據(jù)時候我們首選結(jié)構(gòu)體克婶,因為在數(shù)據(jù)結(jié)構(gòu)上來說結(jié)構(gòu)體的存取效率是高于類的筒严,

反之當(dāng)我們需要一個數(shù)據(jù)結(jié)構(gòu)比較大,需要繼承情萤,變化比較多的時候我們選擇類鸭蛙,因為在變化的過程中結(jié)構(gòu)體可能會發(fā)生寫時復(fù)制,而類不會筋岛;

**下面舉一個簡單的例子:**以Array和NSMutableArray來說:

當(dāng)有一個數(shù)組娶视,數(shù)據(jù)量相對比較小,也不用去經(jīng)常改變它睁宰,只是用來存數(shù)據(jù)和取數(shù)據(jù)肪获,我們首先Array;

當(dāng)數(shù)組的數(shù)據(jù)量很大的時候柒傻,并且經(jīng)常要去對他進行添加孝赫,刪除等操作,并且經(jīng)常賦值給其他變量的話就推薦使用NSMutableArray红符。

7.談?wù)剬wift中extension的理解

首先extension在swift中類似oc的類目青柄,可以擴展方法伐债,計算屬性,不能添加存儲屬性致开;

可以通過extension讓類實現(xiàn)某個協(xié)議峰锁,一般這個用的也比較多,比如實現(xiàn)Comparable這個協(xié)議喇喉;

還有一個很重要的祖今,就是可以通過extension對協(xié)議進行擴展,添加默認實現(xiàn)拣技,屬于黑魔法吧,非常好用耍目。

8.Swift寫時復(fù)制

為什么會有寫時復(fù)制

在 Swift 中膏斤,典型的有 struct,enum邪驮,以及 tuple 都是值類型莫辨。而平時使用的?Int,?Double毅访,?Float沮榜,?String,?Array喻粹,?Dictionary蟆融,Set?其實都是用結(jié)構(gòu)體實現(xiàn)的,也是值類型守呜。

Swift 中型酥,值類型的賦值為深拷貝(Deep Copy),值語義(Value Semantics)即新對象和源對象是獨立的查乒,當(dāng)改變新對象的屬性弥喉,源對象不會受到影響,反之同理玛迄。

如果聲明一個值類型的常量由境,那么就意味著該常量是不可變的(無論內(nèi)部數(shù)據(jù)為?var/let)。

這樣看來一切都沒問題蓖议,但是在寫代碼的時候虏杰,這些值類型每次賦值的時候真的是重新在內(nèi)存中拷貝一份嗎?如果一個數(shù)組里存了上萬個元素拒担,現(xiàn)在把它賦值給另一個變量嘹屯,就必須要拷貝所有元素,即使這兩個數(shù)組的內(nèi)容是完全一致的从撼,那可以預(yù)見這對性能會造成多么糟糕的影響州弟。

既然我們能夠想到這樣的問題钧栖,那蘋果的工程師肯定也想到了。如何才能避免不必要的復(fù)制呢婆翔,Swift給出了優(yōu)化方案:Copy-On-Write(寫時復(fù)制)拯杠,即只有當(dāng)這個值需要改變時才進行復(fù)制行為。在Swift標(biāo)準(zhǔn)庫中啃奴,Array潭陪、Dictionary和Set都是通過寫時復(fù)制來實現(xiàn)的。

什么是寫時復(fù)制最蕾?

var x = [1,2,3]var y = x// 斷點復(fù)制代碼

這個時候我們打印一下 x 和 y 的內(nèi)存地址依溯,這里我用的是 lldb 命令fr v -R [object]?來查看對象內(nèi)存結(jié)構(gòu)。

(lldb) fr v -R x(Swift.Array) x = {? _buffer = {? ? _storage = {? ? ? rawValue = 0x0000600001c0ac40 {? ? ? ? ? ......省略無用信息? ? ? }? ? }? }}(lldb) fr v -R y(Swift.Array) y = {? _buffer = {? ? _storage = {? ? ? rawValue = 0x0000600001c0ac40 {? ? ? ? ? ......省略無用信息? ? ? }? ? }? }}復(fù)制代碼

由此我們可以看到 x 和 y 的內(nèi)存地址都是0x0000600001c0ac40瘟则,說明 x 和 y 此時是共享同一個實例黎炉。這個時候我們再加上下面的代碼:

y.append(4)// 斷點復(fù)制代碼

然后再打印一下地址:

(lldb) fr v -R x(Swift.Array) x = {? _buffer = {? ? _storage = {? ? ? rawValue = 0x000060000126b180 {? ? ? ? ? ......省略無用信息? ? ? }? ? }? }}(lldb) fr v -R y(Swift.Array) y = {? _buffer = {? ? _storage = {? ? ? rawValue = 0x0000600002301b60 {? ? ? ? ? ......省略無用信息? ? ? }? ? }? }}復(fù)制代碼

可以看到 x 和 y 的內(nèi)存地址不在相同了,說明此時它們不再共享同一個實例醋拧,y 進行了數(shù)據(jù)拷貝慷嗜。

Array 結(jié)構(gòu)體內(nèi)部含有指向某塊內(nèi)存的引用。這塊內(nèi)存就是用來存儲數(shù)組中元素丹壕。x 和 y 兩個數(shù)組一開始是共享同一塊內(nèi)存庆械。不過,當(dāng)我們改變 y 的時候菌赖,這個共享會被檢測到缭乘,內(nèi)存中的數(shù)據(jù)被拷貝出來,改變以后賦值給了 y盏袄。昂貴的元素復(fù)制操作只在必要的時候發(fā)生忿峻,這就是寫時復(fù)制。

內(nèi)部實現(xiàn)

在結(jié)構(gòu)體內(nèi)部存儲了一個指向?qū)嶋H數(shù)據(jù)的引用辕羽,在不進行修改操作的普通傳遞過程中逛尚,都是將內(nèi)部的引用的引用計數(shù)+1,在進行修改時刁愿,對內(nèi)部的引用做一次copy操作绰寞,再在這個復(fù)制出來的數(shù)據(jù)上進行真正的修改,從而保持其他的引用者不受影響铣口。

值類型嵌套引用類型

上面我們提到的 Array 內(nèi)部元素是 Int滤钱,兩者都是值類型,那么如果 Array 內(nèi)部的元素是引用類型呢脑题,情況會不會發(fā)生變化件缸?我們一起來看一下~

class Student {var name: Stringinit(name: String) {self.name = name}}struct School {var student = Student(name: "小明")}let school1 = School()var school2 = school1print(school1.student.name)print(school2.student.name)// 斷點1school2.student.name = "小紅"print(school1.student.name)print(school2.student.name)// 斷點2school2.student = Student(name: "小李")print(school1.student.name)print(school2.student.name)// 斷點3輸出結(jié)果:小明小明小紅小紅小紅小李復(fù)制代碼

斷點1,school1 和 school2 的內(nèi)存結(jié)構(gòu)

(lldb) fr v -R school1(Copy_On_WriteTest.ViewController.School) school1 = {? student = 0x00006000024053e0 {? ? name = {? ? ? _guts = {? ? ? ? _object = {? ? ? ? ? _object = 0x9000000105a02356? ? ? ? }? ? ? }? ? }? }}(lldb) fr v -R school2(Copy_On_WriteTest.ViewController.School) school2 = {? student = 0x00006000024053e0 {? ? name = {? ? ? _guts = {? ? ? ? _object = {? ? ? ? ? _object = 0x9000000105a02356? ? ? ? }? ? ? }? ? }? }}復(fù)制代碼

當(dāng)school1?賦值給school2?后叔遂,school1.student和school2.student的內(nèi)存地址都是0x00006000024053e0他炊,其引用類型實例變量?name?的地址也都是?0x9000000105a02356?争剿,它們共享同一個實例,其引用類型的實例變量也共享痊末。

1.修改結(jié)構(gòu)體內(nèi)引用類型的實例變量的值

斷點2蚕苇,school1 和 school2 的內(nèi)存結(jié)構(gòu)

(lldb) fr v -R school1(Copy_On_WriteTest.ViewController.School) school1 = {? student = 0x00006000024053e0 {? ? name = {? ? ? _guts = {? ? ? ? _object = {? ? ? ? ? _object = 0x9000000105a0235c? ? ? ? }? ? ? }? ? }? }}(lldb) fr v -R school2(Copy_On_WriteTest.ViewController.School) school2 = {? student = 0x00006000024053e0 {? ? name = {? ? ? _guts = {? ? ? ? _object = {? ? ? ? ? _object = 0x9000000105a0235c? ? ? ? }? ? ? }? ? }? }}復(fù)制代碼

而執(zhí)行school2.student.name = "小紅"?后,school1.student?與?school2.student?的內(nèi)存地址不變凿叠,其實例變量?name?內(nèi)存地址都發(fā)生改變且相同涩笤,還是共享同一個實例變量,也就是說盒件,雖然對值類型有所修改蹬碧,但沒有發(fā)生拷貝行為。

2.修改結(jié)構(gòu)體內(nèi)引用類型的值

斷點3履恩,school1 和 school2 的內(nèi)存結(jié)構(gòu)

(lldb) fr v -R school1(Copy_On_WriteTest.ViewController.School) school1 = {? student = 0x00006000024053e0 {? ? name = {? ? ? _guts = {? ? ? ? _object = {? ? ? ? ? _object = 0x9000000105a0235c? ? ? ? }? ? ? }? ? }? }}(lldb) fr v -R school2(Copy_On_WriteTest.ViewController.School) school2 = {? student = 0x0000600002405400 {? ? name = {? ? ? _guts = {? ? ? ? _object = {? ? ? ? ? _object = 0x9000000105a02362? ? ? ? }? ? ? }? ? }? }}復(fù)制代碼

school2.student?和school2.student.name?的內(nèi)存地址都發(fā)生變化锰茉,而school1.student?和school1.student.name?的內(nèi)存地址不變,說明切心,此時對結(jié)構(gòu)體進行了拷貝行為,而student?這個引用類型是直接指向另一個實例片吊,而不是對原實例進行修改绽昏。

自定義Struct如何實現(xiàn)寫時復(fù)制

作為一個結(jié)構(gòu)體的作者,你并不能免費獲得這種特性俏脊,你需要自己進行實現(xiàn)全谤。當(dāng)你自己的類型內(nèi)部含有一個或多個可變引用,同時你想要保持值語義爷贫,并且避免不必要的復(fù)制時认然,為你的類型實現(xiàn)寫時復(fù)制是有意義的。

在 Swift 中漫萄,我們可以使用?isKnownUniquelyReferenced?函數(shù)來檢查某個引用只有一個持有者卷员。

struct School {? ? private var student = Student(name: "小明")? ? var studentName: String {? ? ? ? get {? ? ? ? ? ? return student.name? ? ? ? }? ? ? ? set {? ? ? ? ? ? if isKnownUniquelyReferenced(&student) {? ? ? ? ? ? ? ? student.name = newValue? ? ? ? ? ? }? ? ? ? ? ? else {? ? ? ? ? ? ? ? student = Student(name: newValue)? ? ? ? ? ? }? ? ? ? }? ? }}復(fù)制代碼

9.Swift可選類型

為什么引入可選類型?

Swift的nil和OC的nil不是同樣的概念腾务,在OC中毕骡,nil是一個指向不存在對象的指針。在Swift中岩瘦,nil并不是一個指針未巫,而是代表一個特定類型值不存在。不光是對象启昧,基本類型叙凡、結(jié)構(gòu)體、枚舉都可以被設(shè)置為nil

OC允許變量為nil密末,而nil并不明確的代表沒有握爷,而代表指向空對象的指針跛璧,它還是個指針,而空指針無法很明確的表示不存在饼拍。而Swift提出了可選(Optional)的概念赡模,從此,變量只有存在和不存在兩種狀態(tài)师抄、方法調(diào)用因此也只存在調(diào)了和沒調(diào)兩種情況漓柑,去除了OC對于nil指針的種種不確定性。有了存在和不存在叨吮,就可以很明確的指出合法和非法辆布,例如對于存在的對象一定可以調(diào)用它的方法、不存在的就一定不去調(diào)用了茶鉴;String到Int類型轉(zhuǎn)換成功就一定會返回一個特定類型的對象锋玲,失敗就一定返回不存在。而這一強大特性又可以被用在各種類型上涵叮,在很大程度上提高了語言的嚴謹性惭蹂。

可選類型的底層邏輯

Swift實際上是使用枚舉來實現(xiàn)可選類型:

enumOptional :Reflectable,NilLiteralConvertible{caseNonecaseSome(T)init()init(_some:T)/// Haskell's fmap, which was mis-namedfuncmap(f: (T) ->U)->U?funcgetMirror()->MirrorTypestaticfuncconvertFromNilLiteral()->T?}復(fù)制代碼

當(dāng)Optional沒有值時,返回的 nil 其實就是Optional.None割粮,即沒有值盾碗。除了None以外,還有一個Some舀瓢,當(dāng)有值時就是被Some<T>包裝的真正的值廷雅,所以我們拆包的動作其實就是將Some里面的值取出來。

vararray=["one","two","three"]switcharray.index(of:"four") {case.some(letidx):array.remove(at: idx)case.none:break// 什么都不做}復(fù)制代碼

在這個 switch 語句中我們使用了完整的可選值枚舉語法京髓,在當(dāng)值為 some 的時候航缀,將其中的“關(guān)聯(lián)類型”進行了解包。這種做法非常安全堰怨,但是寫起來和讀起來都不是很順暢芥玉。Swift 2.0 中引入了使用 ? 作為在 switch 中對 some 進行匹配的模式后綴的語法,另外诚些,可選值遵守 NilLiteralConvertible 協(xié)議飞傀,因此你可以用 nil 來替代 .none:

switch array.index(of: "four") {case let idx?:array.remove(at: idx)case nil:break // 什么都不做}復(fù)制代碼

可選值解包

可選綁定

let str: String? = "sss"if let s = str {? ? print(s)}guard let s = str else {? ? return}print(s)復(fù)制代碼

強制解包

當(dāng)你確定自定義的可選類型一定有值時,可以使用操作符(!)進行強制解析诬烹,拿到數(shù)據(jù)砸烦,嘆號表示"我知道一定有值,請使用它",但是當(dāng)你判斷錯誤绞吁,可選值為nil時使用(!)進行強制解析幢痘,會有運行錯誤。

var myStr:String? = nilmyStr="強制解析家破,一定有值颜说,否則運行出錯"print(myStr!)復(fù)制代碼

規(guī)則:當(dāng)你能確定你的某個值不可能為 nil 時可以使用強制解包购岗,你應(yīng)當(dāng)會希望如果它不巧意外的是 nil 的話,程序直接掛掉门粪。

改進強制解包的錯誤信息

使用強制解包的時候喊积,如果程序出錯,你從輸出中無法知道發(fā)生問題的原因是什么玄妈。當(dāng)然你實際上可以加上注釋來提醒這里為什么需要強制解包乾吻,那么為什么不考慮把這個注釋直接作為錯誤信息呢?

infix operator !!func !!(wrapped:T?, failureText:@autoclosure() -> String) -> T {? ? if let x = wrapped {? ? ? ? return x? ? }? ? fatalError(failureText())}復(fù)制代碼

這樣在出錯的時候我們就可以在控制臺看到錯誤信息了

let s = "foo"let i = Int(s) !! "Expecting integer, got\"\(s)\""復(fù)制代碼

在調(diào)試版本中進行斷言

在調(diào)試版本中我們可以讓程序崩潰拟蜻,但是在發(fā)布版本中绎签,最好還是不要,而是提供一個默認值酝锅。我們可以選擇在調(diào)試版本中使用斷言诡必,讓程序崩潰,而在最終版本中搔扁,將它替換為默認值爸舒。

我們可以實現(xiàn)一個?!??操作符來代表這個行為,我們將這個操作定義為對失敗的解包進行斷言稿蹲,并且在斷言不觸發(fā)的發(fā)布版本中將值替換為默認值碳抄。

infix operator !?func !?(wrapped: T?, failureText: @autoclosure () -> String) -> T{assert(wrapped != nil, failureText())return wrapped ?? 0}復(fù)制代碼

現(xiàn)在,下面的代碼將在調(diào)試時觸發(fā)斷言场绿,但是在發(fā)布版本中打印 0:

let i = Int(s) !? "Expecting integer, got \"\(s)\"復(fù)制代碼

如果你想要顯式地提供一個不同的默認值,或者是為非標(biāo)準(zhǔn)的類型提供這個操作符嫉入,我們可以定義一個接受多元組為參數(shù)的版本焰盗,多元組中包含默認值和錯誤信息:

func !?(wrapped: T?, nilDefault: @autoclosure () -> (value: T, text: String)) -> T{assert(wrapped != nil, nilDefault().text)return wrapped ?? nilDefault().value}// 調(diào)試版本中斷言,發(fā)布版本中返回 5Int(s) !? (5, "Expected integer")復(fù)制代碼

隱式解包

通過在聲明時的數(shù)據(jù)類型后面加一個感嘆號(!)來實現(xiàn):

var str: String! = "Hello World!"print(str) // Hello World!復(fù)制代碼

可以看到?jīng)]有使用(?)進行顯式的折包也得到了Some中的值咒林,這個語法相當(dāng)于告訴編譯器:在我們使用Optional值前熬拒,這個Optional值就會被初始化,并且總是會有值垫竞,所以當(dāng)我們使用時澎粟,編譯器就幫我做了一次拆包。如果你確信你的變量能保證被正確初始化欢瞪,那就可以這么做活烙,否則還是不要嘗試為好。

可選值map和flatMap

map

我們現(xiàn)在有一個字符數(shù)組遣鼓,我們想要將第一個字符轉(zhuǎn)換為字符串:

let characters: [Character] = ["a", "b", "c"]String(characters[0]) // a復(fù)制代碼

不過啸盏,如果 characters 可能為空的話,我們在就需要用 if let骑祟,只在數(shù)組不為空的時候創(chuàng)建字符串:

var firstCharAsString: String? = nilif let char = characters.first {firstCharAsString = String(char)}復(fù)制代碼

這樣一來回懦,當(dāng)字符數(shù)組至少含有一個元素時气笙,firstCharAsString 將會是一個含有該元素的 String。如果字符數(shù)組為空的話怯晕,firstCharAsString 將會為 nil潜圃。

這種獲取一個可選值,并且在當(dāng)它不是 nil 的時候進行轉(zhuǎn)換的模式十分常見舟茶。Swift 中的可選值里專門有一個方法來處理這種情況谭期,它叫做 map。這個方法接受一個閉包稚晚,如果可選值有內(nèi)容崇堵,則調(diào)用這個閉包對其進行轉(zhuǎn)換。上面的函數(shù)用 map 可以重寫成:

let firstChar = characters.first.map { String($0) } // Optional("a")復(fù)制代碼

顯然客燕,這個 map 和數(shù)組以及其他序列里的 map 方法非常類似鸳劳。但是與序列中操作一系列值所不同的是,可選值的 map 方法只會操作一個值也搓,那就是該可選值中的那個可能的值赏廓。你可以把可選值當(dāng)作一個包含零個或者一個值的集合,這樣 map 要么在零值的情況下不做處理傍妒,要么在有值的時候會對其進行轉(zhuǎn)換幔摸。

flatmap

如果你對一個可選值調(diào)用 map,但是你的轉(zhuǎn)換函數(shù)本身也返回可選值結(jié)果的話颤练,最終結(jié)果將是一個雙重嵌套的可選值既忆。舉個例子,比如你想要獲取數(shù)組的第一個字符串元素嗦玖,并將它轉(zhuǎn)換為數(shù)字患雇。首先你使用數(shù)組上的 first,然后用 map 將它轉(zhuǎn)換為數(shù)字:

let stringNumbers = ["1", "2", "3", "foo"]let x = stringNumbers.first.map { Int($0) } // Optional(Optional(1))復(fù)制代碼

問題在于宇挫,map 返回可選值 (first 可能會是 nil)苛吱,Int(String) 也返回可選值 (字符串可能不是一個整數(shù)),最后 x 的結(jié)果將會是?Int??器瘪。

flatMap 可以把結(jié)果展平為單個可選值:

let y = stringNumbers.first.flatMap { Int($0) } // Optional(1)復(fù)制代碼

這么做得到的結(jié)果 y 將是?Int??類型翠储。

可選鏈

在 Objective-C 中,對 nil 發(fā)消息什么都不會發(fā)生橡疼。Swift 里援所,我們可以通過“可選鏈 (optional chaining)”來達到同樣的效果。

delegate?.callback()復(fù)制代碼

和 Objective-C 不同的是衰齐,Swift 編譯器會在你的值是可選值的時候警告你任斋。如果你的可選值值中確實有值,那么編譯器能夠保證方法肯定會被實際調(diào)用。如果沒有值的話废酷,這里的問號對代碼的讀者來說是一個清晰地信號瘟檩,表示方法可能會不被調(diào)用。

當(dāng)你通過調(diào)用可選鏈得到一個返回值時澈蟆,這個返回值本身也會是可選值墨辛。

多次調(diào)用被鏈接在一起形成一個鏈,如果任何一個節(jié)點為空(nil)將導(dǎo)致整個鏈?zhǔn)А?/p>

空和運算符 ??

很多時候趴俘,你會想要解包一個可選值睹簇,如果可選值是 nil 時,就用一個默認值來替代它寥闪。你可以使用????空合運算符來完成這件事:

let stringteger = "1"let number = Int(stringteger) ?? 0復(fù)制代碼

10.Swift函數(shù)式編程

Swift函數(shù)

要理解 Swift 里面的函數(shù)和閉包太惠,你需要切實弄明白三件事情,我們把這三件事按照重要程度進行了大致排序:

函數(shù)可以被賦值給變量疲憋,也能夠作為函數(shù)的輸入和輸出

函數(shù)可以捕獲存在于他們作用范圍之外的變量

函數(shù)可以使用?{}?來聲明為閉包表達式

Swift對函數(shù)進行簡化的特性:

如果你將閉包作為參數(shù)傳遞凿渊,并且你不再用這個閉包做其他事情的話,就沒有必要現(xiàn)將它存儲到一個局部變量中缚柳。

如果編譯器可以從上下文中推斷出類型的話埃脏,你就不需要指明它了。

如果閉包表達式的主體部分只包括一個單一的表達式的話秋忙,它將自動返回這個表達式的結(jié)果彩掐,你可以不寫 return。

Swift 會自動為函數(shù)的參數(shù)提供簡寫形式灰追,$0?代表第一個參數(shù)堵幽,$1?代表第二個參數(shù),以此類推弹澎。

如果函數(shù)的最后一個參數(shù)是閉包表達式的話谐檀,你可以將這個閉包表達式移到函數(shù)調(diào)用的圓括號的外部。

最后裁奇,如果一個函數(shù)除了閉包表達式外沒有別的參數(shù),那么方法名后面的調(diào)用時的圓括號也可以一并省略麦撵。

[1,2,3].map( { (i:Int) ->Intinreturni*2} )[1,2,3].map( { iinreturni*2} )[1,2,3].map( { iini*2} )[1,2,3].map( {$0*2} )[1,2,3].map() {$0*2}[1,2,3].map {$0*2}復(fù)制代碼

高階函數(shù)

map

可以對序列中的每一個元素做一次處理

// 計算字符串的長度letstringArray=["Objective-C","Swift","HTML","CSS","JavaScript"]funcstringCount(string:String)->Int{returnstring.characters.count}stringArray.map(stringCount)stringArray.map({string ->Intinreturnstring.characters.count})// $0代表數(shù)組中的每一個元素stringArray.map{return$0.characters.count}復(fù)制代碼

Map 的實現(xiàn):

extensionArray{funcmap(_transform: (Element) ->T)-> [T] {varresult: [T]=[]result.reserveCapacity(count)forxinself{result.append(transform(x))}returnresult}}復(fù)制代碼

flatmap(compactMap)

和 map 一樣刽肠,也是對序列元素進行變換,但是和 map 有幾點不同:

1.flatMap返回后的數(shù)組中不存在nil免胃,同時它會把Optional解包

let array = ["Apple", "Orange", "Puple", ""]let arr1 = array.map { a -> Int? in? ? let length = a.characters.count? ? guard length > 0 else { return nil }? ? return length? }arr1 // [{some 5}, {some 6}, {some 5}, nil]let arr2 = array.flatMap { a -> Int? in? ? let length = a.characters.count? ? guard length > 0 else { return nil}? ? return length? ? }arr2 // [5, 6, 5]復(fù)制代碼

2.flatMap還能把數(shù)組中存有數(shù)組的數(shù)組(二維數(shù)組音五、N維數(shù)組)一同打開變成一個新的數(shù)組

let array = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]let arr1 = array.map{ $0 }arr1 // [[1, 2, 3], [4, 5, 6], [7, 8, 9]]let arr2 = array.flatMap{ $0 }arr2 // [1, 2, 3, 4, 5, 6, 7, 8, 9]復(fù)制代碼

3.flatMap也能把兩個不同的數(shù)組合并成一個數(shù)組,這個合并的數(shù)組元素個數(shù)是前面兩個數(shù)組元素個數(shù)的乘積

let fruits = ["Apple", "Orange", "Puple"]let counts = [2, 3, 5]let array = counts.flatMap { count in? ? fruits.map ({ fruit in? ? ? ? return fruit + "? \(count)"? ? ? ? ? ? ? ? })}array // ["Apple 2", "Orange 2", "Puple 2", "Apple 3", "Orange 3", "Puple 3", "Apple 5", "Orange 5", "Puple 5"]復(fù)制代碼

flatmap 實現(xiàn):(Swift 3)

extensionArray{funcflatMap(_transform: (Element) -> [T])-> [T] {varresult: [T]=[]forxinself{result.append(contentsOf: transform(x))}returnresult}}復(fù)制代碼

filter

過濾羔沙,可以對序列中的元素按照某種規(guī)則進行一次過濾

// 篩選出字符串的長度小于10的字符串letstringArray=["Objective-C","Swift","HTML","CSS","JavaScript"]funcstringCountLess10(string:String)->Bool{returnstring.characters.count<10}stringArray.filter(stringCountLess10)stringArray.filter({string ->Boolinreturnstring.characters.count<10})// $0表示數(shù)組中的每一個元素stringArray.filter{return$0.characters.count<10}復(fù)制代碼

filter 實現(xiàn):

extensionArray{funcfilter(_isIncluded: (Element) ->Bool)-> [Element] {varresult: [Element]=[]forxinselfwhereisIncluded(x) {result.append(x)}returnresult}}復(fù)制代碼

reduce

如何將序列元素合并到一個總和的值中

map 和 filter 都作用在一個數(shù)組上躺涝,并產(chǎn)生另一個新的、經(jīng)過修改的數(shù)組扼雏。不過有時候坚嗜,你可能會想把所有元素合并為一個新的值夯膀。比如,要是我們想將元素的值全部加起來苍蔬,可以這樣寫:

let fibs = [1,1,2,3,5]var total = 0for num in fibs {total = total + num}total = 12復(fù)制代碼

reduce 方法對應(yīng)這種模式诱建,它把一個初始值 (在這里是 0) 以及一個將中間值 (total) 與序列中的元素 (num) 進行合并的函數(shù)進行了抽象。使用 reduce碟绑,我們可以將上面的例子重寫為這樣:

let sum = fibs.reduce(0) { total, num in total + num }// 運算符也是函數(shù)俺猿,所以我們也可以把上面的例子寫成這樣fibs.reduce(0, +)復(fù)制代碼

reduce 的輸出值的類型可以和輸入的類型不同。舉個例子格仲,我們可以將一個整數(shù)的列表轉(zhuǎn)換為一個字符串押袍,這個字符串中每個數(shù)字后面跟一個空格:

let s = fibs.reduce("") { str, num in str + "\(num) " }print(s) // "0 1 1 2 3 5"復(fù)制代碼

reduce 實現(xiàn):

extensionArray{funcreduce(_initialResult:Result,_nextPartialResult: (Result,Element) ->Result)->Result{varresult=initialResultforxinself{result=nextPartialResult(result, x)}returnresult}}復(fù)制代碼

11.Swift風(fēng)格指南

對于命名,在使用時能清晰表意是最重要凯肋。因為 API 被使用的次數(shù)要遠遠多于被聲明的次數(shù)谊惭,所以我們應(yīng)當(dāng)從使用者的角度來考慮它們的名字。盡快熟悉 Swift API 設(shè)計準(zhǔn)則否过,并且在你自己的代碼中堅持使用這些準(zhǔn)則午笛。

簡潔經(jīng)常有助于代碼清晰,但是簡潔本身不應(yīng)該獨自成為我們編碼的目標(biāo)苗桂。

務(wù)必為函數(shù)添加文檔注釋 — 特別是泛型函數(shù)药磺。

類型使用大寫字母開頭,函數(shù)煤伟、變量和枚舉成員使用小寫字母開頭癌佩,兩者都使用駝峰式命名法。

使用類型推斷便锨。省略掉顯而易見的類型會有助于提高可讀性围辙。

如果存在歧義或者在進行定義的時候不要使用類型推斷。(比如 func 就需要顯式地指定返回類型)

優(yōu)先選擇結(jié)構(gòu)體放案,只在確實需要使用到類特有的特性或者是引用語義時才使用類姚建。

除非你的設(shè)計就是希望某個類被繼承使用,否則都應(yīng)該將它們標(biāo)記為 final吱殉。

除非一個閉包后面立即跟隨有左括號掸冤,否則都應(yīng)該使用尾隨閉包 (trailing closure) 的語法。

使用 guard 來提早退出方法友雳。

避免對可選值進行強制解包和隱式強制解包稿湿。它們偶爾有用,但是經(jīng)常需要使用它們的話往往意味著有其他不妥的地方押赊。

不要寫重復(fù)的代碼饺藤。如果你發(fā)現(xiàn)你寫了好幾次類似的代碼片段的話,試著將它們提取到一個函數(shù)里,并且考慮將這個函數(shù)轉(zhuǎn)化為協(xié)議擴展的可能性涕俗。

試著去使用 map 和 reduce罗丰,但這不是強制的。當(dāng)合適的時候咽袜,使用 for 循環(huán)也無可厚非丸卷。高階函數(shù)的意義是讓代碼可讀性更高。但是如果使用 reduce 的場景難以理解的話询刹,強行使用往往事與愿違谜嫉,這種時候簡單的 for 循環(huán)可能會更清晰。

試著去使用不可變值:除非你需要改變某個值凹联,否則都應(yīng)該使用 let 來聲明變量沐兰。不過如果能讓代碼更加清晰高效的話,也可以選擇使用可變的版本蔽挠。用函數(shù)將可變的部分封裝起來住闯,可以把它帶來的副作用進行隔離。

Swift 的泛型可能會導(dǎo)致非常長的函數(shù)簽名澳淑。壞消息是我們現(xiàn)在除了將函數(shù)聲明強制寫成幾行以外比原,對此并沒有什么好辦法。我們會在示例代碼中在這點上保持一貫性杠巡,這樣你能看到我們是如何處理這個問題的量窘。

除非你確實需要,否則不要使用 self.氢拥。在閉包表達式中蚌铜,使用 self 是一個清晰的信號,表明閉包將會捕獲 self嫩海。

盡可能地對現(xiàn)有的類型和協(xié)議進行擴展冬殃,而不是寫一些全局函數(shù)。這有助于提高可讀性叁怪,讓別人更容易發(fā)現(xiàn)你的代碼审葬。

12.Swift泛型

使用泛型進行代碼設(shè)計

我們已經(jīng)看到了很多將泛型用來為同樣的功能提供多種實現(xiàn)的例子。我們可以編寫泛型函數(shù)奕谭,但是卻對某些特定的類型提供不同的實現(xiàn)耳璧。同樣,使用協(xié)議擴展展箱,我們還可以編寫同時作用于很多類型的泛型算法。

泛型在你進行程序設(shè)計時會非常有用蹬昌,它能幫助你提取共通的功能混驰,并且減少模板代碼。在這一節(jié)中,我們會將一段普通的代碼進行重構(gòu)栖榨,使用泛型的方式提取出共通部分昆汹。除了可以創(chuàng)建泛型的方法以外,我們也可以創(chuàng)建泛型的數(shù)據(jù)類型婴栽。

讓我們來寫一些與網(wǎng)絡(luò)服務(wù)交互的函數(shù)满粗。比如,獲取用戶列表的數(shù)據(jù)愚争,并將它解析為 User 數(shù)據(jù)類型。我們創(chuàng)建一個 loadUsers 函數(shù),它可以從網(wǎng)上異步加載用戶着倾,并且在完成后通過一個回調(diào)來傳遞獲取到的用戶列表寺旺。

當(dāng)我們用最原始的方式來實現(xiàn)的話,首先我們要創(chuàng)建 URL鞍陨,然后我們同步地加載數(shù)據(jù) (這里只是為了簡化我們的例子步淹,所以使用了同步方式。在你的產(chǎn)品中诚撵,你應(yīng)當(dāng)始終用異步方式加載你的數(shù)據(jù))缭裆。接下來,我們解析 JSON寿烟,得到一個含有字典的數(shù)組澈驼。最后,我們將這些 JSON 對象變形為 User 結(jié)構(gòu)體:

func loadUsers(callback: ([User]?) -> ()) {let usersURL = webserviceURL.appendingPathComponent("/users")let data = try? Data(contentsOf: usersURL)let json = data.flatMap {try? JSONSerialization.jsonObject(with: $0, options: [])}let users = (json as? [Any]).flatMap { jsonObject injsonObject.flatMap(User.init)}callback(users)}復(fù)制代碼

loadUsers 函數(shù)有三種可能發(fā)生錯誤的情況:URL 加載可能失敗韧衣,JSON 解析可能失敗盅藻,通過 JSON 數(shù)組構(gòu)建用戶對象也可能失敗。在這三種情況下畅铭,我們都返回 nil氏淑。通過對可選值使用 flatMap,我們能確保只對那些成功的對象進行接下來的操作硕噩。不這么做的話假残,第一個失敗操作造成的 nil 值將傳播到接下來的操作,直至結(jié)束炉擅。我們在結(jié)束的時候會調(diào)用回調(diào)辉懒,傳回一個有效的用戶數(shù)組,或者傳回 nil谍失。

現(xiàn)在眶俩,如果我們想要寫一個相同的函數(shù)來加載其他資源,我們可能需要復(fù)制這里的大部分代碼快鱼。打個比方颠印,我們需要一個加載博客文章的函數(shù)纲岭,它看起來是這樣的:

func loadBlogPosts(callback: ([BlogPost])? -> ())復(fù)制代碼

函數(shù)的實現(xiàn)和前面的用戶函數(shù)幾乎相同。不僅代碼重復(fù)线罕,兩個方法同時也都很難測試止潮,我們需要確保網(wǎng)絡(luò)服務(wù)可以在測試是被訪問到,或者是找到一個模擬這些請求的方法钞楼。因為函數(shù)接受并使用回調(diào)喇闸,我們還需要保證我們的測試是異步運行的。

提取共通功能

相比于復(fù)制粘貼询件,將函數(shù)中 User 相關(guān)的部分提取出來燃乍,將其他部分進行重用,會是更好的方式雳殊。我們可以將 URL 路徑和解析轉(zhuǎn)換的函數(shù)作為參數(shù)傳入橘沥。因為我們希望可以傳入不同的轉(zhuǎn)換函數(shù),所以我們將 loadResource 聲明為 A 的泛型:

func loadResource(at path: String, parse: (Any) -> A?, callback: (A?) -> ()){let resourceURL = webserviceURL.appendingPathComponent(path)let data = try? Data(contentsOf: resourceURL)let json = data.flatMap {try? JSONSerialization.jsonObject(with: $0, options: [])}callback(json.flatMap(parse))}復(fù)制代碼

現(xiàn)在夯秃,我們可以將 loadUsers 函數(shù)基于 loadResource 重寫:

func loadUsers(callback: ([User]?) -> ()) {loadResource(at: "/users", parse: jsonArray(User.init), callback: callback)}復(fù)制代碼

我們使用了一個輔助函數(shù)座咆,jsonArray,它首先嘗試將一個 Any 轉(zhuǎn)換為一個 Any 的數(shù)組仓洼,接著對每個元素用提供的解析函數(shù)進行解析介陶,如果期間任何一步發(fā)生了錯誤,則返回 nil:

func jsonArray(_ transform: @escaping (Any) -> A?) -> (Any) -> [A]? {return { array inguard let arr = array as? [Any] else {return nil}return arr.flatMap(transform)}}復(fù)制代碼

對于加載博客文章的函數(shù)色建,我們只需要替換請求路徑和解析函數(shù)就行了:

func loadBlogPosts(callback: ([BlogPost]?) -> ()) {loadResource(at: "/posts", parse: jsonArray(BlogPost.init), callback: callback)}復(fù)制代碼

這讓我們能少寫很多重復(fù)的代碼哺呜。如果之后我們決定將同步 URL 處理重構(gòu)為異步加載時,就不再需要分別更新 loadUsers 或者 loadBlogPosts 了箕戳。雖然這些方法現(xiàn)在很短某残,但是想測試它們也并不容易:它們基于回調(diào),并且需要網(wǎng)絡(luò)服務(wù)處于可用狀態(tài)陵吸。

創(chuàng)建泛型數(shù)據(jù)類型

loadResource 函數(shù)中的 path 和 parse 耦合非常緊密玻墅,一旦你改變了其中一個,你很可能也需要改變另一個壮虫。我們可以將它們打包進一個結(jié)構(gòu)體中澳厢,用來描述要加載的資源。和函數(shù)一樣囚似,這個結(jié)構(gòu)體也可以是泛型的:

struct Resource {let path: Stringlet parse: (Any) -> A?}復(fù)制代碼

現(xiàn)在剩拢,我們可以在 Resource 上定義一個新的 loadResource 方法。它使用 resource 的屬性來確定要加載的內(nèi)容以及如何解析結(jié)果饶唤,這樣一來徐伐,方法的參數(shù)就只剩回調(diào)函數(shù)了:

extension Resource {func loadSynchronously(callback: (A?) -> ()) {let resourceURL = webserviceURL.appendingPathComponent(path)let data = try? Data(contentsOf: resourceURL)let json = data.flatMap {try? JSONSerialization.jsonObject(with: $0, options: [])}callback(json.flatMap(parse))}}復(fù)制代碼

相比于之前的用頂層函數(shù)來定義資源募狂,我們現(xiàn)在可以定義 Resource 結(jié)構(gòu)體實例办素,這讓我們可以很容易地添加新的資源魏保,而不必創(chuàng)建新的函數(shù):

let usersResource: Resource<[User]> = Resource(path: "/users", parse: jsonArray(User.init))let postsResource: Resource<[BlogPost]> = Resource(path: "/posts", parse: jsonArray(BlogPost.init))復(fù)制代碼

現(xiàn)在,添加一個異步的處理方法就非常簡單了摸屠,我們不需要改變?nèi)魏维F(xiàn)有的描述 API 接入點的代碼:

extension Resource {func loadAsynchronously(callback: @escaping (A?) -> ()) {? ? ? ? let resourceURL = webserviceURL.appendingPathComponent(path)? ? ? ? let session = URLSession.shared? ? ? ? session.dataTask(with: resourceURL) { data, response, error in? ? ? ? let json = data.flatMap {? ? ? ? try? JSONSerialization.jsonObject(with: $0, options: [])? ? ? ? }? ? ? ? callback(json.flatMap(self.parse))? ? ? ? }.resume()}}復(fù)制代碼

除了使用了異步的 URLSession API 以外,和同步版本相比粱哼,還有一個本質(zhì)上的不同是回調(diào)函數(shù)現(xiàn)在將從方法作用域中逃逸出來季二,所以它必須被標(biāo)記為@escaping。

現(xiàn)在揭措,我們將接入點和網(wǎng)絡(luò)請求完全解耦了胯舷。我們將 usersResource 和 postResource 歸結(jié)為它們的最小版本,它們只負責(zé)描述去哪里尋找資源绊含,以及如何解析它們桑嘶。這種設(shè)計也是可擴展的:你可以進行更多配置,比如添加 HTTP 請求方法或是為請求加上一些 POST 數(shù)據(jù)等躬充,你只需要簡單地在 Resouce 上增加額外屬性就可以了 (為了保持代碼干凈逃顶,你應(yīng)該指定一些默認值。比如對 HTTP 請求方法充甚,可以設(shè)定默認值為 GET)以政。

測試也變得容易很多。Resource 結(jié)構(gòu)體是完全同步伴找,并且和網(wǎng)絡(luò)解耦的盈蛮。測試 Resource 是否配置正確是很簡單的一件事。不過網(wǎng)絡(luò)部分的代碼依然難以測試技矮,當(dāng)然了抖誉,因為它天生就是異步的,并且依賴于網(wǎng)絡(luò)衰倦。但是這個復(fù)雜度現(xiàn)在被很好地隔離到了 loadAsynchronously 方法中袒炉,而代碼的其他部分都很簡單,也沒有受到異步代碼的影響耿币。

在本節(jié)中梳杏,我們從一個非泛型的從網(wǎng)絡(luò)加載數(shù)據(jù)的函數(shù)開始,接下來淹接,我們用多個參數(shù)創(chuàng)建了一個泛型函數(shù)十性,允許我們用簡短得多的方式重寫代碼。最后塑悼,我們把這些參數(shù)打包到一個單獨的 Resource 數(shù)據(jù)類型中劲适,這讓代碼的解耦更加徹底。對于具體資源類型的專用邏輯是于網(wǎng)絡(luò)代碼完全解耦的厢蒜。更改網(wǎng)絡(luò)層的內(nèi)容不會對資源層有任何影響霞势。

13.Swift運行時

方法派發(fā)

方法調(diào)用的派發(fā)方式主要有三種:

直接派發(fā)

函數(shù)表派發(fā)

消息機制派發(fā)

直接派發(fā)

直接派發(fā)是最快的烹植,不止是因為需要調(diào)用的指令集會更少,并且編譯器還能夠有很大的優(yōu)化空間愕贡,例如函數(shù)內(nèi)聯(lián)等草雕,直接派發(fā)也有人稱為靜態(tài)調(diào)用。

然而固以,對于編程來說直接調(diào)用也是最大的局限墩虹,而且因為缺乏動態(tài)性所以沒辦法支持繼承和多態(tài)。

函數(shù)表派發(fā)

函數(shù)表派發(fā)是編譯型語言實現(xiàn)動態(tài)行為最常見的實現(xiàn)方式憨琳。函數(shù)表使用了一個數(shù)組來存儲類聲明的每一個函數(shù)的指針诫钓。大部分語言把這個稱為 “virtual table”(虛函數(shù)表),Swift 里稱為 “witness table”篙螟。每一個類都會維護一個函數(shù)表菌湃,里面記錄著類所有的函數(shù),如果父類函數(shù)被 override 的話遍略,表里面只會保存被 override 之后的函數(shù)惧所。一個子類新添加的函數(shù),都會被插入到這個數(shù)組的最后墅冷。運行時會根據(jù)這一個表去決定實際要被調(diào)用的函數(shù)纯路。

查表是一種簡單,易實現(xiàn)寞忿,而且性能可預(yù)知的方式驰唬。然而,這種派發(fā)方式比起直接派發(fā)還是慢一點腔彰。從字節(jié)碼角度來看叫编,多了兩次讀和一次跳轉(zhuǎn),由此帶來了性能的損耗霹抛。另一個慢的原因在于編譯器可能會由于函數(shù)內(nèi)執(zhí)行的任務(wù)導(dǎo)致無法 優(yōu)化搓逾。(如果函數(shù)帶有副作用的話)

這種基于數(shù)組的實現(xiàn),缺陷在于函數(shù)表無法拓展杯拐。子類會在虛數(shù)函數(shù)表的最后插入新的函數(shù)霞篡,沒有位置可以讓 extension 安全地插入函數(shù).

消息機制派發(fā)

消息機制是調(diào)用函數(shù)最動態(tài)的方式。也是 Cocoa 的基石端逼,這樣的機制催生了 KVO朗兵,UIAppearence 和 CoreData 等功能。這種運作方式的關(guān)鍵在于開發(fā)者可以在運行時改變函數(shù) 的行為顶滩。不止可以通過 swizzling 來改變余掖,甚至可以用 isa-swizzling 修改對象的繼承關(guān)系,可以在面向?qū)ο蟮幕A(chǔ)上實現(xiàn)自定義派發(fā).

Swift運行時

純 Swift 類的函數(shù)調(diào)用已經(jīng)不再是 Objective-c 的運行時發(fā)消息礁鲁,而是類似 C++ 的 vtable盐欺,在編譯時就確定了 調(diào)用哪個函數(shù)赁豆,所以沒法通過 runtime 獲取方法、屬性冗美。

而 Swift 為了兼容 Objective-C魔种,凡是繼承自 NSObjec t的類都會保留其動態(tài)性,所以我們能通過 runtime 拿 到他的方法粉洼。這里有一點說明:老版本的 Swift(如2.2)是編譯期隱式的自動幫你加上了@objc务嫡,而4.0以后版 本的 Swift 編譯期去掉了隱式特性,必須使用顯式添加漆改。

不管是純 Swift 類還是繼承自 NSObject 的類只要在屬性和方法前面添加 @objc 關(guān)鍵字就可以使用 runtime。

值類型總是會使用直接派發(fā)准谚,簡單易懂

而協(xié)議和類的 extension 都會使用直接派發(fā)

NSObject 的 extension 會使用消息機制進行派發(fā)

NSObject 聲明作用域里的函數(shù)都會使用函數(shù)表進行派發(fā)

協(xié)議里聲明的挫剑,并且?guī)в心J實現(xiàn)的函數(shù)會使用函數(shù)表進行派發(fā)

可以在標(biāo)記為 final 的同時,也使用 @objc 來讓函數(shù)可以使用消息機制派發(fā)柱衔。這么做的結(jié)果就是樊破,調(diào)用函數(shù)的時候會使用直接派發(fā),但也會在 Objective-C 的運行時里注冊相應(yīng)的 selector唆铐。函數(shù)可以響應(yīng) perform(selector:) 以及別的 Objective-C 特性哲戚,但在直接調(diào)用時又可以有直接派發(fā)的性能。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末艾岂,一起剝皮案震驚了整個濱河市顺少,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌王浴,老刑警劉巖脆炎,帶你破解...
    沈念sama閱讀 222,627評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異氓辣,居然都是意外死亡秒裕,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,180評論 3 399
  • 文/潘曉璐 我一進店門钞啸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來几蜻,“玉大人,你說我怎么就攤上這事体斩∷笾桑” “怎么了?”我有些...
    開封第一講書人閱讀 169,346評論 0 362
  • 文/不壞的土叔 我叫張陵硕勿,是天一觀的道長哨毁。 經(jīng)常有香客問我,道長源武,這世上最難降的妖魔是什么扼褪? 我笑而不...
    開封第一講書人閱讀 60,097評論 1 300
  • 正文 為了忘掉前任想幻,我火速辦了婚禮,結(jié)果婚禮上话浇,老公的妹妹穿的比我還像新娘脏毯。我一直安慰自己,他們只是感情好幔崖,可當(dāng)我...
    茶點故事閱讀 69,100評論 6 398
  • 文/花漫 我一把揭開白布食店。 她就那樣靜靜地躺著,像睡著了一般赏寇。 火紅的嫁衣襯著肌膚如雪吉嫩。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,696評論 1 312
  • 那天嗅定,我揣著相機與錄音自娩,去河邊找鬼。 笑死渠退,一個胖子當(dāng)著我的面吹牛忙迁,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播碎乃,決...
    沈念sama閱讀 41,165評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼姊扔,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了梅誓?” 一聲冷哼從身側(cè)響起恰梢,我...
    開封第一講書人閱讀 40,108評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎梗掰,沒想到半個月后删豺,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,646評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡愧怜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,709評論 3 342
  • 正文 我和宋清朗相戀三年呀页,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拥坛。...
    茶點故事閱讀 40,861評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡蓬蝶,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出猜惋,到底是詐尸還是另有隱情丸氛,我是刑警寧澤,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布著摔,位于F島的核電站缓窜,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜禾锤,卻給世界環(huán)境...
    茶點故事閱讀 42,196評論 3 336
  • 文/蒙蒙 一私股、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧恩掷,春花似錦倡鲸、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,698評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至逼争,卻和暖如春优床,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背誓焦。 一陣腳步聲響...
    開封第一講書人閱讀 33,804評論 1 274
  • 我被黑心中介騙來泰國打工羔巢, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人罩阵。 一個月前我還...
    沈念sama閱讀 49,287評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像启摄,于是被迫代替她去往敵國和親稿壁。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,860評論 2 361

推薦閱讀更多精彩內(nèi)容