Swift Learning (8) - Functions (Fully Ver.)

Swift Learning (8) - Functions (Fully Ver.)

August 22, 2021·Jensen
Jensen

image

函数是一段完成特定任务的独立代码片段。可以通过函数命名来标识某个函数的功能,这个名字可以被用来在需要的时候“调用”这个函数来玩成它的任务。

Swift统一的函数语法非常灵活,可以用来表示任何函数,包括从最简单的没有参数名字的C风格函数,到复杂的带局部和外部参数名的Objective-C风格函数。参数可以提供默认值,以简化函数调用。参数也可以既当做传入参数也当做传出参数,也就是说,一旦函数执行结束,传入的参数值将被修改。

Swift中,每个函数都有一个由函数的参数值类型和返回值类型组成的类型,可以把函数类型当做任何其他普通变量类型一样处理,这样就可以更简单地把函数当做别的函数的参数,也可以从其他函数中返回函数。函数的定义可以写在其他函数定义中,这样可以在嵌套函数范围内实现功能封装。


函数的定义与调用

当定义一个函数时,可以定义一个或多个有名字和类型的值,作为函数的输入,称为参数,也可以定义某种类型的值作为函数执行结束时的输出,称为返回类型。

每个函数都有函数名,用来描述函数执行的任务。要使用一个函数时,用函数名来“调用”这个函数,并传给它匹配的输入值(称作实参)。函数的实参必须与函数参数表里的参数的顺序一致。

下面例子中的函数名字是greet(person:),之所以叫这个名字,是因为这个函数用一个人的名字当做输入,并返回向这个人问候的语句。为了完成这个任务,需要定义一个输入参数:一个叫做personString值,和一个包含给这个人问候语的String类型返回值。

func greet(person: String) -> String {
    let greeting = "Hello, " + person + "!"
    return greeting
}

所有的这些信息汇总起来称为函数的定义,并以func作为前缀。指定函数返回类型时,用返回箭头->(一个连字符后跟一个右尖括号)后跟返回类型的名称的方式来表示。

该定义描述了函数的功能,它期望接收什么作为参数和执行结束时它返回的结果是什么类型。这样的定义使得函数可以在别的地方以一种清晰的方式被调用:

print(greet(person: "Jensen"))  // 打印输出"Hello, Jensen!"
print(greet(person: "Morris"))  // 打印输出"Hello, Morris!"
---
output: Hello, Jensen!
Hello, Morris!

调用greet(person:)函数时,在圆括号中传给它一个String类型的实参,例如greet(person: "Anna")。正如上面所示,因为这个函数返回一个String类型的值,所以greet可以被包含在print(_:separator:terminator:)的调用中,用来输出这个函数的返回值。

注意

print(_:separator:terminator:)函数的第一个参数并没有设置一个标签,而其他的参数因为已经有了默认值,因此也是可选的。关于这些函数语法上的变化详见下方关于函数参数标签和参数名以及默认参数值。

greet(person:)的函数体中,先定义了一个新的名为greetingString常量,同时,把对personName的问候消息赋值给了greeting。然后用return关键字把这个问候给返回出去。一旦return greeting被调用,该函数结束它的执行并返回greeting的当前值。

为了简化这个定义,可以将问候消息的创建和返回写成一句:

func greetAgain(person: String) -> String {
    return "Hello again, " + person + "!"
}
print(greetAgain(person: "Jensen"))  // 打印输出"Hello again, Jensen!"
---
output: Hello again, Jensen!

函数参数与返回值

函数参数与返回值在Swift中非常灵活。可以定义任何类型的函数,包括从只带一个未名参数的简单函数到复杂的带有表达性参数名和不同参数选项的复杂函数。

无参数函数

函数可以没有参数,下面这个函数就是一个无参数函数,当被调用时,它返回固定的String消息:

func sayHelloWorld() -> String {
    return "Hello, World!"
}
print(sayHelloWorld())  // 打印输出"Hello, World!"
---
output: Hello, World!

尽管这个函数没有参数,但是定义中在函数名后面还是需要一对圆括号。当被调用时,也需要在函数名后面写一对圆括号。

多参数函数

函数可以有多种输入参数,这些参数被包含在函数的圆括号中,以逗号分隔。

下面这个函数用一个人名和是否已经打过招呼作为输入,并返回对这个人的适当的问候语:

func greet(person: String, alreadyGreeted: Bool) -> String {
    if alreadyGreeted {
        return greetAgain(person: person)
    } else {
        return greet(person: person)
    }
}
print(greet(person: "Jensen", alreadyGreeted: true))  // 打印输出"Hello again, Jensen!"
---
output: Hello again, Jensen!

可以通过在括号内使用逗号分隔来传递一个String参数值和一个标识为alreadyGreetedBool值,来调用greet(person:alreadyGreeted:)函数。注意这个函数和上面greet(person:)是不同的。虽然,它们都有着同样的名字greet,但是greet(person:alreadyGreeted:)函数需要两个参数,而greet(person:)只需要一个参数。

无返回值函数

函数可以没有返回值。下面是greet(person:)函数的另一个版本greets,这个函数直接打印一个String值,而不是返回它:

func greets(person: String) {
    print("Hello, \(person)!")
}
greets(person: "Jensen")  // 打印输出"Hello, Jensen!"
---
output: Hello, Jensen!

因为这个函数不需要返回值,所以这个函数的定义中没有返回箭头->和返回类型。

注意

严格地说,即使没有明确定义返回值,该greet(person:)函数依然返回一个值。没有明确定义返回类型的函数返回一个Void类型的特殊值,该值为一个空元组,写成()。

调用函数时,可以忽略该函数的返回值:

func printAndCount(string: String) -> Int {
    print(string)
    return string.count
}
func printWithoutCounting(string: String) {
    let _ = printAndCount(string: string)
}
printAndCount(string: "Hello, World")  // 打印输出"Hello, World",并且返回值12
printWithoutCounting(string: "Hello, World")  // 打印输出"Hello, World",但是没有任何返回值
---
output: Hello, World
Hello, World

第一个函数printAndCount(string:),输出一个字符串并返回Int类型的字符数。第二个函数printWithoutCounting(string:)调用了第一个函数,但是却忽略了它的返回值,当第二个函数被调用时,消息依然会由第一个函数输出,但是返回值不会被用到。

注意

返回值可以被忽略,但是定义了又返回值的函数必须返回一个值,如果在函数定义底部没有返回任何值,将导致编译错误。

多重返回值函数

可以使用元组(tuple)类型让多个值作为一个复合值从函数中返回。

下例中定义了一个名为minMax(array:)的函数,作用是一个Int类型的数组中找出最小值与最大值。

func minMax(array: [Int]) -> (min: Int, max: Int) {
    var currentMin = array[0]
    var currentMax = array[0]
    for value in array[1..<array.count] {
        if value < currentMin {
            currentMin = value
        } else if value > currentMax {
            currentMax = value
        }
    }
    return (currentMin, currentMax)
}

minMax(array:)函数返回一个包含两个Int值的元组,这些值被标记为minmax,以便查询函数的返回值可以通过名字来访问它们。

minMax(array:)的函数体中,在开始的时候设置两个工作变量currentMincurrentMax的值作为数组中的第一个数。然后函数会遍历数组中剩余的值并检查该值是否比currentMincurrentMax更小或更大。最后数组中的最小值和最大值作为一个包含两个Int值的元组返回。

因为元组的成员值已经被命名,因此可以通过.语法访问检索到的最小值与最大值:

let bounds = minMax(array: [8, -6, 2, 109, 3, -71])
print("Min is \(bounds.min) and max is \(bounds.max).")  // 打印输出"Min is -71 and max is 109."
---
output: Min is -71 and max is 109.

需要注意的是,元组的成员不需要在元组从函数中返回时命名,因为它们的名字已经在函数返回类型中指定了。

可选元组返回类型

如果函数返回的元组类型有可能整个元组都“没有值”,可以使用可选的元组返回类型反映整个元组可以是nil的事实。可以通过在元组类型的右括号后面放置一个问号来定义一个可选元组,例如(Int, Int)?(String, Int, Bool)?

注意

可选元组类型如(Int, Int)?与元组包含可选类型如(Int?, Int?)是不同的。可选的元组类型,整个元组是可选的,而不只是元组中每个元素值。

前面的minMax(array:)函数返回了一个包含两个Int值的元组。但是函数不会对传入的数组执行任何安全检查,如果array参数是一个空数组,如上定义的minMax(array:)在试图访问array[0]时会触发一个运行时的错误。

为了安全地处理这个“空数组”问题,将minMax(array:)函数改写为使用可选元组返回类型,并且当数组为空时返回nil:

func MinMax(array: [Int]) -> (min: Int, max: Int)? {
    if array.isEmpty {
        return nil
    }
    var currentMin = array[0]
    var currentMax = array[0]
    for value in array[1..<array.count] {
        if value < currentMin {
            currentMin = value
        } else if value > currentMax {
            currentMax = value
        }
    }
    return (currentMin, currentMax)
}

可以使用可选绑定来检查minMax(array:)函数返回的是一个存在的元组值还是nil

if let bounds = MinMax(array: [8, -6, 2, 109, 3, -71]) {
    print("Min is \(bounds.min) and max is \(bounds.max).")
}  // 打印"Min is -71 and max is 109."
if let bounds = MinMax(array: []) {
    print("Min is \(bounds.min) and max is \(bounds.max).")
} else {
    print("Array is null.")
}
---
output: Min is -71 and max is 109.
Array is null.

隐式返回的函数

如果一个函数的整个函数体是一个单行表达式,这个函数可以隐式地返回这个表达式。举个例子,以后的函数有着同样地作用:

func greeting(for person: String) -> String {
    "Hello, " + person + "!"
}
print(greeting(for: "Jensen"))  // 打印输出"Hello, Jensen!"
func anotherGreeting(for person: String) -> String {
    return "Hello, " + person + "!"
}
print(anotherGreeting(for: "Jensen"))  // 打印输出"Hello, Jensen!"
---
output: Hello, Jensen!
Hello, Jensen!

greeting(for:)函数的完整定义是打招呼内容的返回,这就意味着它能使用隐式返回这样更简短的形式。anotherGreeting(for:)函数返回同样的内容,却因为return关键字显得函数更长。任何一个可以背写成一行return语句的函数都可以忽略return

正如将会在“简略的Getter声明”里看到的,一个属性的getter也可以使用隐式返回的形式。


函数参数标签和参数名称

每个函数参数都有一个参数标签(Argument Label)以及一个参数名称(Parameter Name)。参数标签在调用函数的时候使用;调用的时候需要将函数的参数标签写在对应的参数前面。参数名称在函数的实现中使用,默认情况下,函数参数使用参数名称来作为它们的参数标签。

func someFunction(firstParameterName: Int, secondParameterName: Int) {
    // 在函数体内,firstParameterName和secondParameterName代表参数中第一个和第二个参数值
}
someFunction(firstParameterName: 1, secondParameterName: 2)

所有的参数都必须有一个独一无二的名字。虽然多个参数拥有同样的参数标签是有可能的,但是一个唯一的参数标签能够使得代码更具可读性。

指定参数标签

可以在参数名称前指定它的参数标签,中间以空格分隔:

func someFunction(argumentLabel parameterName: Int) {
    // 在函数体内,parameterName代表参数值
}

下面版本的greet(person:)函数,接收一个人的名字和它的家乡,并返回一句问候:

func greet(person: String, from hometown: String) -> String {
    "Hello \(person)! Glad you could visit from \(hometown)."
}
print(greet(person: "Jensen", from: "Hefei"))  // 打印输出"Hello Jensen! Glad you could visit from Hefei."
---
output: Hello Jensen! Glad you could visit from Hefei.

参数标签的使用能够让一个函数在调用时更具有表达力,更类似自然语言,并且仍然保持了函数内部的可读性以及清晰的意图。

忽略参数标签

如果不希望为某个参数添加一个标签,可以使用一个下划线_来替代一个不明确的参数标签。

func someFunction(_ firstParameterName: Int, secondParameterName: Int) {
    // 在函数体内,firstParameterName和secondParameterName代表参数中第一个和第二个参数值
}
someFunction(1, secondParameterName: 2)

如果一个参数有一个标签,那么在调用的时候必须使用参数标签来标记这个参数。

默认参数值

可以在函数体中通过给参数赋值来为任意一个参数定义默认值(Default Value),当默认值被定义后,调用这个函数时可以忽略这个参数。

func someFunction(parameterWithoutDefault: Int, parameterWithDeafult: Int = 12) {
    // 如果在调用时候不传第二个参数,parameterWithDefault会被默认赋值为12传入到函数体中。
}
someFunction(parameterWithoutDefault: 1)  // parameterWithDefault的值为12
someFunction(parameterWithoutDefault: 2, parameterWithDeafult: 6)  // parameterWithDefault的值为6

将不带有默认值的参数放在函数参数列表的最前,一般来说,没有默认值的参数更加的重要,将不带默认值的参数放在最前面保证在函数调用时,翡默认参数的顺序是一致的,同时也使得相同的函数在不同情况下调用时显得更为清晰。

可变参数

一个可变参数(Variadic Parameter)可以接受零个或多个值。函数调用时,可以用可变参数来指定函数参数可以被传入不确定数量的输入值。通过在变量类型名后面加入...的方式来定义可变参数。

可变参数的传入值在函数体中变为此类型的一个数组。例如,一个叫做numbersDouble...型可变参数,在函数体内可以当做一个numbers[Double]型数组常量。

下面的这个函数用来计算一组任意长度数字的算术平均数(Arithmetic Mean):

func arithmeticMean(_ numbers: Double...) -> Double {
    var total: Double = 0
    for number in numbers {
        total += number
    }
    return total / Double(numbers.count)
}
print(arithmeticMean(1, 2, 3, 4, 5))  // 打印输出3.0,是这五个数的平均数
print(arithmeticMean(3, 8.25, 18.75))  // 打印输出10.0,是这三个数的平均数
---
output: 3.0
10.0

一个函数能拥有多个可变参数。可变参数后的第一个行参前必须加上实参标签。实参标签用于区分实参是传递给可变参数还是后面的行参。

输入输出参数

函数参数默认是常量。试图在函数体中更改参数值将会导致编译错误。这意味着不能错误地更改参数值。如果想要一个函数可以修改参数的值,并且想要这些修改在函数调用结束后仍然存在,那么就需要把这个参数定义为输入输出参数(In-Out Parameters)。

定义一个输入输出参数时,在参数定义前加上inout关键字,一个输入输出参数有传入函数的的值,这个值被函数修改,然后被传出函数,替换原来的值。想获取更多的关于输入输出参数的细节和相关的编译器优化,请查看输入输出参数一节。

只能传递变量给输入输出参数,不能传入常量或者字面量,因为这些量是不能够被修改的。当传入的参数作为输入输出参数时,需要在参数名前加上&符,表示这个值可以被函数修改。

注意

输入输出参数不能有默认值,而且可变参数不能用inout标记。

下例中,swapTwoInts(_:_:)函数有两个分别叫做ab的输入输出参数:

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

swapTwoInts(_:_:)函数简单地交换ab的值。该函数先将a的值存到一个临时常量temporaryA中,然后将b的值赋给a,最后将temporaryA赋值给b

可以用两个Int型变量来调用swapTwoInts(_:_:)。需要注意的是,someIntanotherInt在传入swapTwoInts(_:_:)函数前,都加了&的前缀。

var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("SomeInt is now \(someInt), and anotherInt is now \(anotherInt)!")  // 打印输出"SomeInt is now 107, and anotherInt is now 3!"
---
output: SomeInt is now 107, and anotherInt is now 3!

从上面这个例子中,可以看到someIntanotherInt的原始值在swapTwoInts(_:_:)函数中被修改,尽管它们的定义在函数体外。

注意

输入输出参数和返回值是不一样的。上面的swapTwoInts函数并没有定义任何返回值,但仍然修改了someInt和anotherInt的值。输入输出参数是函数对函数体外产生影响的另一种方式。


函数类型

每个函数都有种特定的函数类型,函数的类型由函数的参数类型和返回类型组成。例如:

func addTwoInts(_ a: Int, _ b: Int) -> Int {
    a + b
}
func multiplyTwoInts(_ a: Int, _ b: Int) -> Int {
    a * b
}

这个例子中定义了两个简单的数学函数:addTwoIntsmultiplyTwoInts。这两个函数都接收两个Int值,返回一个Int值。

这两个函数的类型是(Int, Int) -> Int,可以解读为:

  • “这个函数类型有两个Int型的参数并返回一个Int型的值”。

下面是另一个例子,一个没有参数,也没有返回值的函数:

func printHelloWorld() {
    print("Hello, world!")
}

这个函数的类型是:() -> Void,或者叫“没有参数,并返回Void类型的函数”。

使用函数类型

Swift中,使用函数类型就像使用其他类型一样。例如,可以定义一个类型为函数的常量或变量,并将适当的函数赋值给它。

var mathFunc: (Int, Int) -> Int = addTwoInts

这段代码可以被解读为:

  • “定义一个叫做mathFunc的变量,类型是‘一个有两个Int型的参数并返回一个Int型的值的函数’,并让这个新变量指向addTwoInts函数”。

addTwoIntsmathFunc有同样的类型,所以这个赋值过程在Swift类型检查(Type-Check)中是允许的。

现在可以使用mathFunc来调用被赋值的函数了:

print("Result: \(mathFunc(2, 3)).")  // 打印输出"Result: 5."
---
output: Result: 5.

有相同匹配类型的不同函数可以被赋值给同一个变量,就像非函数类型的变量一样:

mathFunc = multiplyTwoInts
print("Result: \(mathFunc(2, 3)).")  // 打印输出"Result: 6."
---
output: Result: 6.

就像其他类型一样,当赋值一个函数给常量或者变量时,可以让Swift来推断其函数类型:

let anotherMathFunc = addTwoInts
// anotherMathFunc被推断为(Int, Int) -> Int类型

函数类型作为参数类型

可以用(Int. Int) -> Int这样的函数类型作为另一个函数的参数类型,这样可以将函数的一部分实现留给函数的调用者来提供:

func printMathResult(_ mathFunc: (Int, Int) -> Int, _ a: Int, _ b: Int) {
    print("Result: \(multiplyTwoInts(a, b)). ")
}
printMathResult(mathFunc, 8, 2)  // 打印输出"Result: 16."
---
output: Result: 16. 

这个例子定义了printMathResult(_:_:_:)函数,它有三个参数:第一个参数叫mathFunc,类型是(Int, Int) -> Int,可以传入任何这种类型的函数;第二个和第三个参数叫ab,它们的类型都是Int,这两个值作为已给出的函数的输入值。

printMathResult(_:_:_:)被调用时,它被传入multiplyTwoInts函数和整数8和2.它用传入的8和2调用multiplyTwoInts,并输出结果16。

printMathResult(_:_:_:)函数的作用就是输出另一个适当类型的数学函数的调用结果。它不关心传入函数是如何实现的,只关心传入的函数是不是一个正确的类型,这使得printMathResult(_:_:_:)能以一种类型安全(Type—Safe)的方式将一部分功能转给调用者实现。

函数类型作为返回类型

可以用函数类型作为另一个函数的返回类型,需要做的是在返回箭头->后写一个完整的函数类型。

下面的这个例子中定义了两个简单函数,分别是stepForward(_:)stepBackward(_:)stepForward(_:)函数返回一个比输入值大1的值,stepBackward(_:)返回一个比输入值小1的值,这两个函数的类型都是(Int) -> Int

func stepForward(_ input: Int) -> Int {
    input + 1
}
func stepBackward(_ input: Int) -> Int {
    input - 1
}

如下名为chooseStepFunc(backward:)的函数,它的返回类型是(Int) -> Int类型的函数。chooseStepFunc(backward:)函数根据布尔值backward来返回stepForward(_:)函数或stepBackward(_:)函数:

func chooseStepFunc(backward: Bool) -> (Int) -> Int {
    backward ? stepBackward : stepForward
}

现在可以用chooseStepFunc(backward:)来获得两个函数其中的一个:

var  currentValue = 3
let moveNearerToZero = chooseStepFunc(backward: currentValue > 0)

上面这个例子中计算出currentValue逐渐接近到0是需要向正数走还是向负数走。currentValue的初始值是3,这意味着currentValue > 0为真(True),这使得chooseStepFunc(backward:)返回stepBackward(_:)函数。一个指向返回函数的引用保存在了moveNearerToZero常量中。

现在,moveNearerToZero指向了正确的函数,它可以被用来数到零:

print("Counting to zero:")
// Counting to zero:
while currentValue != 0 {
    print("\(currentValue)...")
    currentValue = moveNearerToZero(currentValue)
}
print("Zero!")  // 3...  // 2...  // 1...  // Zero!
---
output: Counting to zero:
3...
2...
1...
Zero!

嵌套函数

到目前为止,本章中所见到的所有函数都叫全局函数(Global Functions),它们定义在全局域中。也可以吧函数定义在别的函数体中,称作嵌套函数(Nested Functions)。

默认情况下,嵌套函数是对外界不可见的,但是可以被他们的外围函数(Enclosing Function)调用。一个外围函数也可以返回它的某一个嵌套函数,使得这个函数可以在其他域中被使用。

可以用返回嵌套函数的方式去重写chooseStepFunc(backward:)函数(chooseStepFunction()):

func chooseStepFunction(backward: Bool) -> (Int) -> Int {
    func stepForward(input: Int) -> Int { return input + 1 }
    func stepBackward(input: Int) -> Int { return input - 1 }
    return backward ? stepBackward(input:) : stepForward(input:)
}
var CurrentValue = -4
let MoveNearerToZero = chooseStepFunction(backward: currentValue > 0)  // moveNearerToZero now refers to the nested stepForward() function
while CurrentValue != 0 {
    print("\(CurrentValue)...")
    CurrentValue = MoveNearerToZero(CurrentValue)
}
print("Zero!")  // -4...  // -3...  // -2...  // -1...  // Zero!
---
output: -4...
-3...
-2...
-1...
Zero!
Last updated on