Swift - Functions(函数)

函数(Functions)是执行特定任务的一段代码。函数名用来表明函数的具体作用,通过函数名来调用函数从而执行任务。
Swift的函数语法非常灵活,从简单的无参C语言函数到复杂的OC方法,Swift函数都能完美表达。在Swift中,参数可以提供默认值来简化函数调用,也可以设置成in-out参数保证函数执行完毕后原参数可以被修改。
每个函数在Swift中都有一个类型,由函数的参数类型和返回值类型组成。你可以像使用其他数据类型一样使用函数类型,比如可以把函数类型当作其他函数的参数类型,或者其他函数的返回值类型。函数还可以写在其他函数内部,通过函数的嵌套实现功能的封装。

函数的定义和调用

定义函数时,可以定义一个或多个有名称的类型值作为函数的输入,即参数(parameters)。也可以定义一个函数执行完毕后作为函数的输出要传回来的值的类型,即返回值类型(return type)
每个函数都有函数名(function name),用来描述函数所执行的任务。使用函数时,你需要通过函数名调用(call)该函数并传入与函数参数类型一致的实参(arguments)。且所传入的参数要与函数的参数列表顺序保持一致。
下面例子中的函数名称为greet(person: ),从函数名就能看出函数的作用——传入姓名然后返回一句问候。为了完成这项任务,你需要定义一个String类型的参数person和一个包含了问候语的String类型的返回值:

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

函数的定义以func开头,以上所有信息(参数和返回值类型)都包含在函数的定义(definition)中。用->把函数的参数和返回值类型隔开。
函数的定义描述了该函数的作用,所需要的参数以及函数执行完毕后的返回值类型。通过函数的定义可以准确无误的调用该函数:

print(greet(person: "Anna"))
// Prints "Hello, Anna!"
print(greet(person: "Brian"))
// Prints "Hello, Brian!"

通过在实参标签(argument label)person后面传入一个String类型的值,如greet(person: “Anna”),即可调用该函数。又函数返回一个String类型的值,因此函数greet(person: )可以嵌套在函数print(_: separator: terminator: )中,用来打印并查看具体的返回值,如上所示。

注意:
函数print(_: separator: terminator: )隐藏了第一个实参标签,且其他的参数也都是可选的。相关函数语法的讨论相见下面的函数参数标签和参数名及默认参数值。

函数greet(person: )的函数体以名称为greetingString类型常量开头,并将问候信息赋值给了该常量,最终通过return关键字将问候语返回。函数在return greeting这行代码完成全部执行并将当前greeting的值返回。
使用不同的输入值可以多次调用greet(person: )函数。以上例子展示了在使用“Anna”“Brian”作为参数调用该函数时的结果:两次调用返回了不一样的问候语。
将消息创建和返回语句合并成一行,可以有效缩短函数体:

func greetAgain(person: String) -> String {
    return "Hello again, " + person + "!"
}
print(greetAgain(person: "Anna"))
// Prints "Hello again, Anna!"

函数的参数和返回值

函数的参数和返回值在Swift中是极其灵活的。不管是只有单个无命名参数的简单函数还是多个不同名称及类型参数的复杂函数,Swift都可以实现。

无参函数

函数可以没有参数。下面的函数就没有参数,任何时间调用都返回同一String类型值:

func sayHelloWorld() -> String {
    return "hello, world"
}
print(sayHelloWorld())
// Prints "hello, world"

但即使函数没有参数,在函数的定义中,函数名后面仍然需要跟一对小括号。调用函数,函数名后也需要跟一对空的小括号。

多参函数

函数可以有多个参数。多个参数都写在函数的小括号内并用逗号隔开。
下面是以姓名及是否已经打过招呼的状态为参数的函数,并返回一段问候:

func greet(person: String, alreadyGreeted: Bool) -> String {
    if alreadyGreeted {
        return greetAgain(person: person)
    } else {
        return greet(person: person)
    }
}
print(greet(person: "Tim", alreadyGreeted: true))
// Prints "Hello again, Tim!"

在小括号内传入一个String类型的实参标签名为person的参数和一个Bool类型的实参标签名为alreadyGreeted的参数,并用逗号隔开。注意该函数与之前的greet(person: )函数并不相同。尽管两个函数的函数名都以greet开头,但greet(person: alreadyGreeted: )函数需要两个参数,而greet(person: )函数只需要一个参数。

无返回值的函数

函数可以没有返回值。下面的greet(person: )函数就只打印了String类型的值而并没有将值返回:

func greet(person: String) {
    print("Hello, \(person)!")
}
greet(person: "Dave")
// Prints "Hello, Dave!"

因为该函数不需要返回值,因此函数的定义中就不需要写->和返回值类型了。

注意:
严格讲,该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")
// prints "hello, world" and returns a value of 12
printWithoutCounting(string: "hello, world")
// prints "hello, world" but does not return a value

第一个函数printAndCount(String: )打印了一个字符串并返回一个Int类型的字符个数。第二个函数printWithoutCounting(String: )调用了第一个函数,但忽略了它的返回值。当第二个函数被调用时,第一个函数的字符串仍然会被打印,但其返回值并没有用到。

注意:
返回值可以被忽略,但有返回值的函数必须返回。有特定返回类型的函数禁止函数穿透不返回,否则会导致编译错误。

多个返回值的参数

将元组作为函数的返回值类型可以以复合值的形式返回多个值。
下面的函数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元素的元组。这两个Int值会被分别标记成minmax,以便通过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)")
// Prints "min is -6 and max is 109"

注意,元组从函数返回后,其中的元素并不需要命名。因为元组中元素的名称已经在函数返回值类型中体现了。

可选的元组返回类型

如果从函数中返回的元组可能没有值,则可以使用可选的元组类型作为函数的返回值,以表示函数可能返回nil的情况。在元组类型的小括号后添加?表示可选的元组,如(Int, Int)? (String, Int, Bool)?

注意
可选的元组类型,如(Int, Int)?与包含可选类型数据的元组,如(Int?, Int?) 不是一回事儿。对于可选元组类型,整个元组都是可选的,跟它内部的元素是否可选无关。

上面的函数minMax(array: )返回一个包含两个Int值的元组。但在传入数组时,函数并没有做任何安全校验。如果传入的是一个空数组,那么当从数组中取第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)")
}
// Prints "min is -6 and max is 109"

函数的实参标签和形参名称

函数的每个参数都同时有一个实参标签(argument label)形参名称(parameter name)。实参标签在调用函数时使用。调用函数时,将实参标签写在实参(argument)之前。形参名称在函数的具体执行中使用。默认情况下,形参(parameter)使用形参名称作为实参标签。

func someFunction(firstParameterName: Int, secondParameterName: Int) {
    // In the function body, firstParameterName and secondParameterName
    // refer to the argument values for the first and second parameters.
}
someFunction(firstParameterName: 1, secondParameterName: 2)

所有参数都应该有独一无二的名称。尽管对于多参函数来说可以设置相同的实参标签,但实参标签的唯一性让代码更加易读。

明确实参标签

实参标签写在形参名称之前,并用空格隔开:

func someFunction(argumentLabel parameterName: Int) {
    // In the function body, parameterName refers to the argument value
    // for that parameter.
}

下面的greet(person: )函数以姓名和家乡作为参数,并返回一串问候:

func greet(person: String, from hometown: String) -> String {
    return "Hello \(person)!  Glad you could visit from \(hometown)."
}
print(greet(person: "Bill", from: "Cupertino"))
// Prints "Hello Bill!  Glad you could visit from Cupertino."

实参标签的使用是为了在阅读函数时更符合自然语言习惯,同时提供一个易读且意图明确的函数体。

省略实参标签

如果不想为形参设置实参标签,则可以用_代替。

func someFunction(_ firstParameterName: Int, secondParameterName: Int) {
    // In the function body, firstParameterName and secondParameterName
    // refer to the argument values for the first and second parameters.
}
someFunction(1, secondParameterName: 2)

如果形参有实参标签,则在调用函数时,实参必须被标记。

默认参数值

函数中,可以通过在参数类型后给参数赋值的方式给任意参数定义一个默认值(default value)。一旦定义了默认值,调用函数时就可以省略该参数。

func someFunction(parameterWithoutDefault: Int, parameterWithDefault: Int = 12) {
    // If you omit the second argument when calling this function, then
    // the value of parameterWithDefault is 12 inside the function body.
}
someFunction(parameterWithoutDefault: 3, parameterWithDefault: 6) // parameterWithDefault is 6
someFunction(parameterWithoutDefault: 4) // parameterWithDefault is 12

将没有定义默认值的参数放在函数参数列表的前端,先于定义了默认值的参数位置。没有默认值的参数对函数来说更为重要——将没有默认值的参数写在前面,函数调用时更便于识别函数,以至于不用考虑是否有任意默认参数被忽略了的情况。

可变参数

一个可变参数可以接收0个或多个特定类型的值。在函数调用时,可以用可变参数来表明该参数可接收不同数量的值。通过在参数类型后面加的方式表示该参数为可变参数。
传给可变参数的值在函数体中可以看作是特定类型的数组。例如,名称为numbers类型为Double…的可变参数在函数体中赋值成了名称为numbers类型为[Double]的数组常量。
下面例子计算了任意长度的一串数字的算术平均数:

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)
// returns 3.0, which is the arithmetic mean of these five numbers
arithmeticMean(3, 8.25, 18.75)
// returns 10.0, which is the arithmetic mean of these three numbers

注意:
一个函数最多可以有一个可变参数。

In-Out参数

函数的参数默认是常量。在函数体内部修改参数的值会导致编译错误。如果参数的值需要被修改,且希望在函数调用结束后这些修改仍然有效,则可以将参数定义成in-out参数
在参数类型前加上inout关键字来实现in-out参数的定义。in-out参数的值被传入函数内,被函数修改,最后传出函数并替代原始值。关于in-out参数及其相关的编译器优化的更详细讨论,相见In-Out参数。
In-out参数只能接收变量。不能接收常量和字面值作为函数的参数,因为常量和字面值不能被修改。在被当作实参传入in-out参数的变量名之前加上&符号,来表示该变量可以被函数修改。

注意
In-out参数不能设置默认值,且可变参数不能被标记为inout参数。

下面的swapTwoInts(_: _:)函数有两个名为ab的in-out整型参数:

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

swapTwoInts(_: _:)函数通过将参数a的值储存到临时变量temporaryA中,再将b的值赋值给a,最后将temporaryA赋值给b的方式,实现了参数ab的值的互换。
调用swapTwoInts(_: _:)函数时,传入两个Int类型的变量即可完成两个变量值的互换。注意,变量someIntanotherInt被当作参数传入函数时,其名称前有&作前缀:

var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// Prints "someInt is now 107, and anotherInt is now 3"

以上例子显示someIntanotherInt的原始值被swapTwoInts(_: _:)函数修改了,尽管他们是定义在函数外的变量。

注意:
In-out参数与函数的返回值并不是一回事儿。swapTwoInts的例子并没有定义一个返回值类型也没有返回值,但函数仍然修改了两个参数的值。In-out参数为函数提供了一个在函数作用域外产生作用的方式。

函数类型

每个函数都有特定的函数类型(function type),由其参数类型和返回值类型组成。
例如:

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

这个例子定义了两个简单的数学函数:addTwoIntsmultiplyTwoInts。这两个函数都是将两个Int值作为参数并通过具体的逻辑运算返回一个Int值。
两个函数的类型都是:(Int, Int) -> Int,读作:
“一个具有两个参数,参数类型都为Int,且返回一个Int类型值的函数。”
下面是一个没有参数和返回值的函数:

func printHelloWorld() {
    print("hello, world")
}

该函数的类型为:() -> Void,即“一个无参,返回Void的函数。”

函数类型的使用

Swift中函数类型的使用与其他数据类型的使用并无二致。例如,定义一个函数类型的常量/变量,并为其赋值一个恰当的函数:

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

读作:
“定义一个名称为mathFunction的变量,并设置该变量的类型为‘一个具有两个Int类型参数并返回一个Int类型值的函数。’最后用变量addTwoInts指向该函数。 ”
函数addTwoInts(_: _:)的类型与变量mathFunction的类型一致,因此该赋值操作是被Swift的类型检查所允许的。
赋值后,就可以用变量mathFunction来调用函数了:

print("Result: \(mathFunction(2, 3))")
// Prints "Result: 5"

具有相同类型的不同函数可以赋值给同一函数类型的变量,赋值方式与非函数类型的数据类型的赋值方式相同:

mathFunction = multiplyTwoInts
print("Result: \(mathFunction(2, 3))")
// Prints "Result: 6"

至于其他类型,在把函数赋值给一个常量/变量时,可以直接交给Swift做函数类型的推断:

let anotherMathFunction = addTwoInts
// anotherMathFunction is inferred to be of type (Int, Int) -> Int

函数类型作为参数类型

函数类型,如(Int, Int) -> Int,可以作为另一函数的参数类型。这样一来,部分函数的实现就可以直接交给函数的调用者来提供了。
这是一个打印上面数学函数结果的例子:

func printMathResult(_ mathFunction: (Int, Int) -> Int, _ a: Int, _ b: Int) {
    print("Result: \(mathFunction(a, b))")
}
printMathResult(addTwoInts, 3, 5)
// Prints "Result: 8"

这个例子定义了一个名为printMathResult(_: _: _:)的函数。该函数有三个参数,第一个参数是mathFunction,类型为(Int, Int) -> Int。可以传入任意相同类型的函数作为该形参的实参。第二和第三个参数分别为ab,均为Int类型。这两个参数用来作为传入的数学函数的输入值。
当函数printMathResult(_: _: _:)被调用时,分别传入了一个addTwoInts(_: _:)函数以及3和5两个整数。函数内部用3和5两个参数调用了addTwoInts(_: _:)函数,并打印出结果为8。
函数printMathResult(_: _: _:)的作用是打印传入的函数的结果。函数printMathResult(_: _: _:)并不关心传入的函数具体做了什么,只关心传入的函数类型是否正确。这让函数printMathResult(_: _: _:)释放掉了一些功能给调用者。

函数类型作为返回值类型

函数类型可以作为另一个函数的返回值类型。在函数的->之后的返回值类型位置加上函数类型即可。
下面例子中定义了两个函数:stepForward(_:)stepBackward(_:)stepForward(_:)函数返回一个比输入值大1的值,stepBackward(_:)函数返回一个比输入值小1的值。两个函数的类型都是(Int) -> Int:

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

下面是返回值类型为(Int) -> Int的函数chooseStepFunction(backward:)。该函数根据布尔类型参数backward的值返回stepForward(_:)stepBackward(_:)函数:

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

使用函数chooseStepFunction(backward:)实现整数的递增/递减:

var currentValue = 3
let moveNearerToZero = chooseStepFunction(backward: currentValue > 0)
// moveNearerToZero now refers to the stepBackward() function

上面的函数决定了让变量currentValue递增/递减到0需要的是加1还是减1操作。变量currentValue的初始值为3,即表达式currentValue>0返回为真,使函数返回stepBackward(_:)函数,并最终使用名为moveNearerToZero的常量接收。
常量moveNearerToZero指代了返回的函数,因此可以用来执行递增/递减到0的操作:

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

嵌套函数

截至目前,本章中介绍的所有函数都是全局函数(global functions),即定义在全局范围内的函数。而定义在其他函数体内的函数,则称为嵌套函数(nested functions)
嵌套函数默认对外界不可见,但仍然可以通过外围函数实现嵌套函数的调用。同时外围函数也可以返回其内部的嵌套函数,实现嵌套函数在其他作用域内的使用。
将上面的chooseStepFunction(backward:)函数重写,让其返回嵌套函数:

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 now refers to the nested stepForward() function
while currentValue != 0 {
    print("\(currentValue)... ")
    currentValue = moveNearerToZero(currentValue)
}
print("zero!")
// -4...
// -3...
// -2...
// -1...
// zero!

后记:
先泛读一遍,翻译的时候精读一遍,最后校对一遍。经过这么三遍,很容易挖掘出普通阅读状态下不容易发现的知识点。
翻译的主要目的是更好的学习官方文档,望谨慎参考。如果也能对你的开发起到半点的借鉴作用,就再好不过了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352

推荐阅读更多精彩内容