现在的位置: 首页 > 综合 > 正文

[连载]Swift开发入门(03)— 函数和闭包

2018年01月24日 ⁄ 综合 ⁄ 共 6526字 ⁄ 字号 评论关闭

函数是现代编程语言中最重要的构建块,它允许你将执行特定任务的业务逻辑封装在一个独立的可重用的工作单元中。对于调用者来说,函数就像是一个独立的黑盒子,调用者在使用函数提供的功能时并不需要了解它的内部实现。Swift支持全局函数和方法,方法是和类或者某种类型的对象相关联的函数。Swift中也提供了对闭包、匿名函数等的支持,这些在Swift中都是被作为一等公民来对待的。

先看一个例子吧。可以创建一个Playground项目,然后键入下面的代码。

import Foundation

let a = 3.0, b = 4.0
let c = sqrt(a * a + b * b)
println(c)

这段代码在做什么相信大家知道,如果你真的那么幽默,看不懂这段代码在干什么就看看下面这个图吧,我就不解释了。

大家可能注意到了,Swift中并没有计算平方的内置函数,不过我们可以自己写一个。可以如下改写上面的代码。

import Foundation

func square(x:Double) -> Double {
    return x * x
}

let a = 3.0, b = 4.0
let c = sqrt(square(a) + square(b))
println(c)

上面的square函数接受一个Double类型的参数并返回一个Double类型的值。定义函数的关键字是func,后面是函数的名字,函数的命名也遵循标识符命名的规则,同时也要做到见名知意,这应该是常识了。函数拥有零个或多个参数和一个返回值或者没有返回值。Swift中被称为函数的东西都是全局性的,如果在类和接口中定义函数,这种函数跟某种类型的对象绑定在一起,我们通常称之为方法。

在Swift中的函数是一等公民,Swift中函数的用法和JavaScript非常类似,你可以将函数赋值给一个变量或常量,当然你也可以将一个函数作为另外一个函数的参数。代码如下所示。

import Foundation

func square(x:Double) -> Double {
    return x * x
}

let f = square

let a = 3.0, b = 4.0
let c = sqrt(f(a) + f(b))
println(c)

你可能觉得上面的代码其实也挺眼熟的,不错,因为这种写法就相当于C语言中的函数指针。还有一点需要说明的时,上面的代码中常量f的类型是通过类型推断得到的,如果想要完整的给出常量f的类型,代码可以如下书写。

var f:(Double) -> Double = square

当然,有的时候你的函数参数可能较多,如果要定义多个这样的变量似乎不那么方便,所以你可以这么做。

typealias FType = (Double) -> Double
var f:FType = square

专家提示:在Xcode中想查看一个函数的定义,可以按住Command键点击函数,就可以让代码跳转到函数定义之处。

Swift中支持函数重载(注意这一点跟JavaScript是不同的),所谓的重载是同名的函数拥有不同的参数列表,那么它们就可以和平共处,而且重载也是实现编译时多态性的重要手段,且看下面的代码。

import Foundation

func assertEquals(value: Int, expected: Int, message: String) {
    if expected != value {
        println(message)
    }
}

func assertEquals(value: String, expected: String, message: String) {
    if expected != value {
        println(message)
    }
}

assertEquals(100, 1000, "Two integers are not equal!")
assertEquals("Hello", "Good", "Two strings are not equal!")

当然,如果重载的函数都向上面一样只是不同的参数却执行了相同的代码,那么这些代码将来的维护将是一场恶梦。世界级的编程大师Martin Fowler在《重构》一书中指出:代码有很多种坏味道,重复是最坏的一种。要消除重复代码,可以使用泛型来改写上面的函数,如下所示。

import Foundation

func assertEquals<T: Equatable>(value: T, expected: T, message: String) {
    if expected != value {
        println(message)
    }
}

assertEquals(100, 1000, "Two integers are not equal!")
assertEquals("Hello", "Good", "Two strings are not equal!")

上面的代码中T是泛型参数,相当于定义了一种虚拟的类型,这种类型在编译时会根据传入该函数的实际参数的类型来决定,就如上面的代码中,第一次调用assertEquals函数时,T被替换成Int类型;而第二次调用时,T被替换成String类型。

Swift中,函数的参数可以是inout参数,所谓inout参数是指可以在函数中被修改并影响到调用者的参数。当然,C#中也有类似的语法,一种是ref参数,一种是out参数。对于下面的代码,inout参数就很有用。

import Foundation

func swap(inout x: Int, inout y: Int) {
    let temp: Int = x;
    x = y;
    y = temp;
}

func bubbleSort(inout x: [Int]) {
    var swapped:Bool = true
    for var i = 1; i <= x.count - 1; i++ {
        swapped = false
        for var j = 0; j < x.count - i; j++ {
            if x[j] > x[j + 1] {
                swap(&x[j], &x[j + 1])
                swapped = true
            }
        }
    }
}

var x = [34, 12, 85, 7, 96, 63, 40]
bubbleSort(&x)
for item in x {
    println(item)
}

上面的代码中有两个函数,swap函数实现交换两个参数的值,下面的bubbleSort函数实现冒泡排序,如果没有inout参数,你只能修改函数参数在函数内部的拷贝而不会影响到调用者,但是有了inout参数一切就不同了,调用者在传入参数的时候用在参数前加上&,有C或者C++开发经验的都知道,这种语法叫做传引用而不是简单的传值。当然,对于inout参数的使用还是应当小心,因为不是所有的时候我们都希望通过函数调用来修改传入的参数并影响调用者,一定要警惕这种形式的参数所带来的副作用。

Swift中还允许为函数的参数定义外部参数名,那么在调用该函数传入参数时,除了要指定参数的值,还要指定参数的名字,这样似乎有点繁琐,但是代码却拥有了更好的可读性。如果我们希望封装一系列的API给调用者,那么就可以使用命名参数,这样调用者在调用函数时就必须执行参数名和对应的参数值,代码如下所示。

import Foundation

func say(g greeting: String, n name: String, c counter: Int) {
    for var i = 0; i < counter; ++i {
        println("\(greeting), \(name)")
    }
}

say(g: "Hello", n: "Jack", c: 3)

当然,如果每个参数都要像上面那样定义外部的名字和内部的名字,这样代码会显得很丑陋,更好的做法是让参数的外部名字和内部名字保持一致,这样在定义参数时可以使用下面的写法。

import Foundation

func say(#greeting: String, #name: String, #counter: Int) {
    for var i = 0; i < counter; ++i {
        println("\(greeting), \(name)")
    }
}

say(greeting: "Hello", name: "Jack", counter: 3)

在类或结构中定义的函数我们通常称之为方法,这一点在下一章中有详细的讲解。

Swift既然支持函数式编程,那么肯定不可避免的要用到闭包(closure)。关于闭包一词,你可能会听到N多种解释,下面给出其中的一种:“闭包是可以包含自由(未绑定到特定对象)变量的代码块;这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)。闭包一词来源于以下两者的结合:要执行的代码块(由于自由变量被包含在代码块中,这些自由变量以及它们引用的对象没有被释放)和为自由变量提供绑定的计算环境(作用域)”。闭包最常见的一种用法就是用闭包表达式实现匿名函数的功能,我们用下面的代码来加以说明。

import Foundation

var animals = ["fish", "cat", "panda", "dog"]

func compare(one: String, two: String) -> Bool {
    return one < two;
}

animals.sort(compare)

println(animals)

上面的代码实现了对字符串数组的排序,我们将一个包含排序规则的函数作为数组sort方法的参数传入,sort方法就能够完成对数组的排序。事实上,完全不需要单独定义这样的一个函数,因为这个函数想表达的就是一个临时的排序规则,我们可以修改成下面的代码。

import Foundation

var animals = ["fish", "cat", "panda", "dog"]

animals.sort({
    (one: String, two: String) -> Bool in return one < two
})

println(animals)

当然,这样看起来仍然不够简单明了。如果你还记得Swift有强大的类型推断能力,那么上面的代码就可以做出如下的改进。

import Foundation

var animals = ["fish", "cat", "panda", "dog"]

animals.sort({
    (one, two) -> Bool in return one < two
})

println(animals)

你甚至还可以省略掉那个return,代码仍然可以很好的工作。还有什么可以省略吗?答案是肯定的,既然有类型推断,这次我们连返回类型也一并省去。

import Foundation

var animals = ["fish", "cat", "panda", "dog"]

animals.sort({
    (one, two) in one < two
})

println(animals)

写到这,我估计大家已经觉得做到极致了,似乎已经没有什么可以写得更简单了。不是的,Swift还能做得更好。

import Foundation

var animals = ["fish", "cat", "panda", "dog"]

animals.sort({ $0 < $1 })

println(animals)

OMG!参数名都可以省略掉,$0很明显表示匿名函数的第一个参数,而$1则是匿名函数的第二个参数。对于那种只有一行代码的闭包,这样写不是更简单明了吗。我们不能不感叹,Swift算是把简单即美践行到极致了。

最后还有一个很特殊的东西叫尾随闭包(tailing closure)需要提一下,请看下面的代码。

import Foundation

var animals = ["fish", "cat", "panda", "dog"]

animals.sort(){ $0 < $1 }

println(animals)

这样代码不是看起来更加自然吗?等一下,还能更简单。

import Foundation

var animals = ["fish", "cat", "panda", "dog"]

animals.sort(<)

println(animals)

荷兰著名的计算机科学家、图灵奖得主Edsger Wybe Dijkstra曾经说过:“完美是我们的追求”,上面的代码已经做到了。

闭包另一个重要的用途是将其所在的代码块中的常量或变量的生命周期延长,在离开代码快以后,我们还能够访问到这些常量或变量的值。我们可以在一个函数中返回一个闭包表达式,它相当于返回了一个匿名函数,在这个匿名函数中我们还能使用刚才返回这个闭包的函数(有的地方称之为封闭函数)中的局部变量或常量,专业的说法称之为“捕获值”(capturing value)。

import Foundation

typealias StateMachineType = () -> Int

func makeStateMachine(maxState: Int) -> StateMachineType {
    var currentState: Int = 0
    return {
        currentState++;
        if currentState > maxState {
            currentState = 1
        }
        return currentState
    }
}

let myStateMachine = makeStateMachine(3)
for i in 1...10 {
    println(myStateMachine())
}

上面的代码中,makeStateMachine函数返回了一个匿名函数(闭包表达式),由于这个闭包的存在,本来是函数局部变量的currentState和参数maxState可以在函数的生命周期结束以后仍然被继续使用。闭包让你免除了对全局变量的使用(因为全局变量总是让你的代码变得糟糕,因为你不知道这个变量什么时候会被哪段代码意外的修改),但是它通过延长局部变量生命周期的方式让你以可控制的方式使用这些值。

可能你已经想到一件事情了,闭包的使用将局部变量的生命周期延长了,那么会不会影响到垃圾回收呢?我们还是再来看一个例子。

class Person {
    let name: String
    private let actionClosure: (() -> ())!
    
    init(name: String) {
        self.name = name
        actionClosure = {
            println("I am \(self.name)!")
        }
    }
    
    func performAction() {
        actionClosure()
    }
    
    deinit {
        println("\(self.name) is over!")
    }
}

如果我们在一个App程序的视图控制器ViewController.swift中重写viewDidLoad方法并创建一个Person对象的局部变量,那么内存中对象间的应用关系如下图所示。


当viewDidLoad执行完毕后,Person对象应该可以被垃圾回收器回收,但是由于存在如上图所示的循环引用,垃圾回收就会失效,内存泄露就会发生。


可以修改Person类的代码使用weak或者unowned引用来解决循环引用的问题并帮助垃圾回收器判定对象是否可以回收,关于weak引用和unowned引用的更多内容,我们会在整个系列课程的其他部分为大家详细的讲解。

class Person {
    let name: String
    private let actionClosure: (() -> ())!
    
    init(name: String) {
        self.name = name
        actionClosure = {
            [unowned self] () -> () in println("I am \(self.name)!")
        }
    }
    
    func performAction() {
        actionClosure()
    }
    
    deinit {
        println("\(self.name) is over!")
    }
}

在上面的代码中,unowned self指示闭包不会持有一个对当前Person对象(self)的引用,这样就没有了循环引用的问题。

最后,我们对闭包做一个小小的总结:

  1. 全局函数都是闭包,有名字但是不能捕获任何值。
  2. 嵌套函数都是闭包,有名字也能捕获封闭函数内的值。
  3. 闭包表达式都是匿名函数,可以根据上下文环境捕获值。
Swift中的闭包较之其他语言更简单更优秀,主要表现在:
  1. 可以根据上下文推断参数和返回值的类型。
  2. 对于单行闭包表达式,可以省略return,隐式返回。
  3. 可以省略掉参数名而使用$0, $1, ... 替换之。
  4. 提供了尾随闭包的语法,让代码更自然。

抱歉!评论已关闭.