首页 » iOS编程基础:Swift、Xcode和Cocoa入门指南 » iOS编程基础:Swift、Xcode和Cocoa入门指南全文在线阅读

《iOS编程基础:Swift、Xcode和Cocoa入门指南》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 = 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章将会对此进行深入介绍。