在函數體中,參數本質上是個局部變量。在默認情況下,它是個隱式使用let聲明的變量。你無法對其賦值:
func say(s:String, times:Int, loudly:Bool) { loudly = true // compile error }
如果代碼需要在函數體中為參數名賦值,那麼請顯式使用var聲明參數名:
func say(s:String, times:Int, var loudly:Bool) { loudly = true // no problem }
在上述代碼中,loudly依舊是個局部變量。為其賦值並不會修改函數體外任何變量的值。不過,還可以這樣配置參數,使得它修改的是函數體外的變量值!一個典型用例就是你希望函數會返回多個結果。比如,我下面要編寫一個函數,它會將給定字符串中出現的某個字符全部刪除,然後返回刪除的字符數量:
func removeFromString(var s:String, character c:Character) -> Int { var howMany = 0 while let ix = s.characters.indexOf(c) { s.removeRange(ix...ix) howMany += 1 } return howMany }
可以這樣調用:
let s = \"hello\" let result = removeFromString(s, character:Character(\"l\")) // 2
很好,不過我們忘記了一件事:初始字符串s依舊是\"hello\"!在函數體中,我們從String參數的本地副本中刪除了所有出現的character,不過這個改變並未影響原來的字符串。
如果希望函數能夠修改傳遞給它的實參的初始值,那就需要做出如下3個改變:
·要修改的參數必須聲明為inout。
·在調用時,持有待修改值的變量必須要聲明為var,而不是let。
·相比於將變量作為實參進行傳遞,我們傳遞的是地址。這是通過在名字前加上&符號做到的。
下面來修改,removeFromString的聲明現在如下所示:
func removeFromString(inout s:String, character c:Character) -> Int {
對removeFromString的調用現在如下所示:
var s = \"hello\" let result = removeFromString(&s, character:Character(\"l\"))
調用之後,結果是2,s為\"heo\"。注意,名字s前的&符號是函數調用中的第1個參數!我喜歡這麼做,因為它強制我顯式告訴編譯器和我自己,我們要做的事情存在一些潛在的風險:函數會修改函數體之外的一個值,這會產生副作用。
當調用具有inout參數的函數時,地址作為實參傳遞給參數的變量總是會被設定,即便函數沒有修改該參數亦如此。
在使用Cocoa時,你常常會遇到該模式的變種。Cocoa API是使用C與Objective-C編寫的,因此你看不到Swift術語inout。你可能會看到一些奇怪的類型,如UnsafeMutablePointer。不過從調用者的視角來看,它們是一回事。依然是準備var變量並傳遞其地址。
比如,考慮Core Graphics函數CGRectDivide。CGRect是個表示矩形的結構體。在將一個矩形切分成兩個矩形時需要調用CGRectDivide。CGRectDivide需要告訴你生成的兩個矩形都是什麼。因此,它需要返回兩個CGRect。其策略就是函數本身不返回值;相反,它會說:「將兩個CGRect作為實參傳遞給我,我會修改它們,這樣它們就是操作的結果了」。
下面是CGRectDivide在Swift中的聲明:
func CGRectDivide(rect: CGRect, slice: UnsafeMutablePointer<CGRect>, remainder: UnsafeMutablePointer<CGRect>, amount: CGFloat, edge: CGRectEdge)
第2個和第3個參數都是針對CGRect的UnsafeMutablePointer。如下代碼來自於我開發的一個應用,它調用了這個函數;請注意我是如何處理第2、3兩個實參的:
var arrow = CGRectZero var body = CGRectZero CGRectDivide(rect, &arrow, &body, Arrow.ARHEIGHT, .MinYEdge)
我需要事先創建兩個var CGRect變量,它們要有值,不過其值立刻會被對CGRectDivide的調用所替換,因此我為其賦值CGRectZero作為佔位符。
Swift擴展了CGRect,提供了一個pide方法。作為一個Swift方法,它實現了一些Cocoa C函數做不到的事情:返回兩個值(以元組的形式,參見第3章)。這樣,一開始就無需調用CGRectDivide了。不過,你依然可以調用CGRectDivide,因此瞭解其調用方式還是很有必要的。
有時,Cocoa會通過UnsafeMutablePointer參數調用你的函數,你可能想要修改其值。為了做到這一點,你不能直接對其賦值,就像removeFromString實現中對inout變量s所做的那樣。你使用的是Objective-C而非Swift,這是個UnsafeMutablePointer而非inout參數。從技術上來說,這是將其賦給了UnsafeMutablePointer的內存屬性。下面來自於我所編寫的代碼的一個片段(不做更多的解釋):
func popoverPresentationController( popoverPresentationController: UIPopoverPresentationController, willRepositionPopoverToRect rect: UnsafeMutablePointer<CGRect>, inView view: AutoreleasingUnsafeMutablePointer<UIView?>) { view.memory = self.button2 rect.memory = self.button2.bounds }
有時當參數是某個類的實例時,函數需要修改這個沒有聲明為inout的參數,這種情況比較常見。這是類的一個特殊的特性,與其他兩種對像類型(枚舉與結構體)風格不同。String不是類,它是個結構體。這也是我們要使用inout才能修改String參數的原因所在。下面聲明一個具有name屬性的Dog類來說明這一點:
class Dog { var name = \"\" }
下面這個函數接收一個Dog實例參數和一個String,並將該Dog實例的name設為該String。注意這裡並未使用inout:
func changeNameOfDog(d:Dog, to tostring:String) { d.name = tostring }
下面是調用方式,該調用沒有使用inout,因此直接傳遞一個Dog實例:
let d = Dog d.name = \"Fido\" print(d.name) // \"Fido\" changeNameOfDog(d, to:\"Rover\") print(d.name) // \"Rover\"
注意,雖然沒有將Dog實例d作為inout參數傳遞,但我們依然可以修改它的屬性,即便它一開始是使用let而非var進行的聲明。這似乎違背了參數修改的規則,但實際上卻並非如此。這是類實例的一個特性,即實例本身是可變的。在changeNameOfDog中,我們實際上並沒有修改參數本身。為了做到這一點,我們本應該用一個不同的Dog實例進行替換。但這並非我們所採取的做法,如果想要這麼做,那就需要將Dog參數聲明為inout(同時需要用var來聲明d,並將其地址作為參數進行傳遞)。
從技術上來說,類是引用類型,而其他對像類型風格則是值類型。在將結構體的實例作為參數傳遞給函數時,實際上使用的是該結構體實例的一個獨立的副本。不過,在將類實例作為參數傳遞給函數時,傳遞的則是類實例本身。第4章將會對此進行深入介紹。