函数(function)
是一组有名字的代码,用来完成某个特定的任务。函数的名字描述了其执行的任务。前面已经用过一些函数,比如Swift提供的print()
,以及我们所写代码创建的其他函数。
函数会执行代码。有些函数会定义参数,用来传递数据以帮助函数完成工作。有些函数在完成工作后会返回一些信息。可以把函数理解为一部小机器,打开后,它就开始运转并完成自己的工作。如果它的工作方式需要数据的话,就要给它传入数据,然后它会返回一块新数据作为工作成果。
一个基本函数
定义一个函数
import Cocoa
func printGreeting() {
print("Hello, playground.")
}
printGreeting()
这段代码用func
关键字后跟函数名字printGreeting()
来定义一个函数。圆括号是空的,因为这个函数不接受任何参数。(稍后会详细介绍参数。)
左花括号({)代表函数实现的开始。你可以在这里写代码,描述函数如何工作。调用函数时,花括号中的代码会执行。printGreeting()函数非常简单,只有一行代码,利用print()来打印Hello, playground.到控制台。
最后调用函数,让它执行内部的代码。
函数参数
函数有参数(parameter)之后就能做更多的事情了。利用参数可以向函数输入数据。我们之所以把函数的这部分称为“参数”,是因为它们可以根据调用者给函数传递的数据来改变自己的值。函数利用传递给自己的参数来执行任务或产生结果。
import Cocoa
func printGreeting() {
print("Hello, playground.")
}
printGreeting()
func printPersonalGreeting(name: String) {
print("Hello \(name), welcome to your playground.")
}
printPersonalGreeting(name: "Matt")
printPersonalGreeting(name:)
接受一个参数,如函数名后面的圆括号中所示。实参(argument)
是调用者传递给函数形参(parameter)
的值。本例的函数有一个String类型的形参name。在紧随着name的:后面指定其类型,跟指定变量和常量的类型一样。
定义一个除法函数
func divisionDescriptionFor(numerator: Double, denominator: Double) {
print("\(numerator) divided by \(denominator) equals \(numerator / denominator)")
}
divisionDescriptionFor(numerator: 9.0, denominator: 3.0)
参数的名字
函数的参数有名字。比如,函数divisionDescriptionFor(numerator:denominator:)
有两个参数,参数名分别是numerator
和denominator
。在用divisionDescriptionFor(numerator: denominator:)
时,我们同时用到了两个参数的名字。这是因为在默认情况下调用函数时会用到所有的参数名。
有时候让函数体外可见的参数名不同于内部也是有用的。也就是说调用函数的时候用一个参数名字,在函数体内用另一个名字。这种参数被称为外部参数
。
外部参数能让函数可读性更高——如果名字起得合适的话。眼下在调用printPersonalGreeting(name:)
时可见的参数名还算能提供有用的信息,但是可读性并不强。通常应该让代码读起来跟我们平常说话一样。
func printPersonalGreeting(to name: String) {
print("Hello \(name), welcome to your playground.")
}
printPersonalGreeting(to: "Matt")
现在printPersonalGreeting(to:)
有了一个外部参数to:。这个参数能让函数读起来更像日常说话:“Print personal greeting to Matt.”在函数内,还是得使用name。这样实际上很好,在函数内部,name的含义很清楚。如果在printPersonalGreeting的内部出现print("Hello \ (to), welcome to your playground.")的话反而会让人困惑。
变长参数
变长(variadic)
参数接受零个或更多输入值作为实参。函数只能有一个变长参数,而且一般应该是参数列表中的最后一个。参数值在函数内部以数组的形式可用。
要声明变长参数,用参数类型后面的三个点表示,如names: String...。在本例中,name在函数体内可用,类型是[String]。
func printPersonalGreetings(to names: String...) {
for name in names {
print("Hello \(name), welcome to the playground.")
}
}
printPersonalGreetings(to: "Alex","Chris","Drew","Pat")
默认参数值
Swift的参数可以接受默认值。默认值应该放在函数参数列表的末尾。如果形参有默认值,那么在调用函数时可以省略实参。(你可能已经猜到了,函数在这种情况下会使用参数的默认值。)
func divisionDescriptionFor(numerator: Double, denominator: Double, withPunctuation punctuation: String = ".") {
print("\(numerator) divided by \(denominator) equals \(numerator/denominator) \(punctuation) ")
}
divisionDescriptionFor(numerator: 9.0, denominator: 3.0)
divisionDescriptionFor(numerator: 9.0, denominator: 3.0,withPunctuation: "!")
现在函数接受三个参数:divisionDescriptionFor(numerator:denominator:withPunctuation:)。注意新增代码:punctuation: String = "."。我们为标点增加了一个新参数,加上其期望的类型,并且用= "."语法给了它一个默认值。这意味着这个函数创建的字符串默认会以句号结尾。
in-out 参数
出于某种原因,函数有时候需要修改实参的值。in-out参数(in-out parameter)
能让函数影响函数体以外的变量。有两个注意事项:首先,in-out参数不能有默认值;其次,变长参数不能标记为inout。假设有一个函数接受一个错误信息作为实参,并根据某些条件在后面添加信息。
var error = "The request failed:"
func appendErrorCode(_ code: Int, toErrorString errorString: inout String) {
if code == 400 {
errorString += " bad request."
}
}
appendErrorCode(400, toErrorString: &error)
error
函数appendErrorCode(_:toErrorString:)有两个参数。第一个是函数要比较的错误码,类型为Int。注意,我们给这个参数的外部名是_。它在Swift中有特殊含义:在参数名前用_会使得函数被调用时省去外部名。由于这个参数名是跟在函数名后面的,在调用的时候没有理由用到这个名字。第二个是命名为toErrorString的inout参数(在名字前用inout关键字标记),类型为String。toErrorString是外部名,用来调用函数;而errorString是内部名,在函数内部使用。
inout加在String前面表示这个函数期望一个特殊的String:它需要一个inout的String。调用这个函数时,传递给inout参数的变量需要在前面加上&。这表示函数会修改这个变量。在这里,errorString被改为The request failed: bad request,可以在运行结果侧边栏看到。
从函数返回
函数结束执行后可以返回一些信息。这些信息称为函数的返回值(return)。常见的情况是写下一个函数来完成一些工作,然后返回一些数据。让divisionDescriptionFor(numerator: denominator:withPunctuation:)函数返回一个String类型的实例。
func divisionDescriptionFor(numerator: Double, denominator: Double, withPunctuation punctuation: String = ".") -> String {return "\(numerator) divided by \(denominator) equals \(numerator / denominator)\(punctuation)" }
print(divisionDescriptionFor(numerator: 9.0, denominator: 3.0, withPunctuation: "!"))
新函数的功能和之前差不多,只有一个变化:新实现有返回值。返回值用-> String语法表示,表明函数会返回指定类型的实例。因为要输出字符串到控制台,所以这个函数返回String。返回字符串的细节在函数体内。
因为divisionDescriptionFor(numerator:denominator:withPunctuation:)返回String,并且print()的参数类型为String,所以可以在print()的调用中再调用除法函数,把字符串输出到控制台。
嵌套函数和作用域
Swift的函数定义可以嵌套。嵌套函数在另一个函数定义的内部声明并实现。嵌套函数在包围它的函数以外不可用。当你需要一个函数只在另一个函数内部做一些事情时,这个特性很有用。
func areaOfTriangleWith(base: Double, height: Double) -> Double {
let numerator = base * height
func divide() -> Double {
return numerator / 2
}
return divide()
}
areaOfTriangleWith(base: 3.0, height: 5.0)
函数areaOfTriangleWith(base:height:)接受两个参数作为底和高,类型是Double。它还会返回一个Double。在函数内部实现中,我们声明并实现了另一个函数,名为divide()。这个函数没有参数,返回一个Double。函数areaOfTriangleWith(base:height:)调divide()函数并返回结果。
divide()函数还用到了areaOfTriangleWith(base:height:)中定义的常量numerator。这样为什么能行?
这个常量是在divide()的闭合作用域中定义的。函数中花括号({})内部的一切都称为被函数的作用域包围。在本例中,常量numerator和函数divide()都被areaOfTriangle(withBase: andHeight:)的作用域包围。
函数的作用域描述了实例或函数的可见性,这是某种范围。任何定义在函数作用域内部的东西都对函数可见,除此以外的一切都超出了函数的可见范围。numerator对函数divide()可见是因为两者共享同一个闭合作用域。
另一方面,因为divide()函数定义在areaOfTriangleWith(base:height:)函数作用域内部,所以在外面是不可见的。如果试图在包围的函数外部调用divide()函数,编译器会报错。可以试一下看看这个错误。
divide()是个很简单的函数。事实上,areaOfTriangleWith(base:height:)不需要它就可以得到同样的结果:return (base * height) / 2。这里要关注的焦点是作用域的原理。
多个返回值
函数可以返回不止一个值。Swift用元组数据类型来做到这一点,第5章已经介绍过了。回忆一下,元组是相关值的有序列表。
func sortedEvenOddNumbers(_ numbers: [Int]) -> (evens: [Int], odds: [Int]) {
var evens = [Int]()
var odds = [Int]()
for number in numbers {
if number % 2 == 0 {
evens.append(number)
} else {
odds.append(number)
}
}
return (evens, odds)
}
首先声明一个叫作sortedEvenOddNumbers(_:)
的函数。这个函数接受一个整数数组作为唯一的参数,并返回一个命名元组(named tuple)。元组的组成部分是有名字的,可以从这一点看出这是一个命名元组:evens是整数数组,odds也是整数数组。
接着,在函数的内部实现中初始化evens和odds数组,准备存放相应的整数。然后遍历函数参数numbers里的整数数组,每循环一次,就用%运算符检查number的奇偶性。如果结果是偶数,就添加到evens数组;如果不是偶数,就添加到odds数组。
我们来调用一下这个函数
let aBunchOfNumbers = [10,1,4,3,57,43,84,27,156,111]
let theSortedNumbers = sortedEvenOddNumbers(aBunchOfNumbers)
print("The even numbers are: \(theSortedNumbers.evens); the odd numbers are: \(theSortedNumbers.odds)")
首先创建一个Array类型的实例存放一组整数。接着把数组传递给sortedEvenOddNumbers(_:)函数,并将返回值赋给常量theSortedNumbers。因为返回值指定为(evens: [Int], odds: [Int]),所以编译器推断新创建的常量就是这个类型。最后把结果打印到控制台。
可空返回值类型
有时候,我们想让函数返回可空实例。如果一个函数在某些情况下返回nil,在其他情况下返回一个值的话,Swift提供了可空返回值以供使用。
func grabMiddleName(fromFullName name: (String, String?, String)) -> String? {
return name.1
}
let middleName = grabMiddleName(fromFullName: ("Matt",nil,"Mathias"))
if let theName = middleName {
print(theName)
}
这段代码创建了一个名为grabMiddleName(fromFullName:)
的函数。这个函数跟之前的有所不同,它接受一个参数:元组类型(String, String?, String)。元组的三个String实例分别是名字、中间名和姓,而中间名为可空类型。
grabMiddleName(fromFullName:)
函数的这个参数叫 name
,有个外部参数名叫fromFullName
。在函数内部实现中可以用要返回名字的索引访问这个参数。元组是零索引的,因此用1来访问实参里的中间名。因为中间名可以是nil,所以函数的返回值可空。
然后调用grabMiddleName(fromFullName:)并传递名字、中间名和姓(可以随意改名字)。因为元组的中间名部分声明为String?,所以可以传递nil。元组的名字和姓部分都不能传nil
。
控制台没有打印东西。因为中间名是nil,可空实例绑定中的布尔值不为真,所以print()
不会执行。
提前退出函数
guard语句
。跟if/else
语句一样,guard语句会根据某个表达式返回的布尔值结果来执行代码;但不同之处是,如果某些条件没有满足,可以用guard语句来提前退出函数,这也是其名称的由来。可以把guard语句想象成一种防止代码在某种不当条件下运行的方式。
func greetByMiddleName(fromFullName name: (first: String,middle: String?,last: String)) {
guard let middleName = name.middle else {
print("Hey there!")
return
}
print("Hey \(middleName)")
}
greetByMiddleName(fromFullName: ("Matt","Danger","Mathias"))
greetByMiddleName(fromFullName:)
类似于grabMiddleName(fromFullName:)
,都接受一样的参数;但不同的是前者没有返回值。另一个区别是元组name中的元素有名字,跟人名的各个部分一致。如你所见,这些元素名字在函数内部可用。
guard let middleName = name.middle这行代码把middle的值绑定到middleName常量上。如果可空实例没有值,那么guard语句中的代码会执行。这样会在控制台打印一句省略了中间名的问候语:Hey there!。之后,用return从函数显式返回,这表示guard语句所要求的条件没有满足,函数需要提前返回。
可以把gurad语句想象成防止以下尴尬情况发生:我们在不知道某人中间名的情况下只能含糊应付过去。不过如果元组带着中间名传递给函数,那么其值会绑定到middleName上,并且在guard语句后可用。这意味着middleName在包围着gurad语句的父作用域中可见。
不过,在调用greetByMiddleName(fromFullName:)函数时,会给元组name一个中间名并传递给函数。这意味着控制台会打印“Hey Danger!”。如果中间名是nil,那么控制台会打印"Hey there!"。
函数类型
函数类型(function type)
由函数参数和返回值组成。以sortedEvenOddNumbers(_:)函数为例,它接受整数数组作为参数,返回有两个整数数组的元组。于是sortedEvenOddNumbers(_:)的类型可以表示为:([Int]) -> ([Int], [Int])。
函数参数在左边的圆括号中列出,返回值类型跟在->后面。可以这么读这个函数类型:“一个接受整数数组作为参数并返回带有两个整数数组的元组的函数。”作为比较,既没有参数也没有返回值的函数类型是:() -> ()。
函数类型很有用,我们可以把函数类型实例赋给变量。在下一章中,当你看到函数可以作为参数和其他函数的返回值时,会意识到这个特性尤其有用。就目前而言,只要知道如何把函数类型实例赋给常量就可以了。
let evenOddFunction: ([Int]) -> ([Int], [Int]) = sortedEvenOddNumbers
这行代码创建了一个evenOddFunction常量,其值是sortedEvenOddNumbers(_:)函数。很酷,是吧?现在可以传递常量了,就跟普通常量一样;甚至可以用这个常量来调用函数。举个例子,evenOddFunction(numbers: [1,2,3])会把作为参数传递的数组中的数字放进有两个数组的元组中——一个数组放奇数,另一个放偶数。
func sortedEvenOddNumbers(_ numbers: [Int]) -> (evens: [Int], odds: [Int]) {
var evens = [Int]()
var odds = [Int]()
for number in numbers {
if number % 2 == 0 {
evens.append(number)
} else {
odds.append(number)
}
}
return (evens, odds)
}
let evenOddFunction: ([Int]) -> ([Int], [Int]) = sortedEvenOddNumbers
evenOddFunction([1,2,3])