讀古今文學網 > iOS編程基礎:Swift、Xcode和Cocoa入門指南 > 2.7 可修改參數 >

2.7 可修改參數

在函數體中,參數本質上是個局部變量。在默認情況下,它是個隱式使用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章將會對此進行深入介紹。