最近接觸swift之后尘执,發(fā)現(xiàn)一個與Object-C區(qū)別很大的地方是Object-C里面很多Class里面都換成了Struct類型了,在自己有限的知識范圍內(nèi)票从,感覺Struct并沒有Class那么好舶沛,因為大部分Struct內(nèi)存分配在棧上面,Class充分利用了堆棧铐望,似乎感覺Class要好一些,但是為什么swift會用這么多Struct呢茂附?還有一個問題是元組正蛙,當時一個同學(xué)在改寫一個數(shù)組元組的時候,感覺很別扭营曼,整個數(shù)組都要拷貝乒验,難道這樣的效率更高,帶著這樣的疑問再網(wǎng)上找了半天蒂阱,冥冥之中好像聽過“不可變變量”這種說法锻全,最后在函數(shù)式編程當中找到了一些答案,所以本文只是函數(shù)式編程的一個基礎(chǔ)概念理解蒜危,如果有上面不對的地方虱痕,歡迎大家指正。
什么是函數(shù)式編程:
In computer science, functional programming is a programming paradigm—a style of building the structure and elements of computer programs—that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data.
這個是維基百科的解釋辐赞,簡單的翻譯為:是一種構(gòu)造程序的方式部翘,這種方式將函數(shù)看待為數(shù)學(xué)方程,并且避免使用狀態(tài)和變量响委。
函數(shù)式編程與其他編程方式的區(qū)別也經(jīng)常叫做聲明式編程與命令式編程新思,這兩個的主要區(qū)別是;
命令式編程遇到問題的解決方式是怎么用算法,一步一步的解決這個問題赘风,一個很形象的比喻是你有個一食譜夹囚,教你怎么一步一步的去做一道菜,要哪些原料邀窃,混合哪些東西荸哟,最后吧這道菜做出來。
聲明式編程遇到問題的解決方式是有什么可以解決這個東西,而不是怎么去解決鞍历。如果用一道菜做比喻的話就是直接給你一個照片舵抹,或者告訴你這個菜是什么樣子的。
函數(shù)式編程的一些基本概念:
1變量不可變和副作用
變量不可變是函數(shù)式編程與命令式編程的主要區(qū)別劣砍,如果你需要改變一個變量惧蛹,那就需要重新Copy一份,在進行修改刑枝,Swift中String類型也是值類型香嗓,所以在函數(shù)中傳遞的時候其實是進行了值拷貝(當然是有修改的時候,編譯器對這個做了優(yōu)化)装畅,所以你在函數(shù)中拿到的String可以很放心的去修改這個字符串而不會被別人串改靠娱。官方文檔是這么介紹的:
Swift’s copy-by-default String behavior ensures that when a function or method passes you a String value, it’s clear that you own that exact String value, regardless of where it came from. You can be confident that the string you are passed won’t be modified unless you modify it yourself.
所以這種Copy屬性就會減少一部分Bug的產(chǎn)生,還有一個優(yōu)點是減少了變量的互斥洁灵,增加了多核的運算能力饱岸,知乎上面有一張圖表示了現(xiàn)代計算機計算能力的增長已經(jīng)不依賴CPU主頻的增長,而是依賴CPU核數(shù)的增多徽千,所以這種不可變變量充分利用了這個好處。
副作用表示調(diào)用一個函數(shù)因為某種外部的原因?qū)е略趨?shù)一致的情況下返回卻不一樣了汤锨,一般有全局變量的情況會導(dǎo)致副作用双抽,也不允許函數(shù)去改變外部變量的狀態(tài),這樣會導(dǎo)致程序的結(jié)果不一致闲礼。
2.模塊化
函數(shù)式編程吧業(yè)務(wù)邏輯封裝在一個一個的函數(shù)里面牍汹,函數(shù)不會影響到參數(shù)列表(因為都是Copy),也不會影響到外部的狀態(tài)柬泽。
3.函數(shù)一等公民與高階函數(shù)
函數(shù)式編程中函數(shù)可以像變量一樣傳來傳去慎菲,可以作為參數(shù)也可以作為返回值,當一個函數(shù)接受函數(shù)參數(shù)是我們叫做高階函數(shù)锨并,swift中常用的有 Map露该,Reduce,F(xiàn)ilter等第煮。
4.柯里化
很多函數(shù)式編程的科普文章都寫了這個名次解幼,柯里化(Currying)是把接受多個參數(shù)的函數(shù)變換成接受一個單一參數(shù)(最初函數(shù)的第一個參數(shù))的函數(shù),并且返回接受余下的參數(shù)且返回結(jié)果的新函數(shù)的技術(shù),比如有一個函數(shù)需要兩個參數(shù)f(x包警,y)撵摆,柯里化之后就變成兩個函數(shù),一個接受x害晦,一個接受y特铝,最后的行為編程f(x)(y),這個東西是由于函數(shù)式編程吧函數(shù)作為一等公民,所以f(x)返回一個函數(shù),這個函數(shù)接受y為參數(shù)鲫剿,柯里化可以實現(xiàn)部分計算痒芝,但是有什么用,很多人在網(wǎng)上討論牵素。柯里化在工程中有什么好處? - 知乎
f(x,y) => 柯里化 f(x)(y)
5.純函數(shù)
函數(shù)式編程的一個重要概念就是純函數(shù)严衬,這種函數(shù)有兩個重要的標準。
1.在輸入一定的情況下輸出確定笆呆。
2.不會對函數(shù)外產(chǎn)生副作用请琳。
func add_pure(a: Int) -> Int {
return a + 1
}
上面的函數(shù)是純函數(shù),輸入確定之后輸出確定赠幕,并且不會改變外面變量的狀態(tài)
var b = 0;
func add_nopure(a: Int) -> Int {
return a + b
}
上面的函數(shù)不是純函數(shù)俄精,因為返回值收到b的影響。
6.引用透明
引用透明和純函數(shù)概念差不多榕堰,引用透明導(dǎo)致一定的輸入與輸出是不會改變的竖慧,這樣可以方便編譯去做優(yōu)化。
7.遞歸
遞歸用于替代在命令式編程里面的循環(huán)逆屡,由于遞歸會導(dǎo)致棧益處圾旨,所以在函數(shù)式編程中,很多編譯器都會用尾遞歸調(diào)用來優(yōu)化魏蔗。尾調(diào)用優(yōu)化 - 阮一峰的網(wǎng)絡(luò)日志
下面我們用一個例子來說明命令式編程與函數(shù)式編程的區(qū)別
假如有一個學(xué)籍成績表單
enum GenderType {
case boy
case girl
}
struct Student{
let name: String
let gender: GenderType
let source: Float
}
let students = [
Student(name: "Make", gender: .boy, source: 75.0),
Student(name: "Jason", gender: .boy, source: 80.0),
Student(name: "Lucy", gender: .girl, source: 82.0),
Student(name: "Lili", gender: .girl, source: 83.0),
Student(name: "Amy", gender: .girl, source: 70.0),
Student(name: "Jenny", gender: .girl, source: 72.0),
Student(name: "Kelly", gender: .girl, source: 90.0),
Student(name: "Helen", gender: .girl, source: 170.0),
]
我們現(xiàn)在有一個需求砍的,要獲取所有已女學(xué)生的成績姓名排名:
如果按照命令式的編程方式,
第一步:過濾所有女學(xué)生
第二部:排序所有女學(xué)生
第三部:吧所有女學(xué)生的名字放入數(shù)組返回
func getUpScoreName(studets : [Student] , type: GenderType)-> [String] {
var genderStudent = [Student]()
var genderName = [String]()
for i in 0..<studets.count {
if studets[i].gender == type {
genderStudent.append(studets[i])
}
}
for i in 0..<genderStudent.count {
let stu = genderStudent[i]
//2
for j in stride(from: i, to: -1, by: -1){
if stu.source < genderStudent[j].source {
genderStudent.remove(at: j + 1)
genderStudent.insert(stu, at: j)
}
}
}
for i in 0..<genderStudent.count {
genderName.append(genderStudent[i].name)
}
return genderName
}
可能代碼會是這樣的莺治。
如果我們用函數(shù)式編程的結(jié)果廓鞠,那可能這么去實現(xiàn):
讓Student實現(xiàn)對比接口:
extension Student : Comparable {
static func ==(lhs: Student, rhs: Student) -> Bool {
return false
}
static func < (lhs: Student, rhs: Student) -> Bool {
return lhs.source < rhs.source
}
}
獲取排序結(jié)果:
func getUpScoreNameFC(studets : [Student] , type: GenderType) -> [String] {
let names = studets.filter{ $0.gender == type }.sorted{ $0 < $1 }.map{ return $0.name }
return names
}
這樣的代碼看起來就是我需要什么,而不是我要一步一步的怎么去實現(xiàn)里面的東西谣旁,雖然Swift給我們提供的Map床佳,reduce等操作接口是系統(tǒng)API,但是我們自己寫方法的時候可以忘這方面靠近榄审,達到這種一看代碼就知道再干什么砌们,不需要一步一步去分析代碼是什么,一目了然的結(jié)果瘟判。
第二個例子是怎么消除局部可變變量怨绣,函數(shù)式編程要求變量不可變,而且不需要變量拷获,函數(shù)是純函數(shù)篮撑,這樣函數(shù)可以不受外部的影響。
加入我們現(xiàn)在要給班級里面的三個學(xué)生發(fā)小紅花:
如果是命令式編程方式:
var count = 0
mutating func flower() {
getFlowerToStudent(studets: self.students, countTotal: 3)
}
mutating func getFlowerToStudent(studets : [Student] , countTotal: Int) {
self.count = countTotal
let max = UInt32(studets.count)
while count > 0 {
let index = Int(arc4random() % max)
count = count - 1
print(studets[index])
}
}
我們首選申請一個局部變量匆瓜,記錄要給多少小朋友發(fā)紅花赢笨,然后在循環(huán)里面去減去這個變量未蝌,已判斷是否發(fā)送完畢。現(xiàn)在的邏輯還不復(fù)雜茧妒,但是如果邏輯很多的時候萧吠,又需要變量去維持狀態(tài)的時候,閱讀代碼就很困難桐筏,而且很容易因為狀態(tài)變量改變而出現(xiàn)Bug纸型。
如果是函數(shù)式編程可能就是這種結(jié)果:
mutating func flower() {
getFlowerToStudentFC(studets: self.students, countTotal: 3)
}
func getFlowerToStudentFC(studets : [Student], countTotal: Int) {
if (countTotal == 0) {
return
}else {
let index = Int(arc4random() % UInt32(studets.count))
print(studets[index])
self.getFlowerToStudentFC(studets: studets, countTotal: countTotal - 1)
}
}
沒有狀態(tài)變量,所有的函數(shù)都是純函數(shù)梅忌,這樣閱讀起來可以根據(jù)函數(shù)名稱很好的理解函數(shù)在干什么狰腌。
總結(jié):
swift不是純函數(shù)式編程的語言,但是再往函數(shù)式編程方向靠攏牧氮,我們在寫代碼的時候可以從我們底層的很model琼腔,或者viewmodel等簡單的模塊嘗試用函數(shù)式編程這種方式。