函數的閉包
函數
函數是一個完成獨立任務的代碼塊,Swift中的函數不僅可以像C語言中的函數一樣作為函數的參數和返回值,而且還支持嵌套,並且有C#一樣的函數參數默認值、可變參數等。
//定義一個函數,注意參數和返回值,如果沒有返回值可以不寫返回值或者寫成Void、空元組()(注意Void的本質就是空元組) funcsum(num1:Int,num2:Int)->Int{ returnnum1+num2 } sum(1,2)
可以看到Swift中的函數僅僅表達形式有所區別(定義形式類似於Javascript,但是js不用書寫返回值),但是本質並沒有太大的區別。不過Swift中對函數參數強調兩個概念就是局部參數名(又叫“形式參數”)和外部參數名,這極大的照顧到了ObjC開發者的開發體驗。在上面的例子中調用sum函數並沒有傳遞任何參數名,因為num1、num2僅僅作為局部參數名在函數內部使用,但是如果給函數指定一個外部參數名在調用時就必須指定參數名。另外前面也提到關於Swift中的默認參數、可變長度的參數,包括一些高級語言中的輸入輸出參數,通過下面的例子大家會有一個全面的了解。
/** /** * 函數參數名分為局部參數名和外部參數名 */ funcsplit(stringa:String,seperatorb:Character)->[String]{ returnsplit(a,maxSplit:Int.max,allowEmptySlices:false,isSeparator: {$0==b}) } //由於給split函數設置了外部參數名string和seperator,所以執行的時候必須帶上外部參數名,此處可以看到一個有意義的外部參數名大大節省開發者使用成本 split(string:"hello,world,!",seperator:",")//結果:["hello", "world", "!"] //下面通過在局部參數名前加上#來簡寫外部參數名(此時局部參數名和外部參數名相同) funcsplit2(#string:String,#seperator:Character)->[String]{ returnsplit(string,maxSplit:Int.max,allowEmptySlices:false,isSeparator: {$0==seperator}) } split2(string:"hello,world,!",seperator:",") //上面的split函數的最後一個參數默認設置為",",注意如果使用默認參數那麼此參數名將默認作為外部參數名(此時局部參數名和外部參數名相同) funcsplit3(#string:String,seperator:Character=",")->[String]{ returnsplit(string,maxSplit:Int.max,allowEmptySlices:false,isSeparator: {$0==seperator}) } split3(string:"hello,world,!",seperator:",")//結果:["hello", "world", "!"] split3(string:"hello world !",seperator:" ")//結果:["hello", "world", "!"] //但是如果有默認值,又不想指定局部參數名可以使用“_”取消外部參數名 funcsplit4(string:String,_seperator:Character=",")->[String]{ returnsplit(string,maxSplit:Int.max,allowEmptySlices:false,isSeparator: {$0==seperator}) } split4("hello,world,!",",")//結果:["hello", "world", "!"] /** * 可變參數,一個函數最多有一個可變參數並且作為最後一個參數 * 下面strings參數在內部是一個[String],對於外部是不定個數的String參數 */ funcjoinStr(seperator:Character=",",strings:String...)->String{ varresult:String="" forvari=0;i ifi!=0{ result.append(seperator) } result+=strings[i] } returnresult } joinStr(seperator:" ","hello","world","!")//結果:"hello world !" /** * 函數參數默認是常量,不能直接修改,通過聲明var可以將其轉化為變量(但是注意C語言參數默認是變量) * 但是注意這個變量對於外部是無效的,函數執行完就消失了 */ funcsum2(varnum1:Int,num2:Int)->Int{ num1=num1+num2 returnnum1 } sum2(1,2)//結果:3 /** * 輸入輸出參數 * 通過輸入輸出參數可以在函數內部修改函數外部的變量(注意調用時不能是常量或字面量) * 注意:下面的swap僅僅為了演示,實際使用時請用Swift的全局函數swap */ funcswap(inouta:Int,inoutb:Int){ a=a+b b=a-b a=a-b } vara=1,b=2 swap(&a,&b)//調用時參數加上“&”符號 println("a=\(a),b=\(b)")//結果:"a=2,b=1"
和很多語言一樣,Swift中的函數本身也可以看做一種類型,既可以作為參數又可以作為返回值。
/** * 函數類型 */ varsum3=sum//自動推斷sum3的類型:(Int,Int)->Int,注意不同的函數類型之間不能直接賦值 sum3(1,2)//結果:3 //函數作為返回值 funcfn()->(Int,Int)->Int{ //下面的函數是一個嵌套函數,作用於是在fn函數內部 funcminus(a:Int,b:Int)->Int{ returna-b } returnminus; } varminus=fn() //函數作為參數 funccaculate(num1:Int,num2:Int,fn:(Int,Int)->Int)->Int{ returnfn(num1,num2) } caculate(1,2,sum)//結果:3 caculate(1,2,minus)//結果:-1
閉包
Swift中的閉包其實就是一個函數代碼塊,它和ObjC中的Block及C#、Java中的lambda是類似的。閉包的特點就是可以捕獲和存儲上下文中的常量或者變量的引用,即使這些常量或者變量在原作用域已經被銷毀了在代碼塊中仍然可以使用。事實上前面的全局函數和嵌套函數也是一種閉包,對於全局函數它不會捕獲任何常量或者變量,而對於嵌套函數則可以捕獲其所在函數的常量或者變量。通常我們說的閉包更多的指的是閉包表達式,也就是沒有函數名稱的代碼塊,因此也稱為匿名閉包。
在Swift中閉包表達式的定義形式如下:
{ ( parameters ) -> returnType in
statements
}
下面通過一個例子看一下如何通過閉包表達式來簡化一個函數類型的參數,在下面的例子中閉包的形式也是一而再再而三的被簡化,充分說明了Swift語法的簡潔性:
funcsum(num1:Int,num2:Int)->Int{ returnnum1+num2 } funcminus(num1:Int,num2:Int)->Int{ returnnum1-num2 } funccaculate(num1:Int,num2:Int,fn:(Int,Int)->Int)->Int{ returnfn(num1,num2) } var(a,b)=(1,2) caculate(a,b,sum)//結果:3 caculate(a,b,minus)//結果:-1 //利用閉包表達式簡化閉包函數 caculate(a,b, {(num1:Int,num2:Int)->Intin returnnum1-num2 })//結果:-1 //簡化形式,根據上下文推斷類型並且對於單表達式閉包(只有一個語句)可以隱藏return關鍵字 caculate(a,b, {num1,num2in num1-num2 })//結果:-1 //再次簡化,使用參數名縮寫,使用$0...$n代表第n個參數,並且此in關鍵字也省略了 caculate(a,b, { $0- $1 })//結果:-1
考慮到閉包表達式的可讀取性,Swift中如果一個函數的最後一個參數是一個函數類型的參數(或者說是閉包表達式),則可以將此參數寫在函數括號之後,這種閉包稱之為“尾隨閉包”。
funcsum(num1:Int,num2:Int)->Int{ returnnum1+num2 } funcminus(num1:Int,num2:Int)->Int{ returnnum1-num2 } funccaculate(num1:Int,num2:Int,fn:(Int,Int)->Int)->Int{ returnfn(num1,num2) } var(a,b)=(1,2) //尾隨閉包,最後一個參數是閉包表達式時可以卸載括號之後,同時注意如果這個函數只有一個閉包表達式參數時可以連通括號一塊省略 //請注意和函數定義進行區分 caculate(a,b){ $0- $1 }//結果:-1
前面說過閉包之所以稱之為“閉包”就是因為其可以捕獲一定作用域內的常量或者變量進而閉合並包裹著。
funcadd()->()->Int{ vartotal=0 varstep=1 funcfn()->Int{ total+=step returntotal } returnfn } //fn捕獲了total和step,盡管下面的add()執行完後total和step被釋放,但是由於fn捕獲了二者的副本,所以fn會隨著兩個變量的副本一起被存儲 vara=add() a()//結果:1 a()//結果:2,說明a中保存了total的副本(否則結果會是1) varb=add() b()//結果:1 ,說明a和b單獨保存了total的副本(否則結果會是3) varc=b c()//結果:2,說明閉包是引用類型,換句話說函數是引用類型(否則結果會是1)
Swift會自動決定捕獲變量或者常量副本的拷貝類型(值拷貝或者引用拷貝)而不需要開發者關心,另外被捕獲的變量或者常量的內存管理同樣是由Swift來管理,當上面的函數a不再使用了那麼fn捕獲的兩個變量也就釋放了。
類
作為一門面向對象語言,類當然是Swift中的一等類型。首先通過下面的例子讓大家對Swift的class有一個簡單的印象,在下面的例子中可以看到Swift中的屬性、方法(包括構造方法和析構方法):
//Swift中一個類可以不繼承於任何其他基類,那麼此類本身就是一個基類 classPerson{ //定義屬性 varname:String varheight=0.0 //構造器方法,注意如果不編寫構造方法默認會自動創建一個無參構造方法 init(name:String){ self.name=name } //定義方法 funcshowMessage(){ println("name=\(name),height=\(height)") } //析構方法,在對象被釋放時調用,類似於ObjC的dealloc,注意此函數沒有括號,沒有參數,無法直接調用 deinit{ println("deinit...") } } varp=Person(name:"Kenhin") p.height=172.0 p.showMessage()//結果:name=Kenhin,height=172.0 //類是引用類型 varp2=p p2.name="Kaoru" println(p.name)//結果:Kaoru ifp===p2{//“===”表示等價於,這裡不能使用等於“==”(等於用於比較值相等,p和p2是不同的值,只是指向的對象相同) println("p===p2")//p等價於p2,二者指向同一個對象 }
從上面的例子不難看出:
Swift中的類不必須繼承一個基類(但是ObjC通常必須繼承於NSObject),如果一個類沒有繼承於任何其他類則這個類也稱為“基類”;
Swift中的屬性定義形式類似於其他語句中的成員變量(或稱之為“實例變量”),盡管它有著成員變量沒有的特性;
Swift中如果開發者沒有自己編寫構造方法那麼默認會提供一個無參數構造方法(否則不會自動生成構造方法);
Swift中的析構方法沒有括號和參數,並且不支持自行調用;
屬性
Swift中的屬性分為兩種:存儲屬性(用於類、結構體)和計算屬性(用於類、結構體、枚舉),並且在Swift中並不強調成員變量的概念。 無論從概念上還是定義方式上來看存儲屬性更像其他語言中的成員變量,但是不同的是可以控制讀寫操作、通過屬性監視器來屬性的變化以及快速實現懶加載功能。
classAccount{ varbalance:Double=0.0 } classPerson{ //firstName、lastName、age是存儲屬性 varfirstName:String varlastName:String letage:Int //fullName是一個計算屬性,並且由於只定義了get方法,所以是一個只讀屬性 varfullName:String{ get{ returnfirstName+"."+lastName } set{ letarray=split(newValue,maxSplit:Int.max,allowEmptySlices:false,isSeparator: {$0=="."}) ifarray.count==2{ firstName=array[0] lastName=array[1] } } //set方法中的newValue表示即將賦的新值,可以自己設置set中的newValue變量,如下: // set(myValue){ // } } //如果fullName只有get則是一個只讀屬性,只讀屬性可以簡寫如下: // var fullName:String{ // return firstName + "." + lastName // } //屬性的懶加載,第一次訪問才會計算初始值,在Swift中懶加載的屬性不一定就是對象類型,也可以是基本類型 lazyvaraccount=Account() //構造器方法,注意如果不編寫構造方法默認會自動創建一個無參構造方法 init(firstName:String,lastName:String,age:Int){ self.firstName=firstName self.lastName=lastName self.age=age } //定義方法 funcshowMessage(){ println("name=\(self.fullName)") } } varp=Person(firstName:"Kenshin",lastName:"Cui",age:29) p.fullName="Kaoru.Sun" p.account.balance=10 p.showMessage()
需要提醒大家的是:
計算屬性並不直接存儲一個值,而是提供getter來獲取一個值,或者利用setter來間接設置其他屬性;
lazy屬性必須有初始值,必須是變量不能是常量(因為常量在構造完成之前就已經確定了值);
在構造方法之前存儲屬性必須有值,無論是變量屬性(var修飾)還是常量屬性(let修飾)這個值既可以在屬性創建時指定也可以在構造方法內指定;
從上面的例子中不難區分存儲屬性和計算屬性,計算屬性通常會有一個setter、getter方法,如果要監視一個計算屬性的變化在setter方法中即可辦到(因為在setter方法中可以newValue或者自定義參數名),但是如果是存儲屬性就無法通過監視屬性的變化過程了,因為在存儲屬性中是無法定義setter方法的。不過Swift為我們提供了另外兩個方法來監視屬性的變化那就是willSet和didSet,通常稱之為“屬性監視器”或“屬性觀察器”。
classAccount{ //注意設置默認值0.0時監視器不會被調用 varbalance:Double=0.0{ willSet{ self.balance=2.0 //注意newValue可以使用自定義值,並且在屬性監視器內部調用屬性不會引起監視器循環調用,注意此時修改balance的值沒有用 println("Account.balance willSet,newValue=\(newValue),value=\(self.balance)") } didSet{ self.balance=3.0 //注意oldValue可以使用自定義值,並且在屬性監視器內部調用屬性不會引起監視器循環調用,注意此時修改balance的值將作為最終結果 println("Account.balance didSet,oldValue=\(oldValue),value=\(self.balance)") } } } classPerson{ varfirstName:String varlastName:String letage:Int varfullName:String{ get{ returnfirstName+"."+lastName } set{ //對於計算屬性可以直接在set方法中進行屬性監視 letarray=split(newValue,maxSplit:Int.max,allowEmptySlices:false,isSeparator: { $0=="."}) ifarray.count==2{ firstName=array[0] lastName=array[1] } } } lazyvaraccount=Account() init(firstName:String,lastName:String,age:Int){ self.firstName=firstName self.lastName=lastName self.age=age } //類型屬性 staticvarskin:Array{ return["yellow","white","black"]; } } varp=Person(firstName:"Kenshin",lastName:"Cui",age:29) p.account.balance=1.0 println("p.account.balance=\(p.account.balance)")//結果:p.account.balance=3.0 forcolorinPerson.skin{ println(color) }
和setter方法中的newValue一樣,默認情況下載willSet和didSet中會有一個newValue和oldValue參數表示要設置的新值和已經被修改過的舊值(當然參數名同樣可以自定義);
存儲屬性的默認值設置不會引起屬性監視器的調用(另外在構造方法中賦值也不會引起屬性監視器調用),只有在外部設置存儲屬性才會引起屬性監視器調用;
存儲屬性的屬性監視器willSet、didSet內可以直接訪問屬性,但是在計算屬性的get、set方法中不能直接訪問計算屬性,否則會引起循環調用;
在didSet中可以修改屬性的值,這個值將作為最終值(在willSet中無法修改);
方法
方法就是與某個特定類關聯的函數,其用法和前面介紹的函數並無二致,但是和ObjC相比,ObjC中的函數必須是C語言,而方法則必須是ObjC。此外其他語言中方法通常存在於類中,但是Swift中的方法除了在類中使用還可以在結構體、枚舉中使用。關於普通的方法這裡不做過多贅述,用法和前面的函數區別也不大,這裡主要看一下構造方法。
classPerson{ //定義屬性 varname:String varheight:Double varage=0 //指定構造器方法,注意如果不編寫構造方法默認會自動創建一個無參構造方法 init(name:String,height:Double,age:Int){ self.name=name self.height=height self.age=age } //便利構造方法,通過調用指定構造方法、提供默認值來簡化構造方法實現 convenienceinit(name:String){ self.init(name:name,height:0.0,age:0) } //實例方法 funcmodifyInfoWithAge(age:Int,height:Double){ self.age=age self.height=height } //類型方法 classfuncshowClassName(){ println("Class name is \"Person\"") } //析構方法,在對象被釋放時調用,類似於ObjC的dealloc,注意此函數沒有括號,沒有參數,無法直接調用 deinit{ println("deinit...") } } //通過便利構造方法創建對象 varp=Person(name:"kenshin")
除構造方法、析構方法外的其他方法的參數默認除了第一個參數是局部參數,從第二個參數開始既是局部參數又是外部參數(這種方式和ObjC的調用方式很類似,當然,可以使用“#”將第一個參數同時聲明為外部參數名,也可以使用“_”將其他參數設置為非外部參數名)。但是,對於函數,默認情況下只有默認參數既是局部參數又是外部參數,其他參數都是局部參數。
構造方法的所有參數默認情況下既是外部參數又是局部參數;
Swift中的構造方法分為“指定構造方法”和“便利構造方法(convenience)”,指定構造方法是主要的構造方法,負責初始化所有存儲屬性,而便利構造方法是輔助構造方法,它通過調用指定構造方法並指定默認值的方式來簡化多個構造方法的定義,但是在一個類中至少有一個指定構造方法。
下標腳本
下標腳本是一種訪問集合的快捷方式,例如:var a:[string],我們經常使用a[0]、a[1]這種方式訪問a中的元素,0和1在這裡就是一個索引,通過這種方式訪問或者設置集合中的元素在Swift中稱之為“下標腳本”(類似於C#中的索引器)。從定義形式上通過“subscript”關鍵字來定義一個下標腳本,很像方法的定義,但是在實現上通過getter、setter實現讀寫又類似於屬性。假設用Record表示一條記錄,其中有多列,下面示例中演示了如何使用下標腳本訪問並設置某一列的值。
classRecord{ //定義屬性,假設store是Record內部的存儲結構 varstore:[String:String] init(data:[String:String]){ self.store=data } //下標腳本(注意也可以實現只有getter的只讀下標腳本) subscript(index:Int)->String{ get{ varkey=sorted(Array(self.store.keys))[index] returnself.store[key]! } set{ varkey=sorted(Array(self.store.keys))[index] self.store[key]=newValue//newValue參數名可以像屬性一樣重新自定義 } } //下標腳本(重載) subscript(key:String)->String{ get{ returnstore[key]! } set{ store[key]=newValue } } } varr=Record(data:["name":"kenshin","sex":"male"]) println("r[0]=\(r[0])")//結果:r[0]=kenshin r["sex"]="female" println(r[1])//結果:female
繼承
和ObjC一樣,Swift也是單繼承的(可以實現多個協議,此時協議放在後面),子類可以調用父類的屬性、方法,重寫父類的方法,添加屬性監視器,甚至可以將只讀屬性重寫成讀寫屬性。
classPerson{ varfirstName:String,lastName:String varage:Int=0 varfullName:String{ get{ returnfirstName+" "+lastName } } init(firstName:String,lastName:String){ self.firstName=firstName self.lastName=lastName } funcshowMessage(){ println("name=\(fullName),age=\(age)") } //通過final聲明,子類無法重寫 finalfuncsayHello(){ println("hello world.") } } classStudent:Person{ //重寫屬性監視器 overridevarfirstName:String{ willSet{ println("oldValue=\(firstName)") } didSet{ println("newValue=\(firstName)") } } varscore:Double //子類指定構造方法一定要調用父類構造方法 //並且必須在子類存儲屬性初始化之後調用父類構造方法 init(firstName:String,lastName:String,score:Double){ self.score=score super.init(firstName:firstName,lastName:lastName) } convenienceinit(){ self.init(firstName:"",lastName:"",score:0) } //將只讀屬性重寫成了可寫屬性 overridevarfullName:String{ get{ returnsuper.fullName; } set{ letarray=split(newValue,maxSplit:Int.max,allowEmptySlices:false,isSeparator: { $0=="."}) ifarray.count==2{ firstName=array[0] lastName=array[1] } } } //重寫方法 overridefuncshowMessage() { println("name=\(fullName),age=\(age),score=\(score)") } } varp=Student() p.firstName="kenshin"
在使用ObjC開發時init構造方法並不安全,首先無法保證init方法只調用一次,其次在init中不能訪問屬性。但是這些完全依靠文檔約定,編譯時並不能發現問題,出錯檢測是被動的。在Swift中構造方法(init)有了更為嚴格的規定:構造方法執行完之前必須保證所有存儲屬性都有值。這一點不僅在當前類中必須遵循,在整個繼承關系中也必須保證,因此就有了如下的規定:
子類的指定構造方法必須調用父類構造方法,並確保調用發生在子類存儲屬性初始化之後。而且指定構造方法不能調用同一個類中的其他指定構造方法;
便利構造方法必須調用同一個類中的其他指定構造方法(可以是指定構造方法或者便利構造方法),不能直接調用父類構造方法(用以保證最終以指定構造方法結束);
如果父類僅有一個無參構造方法(不管是否包含便利構造方法),子類的構造方法默認就會自動調用父類的無參構造方法(這種情況下可以不用手動調用);
常量屬性必須默認指定初始值或者在當前類的構造方法中初始化,不能在子類構造方法中初始化;