在函数体中,参数本质上是个局部变量。在默认情况下,它是个隐式使用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 = CGRectZerovar body = CGRectZeroCGRectDivide(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 = Dogd.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章将会对此进行深入介绍。