本文適合哪些人?
本文針對的是已經(jīng)有一部分Swift開發(fā)的基礎(chǔ),同時(shí)對函數(shù)式范式比較感興趣的開發(fā)者。 當(dāng)然,如果只對函數(shù)式范式感興趣,我覺得這篇文章也值得一看。
函數(shù)式編程是什么?
首先來看這個(gè)詞語”Functional Programming“,它是什么?
當(dāng)需要去查一個(gè)專業(yè)術(shù)語的定義的時(shí)候,我的第一反應(yīng)是來查詢Wikipedia:
?
In computer science, fucnitonal programming is a programming paradigm where programs are constructed by applying and composing fucntions.
在這個(gè)定義里,有一個(gè)很熟悉的詞——programming paradigm, 一般翻譯為編程范式,可是我對這個(gè)翻譯還是有些迷糊,于是我又在wikipedia中查找這個(gè)詞語的含義:
?
Programming paradigms are a way to classify programming languages based on their features.
編程范式(編程范例)是一種基于語言自身的特性來給編程語言分類的方式。
同時(shí)wikipedia中還總結(jié)了常見的編程范式的分類:
- imperative
- procedural
- object-oriented
- declarative
- functional
- logic
- mathematical
- reactive
那么究竟什么是編程范式呢?我們知道編程是一門工程學(xué),它的目的是去解決問題,而解決問題可以有很多的方法,編程范例就是代表著解決問題的不同思路。如果說我們是編程世界的造物主的話,那么編程范例應(yīng)該就是我們創(chuàng)造這個(gè)世界的方法論。所以我非常喜歡臺灣那邊對programming paradigm 的翻譯:程式設(shè)計(jì)法。
為什么我要強(qiáng)調(diào)編程范例是什么東西,而且還分門別類的列舉了出來這些編程范例呢?
因?yàn)榫幊瘫旧硎浅橄蟮模幊谭独鋵?shí)就是我們?nèi)绾纬橄筮@個(gè)世界的方法,我只是想通過這個(gè)具體的定義來說明函數(shù)式本身就是一種方法論。 所以我們學(xué)習(xí)的時(shí)候沒必要害怕它,遇到引用透明,副作用,科里化,函子,單子,惰性求值等等等等這些概念的時(shí)候,畏懼的原因只是不熟悉而已,就想我們學(xué)習(xí)面向?qū)ο蟮臅r(shí)候:繼承,封裝,多態(tài),動(dòng)態(tài)綁定,消息傳遞等等等等,這些概念我們一開始也不熟悉,所以當(dāng)我們熟悉了函數(shù)式這些概念的時(shí)候,一切自然水到渠成。 在我們熟悉的面向?qū)ο蟮木幊谭妒街?,我們知道它的思想是?strong>一切皆對象,而在純函數(shù)式的編程范式中,可以說:一切皆函數(shù)。在函數(shù)式編程中,函數(shù)是一等公民,那什么是一等公民呢?就是它可以作為參數(shù),返回值,也可以賦值給變量,也就是說它的地位其實(shí)是和Int,String, Double等基本類型是一樣的,換言之,要像使用基本類型一樣去使用它!
不同的思想就是創(chuàng)建世界的方法論的不同之處,這里我舉個(gè)例子,那就是狀態(tài),比如登錄的各種狀態(tài),維護(hù)狀態(tài)會(huì)大大增加系統(tǒng)的復(fù)雜性,特別是狀態(tài)很多的時(shí)候,而且引入狀態(tài)這個(gè)概念之后,會(huì)帶來很多復(fù)雜的問題:狀態(tài)持久化,環(huán)境模型等等等,而如果使用面向?qū)ο蟮木幊谭独?,可以?strong>每一個(gè)狀態(tài)都定義為一個(gè)對象,如C#中的狀態(tài)機(jī)的實(shí)現(xiàn),而在函數(shù)式編程里呢? 在SICP中提到,狀態(tài)是隨著時(shí)間改變的,所以狀態(tài)是否可以使用f(t)來表示呢?這就是使用函數(shù)式的思路來抽象狀態(tài)。
當(dāng)然,我這里并不是說只能使用一種編程范式,我也并不鼓吹函數(shù)式就一直是好的,但是掌握函數(shù)式可以讓我們在解決問題的時(shí)候提供更多的選擇,更有效率的解決問題,事實(shí)上,我們解決問題(創(chuàng)造世界)肯定會(huì)使用很多種方法論即多種編程范式,一般情況下,更現(xiàn)代的編程語言都支持多范式編程,這里用swift里的RxSwift來舉例:
public class Observable<Element> : ObservableType {
internal init()
public func subscribe<Observer>(_ observer: Observer) -> Disposable where Element == Observer.Element, Observer : RxSwift.ObserverType
public func asObservable() -> Observable<Element>
}
// 觀察者
final internal class AnonymousObserver<Element> : ObserverBase<Element> {
internal typealias EventHandler = (Event<Element>) -> Void
internal init(_ eventHandler: @escaping EventHandler)
override internal func onCore(_ event: Event<Element>)
}
extension ObservableType {
public func flatMap<Source>(_ selector: @escaping (Element) throws -> Source) -> Observable<Source.Element> where Source : RxSwift.ObservableConvertibleType
}
extension ObservableType {
public func map<Result>(_ transform: @escaping (Element) throws -> Result) -> Observable<Result>
}
它的Observable和Observer都抽象成了類,并且添加了相應(yīng)的行為,承擔(dān)了相應(yīng)的職責(zé),這是面向?qū)ο蠓妒?/strong>;它實(shí)現(xiàn)了OberveableType協(xié)議,并且拓展了該協(xié)議,添加了大量的默認(rèn)實(shí)現(xiàn),這是面向協(xié)議范式;它實(shí)現(xiàn)了map,和flatMap方法,可以說Observable是一個(gè)函數(shù)單子(Monad),同時(shí)也提供了大量的操作符可供使用和組合,這是函數(shù)式范式;同時(shí),總所周知,Reactive框架是一個(gè)響應(yīng)式的框架,所以它也是響應(yīng)式范式......
更何況,編程能力不就是抽象能力的體現(xiàn)嗎?所以我認(rèn)為掌握函數(shù)式是非常必要的!那么具體來說為什么重要呢?
在1984年的時(shí)候,John Hughes 有一篇很著名的論文《Why Functional Programming Matters》, 它解答了我們的疑問。
為什么函數(shù)式編程重要?
通常網(wǎng)絡(luò)上的一些文章都會(huì)總結(jié)它的優(yōu)點(diǎn):它沒有賦值,沒有副作用,沒有控制流等等等等,不同的只是它們對于各個(gè)關(guān)鍵詞諸如引用透明,無副作用的種種解釋,單是這只是列出了很多函數(shù)式程序 "沒有" 什么,卻沒有說它 “有” 什么,所以這些優(yōu)點(diǎn)其實(shí)沒有太大的說服力。而且我們實(shí)際上去寫程序的時(shí)候,也不可能特意去寫一個(gè) 缺少了賦值語句或者特別引用透明的程序,這也不是衡量質(zhì)量的尺度,那么真正重要的是什么呢?
在這篇論文中提到,模塊化設(shè)計(jì)是成功的程序化設(shè)計(jì)的關(guān)鍵,這一觀點(diǎn)已經(jīng)被普遍接受了,但有一點(diǎn)經(jīng)常容易被忽略,那就是編寫一個(gè)模塊化程序解決問題的時(shí)候,程序員首先要把問題分解為子問題,然后解決這些子問題并把解決方案合并。程序員能夠以什么方式分解問題,直接取決于他能以什么方式把解決方案粘起來。而函數(shù)式范式其實(shí)提供給我們非常重要的粘合劑,它可以讓我們設(shè)計(jì)一些更小、更簡潔、更通用的模塊,同時(shí)使用黏合劑粘合起來。
那么它提供了哪些黏合劑呢?這篇論文介紹了兩種:
黏合函數(shù):高階函數(shù)
?
The first of the two new kinds of glue enables simple functions to be glued together to make more complex ones.
黏合簡單的函數(shù)變?yōu)楦鼜?fù)雜的函數(shù)。這樣的好處是我們模塊化的顆粒度是更細(xì)的,可以組合的復(fù)雜函數(shù)也是更多的。如果非要做一個(gè)比喻的話,我覺得就像樂高的基礎(chǔ)組件:

這種聚合就是一個(gè)泛化的高階函數(shù)和一些特化函數(shù)的聚合,這樣的高階函數(shù)一旦定義,很多操作都可以很容易地編寫出來。
黏合程序:惰性求值
?
The other new kind of glue that functional languages provide enables whole programs to be glued together.
函數(shù)式語言提供的另一種黏合劑就是可以使得程序黏在一起。假設(shè)有這么一個(gè)函數(shù):
g(f(input))
傳統(tǒng)上,需要先計(jì)算f,然后再計(jì)算g,這是通過將f的輸出存儲(chǔ)在臨時(shí)文件中實(shí)現(xiàn)的,這種方法的問題是臨時(shí)文件會(huì)占用太大的空間,會(huì)讓程序之間的黏合變得不太現(xiàn)實(shí)。而函數(shù)式語言提供的這一種解決方案,程序f和g嚴(yán)格的同步運(yùn)行,只有當(dāng)g視圖讀取輸入時(shí),f才啟動(dòng)。這種求值方式盡可能得少運(yùn)行,因此被稱為 "惰性求值" 。它將程序模塊化為一個(gè)產(chǎn)生大量可能解的生成器與一個(gè)選取恰當(dāng)解的選擇器的方案變得可行。
大家如果有時(shí)間還是應(yīng)該去讀讀這一篇論文,在論文中,它講述了三個(gè)實(shí)例:牛頓-拉夫森求根法,數(shù)值微分,數(shù)值積分,以及啟發(fā)性搜索,并使用函數(shù)式來實(shí)現(xiàn)它們,非常的精彩,這里我就不復(fù)述這些實(shí)例了。最后我再引用一下該論文的結(jié)論:
?
在本文中,我們指出模塊化是成功的程序設(shè)計(jì)的關(guān)鍵。以提高生產(chǎn)力為目標(biāo)的程序語言,必須良好地支持模塊化程序設(shè)計(jì)。但是,新的作用域規(guī)則和分塊編譯的技巧是不夠的——“模塊化”不僅僅意味著“模塊”。我們分解程序的能力直接取決于將解決方案粘在一起的能力。為了協(xié)助模塊化程序設(shè)計(jì),程序語言必須提供優(yōu)良的黏合劑。函數(shù)式程序語言提供了兩種新的黏合劑——高階函數(shù)與惰性求值。
一顆棗樹(例子)
這個(gè)例子我參考了Objc.io的《函數(shù)式Swift》書籍中關(guān)于如何使用函數(shù)式的方式來封裝濾鏡的案例。
Core Image是一很強(qiáng)大的圖像處理框架,但是它的API是弱類型的 —— 可以通過鍵值編碼來配置圖像濾鏡,這樣就導(dǎo)致很容易出錯(cuò),所以可以使用類型來避免這些原因?qū)е碌倪\(yùn)行時(shí)錯(cuò)誤,什么意思呢?就是說我們可以封裝一些基礎(chǔ)的濾鏡Filter, 并且還可以實(shí)現(xiàn)它們之間的聚合方式。這就是上述論文中介紹的函數(shù)式編程提供的黏合劑之一:使簡單的函數(shù)可以聚合起來形成復(fù)雜的函數(shù)。
首先確定我們的濾鏡類型,該函數(shù)應(yīng)該接受一個(gè)圖像作為參數(shù)并返回一個(gè)新的圖像:
typalias Filter = (CIImage) -> CIImage
在這里引用一段書中的原話:
?
我們應(yīng)該謹(jǐn)慎地選擇類型。這比其他任何事情都重要,因?yàn)轭愋蛯⒆笥议_發(fā)流程。
然后可以開始定義函數(shù)來構(gòu)件特定的基礎(chǔ)濾鏡了:
/// sobel提取邊緣濾鏡
func sobel() -> Filter {
return { image in
let sobel: [CGFloat] = [-1, 0, 1, -2, 0, 2, -1, 0, 1]
let weight = CIVector(values: sobel, count: 9)
guard let filter = CIFilter(name: "CIConvolution3X3",
parameters: [kCIInputWeightsKey: weight,
kCIInputBiasKey: 0.5,
kCIInputImageKey: image]) else { fatalError() }
guard let outImage = filter.outputImage else { fatalError() }
return outImage.cropped(to: image.extent)
}
}
/// 顏色反轉(zhuǎn)濾鏡
func colorInvert() -> Filter {
return { image in
guard let filter = CIFilter(name: "CIColorInvert",
parameters: [kCIInputImageKey: image]) else { fatalError() }
guard let outImage = filter.outputImage else { fatalError() }
return outImage.cropped(to: image.extent)
}
}
/// 顏色變色濾鏡
func colorControls(h: NSNumber, s: NSNumber, b: NSNumber) -> Filter {
return { image in
guard let filter = CIFilter(name: "CIColorControls", parameters: [kCIInputImageKey: image, kCIInputSaturationKey: h, kCIInputContrastKey: s, kCIInputBrightnessKey: b]) else { fatalError() }
guard let outImage = filter.outputImage else { fatalError() }
return outImage.cropped(to: image.extent)
}
}
直接黏合
基礎(chǔ)組件已經(jīng)有了,接下來就可以堆積木了。如果有一個(gè)濾鏡需要:先提取邊緣 -> 顏色反轉(zhuǎn) -> 顏色變色,那么我們可以實(shí)現(xiàn)如下:
let newFilter: Filter = { image in
return colorControls(h: 97, s: 8, b: 85)(colorInvert()(sobel()(image)))
}
上述做法有一些問題:
- 可讀性差:無法代碼即注釋,無法很容易的知道濾鏡的執(zhí)行順序
- 不易拓展:API不友好,添加新的濾鏡時(shí),需要考慮順序和括號,很容易出錯(cuò)
自定義函數(shù)黏合
首先我們解決可讀性差的問題,因?yàn)橹苯邮褂们短渍{(diào)用方法,所以會(huì)可讀性差。所以我們要避免嵌套調(diào)用,直接定義combine方法來組合濾鏡:
func compose(filter filter1: @escaping Filter, with filter2: @escaping Filter) -> Filter {
return { image in
filter2(filter1(image))
}
}
// sobel -> invertColor
let newFilter1: Filter = compose(sobel(), colorInvert()) // 左結(jié)合的
這是左結(jié)合的,所以可讀性是OK的,但是如果有三個(gè)濾鏡組合呢?四個(gè)濾鏡組合呢?要定義那么多方法嗎? 巧了,還真有人是這么干的:

如果大家去看RxSwift的話,就會(huì)看見它組合多個(gè)Observable的函數(shù): zip , combineLastest ,每一個(gè)方法簇都提供了支持多個(gè)參數(shù)的組合方法,可是這就意味著我們在這個(gè)案例也是可以這樣做的,但是這顯然不是最好的解決方案。
如果使用combine這里三個(gè)濾鏡組合的方案:
let newFilter2: Filter = compose(compose(sobel(), colorInvert()), colorControls(h:97, s:8, b:85)))
可讀性還行,但是還是在添加新的濾鏡的時(shí)候容易出錯(cuò),不那么容易拓展。如果要再組合多個(gè)濾鏡,那么就需要多個(gè)combine函數(shù)嵌套調(diào)用。
自定義操作符黏合
如果對應(yīng)到數(shù)學(xué)領(lǐng)域的話,其實(shí)這幾個(gè)濾鏡的組合不就是四則運(yùn)算中的 + 嗎?一層一層效果的疊加,當(dāng)然,確切地說,從效果上和 + 更相似,但是從特性來說更符合減法 -的,都是向左結(jié)合,而且都不滿足交換律。
所以我們可以自定義操作符來處理濾鏡的結(jié)合:
infix operator >>>
func >>>(filter1: @escaping Filter, filter2: @escaping Filter) -> Filter {
return { image in
filter2(filter1(image))
}
}
當(dāng)然還有一個(gè)小問題,就是如果有三個(gè)濾鏡組合的話,會(huì)報(bào)錯(cuò),因?yàn)槲覀儧]有指定它組合的方式(左結(jié)合,還是右結(jié)合)所以這里我們讓它繼承加法的優(yōu)先級,因?yàn)樗图臃ㄒ粯佣际亲蠼Y(jié)合的:
infix operator >>>: AdditionPrecedence // 讓它繼承+操作符的優(yōu)先級, 左結(jié)合
func >>>(filter1: @escaping Filter, filter2: @escaping Filter) -> Filter {
return { image in
filter2(filter1(image))
}
}
那接下來我們愉快地使用它吧:
let filter = sobel() >>> colorInvert() >>> colorControls(h: 97, s: 8, b: 85)
let outputImage = filter(inputImage)
imageView.image = UIImage(ciImage: outputImage)

函數(shù)式Swift.001.jpeg
那么這里來總結(jié)一下這一波過程,假設(shè)需求是存在的:
我們定義了很多基礎(chǔ)濾鏡層(Filter),接下來肯定需要組合基礎(chǔ)濾鏡為我們實(shí)際需求需要的濾鏡,有的濾鏡可能是有三個(gè)基礎(chǔ)濾鏡組合的,有的需要五個(gè)基礎(chǔ)濾鏡組合,當(dāng)然極限情況下,可能還有需要十個(gè)濾鏡組合的。
所以我們需要定義不同濾鏡組合的黏合函數(shù), 我們一共經(jīng)歷了三個(gè)組合方案的變遷:
- 直接組合
- 定義compose函數(shù)
- 自定義操作符
當(dāng)然,諸君也可以使用更好的組合方案,如果可以希望留個(gè)言,共同探討探討。
還有一顆也是棗樹(例子)
接下來這個(gè)例子,是一個(gè)我們使用Objective-C編程的時(shí)候經(jīng)常會(huì)遇到的問題,需求如下:第二行數(shù)據(jù)必須等待第一行請求結(jié)束之后才可以開始請求。

那么開始吧!
首先我們來看最容易的實(shí)現(xiàn)方案:
@objc func syncData() {
self.statusLabel.text = "正在同步火影忍者數(shù)據(jù)"
WebAPI.requestNaruto { (firstResult) in
if case .success(let result) = firstResult {
self.sectionOne = result.map { $0 as? String ?? "" }
DispatchQueue.main.async {
self.tableView.reloadSections([0], with: .automatic)
self.statusLabel.text = "正在同步海賊王數(shù)據(jù)"
WebAPI.requestOnePiece { (secondResult) in
if case Result.success(let result) = secondResult {
self.sectionTwo = result.map { $0 as? String ?? "" }
DispatchQueue.main.async {
self.statusLabel.text = "同步海賊王數(shù)據(jù)成功"
self.tableView.reloadSections([1], with: .automatic)
}
}
}
}
}
}
}
熟悉嗎?當(dāng)然熟悉,直接在第一個(gè)請求的callback中直接進(jìn)行第二個(gè)請求,但是請注意,這和OC寫的有區(qū)別嗎?我們這樣和寫和簡單的人肉翻譯機(jī)有區(qū)別嗎?我們寫的是Swift這個(gè)多范式的編程語言嗎?
回到例子,我們就事論事,我覺得這樣寫會(huì)有幾個(gè)問題:
- 數(shù)據(jù)修改和UI修改耦合在了一起
- 多重嵌套
- 違背了OCP(Open Closed Principle)法則:應(yīng)該對修改閉合,對拓展開放
- 丑!
解決數(shù)據(jù)和UI耦合
從重要性的角度,我覺得應(yīng)該先解決第4個(gè)問題,但是出于節(jié)奏,我們還是從第一個(gè)問題開始解決吧~
@objc func syncDataThere() {
// 嵌套函數(shù)
func updateStatus(text: String, reload: (isReload: Bool, section: Int)) {
DispatchQueue.main.async {
self.statusLabel.text = text
if reload.isReload { self.tableView.reloadSections([reload.section], with: .automatic) }
}
}
updateStatus(text: "正在同步火影忍者數(shù)據(jù)", reload: (false, 0))
requestNaruto {
updateStatus(text: "正在同步海賊王數(shù)據(jù)", reload: (true, 0))
self.requestOnePiece {
updateStatus(text: "同步數(shù)據(jù)成功", reload: (true, 1))
}
}
}
這里我把網(wǎng)絡(luò)請求和數(shù)據(jù)處理都封裝到了網(wǎng)絡(luò)請求中,而且使用了swift的特性:嵌套函數(shù),剝離了一部分重復(fù)代碼,這樣整個(gè)請求就變得非常清晰明了了,而且數(shù)據(jù)和UI就隔離開來了,并沒有耦合在一起。
可是嵌套的問題還是存在,如何解決呢?
解決多重嵌套
還記得我介紹的第一棵棗樹嗎?我使用了自定義操作符來解決了函數(shù)調(diào)用的嵌套,這里其實(shí)也是一樣的思路,但是要更復(fù)雜些。
這里我還需要重復(fù)引用一下《函數(shù)式Swift》中的那句話:
?
我們應(yīng)該謹(jǐn)慎地選擇類型。這比其他任何事情都重要,因?yàn)轭愋蛯⒆笥议_發(fā)流程。
第一步抽象
這里有兩個(gè)類型需要抽象,第一是執(zhí)行單個(gè)語句的函數(shù)(這里是更新UI),第二個(gè)是對應(yīng)網(wǎng)絡(luò)請求的函數(shù)
infix operator ->> AdditionPrecedence
typealias Action = () -> Void
typealias Request = (@escaping Action) -> Void
第二步抽象
那么如何將原來的函數(shù)拆解為使用類型表示的函數(shù)呢?
func syncDataF() {
......
requestNaruto {
updateStatus(text: "正在同步海賊王數(shù)據(jù)", reload: (true, 0))
self.requestOnePiece {
updateStatus(text: "同步數(shù)據(jù)成功", reload: (true, 1))
}
}
)
我們由上往下,那么抽象的過程應(yīng)該就是
(Request, Action) -> Request
第一個(gè)請求 和 回調(diào)中的第一個(gè)Action,但是第一個(gè)請求還沒有結(jié)束,所以返回的還是Request
(Request, Request) -> Request
處理了第一個(gè)Action的第一請求 + 第二個(gè)請求, 但是請求還是沒有結(jié)束,所以返回的還是Request
(Request, Action) -> Action
第二個(gè)請求加上最后需要處理的Action , 完畢!
所以結(jié)果如下:
@objc func syncDataFour() {
func updateStatus(text: String, reload: (isReload: Bool, section: Int)) {
DispatchQueue.main.async {
self.statusLabel.text = text
if reload.isReload {
self.tableView.reloadSections([reload.section], with: .automatic)
}
}
}
updateStatus(text: "正在同步火影忍者數(shù)據(jù)", reload: (false, 0))
// 我們來拆解一下函數(shù):要把函數(shù)抽象出來,這一點(diǎn)非常的重要
// (Request, Action) -> Request
// (Request, Request) -> Request
// (Request, Action) -> Action
// 通過這樣的拆解方式就可以開始定義方法了
let task: Action =
requestNaruto
->> { updateStatus(text: "正在同步海賊王數(shù)據(jù)", reload: (true, 0)) }
->> requestOnePiece
->> { updateStatus(text: "同步數(shù)據(jù)成功", reload: (true, 1)) }
task()
}
結(jié)果呢?我解決了嵌套的問題,很好,很完美,可是也很天真。
解決OCP問題
即使我們使用了自定義操作符,也沒有解決OCP問題,因?yàn)槿绻覀円砑诱埱蟮脑?,我們還是需要修改原來的方法,依然違背了OCP法則。
那么怎么解決呢?
嗯嗯,具體的,請各位自己去試驗(yàn)吧!
我在文章尾部添加了相應(yīng)的引用信息,這個(gè)例子是基于2016年的國內(nèi)的Swift大會(huì)中翁陽的分享《Swift, 改善既有代碼的設(shè)計(jì)》,如果有時(shí)間,希望大家可以去看看這個(gè)分享。
在分享中,他使用了面向協(xié)議的思路解決了OCP問題,很抽象,很精彩。
總結(jié)
很開心諸位看到了這里,我覺得這篇文章的能量密度應(yīng)該不會(huì)浪費(fèi)你們的時(shí)間。
在這邊文章中,我首先是追問了函數(shù)式編程,以及編程范式的定義,只是想告訴大家:函數(shù)式編程之所以復(fù)雜只是因?yàn)槲覀儾皇煜?,同時(shí)它也應(yīng)該是我們必須的工具。
然后我介紹了《Why Functional Programming Matters》這篇論文,它說明了為什么函數(shù)式編程重要,提到函數(shù)式范式的兩大武器:高階函數(shù)和惰性求值。
最后我使用了兩顆棗樹來給大家看一看Swift語言結(jié)合函數(shù)式的思想可以有哪些奇妙的化學(xué)反應(yīng)。
那么這一次Swift的一次函數(shù)式之旅就結(jié)束了。但是還是想補(bǔ)充幾句,每一年的WWDC其實(shí)Swift都更新了很多的內(nèi)容,Swift本身也一直在增加新的特性,一直在穩(wěn)健的迭代著,如果我們還是使用Objective-C的思維去寫Swift的話,其實(shí)本身是落后于語言發(fā)展的。
最后引用王安石的《游褒禪山記》中的一段話:
?
而世之奇?zhèn)ァ⒐骞?,非常之觀,常在于險(xiǎn)遠(yuǎn),而人之所罕至焉,故非有志者不能至也。
與君共勉!