函数

作者: 曹来东 | 来源:发表于2018-10-09 13:41 被阅读8次

    在Objective-C中存在三种子例程(subroutine)调用方式——函数,方法和Blocks。在Swift中同样也对应含有这三种子例程定义与调用方式——函数、方法和闭包。
    Apple官方将Swift中的函数定义为:执行一个特定任务的、自包含的代码块。

    函数的定义与调用

    Swift中的函数定义由四个部分构成:函数名(function name),形参列表(parameter-list)、返回类型(return type),函数体(function body)。一个函数定义的基本语法如以下代码所示:

    func function_name (param1: Int, param2: Float, param3: Double) -> return_type {
    // function-body
    /* ... */
    }
    

    对一个函数的定义总是由关键字 func 引出;然后后面跟函数名;再紧接着跟函数形参列表,函数形参列表由一对圆括号包围,其中的每个形式参数(parameter)由一个实参标签( argument label)加形参名(parameter name)跟一个冒号,后面再加类型名构成;然后由 -> 符号引出返回类型,如果返回类型为 Void,那么 -> Void 可缺省;最后由一对花括号引出函数体,函数体中就存放实现该函数功能的代码。如果一个函数不需要任何输入参数,那么形参列表可空,但是用于表示形参列表的圆括号仍然需要保留。
    对某一函数调用的表达式称为函数调用表达式,它由用于表示函数标识的表达式结合函数调用操作符构成。函数调用操作符就是 ( ),里面存放的是传递给当前调用函数的实参列表(argument-list),它是一个单目后缀操作符,其操作数就是它前面的表示函数标识的表达式。对上述所定义的函数的调用表达式如下:
    let result = function_name(param1: 1, param2: 1.5, param3: 2.25)
    这里,function_name 即为表示函数标识的表达式,当然这里就是所调函数的函数名。后面 param1、param2、param3 后面的整数、浮点数字面量就是传递给所调函数的实际参数( argument)。而这里的 result 常量则用于接受函数所返回的值,其类型为 return_type
    Swift编程语言中函数的形参类型与返回类型可以是Swift所支持的任一类型。下面我们举一些函数定义与函数调用的例子。

    /// 这里定义了singlePrint函数,
    /// 其形参列表为空,返回类型为Void,
    /// 我们可以看到,这里 -> 也可缺省,
    /// 然而()不能缺省
    func singlePrint() {
    // 这里的函数体就实现打印一条
    // Hello, world! 的语句
    print("Hello, world!")
    }
     
    // 对singlePrint函数的调用
    singlePrint()
    // 我们也可以使用通配符来“接受”返回值
    _ = singlePrint()
     
    // 由于返回类型为Void,因此如果要接受其返回值,
    // 那么只能使用Void类型的对象,即空元组!
    var v: Void = singlePrint()
     
    /// 这里定义了一个voidFunc函数,
    /// 它具有一个形参,其类型为Void,
    /// 返回类型也为Void
    func voidFunc(v: Void) -> () {
    print("void parameter function!")
    }
     
    // 这里对voidFunc函数进行调用,
    // 实参传入的是空元组,
    // 这里的v所对应的实参不可缺省,
    // 因此与singlePrint函数的调用是有所区别的
    voidFunc(v: ())
     
    /// 这里定义了arrayFunc函数,
    /// 它具有两个参数,均为[Int]数组类型,
    /// 返回类型也为[Int],它将a1与a2拼接在一起,
    /// 然后作为返回值
    func arrayFunc(a1: [Int], a2: [Int]) -> [Int] {
    return a1 + a2
    }
     
    // 这里array的类型为[Int]
    let array = arrayFunc(a1: [1, 2, 3], a2: [4, 5, 6])
    // 这里打印:array = [1, 2, 3, 4, 5, 6]
    print("array = \(array)")
     
    /// 这里定义了tupleFunc函数,
    /// 它用一个元组作为其形参,然后返回类型也是一个元组
    func tupleFunc(t: (Int, Double)) -> (i: Int, d: Double) {
    return (i: t.0 + 1, d: t.1 - 1.0)
    }
     
    // 各位注意,由于tupleFunc所返回的元组类型带有标签,
    // 因此作为接受该函数返回值的元组对象类型也需要含有相应的标签
    let tuple: (i: Int, d: Double) = tupleFunc(t: (1, 1.0))
    // 输出:tuple = (i: 2, d: 0.0)
    print("tuple = \(tuple)")
     
    /// 这里定义了函数stringFunc,
    /// 其形参为一个 String? 字符串Optional对象,
    /// 返回类型为 Int? 整数Optional对象
    func stringFunc(s: String?) -> Int? {
    return s?.count
    }
     
    // 这里用空值作为实参对stringFunc进行调用,
    // cnt也为空,其类型当然也是 Int? 类型
    let cnt = stringFunc(s: nil)
    // 这里输出:cnt = nil
    print("cnt = \(String(describing: cnt))")
     
    // 由于这里在调用了stringFunc函数之后对它作为强制拆解,
    // 因此也就是相当于对返回值做了Optional的强制拆解。
    // count的类型为Int。
    let count = stringFunc(s: "abcd")!
    // 输出:count = 4
    print("count = \(count)")
    

    函数的实参标签

    Swift编程语言的函数与Objective-C中的方法类似,每个形参都包含有一个实参标签(argument label)和一个形参名(parameter name)。实参标签主要提供给其调用者,说明当前所需要传递实参的含义;而形参名则用于函数体内的引用。所以我们在调用一个函数为其传递参数时,往往需要加上当前形参所对应的实参标签。比如以下代码:

    /// 定义一个名为function的函数
    /// - parameter paramName: 一个整数形参
    func function(label_name paramName: Int) {
    print("parameter is: \(paramName)")
    }
     
    function(label_name: 10)
    

    这里,label_name 就是形参的实参标签;paramName 则是形参的名字,我们可以看到在函数体内所使用的就是该形参名。当我们在调用 function 时,则需要显式地使用实参标签。另外这里需要大家注意的是,当我们对此函数做文档化注释的时候,在对形参描述时,需要用形参名,而不是实参标签。
    如果一个函数的形参列表里含有多个形参,那么实参标签名可以相同,但形参名必须各不相同的,见以下例子:

    func function(label p1: Int, label p2: Int) {
    print("parameter is: \(p1 + p2)")
    }
     
    function(label: 10, label: 20)
    

    上述代码片段中,function 函数具有两个形参,这两个形参都具有相同的实参标签 label,但形参名都各不相同,前者为 p1,后者为 p2。我们在调用 function 时也能看到,所使用的实参标签都是相同的 label。
    我们在上一节中看的代码片段中能看到,函数的形参就只有一个名字,此时具有一个名字的函数形参是将其实参标签与形参名都作为这个名字来命名了,所以我们在调用这些函数的时候,所使用的实参标签也就是该名字。比如再看以下代码:

    func function(param: Int) {
    // 这里的param也作为形参名
    print("parameter is: \(param)")
    }
     
    // 这里function的实参标签也是param
    function(param: 10)
    

    在上述代码中,param 既作为形参名又作为实参标签。在调用 function 时, param 实参标签依然不可缺省。
    不过对于某些简单函数,调用时都要加上实参标签显然会显得有些啰嗦,因此Swift编程语言中可使用通配符 _ 来作为缺省的实参标签。如果某个形参的实参标签缺省,那么我们在传递实参时就无需给出实参标签了。比如以下代码:

    func function(_ param: Int) {
    print("parameter is: \(param)")
    }
     
    function(10)
     
    // 我们也可以使用通配符来作为实参标签
    function(_: 20)
     
    /// 这里我们定义了含有一个形参的函数voidFunc
    /// - parameter param: 一个Void类型的对象
    func voidFunc(_ param: Void) {
    print("Do nothing...")
    }
     
    // 我们可以显式地传递空元组进行调用
    voidFunc(())
     
    // 我们也可以使用通配符作为实参标签
    voidFunc(_: ())
    /// 我们通过空形参列表的noneFunc来做个对比
    func noneFunc() {
    print("Hello, world~")
    }
     
    // noneFunc可以通过缺省实参的形式进行调用。
    // 注意!这里不能给noneFunc传递任何实参,
    // 包括空元组也不行
    noneFunc()
     
    /// 这里定义了函数foo,
    /// 它具有两个Int类型的形参,
    /// 由于这里函数体内不对任何形参进行使用,
    /// 所以形参名均可缺省
    func foo(a _: Int, _: Int) {
    print("foo Int!")
    }
     
    // 对foo进行调用
    foo(a: 100, 200)
     
    /// 这里定义了含有两个形参的函数boo,
    /// 这两个形参均为Void类型
    func boo(_: Void, _: Void) {
     
    }
    // 在调用boo函数时,由于它具有两个形参,
    // 因此这里两个形参所对应的实参均不能缺省,
    // 都需要用空元组显式给出
    boo(_: (), _: ())
    

    上述代码我们可以看到,对于使用通配符作为实参标签的形参,在做实参传递时无需给出实参标签,如果要给的话也可以给出通配符作为实参标签。另外从Swift4.0起,调用形参类型为 Void 类型的函数时,对应的实参必须显式传入空元组。

    默认形参值

    Swift编程语言可以对函数形参设置一个默认值。如果一个形参具有一个默认值,那么我们在调用该函数时可以无需对此形参传递实参,也就是说带有默认值的形参所对应的实参可以缺省,并且连同实参标签一起缺省。

    /// 定义了带有两个形参的函数foo,
    /// 其中第二个形参带有默认值2.0
    func foo(param1: Int, param2: Float = 2.0) {
    print("Value = \(Float(param1) + param2)")
    }
     
    // 这里对foo函数的调用缺省了对param2形参的实参传递
    foo(param1: 10)
     
    // 这里对foo函数的调用也传递了param2所对应的实参
    foo(param1: 20, param2: 5.0)
     
    /// 这里定义了一个带有Int类型的形参param,
    /// 它具有默认值10
    func boo(param: Int = 10) {
    print("param = \(param)")
    }
    // 调用boo函数时使用默认的形参值
    boo()
     
    // 调用boo函数时显式传递实参3
    boo(param: 3)
    

    在调用一个带有默认形参值的函数时,倘若我们使用该形参的默认值,那么我们连它的实参标签都不需要写,实参标签加上实参值全都缺省。
    此外,Swift编程语言还有一点比C++等也具有默认形参值的编程语言所要灵活的是,带有默认值的形参可以放在参数列表中的任何位置,而无需只是放在形参列表的末尾端。但是这里需要注意,如果带有默认值的形参摆放在不带默认值的形参之间的话,我们需要用不同的实参标签来进行标识,否则就可能会失去带有默认值形参的意义。我们下面看一些例子:

    /// 定义了带有三个形参的函数foo,
    /// 其中第二个形参带有默认值2.0,
    /// 第一个与第三个形参都不带默认值
    func foo(param1: Int, param2: Float = 2.0, param3: String) {
     
    }
     
    // 这里缺省对带有默认形参值的param2传递实参
    foo(param1: 10, param3: "abc")
    // 这里对param2也添加了默认值
    foo(param1: 10, param2: 5.0, param3: "Hello")
     
    /// 定义了带有三个形参的函数boo,
    /// 其中第一个形参与第三个形参含有默认值
    func boo(param1: Int = 1, param2: Int, param3: Float = 0.5) {
     
    }
     
    // 这里,param1与param3都采用了默认值
    boo(param2: 10)
     
    // 这里只对param1使用默认值
    boo(param2: 10, param3: 1.0)
     
    // 这里对param1与param3都不采用默认值
    boo(param1: 10, param2: 20, param3: 3.0)
     
    /// 定义了带有三个形参的函数oops
    /// 但与boo所不同的是,
    /// 这里oops的三个实参标签均为缺省的通配符
    func oops(_ a: Int = 1, _ b: Int, _ c: Int = 2) {
     
    }
    // 这里调用没问题,仅对第三个形参使用默认值
    oops(10, 20)
     
    // 编译错误:缺失了第二个形参的实参
    oops(10)
    

    不定个数的形参

    Swift编程语言同C语言一样,支持不定个数的形参(variadic parameters)。不过与C语言有所区别的是,Swift中的不定个数的形参表示符 ... 前面需要跟一个类型,并且同带有默认值的形参一样,无需放在形参列表的最后,可以放在中间。当我们在函数体内使用不定个数的形参时,可以将它视作为它所指定类型的数组,我们可以通过 count 属性获取实参所传递过来的参数个数;通过下标操作符来获取对应的实参值。我们先看以下例子。

    /// 这里定义了一个带有不定参数个数的函数foo
    /// 这里不定个数的形参的类型为Int
    func foo(a: Int, b: Int...) {
    var sum = a
     
    // 判定传递过来的实参个数是否大于0
    if b.count > 0 {
    for i in b {
    sum += i
    }
    }
     
    print("sum = \(sum)")
    }
    // 不传任何不定个数形参所对应的实参
    foo(a: 0)
     
    // 传了1个不定个数形参所对应的实参
    foo(a: 0, b: 1)
     
    // 传了3个不定个数形参所对应的实参
    foo(a: 0, b: 1, 2, 3)
     
    /// 这里定义了含有两个形参的函数boo,
    /// 其中第一个形参为不定个数的形参a,
    /// 第二个形参为带有默认值的形参b
    func boo(a: Int..., b: Int = 1) {
    if a.count > 1 {
    print("value = \(a[0] + a[1])")
    }
    else {
    print("value = \(b)")
    }
    }
     
    // 对不定个数的形参a所对应的实参缺省
    // 对带有默认值的形参b使用其默认值
    boo()
     
    // 对不定个数的形参a传了三个实参,
    // 对带有默认值的形参b使用其默认值
    boo(a: 1, 2, 3)
     
    // 对不定个数的形参a所对应的实参缺省
    // 对带有默认值的形参b传递实参10
    boo(b: 10)
     
    // 对不定个数的形参a传了两个实参,
    // 对带有默认值的形参b传递实参7
    boo(a: 5, 6, b: 7)
     
    /// 这里定义了一个带有不定个数形参的函数foo
    func goo(params: Int? ...) {
    // 这里不定个数的形参params被视作为[Int?]类型
    print("params = \(String(describing: params))")
    }
     
    // 输出:params = []
    goo()
     
    // 输出:params = [nil, Optional(10)]
    goo(params: nil, 10)
    

    从上述代码片段中我们可以发现Swift中的不定个数的形参非常灵活,同时又是类型安全的。

    输入输出形参

    在Swift编程语言中,所有形参默认都为常量。因此我们在函数体内如果对一个普通的形参修改其值,那么就会引发编译报错。Swift编程语言中引入了一种输入输出形参(in-out parameters),使得该形参的值不仅能被修改,而且还能影响它所对应的实参值。这种机制乍一看很像C语言中的指针传参,更像C++中的传引用,但是Swift中的输入输出形参机制与传统面向对象编程语言中传引用的方式有所不同。Swift中的输入输出形参其实是以“输入-处理-输出”这种经典的数据流的形式进行处理的,也就是说,当我们调用一个函数时,对其中某一个输入输出形参传递了某个实参,在该函数内会做以下三步:

    • 当函数在调用前,先将实参的值拷贝到所对应的输入输出形参中。
    • 在执行函数中的代码时,对输入输出形参的值进行修改。
    • 在函数返回之后,将修改后输入输出形参的值拷贝回对应的实参中。

    由于在调用一个函数时,编译器知道所调函数对应的形参存放在当前栈空间的哪个位置,因此在函数返回时只要不紧接着调用其他函数,当前形参的上下文依然能保持不动,这就使得编译器能够很容易地生成对应的输入输出形参拷贝到相应实参的代码。

    所以Swift编程语言中用于修饰输入输出形参的关键字 inout 所对应的类型并非相当于C++中的引用类型,它实际上仅仅只是个类型限定符,用于描述当前形参在该函数体内可被修改而已。而在调用函数时,我们在表示实参的表达式之前加上 & 单目前缀操作符,表示该实参将会在函数返回后被所对应的形参进行赋值。 单目前缀操作符 & 的操作数必须是一个变量,而不能是一个常量。下面我们来看一些例子。

    /// 这里定义了一个函数foo,
    /// 其第二个形参b为输入输出形参
    func foo(a: Int, b: inout Int) {
    // 我们可以对输入输出形参b进行修改
    // 但这里不能对形参a进行修改
    b += a
    }
     
    var x = 100
     
    // 当foo函数返回之后,
    // 我们可以看到实参x的值也被修改了
    foo(a: 10, b: &x)
    // 这里输出:x = 110
    print("x = \(x)")
     
    var array = [1, 2, 3]
    // 调用此函数的过程可以看作为:
    // foo(let a: array[0], var b: array[2])
    // 等foo返回之后,array[2] = b
    foo(a: array[0], b: &array[2])
     
    // 这里输出:array[2] = 4
    print("array[2] = \(array[2])")
     
     
    /// 定义了一个函数boo,
    /// 它带有一个输入输出形参dst,
    /// 一个普通形参src
    func boo(dst: inout [Int], src: [Int]) {
    if dst.count < src.count {
    return
    }
     
    // 这里将输入输出形参数组的相应元素的值
    // 与源数组相应元素的值进行相加,
    // 然后将结果存放到输入输出目的数组中。
    // 这里对src数组中的元素不可修改
    for i in 0 ..< src.count {
    dst[i] += src[i]
    }
    }
     
    // 要作为输入输出形参的实参,必须使用变量
    var arr = [1, 2, 3, 4]
    // 对一个数组变量作为输入输出形参的实参,
    // 与普通类型一样,前面直接加 & 单目前缀操作符
    boo(dst: &arr, src: [10, 20, 30, 40])
    // 输出:arr = [11, 22, 33, 44]
    print("arr = \(arr)")
     
    /// 这里定义了一个函数goo,
    /// 它带有一个输入输出形参t,
    /// 它是一个元组
    func goo(t: inout (Int, Float)) {
    t.0 += 10
    t.1 *= 0.5
    }
     
    var tuple = (5, Float(8.0))
     
    goo(t: &tuple)
    // 输出:tuple = (15, 4.0)
    print("tuple = \(tuple)")
     
    /// 定义了一个函数moo,
    /// 它带有一个输入输出形参a,
    /// 并且是一个Optional类型
    func moo(a: inout Int?) {
    // 这里表示如果a不为空,
    // 那么将它的值加100
    a? += 100
    }
     
    var a: Int? = nil
     
    moo(a: &a)
    // 这里输出:a = nil
    print("a = \(String(describing: a))")
     
    a = 10
    moo(a: &a)
    // 这里输出:a = Optional(110)
    print("a = \(String(describing: a))")
     
    /// 这里定义了函数poo,
    /// 输入输出参数的类型为Int!
    func poo(a: inout Int!) {
    // 如果形参a为空,那么将它赋值为0
    if a == nil {
    a = 0
    }
    else {
    // 否则将原有的值加10
    a! += 10
    }
    }
     
    // 我们这里使用 Int! 来声明变量b
    var b: Int! = nil
    // 请注意,这里只能传变量b,不能传变量a,
    // 因为输入输出参数类型的匹配比一般的要求更为严苛,
    // inout Int! 与 inout Int? 作为不兼容的参数类型
    // 同样,对于上面的moo函数,
    // 只能传变量a,不能传这里的b
    poo(a: &b)
     
    // 这里输出:b = Optional(0)
    print("b = \(b)")
     
    b = 5
    // 这里输出:b = 5
    print("b = \(b!)")
     
     
    // 作为赋值表达式,以下都是合法的
    a = b
    b = a
     
    func test(a: Int?, b: Int!) {
     
    }
     
    // 这里 Int! 类型的b可以作为 Int? 类型形参a的实参
    // Int? 类型的a可以作为 Int! 类型形参b的实参
    test(a: b, b: a)
    

    函数重载

    Swift编程语言同C++一样,默认支持函数重载。不过Swift的形参因为还含有实参标签,并且对函数返回类型也做重载依据,因此在重载上灵活性更高。什么是函数重载呢?如果在同一作用域及名字空间中出现一组相同名字的函数,这些函数具有不同的形参类型或形参个数,或是带有不同的返回类型,那么称这组函数为重载函数(overloaded functions)。所以判断重载函数的依据是形参的类型以及个数。我们下面举一些例子。

    /// 不带有任何形参的函数foo
    func foo() {
    print("default foo")
    }
     
    /// 带有一个形参的函数foo,
    /// 形参类型为Int
    func foo(_ a: Int) {
    print("foo a = \(a)")
    }
     
    /// 带有一个形参的函数foo,
    /// 形参类型为Float
    func foo(_ b: Float) {
    print("foo b = \(b)")
    }
     
    /// 带有一个形参的函数foo,
    /// 形参类型为Int?
    func foo(_ c: Int?) {
    print("foo c = \(String(describing: c))")
    }
     
    /// 带有一个形参的函数foo,
    /// 形参类型为inout Int
    func foo(_ d: inout Int) {
    print("foo d = \(d)")
    }
     
    /// 带有一个形参的函数foo,
    /// 形参类型为inout Int?
    func foo(_ e: inout Int?) {
    print("foo e = \(String(describing: e))")
    }
     
    /// 带有一个形参的函数foo,
    /// 形参类型为inout Int!
    func foo(_ f: inout Int!) {
    print("foo f = \(String(describing: f))")
    }
     
    // 尽管这里形参g的类型与foo(_ c: Int?)的一样,
    // 但它与foo(_ c: Int?)的返回类型不同,
    // 所以允许重载。如果把此函数的返回类型改为Void,
    // 那么编译即会报错:对'foo'的无效重声明
    func foo(_ g: Int?) -> Int {
    return g ?? 0
    }
     
    /// 带有一个形参的函数foo,
    /// 形参类型为Int,但这里具有实参标签h
    func foo(h: Int) {
    print("foo h = \(h)")
    }
     
    // 调用没有形参的foo
    foo()
     
    // 调用foo(_ a: Int)
    foo(1)
     
    // 调用foo(_ b: Float)
    foo(0.5)
     
    // 由于这里对foo的调用既可以是foo(_ c: Int?),
    // 也可以是foo(_ g: Int?) -> Int,
    // 因此我们使用 as 投射操作符显式指定foo的函数类型,
    // 使得这里调用的是foo(_ c: Int?)
    (foo as (Int?) -> Void)(Int?(2))
    var x: Int = 3
    var y: Int? = 4
    var z: Int! = 5
     
    // 调用foo(_ d: inout Int)
    foo(&x)
     
    // 调用foo(_ e: inout Int?)
    foo(&y)
     
    // 调用foo(_ f: inout Int!)
    foo(&z)
     
    // 调用foo(_ g: Int?) -> Int
    let g: Int = foo(Int?(6))
    // 输出:g = 6
    print("g = \(g)")
     
    // 调用foo(h: Int)
    foo(h: 7)
     
    

    上述代码例子中的函数foo就是一组重载函数。这里我们可以看到函数重载的判断依据为形参个数、形参类型以及函数的返回类型,而这三个要素也表征了一个完整的函数类型。此外,对于最后一个 foo(h: Int) 函数,由于它的实参标签与上述函数的有所不同,因此严格意义上,它不属于其他foo函数的同类,因为它与其他foo函数相比具有不同的函数签名。
    这里我们还看到了,对于 foo(_ c: Int?) 函数以及 foo(_ g: Int?) -> Int 函数,由于它们具有相同的形参个数以及相同的形参类型,因此这里编译器是通过函数返回类型作为判断依据的。对于此类重载函数,由于我们难以判别当前所调用的是哪个函数,因此我们往往会使用 as 投射操作符将函数标识显式转换为特定的函数类型进行调用;或者通过对左值对象显式标注类型来告诉编译器,此时应该调用哪个类型的函数。不过为了避免这种麻烦,我们通常使用不同的实参标签来更好地加以区分。

    函数类型与函数签名

    几乎所有类C编程语言中的函数都有特定的类型,Swift编程语言也是如此。Swift中一个函数的类型由其形参列表中各个形参的类型与函数返回类型构成。既然函数有其特定的类型,那么我们就可以用某一函数类型声明指向它的一个引用对象,这称为对函数的引用对象。我们下面先看一些例子。

    // 这里foo的类型为() -> Void
    func foo() {
    print("foo void")
    }
     
    // 这里foo的类型为(Int) -> Void
    func foo(_ a: Int) {
    print("foo Int")
    }
     
    // 这里foo的类型也是(Int) -> Void
    func foo(a: Int) {
    print("Hello, world!")
    }
     
    // 这里声明了一个指向函数的引用变量ref
    // 其类型为:(Int) -> Void,
    // 并且指向了foo(_ a: Int)函数
    var ref = foo(_:)
     
    // 对函数的引用对象ref进行调用。
    ref(10)
     
    // 对函数引用对象的调用时,
    // 实参标签也可以显式使用通配符来表示
    ref(_: 20)
     
    /// 定义一个函数magic,
    /// 它含有一个输入输出参数fun,
    /// 其类型为(Int) -> Void函数类型
    /// 而magic函数完整的函数类型为:(inout (Int) -> Void) -> Void
    /// 注意,这里形参中的inout不能省
    func magic(fun: inout (Int) -> Void) {
    // 这里将fun修改为指向foo(a:)函数
    fun = foo(a:)
    }
     
    magic(fun: &ref)
     
    // 调用完magic函数之后,
    // 再调用ref,输出:Hello, world!
    ref(10)
     
    // 我们为了更直观地观察magic函数的类型,
    // 这里声明了一个指向它的引用对象magicRef,
    // 并且显式地给出了函数类型
    let magicRef: (inout (Int) -> Void) -> Void = magic
     
    magicRef(&ref)
     
    // 定义了函数poo,它带有一个Void类型的形参
    func poo(_: Void) {
    print("poo")
    }
     
    // 指向函数的引用对象也能使用Optional类型,
    // 但这里必须注意,函数类型需要用圆括号包起来,
    // 否则 ? 所作用的将是函数的返回类型。
    // 另外这里不需要对foo使用类型投射操作,
    // 因为在声明fun的时候就已经显式给出了
    // 它所引用的函数类型
    let fun: (() -> Void)? = foo
     
    // 对fun的调用需要使用optional-chaining操作符
    fun? ()
    fun! ()
     
    /// 这里所定义的boo函数具有一个形参,
    /// 该形参类型为(Int, Float)元组类型
    func boo(tuple: (Int, Float)) {
    print("boo tuple")
    }
     
    /// 这里定义的moo函数具有两个形参,
    /// 分别是Int与Double类型
    func moo(i: (Int), d: (Double)) {
    print("boo 2 params")
    }
     
    // 我们在声明一个函数引用对象的时候,
    // 其函数形参类型不能带有实参标签
    let fun1: ((Int, Float)) -> Void = boo
     
    // fun2的类型被推导为:(Int, Double) -> Void
    let fun2 = moo
     
    // 我们在通过函数引用对象做函数调用时,
    // 都不需要使用实参标签,
    // 因为对于一个具体函数类型的函数引用对象而言,
    // 它所指向的函数实体已经是确定的了
    fun1((10, Float(1.0)))
    fun2(10, 1.0)
    

    从上述代码片段中我们可以看到,一个指向函数的引用对象与其他类型的对象一样,可以将它作为函数形参类型,甚至是输入输出的形参类型,当然也能作为函数返回类型,甚至可以作为数组、元组等类型的成员,并且也能作为一个Optional对象。Swift编程指南中在介绍闭包的章节中提到了闭包是引用类型(Closures Are Reference Types),并且专门做了一小节进行描述。不过函数类型也是如此,因此一个指向函数的引用对象也是事实上的引用(reference),跟类类型一样。
    此外,最后还描述了一个指向函数的引用对象类型的函数形参中不能含有形参标签,并且在通过它做函数调用时也不需要给出实参标签,因为实参标签属于函数签名的一部分,用于标识一个特定的函数实体,而函数类型本身就是一个纯粹的类型,因此不需要实参标签,这一点与带有标签名的元组不同。这么一来我们就很自然地引出了函数签名(function signature)这个语法特性!
    函数签名这个语法特性其实引申自Objective-C的方法签名。在Swift编程语言中,一个函数名并不是唯一用于标识当前函数的标志(designator),而是需要通过结合每个形参所持有的实参标签。对于一组重载函数,如果有任意两个重载函数的函数签名全都相同,那么再去判定那两个函数的类型。而严格意义上,两个具有不同函数签名的函数就已经表征了其不同的实体。函数签名的表示如下代码所示:

    /// 函数foo的签名为:foo
    func foo() {
    print("foo")
    }
    /// 函数boo的签名为:boo(_:)
    func boo(_: Void) {
    print("boo")
    }
     
    /// 函数moo的签名为:moo(a:)
    func moo(a: Int) {
    print("moo")
    }
     
    /// 函数poo的签名为:poo(a:_:c:)
    func poo(a: Int, _: Int, c: Int) {
    print("poo")
    }
    

    这与Objective-C中方法签名十分类似。我们可以直接利用这些函数的函数签名对相应函数进行调用,如下代码所示:

    foo()
     
    boo(_:)(())
     
    moo(a:)(0)
     
    poo(a:_:c:)(1, 2, 3)
    

    我们可以看到,一个函数签名表达式就已经表征了一个指向它所表示的函数的引用,所以该表达式的类型就是一个函数类型,我们在通过函数签名表达式去做函数调用时,也不需要显式给出实参标签,并且也不允许给出。
    这里大家还要注意的一点就是 foo 的函数签名就是函数名自身,因为其形参列表为空。而在函数定义中,如果实参标签以通配符的形式给出,那么该实参标签也将作为此函数签名的一部分,我们可以参考 boo 函数以及 poo 函数。
    有了函数签名之后,我们对一组具有相同名字的函数进行重载时也无需关心参数类型了,我们完全可以使用不同的实参标签来区分各个不同的函数实体。可以参考以下代码。

    func foo(_: Void) {
    print("foo Void")
    }
     
    func foo(_: Int) {
    print("foo Int")
    }
     
    func foo(i: Int) {
    print("foo Int i")
    }
     
    func foo(f: Float) {
    print("foo Float")
    }
    func foo(d: Double) {
    print("foo Double")
    }
     
    func foo(_: Void, _: Int) {
    print("foo Void and Int")
    }
     
    // 由于函数签名foo(_:)对应的函数有两个,
    // 因此这里使用投射操作显式指明函数类型
    let ref1 = foo(_:) as () -> Void
     
    // 由于这里在声明ref2时显式地给出了函数类型,
    // 因此这里对于foo(_:)表达式而言
    // 就无需使用投射操作显式给出函数类型了
    let ref2: (Int) -> Void = foo(_:)
     
    // 这里ref3引用的函数为foo(i:)
    let ref3 = foo(i:)
     
    // 这里ref4引用的函数为foo(f: Float)
    let ref4 = foo(f:)
     
    // 这里ref5引用的函数为foo(d:)
    let ref5 = foo(d:)
     
    // 这里ref6所引用的函数为foo(_:_:)
    let ref6 = foo(_:_:)
     
    // 以下为上述6个函数引用对象的调用
    ref1()
    ref2(1)
    ref3(1)
    ref4(Float(1.0))
    ref5(1.0)
    ref6((), 1)
    

    上述代码中,foo(: Void) 与 foo(: Int) 的函数签名是完全一样的,所以这两个函数属于名副其实的函数重载。而 foo(i: Int) 与 foo(_: Int) 尽管在函数类型上都一样,两者都是 (Int) -> Void,但它们的函数签名不同,所以自然就表示为不同的函数标志。
    在Swift 4.1版本之前有一个函数签名的设计BUG。当存在两个相同函数签名的两个函数时,其中一个函数没有任何形参,而另一个函数带有一个 Void 类型的形参,那么不带任何形参的函数将永远无法被引用到!我们看以下示例:

    /// 定义了一个不带任何形参的foo函数
    func foo() {
    print("No parameters")
    }
     
    /// 定义了一个带有Void类型的foo函数
    func foo(_: Void) {
    print("Void foo")
    }
     
    // 这里调用的是第一个foo函数
    foo()
     
    /// 这里的ref引用的事第二个foo函数
    let ref = foo(_:)
    ref(())
     
    /// 这里对ref2的类型显式标注,
    /// 我们看到,foo(_:)类型可以从
    /// (Void) -> Void 无缝地转换为
    /// () -> Void
    let ref2: () -> Void = foo(_:)
    // 这里的调用不再需要传空元组作为实参
    ref2()
     
    // 这里对foo的引用将会发生编译错误!
    // 编译器不知道foo应该引用的是foo还是foo(_:)
    // 并且这种情况下,我们永远无法对foo进行引用
    let ref3 = foo
    

    从上述例子可以看出,目前Swift 4.0对函数形参类型的把控得更为严格,但由于形参为 Void 类型时可以直接隐式转换为无形参类型,使得存在这两种重载函数时,缺省形参的函数将无法被引用到。不过在Swift4.0中,一个只带有一个 (Void) 形参的函数类型可以隐式转换为不带任何形参的函数类型;同样,不带任何形参的函数类型也能隐式转换为只带有一个 (Void) 形参的函数类型。我们看以下代码

    func foo() {
    print("foo1")
    }
     
    func foo(_: Void = ()) {
    print("foo2")
    }
     
    // 这里调用的是foo,
    // 而不是foo(_:)
    foo()
     
    // (Void) -> Void 可隐式转换为 () -> Void
    let ref: () -> Void = foo(_:)
     
    // 这里就不需要传空元组作为实参了
    ref()
     
    /// () -> Void 也可隐式转换为 (Void) -> Void
    /// 但使用 (Void) -> Void 函数类型时,形参必须显式带上空元组
    let ref1: (Void) -> Void = foo
    ref1( () )
    

    上述代码中还展示了,当同时存在不带任何参数与带有一个 (Void) 形参的同名函数,且该函数使用了空元组作为默认形参值时,直接对该函数不传任何实参的调用,将调用的是不带任何形参的那个函数。此外我们这里还能看到,如果我们给一个带有 Void 类型的形参添加上空元组作为默认值,那么此时编译器能把不带任何参数的 foo 与带有一个 Void 类型的形参的 foo(_:) 区分出。
    而在Swift 4.1版本中,Apple终于修复了这个历经将近4年之多的大BUG!由于在Swift 4.0.x中,不具有默认函数实参值的任何形参都需要显式地传递实参对象,这使得Swift 4.0.x中对同一函数名的带有一个 Void 类型参数的函数与不具有任何参数的函数调用不会引发歧义现象!而在Swift 4.1中能进一步通过函数类型来区分这两者的差别,这在此前版本中都会认为是同一种类型。下面我们看一个具体的例子:

    /// 定义了一个函数foo,它不具有任何参数。
    /// 其类型为:() -> Void
    fileprivate func foo() {
    print("foo with no arguments")
    }
     
    /// 定义了一个函数foo,它具有一个Void类型的形参。
    /// 其类型为:(Void) -> Void
    fileprivate func foo(v: Void) {
    print("foo with a Void argument: \(v)")
    }
     
    /// 定义了一个函数foo,它具有一个Void类型的形参。
    /// 其类型为:(Void) -> Void
    fileprivate func foo(_: Void) {
    print("foo with a Void argument!")
    }
     
    foo(v: ())
    foo()
    foo(())
     
    var ref = foo(v:)
    let ref2 = foo as () -> Void
     
    ref(())
    ref2()
     
    ref = foo(_:)
    ref(())
     
    

    上述代码在Swift 4.1环境中编译、执行都不会有任何问题。这里,第一个foo的类型为 () -> Void ,因此如果我们要用一个函数引用对象去引用它,要么显式指定函数引用的类型,要么通过 as 关键字显式将此函数做类型标注,这样就能避免发生引用歧义。而由于在之前的版本中 () ->Void 与 (Void) -> Void 这两者是相兼容的,因此会引发引用歧义的现象~

    嵌套函数定义

    我们之前所定义的函数都是放在文件作用域中的,而Swift编程语言允许在函数中定义一个嵌套函数(nested functions),这是许多传统面向对象的编程语言所不具备的。Swift中定义在某一函数内的嵌套函数名可以将定义在函数外部的相同函数名给覆盖掉。其外部函数可以直接调用此嵌套函数,当然也可以将它作为返回值返回出去。嵌套函数可以访问它所在函数的局部对象,此时嵌套函数就不是一般的函数了,而是作为一个命名闭包。我们下面先看一个相对比较简单的例子。

    /// 在文件作用域定义了一个函数closure
    /// 其类型为:(Int) -> Void
    func closure(a: Int) {
    print("closure value = \(a)")
    }
     
    /// 在文件作用域定义了一个函数outside
    /// 它返回一个 (Int) -> Void 类型的函数引用对象
    func outside() -> (Int) -> Void {
     
    let a = 10
    var b = 20
    /// 在outside内部定义了一个嵌套函数inner,
    /// 它不引用任何其外部函数的局部对象
    func inner(input: Int) {
    print("inner value = \(input)")
    }
     
    /// 在outside内部定义了一个嵌套函数closure,
    /// 它引用了其外部函数的局部对象a和b,
    /// 并且覆盖了文件作用域所定义的closure函数
    func closure(input: Int) {
    b += a + input
    print("b = \(b)")
    }
     
    // 这里直接调用inner嵌套函数
    inner(input: a)
     
    // 这里所返回的是嵌套函数closure,
    // 而不是文件作用域中定义的closure函数
    return closure
    }
     
    // 调用文件作用域的closure函数
    closure(a: 100)
     
    // 这里声明了一个常量ref
    // 来接收outside函数所返回的嵌套函数引用
    let ref = outside()
     
    // 调用一次ref,输出:b = 35
    ref(5)
     
    // 再调用一次ref,输出:b = 50
    ref(5)
    

    Swift的嵌套函数不仅可以直接定义在函数体内,而且还能定义在语句块作用域内。我们看以下例子。

    /// 在文件作用域定义了函数foo
    func foo() {
     
    print("outside foo")
     
    /// 在foo的函数体内定义了嵌套函数foo,
    /// 并将其外部的foo名字给覆盖
    func foo() {
    print("middle foo")
     
    /// 在嵌套foo的函数体内再度定义了一个嵌套函数foo,
    /// 这里将其外部函数foo给覆盖
    func foo() {
    print("inner foo")
    }
     
    // 调用第一层嵌套函数foo中所定义的foo函数,
    // 输出:inner foo
    foo()
    }
     
    // 调用第一层嵌套的foo函数
    foo()
     
    do {
    /// 在do语句块内定义了嵌套函数doFoo
    func doFoo() {
    print("do foo")
    }
     
    /// 这里声明了一个doFoo的函数引用变量ref
    var ref = doFoo
     
    if true {
    /// 在if语句块内定义了嵌套函数ifFoo
    func ifFoo() {
    print("if foo")
    }
     
    // 这里将ref指向ifFoo函数
    ref = ifFoo
    }
     
    // 调用do语句块内的doFoo函数
    doFoo()
    // 调用ref函数引用
    ref()
    }
    }
     
    foo()
     
    在调用了最外部的 foo 函数之后,输出结果如下:
    outside foo
    middle foo
    inner foo
    do foo
    if foo
    

    相关文章

      网友评论

          本文标题:函数

          本文链接:https://www.haomeiwen.com/subject/vmpdaftx.html