今天我们开始一起学习函数 Function。
在编程语言中,函数的作用是让我们把代码片段包起来,这样它们就可以在很多地方使用,而我们不必重复写被包起来的那些代码片段。我们还可以将数据发送到函数中,自定义它们的工作方式,并取回最终的计算结果。
函数最大的贡献是我们不必再重复复制和粘贴相同的代码,就可以在不同的地方使用同一段代码。这意味着更少的代码重复,也意味着更改工作的便利性大幅提高,且可以避免漏改的错误发生。
今天,我们将学习如何编写我们自己的函数,如何接受参数以及如何返回数据。
让我们开始吧!
如何使用函数重复使用代码
当你写了一些非常喜欢的代码,并想反复使用时,你应该考虑将其放入函数中。函数就是从程序的其他部分分割出来的代码块,并为其命名,这样就可以方便地引用它们。
例如,假设我们有这样一段简单漂亮的代码:
print("Welcome to my app!")
print("By default This prints out a conversion")
print("chart from centimeters to inches, but you")
print("can also set a custom range if you want.")
这是一个应用程序的欢迎信息,你可能希望在应用程序启动时打印出来,或者在用户寻求帮助时打印出来。
但如果你希望在两个地方都打印呢?是的,你可以复制这四行 print()
并将它们放在两个地方,但如果你想将这些文字放在十个地方呢?或者,如果您以后想修改措辞——你真的会记得修改代码中出现的所有地方吗?
这就是函数的作用所在:我们可以调出代码,给它一个名字,然后随时随地运行它。这意味着所有的 print()
行都会留在一个地方,并在其他地方重复使用。
如下所示:
func showWelcome() {
print("Welcome to my app!")
print("By default This prints out a conversion")
print("chart from centimeters to inches, but you")
print("can also set a custom range if you want.")
}
让我们来分析一下…
- 首先是
func
关键字,它标志着一个函数的开始。 - 我们将函数命名为
showWelcome
。这个名字可以是你想要的任何名字,但尽量让人记住——printInstructions()
、displayHelp()
等都是不错的选择。 - 函数的主体包含在打开和关闭的大括号中,就像循环的主体和条件的主体一样。
这里还有一个额外的东西,你可能会从我们迄今为止的工作中认出它:showWelcome
后面的**()
。早在我们学习字符串的时候,我就说过 count
后面没有()
,但upercased()
**却有。
现在你知道为什么了:这些 ()
是在函数中使用的。它们不仅在创建函数时使用(如上图所示),还在调用函数时使用(即要求 Swift 运行其代码时)。在我们的例子中,我们可以这样调用函数:
showWelcome()
这就是函数的调用位置,这是一个花哨的名字,意思是 “函数被调用的地方”。
那么,括号的实际作用是什么呢?这就是我们为函数添加配置选项的地方——我们可以传递数据,自定义函数的工作方式,从而使函数变得更加灵活。
举个例子,我们已经使用过这样的代码:
let number = 139
if number.isMultiple(of: 2) {
print("Even")
} else {
print("Odd")
}
isMultiple(of:)
是一个属于整数的函数。如果它不允许任何形式的自定义,那就没有意义了——它是什么的倍数?当然,苹果本可以让它成为类似 isOdd()
或 isEven()
的函数,这样它就不需要配置选项了,但通过编写 (of: 2)
,这个函数变得更加强大,因为现在我们可以检查 2、3、4、5、50 或任何其他数字的倍数。
同样,当我们掷虚拟骰子时,我们使用了这样的代码:
let roll = Int.random(in: 1...20)
同样,random()
是一个函数,**(in: 1...20)
**部分标志着配置选项——如果没有它,我们就无法控制随机数的范围,这将大大降低其实用性。
我们可以在创建函数时,在括号中加入额外的代码,创建自己的函数,并对其进行配置。这个函数应该给定一个整数,比如 8,然后计算 1 到 12 的乘法表。
代码如下:
func printTimesTables(number: Int) {
for i in 1...12 {
print("\(i) x \(number) is \(i * number)")
}
}
printTimesTables(number: 5)
注意到我在括号内输入的数字:Int
吗?这就是所谓的参数,也是我们的自定义点。我们的意思是,无论谁调用这个函数,都必须在这里传递一个整数,Swift 会强制执行。在函数内部,number
可以像其他常量一样使用,因此它会出现在 print()
调用中。
正如你所看到的,在调用 printTimesTables()
时,我们需要明确写入 number:5
——我们需要将参数名称作为函数调用的一部分。这在其他语言中并不常见,但我认为这在 Swift 中非常有用,可以提醒我们每个参数的作用。
当你有多个参数时,参数命名就变得更加重要。例如,如果我们想自定义乘法表的高度,我们可以使用第二个参数设置范围的终点,就像这样:
func printTimesTables(number: Int, end: Int) {
for i in 1...end {
print("\(i) x \(number) is \(i * number)")
}
}
printTimesTables(number: 5, end: 20)
这需要两个参数:一个叫 number
的整数和一个叫 end
的终点。在运行 printTablesTable()
时,这两个参数都需要特别命名,我希望你现在能明白为什么它们很重要——想象一下,如果我们的代码是这样的:
printTimesTables(5, 20)
你还记得哪个号码是哪个吗?可能记得 但六个月后你还记得吗?可能记不住。
现在,从技术上讲,我们给发送数据和接收数据起的名字略有不同,虽然很多人都忽略了这种区别,但我至少要让你意识到这一点,以免以后措手不及。
请看这段代码:
func printTimesTables(number: Int, end: Int) {
这里的 number
和 end
都是参数:它们是占位符名称,在调用函数时会被填入值,这样我们就有了函数内部这些值的名称。
现在看看这段代码:
printTimesTables(number: 5, end: 20)
在这里,5 和 20 是参数:它们是发送到函数中的实际值,用于填充数字和结尾。
因此,我们既有参数又有参数值:一个是占位符,另一个是实际值,所以如果你分不清楚,只需记住参数/占位符,参数值/实际值。
这种名称上的区别重要吗?并不重要,事实上,在 Swift 中你很快就会发现,这种区别会让人格外困惑,所以根本不值得考虑。
当你要求 Swift 调用函数时,都必须按照创建函数时列出的顺序传递参数。
因此,对于这段代码:
func printTimesTables(number: Int, end: Int) {
这不是有效代码,因为它将结束置于数字之前:
printTimesTables(end: 20, number: 5)
提示:在函数内创建的任何数据都会在函数结束时自动销毁。
函数中应包含哪些代码?
函数的设计目的是让我们可以轻松地重复使用代码,这意味着我们不必复制和粘贴代码来获得常见的行为。如果你愿意,可以很少使用函数,但老实说,没有必要——函数是帮助我们编写更清晰、更灵活代码的绝佳工具。
有三种情况你会想要创建自己的函数:
- 最常见的情况是,你需要在多个地方使用相同的功能。在这种情况下,使用函数意味着你可以修改一段代码,然后更新所有使用你的函数的地方。
- 函数对于分割代码也很有用。如果你有一个很长的函数,就很难跟上所有的内容,但如果你把它分成三到四个较小的函数,就会变得更容易跟上。
- 最后一个原因更为先进: Swift 允许我们在现有函数的基础上构建新函数,这是一种称为函数组合的技术。通过将工作分割成多个小函数,函数组合让我们可以通过以各种方式组合这些小函数来构建大函数,这有点像乐高积木。
函数应接收多少个参数?
乍一看,这个问题就像是 “一段字符串有多长?”。也就是说,这个问题没有真正的、硬性的答案——一个函数可以不带参数,也可以带 20 个参数。
这当然没错,但当一个函数需要很多参数时——也许是 6 个或更多,但这是非常主观的——你就需要开始问这个函数是否做了太多的工作。
- 它需要所有六个参数吗?
- 能否将该函数拆分成参数更少的小函数?
- 是否应该以某种方式对这些参数进行分组?
我们稍后将讨论解决这个问题的一些技巧,但这里有一个重要的教训:这就是所谓的 “代码气味”——代码中的某些东西暗示着我们构建程序的方式存在潜在问题。
如何从函数中返回值
我们已经了解了如何创建函数以及如何为函数添加参数,但函数通常也会返回数据——它们会执行一些计算,然后将计算结果返回给调用者。
Swift 内置了大量这样的函数,而苹果的框架中还有数以万计的函数。例如,我们的 playground 顶部一直有 import Cocoa,其中包括各种数学函数,如用于计算数字平方根的 sqrt()
。
**sqrt()
**函数只接受一个参数,即我们要计算平方根的数字,然后它会继续计算,并将平方根返回给我们。
例如,我们可以这样写:
let root = sqrt(169)
print(root)
如果想从函数中返回自己的值,需要做两件事:
- 在函数的开头括号前写一个箭头,然后写一个数据类型,告诉 Swift 将返回哪种数据。
- 使用
return
关键字发回数据。
例如,也许你想在程序的不同部分掷骰子,但与其总是强制使用 6 面骰子掷骰子,不如将其作为一个函数:
func rollDice() -> Int {
return Int.random(in: 1...6)
}
let result = rollDice()
print(result)
因此,函数必须返回一个整数,而实际值会通过**return
**关键字发送回来。
使用这种方法,你可以在程序的许多地方调用 rollDice(),它们都将使用 6 面骰子。但如果将来您决定使用 20 面骰子,您只需修改这一个函数,就可以更新程序的其他部分。
重要提示:当你说你的函数将返回一个 Int 时,Swift 会确保它始终返回一个 Int —— 你不能忘记返回一个值,否则你的代码将无法构建。
让我们举一个更复杂的例子:两个字符串是否包含相同的字母,无论它们的顺序如何?这个函数应该接受两个字符串参数,如果它们的字母相同,则返回 true
– 因此,”abc “和 “cab “应该返回 true
,因为它们都包含一个 “a”、一个 “b”和一个 “c”。
实际上,你已经知道了足够多的知识来自己解决这个问题,但是你已经学了太多,可能已经忘记了让这个任务变得如此简单的一件事:如果你在任何字符串上调用 sorted()
,你会得到一个新的字符串,其中所有字母都是按字母顺序排列的。因此,如果对两个字符串都调用 sorted()
,就可以使用 ==
来比较它们的字母是否相同。
请继续尝试自己编写函数。同样,如果你写得很吃力,也不用担心——这对你来说都是新知识,努力记住新知识也是学习过程的一部分。
这里有一个示例解决方案:
func areLettersIdentical(string1: String, string2: String) -> Bool {
let first = string1.sorted()
let second = string2.sorted()
return first == second
}
让我们来分析一下:
- 它创建了一个名为
areLettersIdentical()
的新函数。 - 该函数接受两个字符串参数:
string1
和string2
。 - 函数说它返回一个
Bool
,所以在某些时候我们必须总是返回true
或false
。 - 在函数体中,我们对两个字符串进行排序,然后使用
==
对字符串进行比较——如果相同,则返回true
,否则返回false
。
这段代码对 string1
和 string2
进行了排序,并将它们的排序值赋值给新常量 first
和 second
。然而,这并不是必需的,我们可以跳过这些临时常量,直接比较 sorted()
的结果,就像下面这样:
func areLettersIdentical(string1: String, string2: String) -> Bool {
return string1.sorted() == string2.sorted()
}
代码少了,但我们可以做得更好。我们告诉 Swift 这个函数必须返回布尔值,因为函数中只有一行代码,所以 Swift 知道这一行必须返回数据。正因为如此,当函数只有一行代码时,我们可以完全删除 return
关键字,就像这样:
func areLettersIdentical(string1: String, string2: String) -> Bool {
string1.sorted() == string2.sorted()
}
我们还可以对 rollDice()
函数进行同样的处理:
func rollDice() -> Int {
Int.random(in: 1...6)
}
请记住,只有当你的函数只包含一行代码时,这才有效,尤其是这行代码必须实际返回您承诺返回的数据。
让我们举第三个例子。还记得学校里的勾股定理吗?它指出,如果一个三角形内有一个直角,那么可以通过将三角形的其他两边平方、相加,然后计算结果的平方根,来计算斜边的长度。
你已经学会了如何使用 sqrt()
,所以我们可以建立一个 pythagoras()
函数,接受两个Double数并返回另一个Double数:
func pythagoras(a: Double, b: Double) -> Double {
let input = a * a + b * b
let root = sqrt(input)
return root
}
let c = pythagoras(a: 3, b: 4)
print(c)
因此,这是一个名为 pythagoras()
的函数,它接受两个 Double 参数,并返回另一个 Double。该函数将 a 和 b 相乘,然后将其传入 sqrt()
,并返回结果。
这个函数也可以精简为一行,去掉返回关键字——试试看吧。
这就是解决方案:
func pythagoras(a: Double, b: Double) -> Double {
sqrt(a * a + b * b)
}
在我们继续之前,我还想提到最后一件事:如果函数不返回值,你仍然可以使用 return 本身来强制函数提前退出。例如,你可能要检查输入是否与预期一致,如果不一致,你就想在继续之前立即退出函数。
Swift 函数中何时不需要返回关键字?
在 Swift 中,我们使用 return 关键字从函数发回值,但有一种特殊情况不需要:当我们的函数只包含一个表达式时。
现在,”表达式 “并不是经常使用的一个词,但在这里理解它很重要。当我们编写程序时,我们会这样做:
5 + 8
或这样:
greet("Alan")
这些代码行会被解析为单个值:5 + 8 会被解析为 13,而 greet(“Alan”) 可能会返回字符串 “Hi, Alan!”。
甚至一些较长的代码也会被解析为一个值。例如,如果我们有这样三个布尔常量:
let isAdmin = true
let isOwner = false
let isEditingEnabled = false
那么这行代码就会解析为一个单一的值:
isOwner == true && isEditingEnabled || isAdmin == true
这将变为 “true”,因为即使 isOwner 为假,isAdmin 为真,所以整个表达式变为 true。
因此,我们编写的许多代码都可以解析为单一值。但也有很多代码无法解析为单一值。例如,这里的值是什么:
let name = "Otis"
是的,这创建了一个常量,但它本身并不成为一个值——我们不能写 return let name = "Otis"
。
同样,我们也可以这样写条件:
if name == "Maeve" {
print("Hello, Maeve!")
}
这也不能成为一个单一的值,因为其中有一个条件。
现在,所有这些都很重要,因为这些分隔符都有名字:当我们的代码可以归结为一个单一的值,如 true、false、”Hello”或 19 时,我们称之为表达式。表达式是可以赋值给变量或使用 print() 打印的东西。另一方面,当我们执行创建变量、启动循环或检查条件等操作时,我们称之为语句。
这一切之所以重要,是因为当我们的函数中只有一个表达式时,Swift 允许我们跳过使用 return 关键字。因此,这两个函数做的是同一件事:
func doMath() -> Int {
return 5 + 5
}
func doMoreMath() -> Int {
5 + 5
}
记住,里面的表达式可以很长,但不能包含任何语句——不能有循环、条件、新变量等。
现在,你可能会认为这样做毫无意义,因为你总是会使用 return 关键字。但是,这个功能在 SwiftUI 中非常常用,所以值得牢记。
在结束之前,我还想提一件事。你已经看到我们如何在表达式中使用 +、&& 和 || 等运算符,因为它们仍然解析为单个值。那么,三元操作符在这里也可以使用,事实上,这也是它的主要用例:当你想使用单个表达式,但又不想使用完整的 if 时。
为了演示这一点,请看下面的函数:
func greet(name: String) -> String {
if name == "Taylor Swift" {
return "Oh wow!"
} else {
return "Hello, \(name)"
}
}
如果我们想删除其中的返回语句,就无法写出这样的内容:
func greet(name: String) -> String {
if name == "Taylor Swift" {
"Oh wow!"
} else {
"Hello, \(name)"
}
}
这是不允许的,因为我们有实际的语句 —— if 和 else。
不过,我们可以这样使用三元运算符:
func greet(name: String) -> String {
name == "Taylor Swift" ? "Oh wow!" : "Hello, \(name)"
}
这是一个单一表达式。如果 name 等于 “Taylor Swift”,那么它的解析结果是这样的:
- Swift 将检查 name 是否为 Taylor Swift。
- 是,所以 name ==”Taylor Swift “为真。
- 三元运算符会意识到它的条件现在为真,所以它会选择 “哦哇”,而不是 “你好,(名字)”。
三元运算符的真正优势在于可以将条件功能放入一行代码中。而且,由于 SwiftUI 经常使用单表达式函数,这意味着三元运算符在 SwiftUI 中也会经常使用。
如何从函数中返回多个值
当你想从函数中返回单个值时,你可以在函数的开头括号前写入箭头和数据类型,就像这样:
func isUppercase(string: String) -> Bool {
string == string.uppercased()
}
将字符串与它本身的大写版本进行比较。如果字符串已经完全大写,则不会有任何变化,两个字符串将完全相同,否则它们将不同,== 将返回 false。
如果想从函数中返回两个或多个值,可以使用数组。例如,这里有一个返回用户详细信息的数组:
func getUser() -> [String] {
["Taylor", "Swift"]
}
let user = getUser()
print("Name: \(user[0]) \(user[1])")
这就有问题了,因为我们很难记住 user[0] 和 user[1] 是什么,而且如果我们调整了数组中的数据,那么 user[0] 和 user[1] 最终可能会变成别的东西,或者根本就不存在。
我们可以使用字典来代替,但这也有自己的问题:
func getUser() -> [String: String] {
[
"firstName": "Taylor",
"lastName": "Swift"
]
}
let user = getUser()
print("Name: \(user["firstName", default: "Anonymous"]) \(user["lastName", default: "Anonymous"])")
是的,我们现在已经为用户数据的各个部分赋予了有意义的名称,但看看 print() 的调用——虽然我们知道 name 和 lastName 都会存在,但我们仍然需要提供默认值,以防情况与我们的预期不同。
这两种解决方案都很糟糕,但 Swift 有一种元组(Tuple)形式的解决方案。与数组、字典和集合一样,元组允许我们将多个数据放入一个变量中,但与其他选项不同的是,元组有固定的大小,并且可以有多种数据类型。
下面是我们的函数返回元组时的样子:
func getUser() -> (firstName: String, lastName: String) {
(firstName: "Taylor", lastName: "Swift")
}
let user = getUser()
print("Name: \(user.firstName) \(user.lastName)")
让我们分析一下…
- 现在我们的返回类型是**
(firstName: String, lastName: String)
**,这是一个包含两个字符串的元组。 - 元组中的每个字符串都有一个名称。这些名称没有加引号:它们是元组中每个项目的特定名称,与字典中的任意键不同。
- 在函数中,我们会发回一个元组,其中包含我们承诺的所有元素,并附加了名称:firstName 将被设置为 “Taylor”,等等。
- 当我们调用 getUser() 时,我们可以使用键名读取元组的值:firstName、lastName 等。
我知道元组看起来与字典非常相似,但它们是不同的:
- 访问字典中的值时,Swift 无法提前知道它们是否存在。是的,我们知道 user[“firstName”] 会存在,但 Swift 无法确定,所以我们需要提供一个默认值。
- 当您访问元组中的值时,Swift 会提前知道它是否可用,因为元组表示它将可用。
- 我们使用 user.firstName 访问值:它不是字符串,因此也不会出现错别字。
- 除了 “firstName “之外,我们的字典可能还包含数百个其他值,但我们的元组不可能——我们必须列出它将包含的所有值,因此它保证包含所有值,而不包含其他值。
因此,与字典相比,元组有一个关键优势:我们可以精确指定哪些值会存在以及它们的类型,而字典可能包含也可能不包含我们所要求的值。
使用元组时,还有三件事必须知道。
首先,如果要从函数中返回一个元组,Swift 已经知道元组中每个项的名称,因此在使用 return 时不需要重复这些名称。因此,这段代码的作用与我们之前的元组相同:
func getUser() -> (firstName: String, lastName: String) {
("Taylor", "Swift")
}
其次,有时你会发现你得到的元组中的元素没有名称。遇到这种情况,你可以使用从 0 开始的数字索引访问元组的元素,就像这样:
func getUser() -> (String, String) {
("Taylor", "Swift")
}
let user = getUser()
print("Name: \(user.0) \(user.1)")
这些数字索引也适用于有命名元素的元组,但我一直认为使用命名更可取。
最后,如果函数返回的是一个元组,你可以根据需要将元组拆分成单个值。
要理解我的意思,请先看一下这段代码:
func getUser() -> (firstName: String, lastName: String) {
(firstName: "Taylor", lastName: "Swift")
}
let user = getUser()
let firstName = user.firstName
let lastName = user.lastName
print("Name: \(firstName) \(lastName)")
这又回到了 getUser() 的命名版本,当元组出现时,我们会将其中的元素复制到单个内容中,然后再使用它们。这没有什么新意,我们只是稍微移动了一下数据而已。
不过,我们可以跳过第一步,将 getUser() 的返回值拆分成两个独立的常量,而不是先将元组赋值给 user,然后再从其中复制单个值:
let (firstName, lastName) = getUser()
print("Name: \(firstName) \(lastName)")
这种语法一开始可能会让你头疼,但它实际上只是我们之前所使用的语法的简写:将 getUser() 返回的两个元素的元组转换成两个独立的常量。
事实上,如果你不需要元组中的所有值,你可以进一步使用 _ 来告诉 Swift 忽略元组中的这一部分:
let (firstName, _) = getUser()
print("Name: \(firstName)")
在 Swift 中,什么时候应该使用数组、集合或元组?
由于数组、集合和元组的工作方式略有不同,因此必须确保选择正确的方式,以便正确、高效地存储数据。
请记住:数组保持顺序,可以有重复数据;集合是无序的,不能有重复数据;而元组内部有固定数量、固定类型的值。
所以:
- 如果你想存储游戏字典中所有单词的列表,那么这个列表没有重复,顺序也不重要,所以你会选择集合。
- 如果要存储用户阅读过的所有文章,如果顺序无关紧要(如果只关心用户是否阅读过),则使用集合;如果顺序重要,则使用数组。
- 如果要存储一个视频游戏的高分列表,则顺序很重要,而且可能包含重复内容(如果两个玩家获得了相同的分数),因此要使用数组。
- 如果要存储待办事项列表中的项目,如果顺序是可预测的,则效果最佳,因此应使用数组。
- 如果要保存两个字符串,或两个字符串和一个整数,或三个布尔值,或类似的内容,则应使用元组。
有关元组的一些例子
由多种数据组成,可以是不同的类型的数据。更简单的处理复合型数据,省去建立变量和命名的痛苦。
let fruits = ("orange", 10)
fruits.0 //调用时
fruits.1
let fruits = (name: "orange", price: 10)
fruits.name
fruits.price
可以结合别名使用:
typealias Human = (name: String, height: Double, hairColor: String)
//typealias是声明类型别名
例子1:
//例子
let girl = ("Amy", 155, "Gold")
print("This girl's name is \(girl.0), height is \(girl.1), hair's color is \(girl.2)")
例子2:
//例子2
typealias Human = (name: String, height: Int, hairColor: String)
let girl: Human = ("Amy", 155, "Gold")
print("Girl's name is \(girl.name), height is \(girl.height), hair color is \(girl.hairColor)")
//还可以反过来调用Tuple中的资料
let (name, height, hairColor) = girl
print(name) //显示Amy
print(height) //显示155
print(hairColor) //显示Gold
//如果只是想调用其中一个资料,可以将name、height或hairColor换成下划线"_"
例子3:
//例子3
typealias student = (name: String, 数学: Int, 英文: Int, 历史: Int, 国文: Int)
let studentA: student = ("小鸭", 93, 68, 77, 72)
let studentB: student = ("贝贝", 84, 89, 59, 72)
let totalA = studentA.数学 + studentA.历史 + studentA.国文 + studentA.英文
let totalB = studentB.数学 + studentB.历史 + studentB.国文 + studentB.英文
let (result, average) = totalA > totalB ? (studentA.name, totalA / 4) : (studentB.name, totalB / 4)
print("\(result)的平均分数较高,平均分数为\(average)分。")
如何自定义参数标签
你一定见过 Swift 开发人员喜欢给函数参数命名,因为这样可以让人更容易记住函数被调用时参数的作用。例如,我们可以编写一个函数来掷一定次数的骰子,使用参数来控制骰子的面数和掷的次数:
func rollDice(sides: Int, count: Int) -> [Int] {
// Start with an empty array
var rolls = [Int]()
// Roll as many dice as needed
for _ in 1...count {
// Add each result to our array
let roll = Int.random(in: 1...sides)
rolls.append(roll)
}
// Send back all the rolls
return rolls
}
let rolls = rollDice(sides: 6, count: 4)
即使你在六个月后再来看这段代码,我也确信 rollDice(sides: 6, count: 4) 的意思不言自明。 这种为外部使用的参数命名的方法对 Swift 来说是如此重要,以至于在确定调用哪个方法时,它实际上会使用这些名称。这与许多其他语言完全不同,但在 Swift 中却是完全正确的:
func hireEmployee(name: String) { }
func hireEmployee(title: String) { }
func hireEmployee(location: String) { }
是的,这些函数都叫 hireEmployee(),但当你调用它们时,Swift 会根据你提供的参数名知道你指的是哪个函数。为了区分不同的选项,文档中经常会提到每个函数都包含其参数,例如:hireEmployee(name:) 或 hireEmployee(title:)。 不过,有时这些参数名称并不那么有用,我想从两个方面来说明。 首先,回想一下之前学习的 hasPrefix() 函数:
let lyric = "I see a red door and I want it painted black"
print(lyric.hasPrefix("I see"))
当我们调用 hasPrefix() 时,我们会直接传入要检查的前缀 —— 我们不会说 hasPrefix(string:),或者更糟的是,hasPrefix(prefix:)。怎么会这样呢? 当我们为函数定义参数时,实际上可以添加两个名称:一个用于函数被调用的地方,另一个用于函数内部。hasPrefix() 利用这一点将 _ 指定为参数的外部名称,这是 Swift 表示 “忽略此参数”的方式,并导致该参数没有外部标签。 如果你觉得这样读起来更好,我们也可以在自己的函数中使用同样的技巧。例如,我们之前有这样一个函数:
func isUppercase(string: String) -> Bool {
string == string.uppercased()
}
let string = "HELLO, WORLD"
let result = isUppercase(string: string)
你可能看了之后会觉得完全正确,但你也可能看了 string: string 之后会觉得重复很烦人。毕竟,除了字符串,我们还能传递什么呢? 如果我们在参数名前加上下划线,就可以像这样去掉外部参数标签:
func isUppercase(_ string: String) -> Bool {
string == string.uppercased()
}
let string = "HELLO, WORLD"
let result = isUppercase(string)
这在 Swift 中经常使用,例如 append() 用于将项目添加到数组中,contains() 用于检查项目是否在数组中,在这两种情况下,不需要标签也能很明显地看出参数是什么。 外部参数名的第二个问题是它们不合适——你想要参数名,所以 _ 并不是一个好主意,但它们在函数的调用位置读起来并不自然。 举个例子,这是我们之前看过的另一个函数:
func printTimesTables(number: Int) {
for i in 1...12 {
print("\(i) x \(number) is \(i * number)")
}
}
printTimesTables(number: 5)
这段代码是有效的 Swift 代码,我们可以不加改动。但调用时读起来并不顺畅:printTimesTables(number: 5)。这样会更好:
func printTimesTables(for: Int) {
for i in 1...12 {
print("\(i) x \(for) is \(i * for)")
}
}
printTimesTables(for: 5)
这在调用位置上读起来要好得多——你可以大声说 “打印 5 的乘法表”,这样就说得通了。但现在我们遇到了无效的 Swift:虽然 for 是允许的,而且在调用位置读起来也很好,但在函数内部却是不允许的。 我们已经看到了如何在参数名前加上 _,这样就不需要编写外部参数名了。那么,另一种方法就是在这里写第二个名称:一个用于外部,一个用于内部。 如下所示:
func printTimesTables(for number: Int) {
for i in 1...12 {
print("\(i) x \(number) is \(i * number)")
}
}
printTimesTables(for: 5)
其中有三点需要仔细研究:
- 我们写 for number: Int:外部名称是 for,内部名称是 number,类型是 Int。
- 调用函数时,我们使用外部名称作为参数:printTimesTables(for: 5)。
- 在函数内部,我们使用参数的内部名称:print(“\(i) x \(number) is \(i * number)”)。
因此,Swift 提供了两种重要的方法来控制参数名:我们可以使用 _ 作为外部参数名,这样它就不会被使用;或者在这里添加第二个参数名,这样我们就同时拥有了外部和内部参数名。 提示:前面提到,从技术上讲,传递给函数的值称为 “参数”,而函数内部接收的值称为参数值。这就是事情变得有点混乱的地方,因为现在我们的参数标签和参数名称并列在函数定义中。正如前面所说,将对这两种情况都使用 “参数”一词,当需要区分时,你会看到我使用 “外部参数名 “和 “内部参数名 “来区分它们。
何时省略参数标签?
如果我们在函数参数的外部标签中使用下划线,Swift 就会允许我们不使用该参数的任何名称。这在 Swift 开发的某些部分是非常常见的做法,尤其是在构建不使用 SwiftUI 的应用程序时。 跳过参数名的主要原因是,当你的函数名是一个动词,而第一个参数是动词所作用的名词时。例如:
- 启用闹钟应该是 enable(alarm),而不是 enable(alarm: alarm)
- 唱一首歌应该是 sing(song),而不是 sing(song: song)
- 查找客户应使用 find(customer),而不是 find(user: customer)
当参数标签很可能与输入的名称相同时,这一点尤为重要:
- 购买产品应使用 buy(toothbrush) 而不是 buy(item: toothbrush)
- 问候一个人应使用 greet(taylor) 而不是 greet(person: taylor)
- 阅读一本书应该是 read(book),而不是 read(book: book)
在 SwiftUI 出现之前,应用程序是使用苹果的 UIKit、AppKit 和 WatchKit 框架构建的,而这些框架是使用一种名为 Objective-C 的旧语言设计的。在这种语言中,函数的第一个参数总是不命名的,因此当你在 Swift 中使用这些框架时,你会看到很多函数的第一个参数标签都是下划线,以保持与 Objective-C 的互操作性。
好了,今天对函数就暂时学到这里,下一章,我们继续!