2.6 函数

原文链接: https://docs.swift.org/latest/documentation/the-swift-programming-language/functions/

2.6 函数

定义和调用函数,为它们的实参添加标签,并使用它们的返回值。

函数是完成特定任务的独立代码片段。你给函数起一个名字,用来标明它的功能;需要时用这个名字"调用"函数来完成它的任务。

Swift 统一的函数语法足够灵活,既能表达没有参数名的简单 C 风格函数,也能表达每个参数都带名字和实参标签的复杂 Objective-C 风格方法。参数可以提供默认值以简化函数调用,也可以作为输入输出参数传入,从而在函数执行完毕后修改传入的变量。

Swift 中的每个函数都有类型,由函数的参数类型和返回类型构成。你可以像使用 Swift 中其他类型那样使用函数类型,这让人很容易把函数作为参数传给其他函数,也很容易从函数中返回函数。函数还可以写在其他函数内部,把有用的功能封装在嵌套的函数作用域中。

定义和调用函数

定义函数时,你可以选择性地定义一个或多个具名、有类型的值作为函数输入,这些值称为参数;也可以选择性地定义函数完成后作为输出传回的值类型,称为返回类型。

每个函数都有一个函数名,用来描述该函数所完成的任务。使用函数时,你用函数名"调用"它,并传入与函数参数类型相符的输入值(称为实参)。函数的实参必须始终按照函数参数列表的顺序提供。

下面例子中的函数名叫 greet(person:),因为它做的事情就是接收一个人的名字作为输入,并返回对这个人的问候。为此,你定义一个输入参数——一个名为 person 的 String 值——以及返回类型 String,其中将包含对这个人的问候:

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

所有这些信息汇总成函数的定义,它以 func 关键字开头。你用返回箭头 ->(一个连字符加一个右尖括号)标明函数的返回类型,后面跟着要返回的类型的名字。

这个定义描述了函数做什么、期望接收什么、完成时返回什么。有了这个定义,你就可以在代码的其他地方毫无歧义地调用这个函数:

1
2
3
4
print(greet(person: "Anna"))
// 输出 "Hello, Anna!"
print(greet(person: "Brian"))
// 输出 "Hello, Brian!"

调用 greet(person:) 函数时,在 person 实参标签后面传入一个 String 值,例如 greet(person: "Anna")。由于函数返回 String 值,greet(person:) 可以包在 print(_:separator:terminator:) 函数的调用里,打印该字符串并查看它的返回值,如上所示。

注意:print(_:separator:terminator:) 函数 的第一个实参没有标签, 其他实参因为有默认值而是可选的。 这些函数语法的变化形式会在下文的 函数实参标签与参数名 和默认参数值中讨论。

greet(person:) 函数的语句体首先定义一个新的 String 常量 greeting,并把它设为一句简单的问候语。然后这句问候通过 return 关键字被传出函数。在 return greeting 这一行,函数结束执行并返回 greeting 的当前值。

你可以用不同的输入值多次调用 greet(person:) 函数。上面的例子展示了用输入值 "Anna" 和 "Brian" 调用时会发生什么,每次函数都会返回一句为对方量身定制的问候。

要让这个函数的语句体更短,可以把消息的创建和返回语句合并成一行:

1
2
3
4
5
func greetAgain(person: String) -> String {
    return "Hello again, " + person + "!"
}
print(greetAgain(person: "Anna"))
// 输出 "Hello again, Anna!"

函数参数与返回值

在 Swift 中,函数参数和返回值极其灵活。从只有一个无名参数的简单工具函数,到具有富有表现力的参数名和多种参数选项的复杂函数,你都可以定义。

无参数的函数

函数并非必须定义输入参数。下面这个函数没有输入参数,每次调用都返回同一条 String 消息:

1
2
3
4
5
func sayHelloWorld() -> String {
    return "hello, world"
}
print(sayHelloWorld())
// 输出 "hello, world"。

即使函数不接受任何参数,函数定义中函数名后面仍然需要一对圆括号。调用该函数时,函数名后面同样要跟一对空圆括号。

带多个参数的函数

函数可以有多个输入参数,它们写在函数的圆括号内,用逗号分隔。

下面这个函数接收一个人的名字以及他是否已经被问候过作为输入,并返回对该人合适的问候语:

1
2
3
4
5
6
7
8
9
func greet(person: String, alreadyGreeted: Bool) -> String {
    if alreadyGreeted {
        return greetAgain(person: person)
    } else {
        return greet(person: person)
    }
}
print(greet(person: "Tim", alreadyGreeted: true))
// 输出 "Hello again, Tim!"

调用 greet(person:alreadyGreeted:) 函数时,把标签为 person 的 String 实参值和标签为 alreadyGreeted 的 Bool 实参值都写进圆括号里,用逗号分隔。注意这个函数与前面小节中的 greet(person:) 函数是不同的函数。虽然两个函数的名字都以 greet 开头,但 greet(person:alreadyGreeted:) 接受两个实参,而 greet(person:) 只接受一个实参。

无返回值的函数

函数并非必须定义返回类型。下面是 greet(person:) 函数的另一个版本,它自己打印 String 值,而不是把它返回:

1
2
3
4
5
func greet(person: String) {
    print("Hello, \(person)!")
}
greet(person: "Dave")
// 输出 "Hello, Dave!"

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

注意:严格来说,这个版本的 greet(person:) 函数仍然返回一个值, 尽管没有定义返回值。 没有定义返回类型的函数返回一个 Void 类型的特殊值。 它就是一个空元组, 写作 ()。

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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",但不返回值

第一个函数 printAndCount(string:) 打印一个字符串,然后以 Int 返回它的字符数。第二个函数 printWithoutCounting(string:) 调用第一个函数,但忽略它的返回值。调用第二个函数时,消息仍由第一个函数打印,但返回的值没有被使用。

注意:返回值可以被忽略, 但一个声明会返回值的函数必须始终返回值。 定义了返回类型的函数 不能让控制权在未返回值的情况下从函数底部离开, 这样做会导致编译期错误。

返回多个值的函数

你可以把元组类型用作函数的返回类型,从而以单个复合返回值的形式返回多个值。

下面的例子定义了一个名为 minMax(array:) 的函数,它找出 Int 值数组中的最小值和最大值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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 值的元组。这两个值被标记为 min 和 max,因此查询函数返回值时可以按名字访问它们。

minMax(array:) 函数的语句体先把两个工作变量 currentMin 和 currentMax 设为数组中第一个整数的值,然后遍历数组中剩余的值,检查每个值是否分别小于 currentMin 或大于 currentMax。最后,把整体的最小值和最大值作为包含两个 Int 值的元组返回。

由于元组的成员值已经作为函数返回类型的一部分命名了,它们可以用点语法访问,以取出找到的最小值和最大值:

1
2
3
let bounds = minMax(array: [8, -6, 2, 109, 3, 71])
print("min is \(bounds.min) and max is \(bounds.max)")
// 输出 "min is -6 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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:

1
2
3
4
if let bounds = minMax(array: [8, -6, 2, 109, 3, 71]) {
    print("min is \(bounds.min) and max is \(bounds.max)")
}
// 输出 "min is -6 and max is 109"。

带隐式返回的函数

如果函数的整个语句体就是一个表达式,那么函数会隐式返回该表达式。例如,下面两个函数行为相同:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func greeting(for person: String) -> String {
    "Hello, " + person + "!"
}
print(greeting(for: "Dave"))
// 输出 "Hello, Dave!"

func anotherGreeting(for person: String) -> String {
    return "Hello, " + person + "!"
}
print(anotherGreeting(for: "Dave"))
// 输出 "Hello, Dave!"

greeting(for:) 函数的整个定义就是它要返回的那句问候语,因此可以使用这种更简短的形式。anotherGreeting(for:) 函数返回同样的问候语,但像更长的函数那样使用了 return 关键字。任何只写一行 return 的函数都可以省略 return。

正如你会在简写取值器声明中看到的,属性的取值器也可以使用隐式返回。

注意:作为隐式返回值写下的代码 必须返回某个值。 例如, 你不能把 print(13) 用作隐式返回值。 不过,你可以把 fatalError("Oh no!") 这类永不返回的函数 用作隐式返回值, 因为 Swift 知道隐式返回并不会真的发生。

函数实参标签与参数名

每个函数参数都同时拥有实参标签和参数名。实参标签在调用函数时使用:函数调用中的每个实参都要在前面写上它的实参标签。参数名则在函数的实现中使用。默认情况下,参数会把自己的参数名用作实参标签。

1
2
3
4
5
func someFunction(firstParameterName: Int, secondParameterName: Int) {
    // 在函数体内,firstParameterName 和 secondParameterName
    // 分别指第一个和第二个参数的实参值。
}
someFunction(firstParameterName: 1, secondParameterName: 2)

所有参数的(参数)名必须唯一。虽然多个参数可以拥有相同的实参标签,但让实参标签保持唯一有助于代码更易读。

指定实参标签

把实参标签写在参数名之前,用空格分隔:

1
2
3
func someFunction(argumentLabel parameterName: Int) {
    // 在函数体内,parameterName 指该参数的实参值。
}

下面是 greet(person:) 函数的一个变体,它接收一个人的名字和家乡,并返回问候语:

1
2
3
4
5
func greet(person: String, from hometown: String) -> String {
    return "Hello \(person)!  Glad you could visit from \(hometown)."
}
print(greet(person: "Bill", from: "Cupertino"))
// 输出 "Hello Bill!  Glad you could visit from Cupertino."。

使用实参标签可以让函数调用读起来像一句话那样富有表现力,同时又让函数体保持易读、意图清晰。

省略实参标签

如果你不希望某个参数有实参标签,就把下划线(_)写在它的实参标签位置。

1
2
3
4
5
func someFunction(_ firstParameterName: Int, secondParameterName: Int) {
    // 在函数体内,firstParameterName 和 secondParameterName
    // 分别指第一个和第二个参数的实参值。
}
someFunction(1, secondParameterName: 2)

如果某个参数有实参标签,那么调用函数时该实参必须带上标签。

默认参数值

在参数类型后面给参数赋值,就可以为函数中的任何参数定义默认值。如果定义了默认值,调用函数时可以省略该参数。

1
2
3
4
5
6
func someFunction(parameterWithoutDefault: Int, parameterWithDefault: Int = 12) {
    // 如果调用这个函数时省略第二个实参,
    // 那么函数体内 parameterWithDefault 的值就是 12。
}
someFunction(parameterWithoutDefault: 3, parameterWithDefault: 6) // parameterWithDefault 是 6
someFunction(parameterWithoutDefault: 4) // parameterWithDefault 是 12

把没有默认值的参数放在参数列表的开头、有默认值的参数之前。没有默认值的参数通常对函数的意义更为重要——把它们写在前面,让人更容易辨认出调用的是同一个函数,无论是否省略了默认参数。

可变参数

可变参数接受指定类型的零个或多个值。用可变参数表明:调用函数时可以向该参数传入数量不定的输入值。书写可变参数时,在参数的类型名后面插入三个句点字符(...)。

传给可变参数的值在函数体中作为相应类型的数组提供。例如,名为 numbers、类型为 Double... 的可变参数,在函数体中就是一个名为 numbers、类型为 [Double] 的常量数组。

下面的例子计算任意长度数字列表的算术平均值(也叫平均数):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func arithmeticMean(_ numbers: Double...) -> Double {
    var total: Double = 0
    for number in numbers {
        total += number
    }
    return total / Double(numbers.count)
}
arithmeticMean(1, 2, 3, 4, 5)
// 返回 3.0,即这五个数的算术平均值
arithmeticMean(3, 8.25, 18.75)
// 返回 10.0,即这三个数的算术平均值

一个函数可以有多个可变参数。紧跟在可变参数之后的第一个参数必须有实参标签。这个实参标签能明确区分哪些实参传给了可变参数,哪些实参传给了可变参数之后的参数。

输入输出参数

函数参数默认是常量。试图在函数体内改变函数参数的值会导致编译期错误,这意味着你不可能误改参数的值。如果你希望函数修改某个参数的值,并且希望这些修改在函数调用结束后仍然有效,就把该参数定义为输入输出参数。

把 inout 关键字写在参数类型之前,就声明了一个输入输出参数。输入输出参数的值先被传入函数,由函数修改,然后被传出函数去替换原来的值。关于输入输出参数的行为以及相关编译器优化的详细讨论,参见输入输出参数。

输入输出参数的实参只能传变量,不能传常量或字面量,因为常量和字面量不能被修改。把变量作为实参传给输入输出参数时,在变量名前直接写一个和号(&),表示它可以被函数修改。

注意:输入输出参数不能有默认值, 可变参数也不能标记为 inout。

下面是一个名为 swapTwoInts(_:_:) 的函数示例,它有两个名为 a 和 b 的输入输出整数参数:

1
2
3
4
5
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

swapTwoInts(_:_:) 函数只是把 b 的值换到 a,把 a 的值换到 b。它先把 a 的值存到临时常量 temporaryA,再把 b 的值赋给 a,最后把 temporaryA 赋给 b,从而完成交换。

你可以用两个 Int 类型的变量调用 swapTwoInts(_:_:) 函数来交换它们的值。注意,把 someInt 和 anotherInt 传给 swapTwoInts(_:_:) 函数时,它们的名字前面都加了和号:

1
2
3
4
5
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"。

上面的例子表明,someInt 和 anotherInt 的原始值被 swapTwoInts(_:_:) 函数修改了,尽管它们最初是在函数外部定义的。

注意:输入输出参数与从函数返回值并不相同。 上面的 swapTwoInts 例子既没有定义返回类型,也没有返回值, 但它仍然修改了 someInt 和 anotherInt 的值。 输入输出参数是函数在函数体作用域之外产生效果的另一种方式。

函数类型

每个函数都有一个特定的函数类型,由函数的参数类型和返回类型构成。

例如:

1
2
3
4
5
6
func addTwoInts(_ a: Int, _ b: Int) -> Int {
    return a + b
}
func multiplyTwoInts(_ a: Int, _ b: Int) -> Int {
    return a * b
}

这个例子定义了两个简单的数学函数:addTwoInts 和 multiplyTwoInts。它们各自接受两个 Int 值,并返回一个 Int 值,也就是执行相应数学运算的结果。

这两个函数的类型都是 (Int, Int) -> Int。它可以读作:

“一个有两个参数、两者都是 Int 类型、并返回 Int 类型值的函数。”

再举一个没有参数、也没有返回值的函数为例:

1
2
3
func printHelloWorld() {
    print("hello, world")
}

这个函数的类型是 () -> Void,也就是"一个没有参数、返回 Void 的函数"。

使用函数类型

使用函数类型就像使用 Swift 中其他类型一样。例如,你可以把常量或变量定义为函数类型,并把合适的函数赋给该变量:

1
var mathFunction: (Int, Int) -> Int = addTwoInts

这可以读作:

“定义一个名为 mathFunction 的变量,它的类型是’一个接受两个 Int 值、并返回一个 Int 值的函数’。把这个新变量指向名为 addTwoInts 的函数。”

addTwoInts(_:_:) 函数与 mathFunction 变量类型相同,因此 Swift 的类型检查允许这次赋值。

现在你就可以用 mathFunction 这个名字调用被赋值的函数:

1
2
print("Result: \(mathFunction(2, 3))")
// 输出 "Result: 5"。

类型匹配的另一个函数可以赋给同一个变量,和非函数类型的情况完全一样:

1
2
3
mathFunction = multiplyTwoInts
print("Result: \(mathFunction(2, 3))")
// 输出 "Result: 6"。

和其他类型一样,把函数赋给常量或变量时,你也可以交给 Swift 去推断函数类型:

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

函数类型作为参数类型

你可以把 (Int, Int) -> Int 这样的函数类型用作另一个函数的参数类型。这样就能把函数实现的某些部分留给函数的调用方在调用时提供。

下面这个例子打印前面那些数学函数的结果:

1
2
3
4
5
func printMathResult(_ mathFunction: (Int, Int) -> Int, _ a: Int, _ b: Int) {
    print("Result: \(mathFunction(a, b))")
}
printMathResult(addTwoInts, 3, 5)
// 输出 "Result: 8"。

这个例子定义了一个名为 printMathResult(_:_:_:) 的函数,它有三个参数。第一个参数名为 mathFunction,类型是 (Int, Int) -> Int,你可以把任何该类型的函数作为第一个参数的实参传入。第二个和第三个参数名为 a 和 b,都是 Int 类型,它们用作所提供数学函数的两个输入值。

调用 printMathResult(_:_:_:) 时,传入了 addTwoInts(_:_:) 函数以及整数值 3 和 5。它用值 3 和 5 调用所提供的函数,并打印结果为 8。

printMathResult(_:_:_:) 的职责是打印对某个合适类型的数学函数调用的结果。那个函数的实现究竟做什么并不重要——重要的是它的类型正确。这让 printMathResult(_:_:_:) 能以类型安全的方式把自己的一部分功能交给函数的调用方。

函数类型作为返回类型

你可以把函数类型用作另一个函数的返回类型:在返回函数的返回箭头(->)后面紧跟一个完整的函数类型。

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

1
2
3
4
5
6
func stepForward(_ input: Int) -> Int {
    return input + 1
}
func stepBackward(_ input: Int) -> Int {
    return input - 1
}

下面这个函数名为 chooseStepFunction(backward:),返回类型是 (Int) -> Int。它根据名为 backward 的布尔参数返回 stepForward(_:) 函数或 stepBackward(_:) 函数:

1
2
3
func chooseStepFunction(backward: Bool) -> (Int) -> Int {
    return backward ? stepBackward : stepForward
}

现在你可以用 chooseStepFunction(backward:) 得到一个朝某个方向步进的函数:

1
2
3
var currentValue = 3
let moveNearerToZero = chooseStepFunction(backward: currentValue > 0)
// moveNearerToZero 现在指向 stepBackward() 函数

上面的例子判断要让变量 currentValue 逐步接近零,需要正向还是负向的步进。currentValue 的初始值是 3,也就是说 currentValue > 0 返回 true,从而让 chooseStepFunction(backward:) 返回 stepBackward(_:) 函数。返回函数的引用存放在常量 moveNearerToZero 中。

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
print("Counting to zero:")
// Counting to zero:
while currentValue != 0 {
    print("\(currentValue)... ")
    currentValue = moveNearerToZero(currentValue)
}
print("zero!")
// 3...
// 2...
// 1...
// zero!

嵌套函数

本章到目前为止遇到的所有函数都是全局函数的例子,它们定义在全局作用域中。你也可以把函数定义在其他函数的语句体内部,这类函数称为嵌套函数。

嵌套函数默认对外部世界不可见,但仍然可以被它的外层函数调用和使用。外层函数也可以返回它的某个嵌套函数,让该嵌套函数能在其他作用域中使用。

你可以把上面的 chooseStepFunction(backward:) 例子改写成使用并返回嵌套函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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 : stepForward
}
var currentValue = -4
let moveNearerToZero = chooseStepFunction(backward: currentValue > 0)
// moveNearerToZero 现在指向嵌套的 stepForward() 函数
while currentValue != 0 {
    print("\(currentValue)... ")
    currentValue = moveNearerToZero(currentValue)
}
print("zero!")
// -4...
// -3...
// -2...
// -1...
// zero!