第7天 函数、参数和返回值
函数能让我们将代码片段封装起来,以便在多个地方重复使用。我们可以向函数中传入数据来定制其功能,并获取经过计算后返回的结果数据。
信不信由你,函数调用在过去其实是非常慢的。史蒂夫・约翰逊(Unix 操作系统许多早期编程工具的作者)曾说过:
“C 编程语言的创造者丹尼斯・里奇鼓励模块化编程,他告诉所有人 C 语言中的函数调用成本真的非常低。于是大家都开始编写小型函数并进行模块化设计。多年后我们才发现,函数调用仍然很耗时,而且我们的代码常常有 50% 的时间都花在了调用函数上。丹尼斯骗了我们!但为时已晚,我们都已经对函数调用‘上瘾’了……”
为什么他们会对函数调用 “上瘾” 呢?因为函数在简化代码方面作用巨大:我们不必在十几个地方复制粘贴同样的 10 行代码,而是可以把这些代码封装到一个函数中,然后直接使用这个函数。这不仅减少了代码重复,而且意味着如果你修改了这个函数(比如增加了一些功能),那么所有使用该函数的地方都会自动获得新的功能,而且不用担心会忘记更新某个粘贴过代码的地方。
今天你需要学习四个教程,你将学会如何编写自己的函数、如何接收参数以及如何返回数据。完成每个视频后,你可以查看任何可选阅读材料(如果需要的话),然后参加一个简短的测试,以确保自己理解了所教的内容。
7.1 如何通过函数重用代码
作者:Paul Hudson 2024 年 4 月 11 日 已针对 Xcode 16.4 更新
当你写出了一些自己非常满意且想要反复使用的代码时,你应该考虑把它放进一个函数里。函数就是从程序其余部分中拆分出来的代码块,并被赋予了一个名称,这样你就能轻松地引用它们。
例如,假设我们有这样一段简单的代码:
print("欢迎使用我的应用!")
print("默认情况下,这会打印出从厘米到英寸的转换表,不过")
print("如果你想的话,也可以设置一个自定义范围。")这是一个应用的欢迎消息,你可能希望在应用启动时打印它,或者在用户请求帮助时打印。
但要是你想让它同时出现在两个地方呢?没错,你可以只是复制这四句print()代码,然后把它们放到两个地方。可如果想让这段文字出现在十个地方呢?又或者之后你想修改措辞 —— 你真的能记得在代码中所有出现这段文字的地方都进行修改吗?
这就是函数的用武之地:我们可以把那段代码提取出来,给它起个名字,然后在任何需要的时候、任何需要的地方运行它。这意味着所有的print()语句都只放在一个地方,然后在其他地方重用。
下面是具体的实现方式:
func showWelcome() {
print("欢迎使用我的应用!")
print("默认情况下,这会打印出从厘米到英寸的转换表,不过")
print("如果你想的话,也可以设置一个自定义范围。")
}我们来详细解析一下:
- 它以
func关键字开头,这个关键字标志着一个函数的开始。 - 我们给这个函数命名为
showWelcome。这个名称可以是你想要的任何名字,但尽量要容易记住 ——printInstructions()、displayHelp()等等都是不错的选择。 - 函数体包含在一对花括号中,就像循环体和条件语句体一样。
这里面还有一个额外的东西,你可能在我们之前的学习中见过:showWelcome后面紧跟的()。我们早在学习字符串的时候就第一次见到过它们,当时我说count后面没有(),但uppercased()后面有。
现在你就要知道原因了:这些()是和函数一起使用的。正如你在上面看到的,在创建函数时会用到它们,而当你调用函数 —— 也就是让 Swift 运行函数中的代码时,也会用到它们。在我们这个例子中,我们可以像这样调用函数:
showWelcome()这被称为函数的调用点,这是一个专业术语,意思是 “调用函数的地方”。
那么这些括号实际上是用来做什么的呢?其实,它们是我们为函数添加配置选项的地方 —— 我们可以传入一些数据来定制函数的工作方式,这样函数就会变得更加灵活。
举个例子,我们已经用过这样的代码:
let number = 139
if number.isMultiple(of: 2) {
print("偶数")
} else {
print("奇数")
}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) = \(i * number)")
}
}
printTimesTables(number: 5)注意到我在括号里写了number: Int吗?这叫做参数,它是我们的定制点。我们的意思是,任何调用这个函数的人必须在这里传入一个整数,而且 Swift 会强制执行这一点。在函数内部,number就像其他常量一样可以被使用,所以它出现在了print()调用中。
如你所见,当调用printTimesTables()时,我们需要明确地写出number: 5—— 我们需要在函数调用中写出参数名。这在其他语言中并不常见,但我认为在 Swift 中这非常有帮助,因为它能提醒我们每个参数的作用。
当你有多个参数时,参数的命名就显得更加重要了。例如,如果我们想自定义乘法表的上限,我们可以用第二个参数来设置范围的终点,就像这样:
func printTimesTables(number: Int, end: Int) {
for i in 1...end {
print("\(i) x \(number) = \(i * number)")
}
}
printTimesTables(number: 5, end: 20)现在这个函数有两个参数:一个叫做number的整数,和一个叫做end的终点值。调用printTimesTables()时,这两个参数都需要被明确命名。我希望你现在能明白为什么它们很重要了 —— 想象一下,如果我们的代码是这样的:
printTimesTables(5, 20)你能记住这两个数字分别代表什么吗?也许现在能记住。但六个月后你还能记住吗?很可能记不住了。
从技术上讲,我们对发送数据和接收数据的命名略有不同。虽然很多人(包括我自己)都会忽略这种区别,但我还是要让你知道这一点,以免你日后感到困惑。
看看这段代码:
func printTimesTables(number: Int, end: Int) {这里的number和end都是参数:它们是占位符名称,当函数被调用时,这些名称会被具体的值填充,这样我们在函数内部就能用这些名称来指代那些值了。
再看这段代码:
printTimesTables(number: 5, end: 20)这里的 5 和 20 是实参:它们是实际传入函数中用来进行运算的值,用来填充number和end。
所以,我们既有参数又有实参:一个是占位符,另一个是实际值。如果你忘了哪个是哪个,只要记住 “Parameter(参数)” 对应 “Placeholder(占位符)”,“Argument(实参)” 对应 “Actual Value(实际值)” 就行了。
这种名称上的区别重要吗?其实不太重要:我会用 “参数” 来指代两者,我也知道其他人会用 “实参” 来指代两者,说实话,在我的职业生涯中,这从未造成过哪怕一点点问题。事实上,你很快就会发现,在 Swift 中这种区别格外令人困惑,所以真的没必要去纠结。
重要提示: 如果你更喜欢用 “实参” 来表示传入的数据,用 “参数” 来表示接收的数据,那是你的自由。但我确实会用 “参数” 来指代两者,而且在这本书以及以后的内容中都会这样做。
不管你把它们叫做 “实参” 还是 “参数”,当你让 Swift 调用函数时,你必须按照创建函数时列出的顺序来传递参数。
所以,对于这段代码:
func printTimesTables(number: Int, end: Int) {下面这段代码是无效的,因为它把end放在了number前面:
printTimesTables(end: 20, number: 5)提示: 在函数内部创建的任何数据,当函数执行完毕后都会被自动销毁。
【可选阅读】哪些代码应该放在函数中?
作者:Paul Hudson 2020 年 5 月 28 日 已针对 Xcode 16.4 更新
函数的设计目的是让我们能轻松复用代码,这意味着我们不必通过复制粘贴代码来实现常见功能。你可以很少使用函数,但说实话,没这个必要 —— 它们是帮助我们编写更清晰、更灵活代码的绝佳工具。
在三种情况下你会想要创建自己的函数:
- 最常见的情况是当你需要在多个地方使用相同功能时。在这里使用函数意味着你只需修改一处代码,所有使用该函数的地方就都会得到更新。
- 函数对于拆分代码也很有用。如果有一个很长的函数,可能很难弄清楚其中所有的操作,但如果把它拆分成三四个较小的函数,就会更容易理解。
- 最后一个原因更高级一些:Swift 允许我们用已有的函数构建新函数,这种技术称为 “函数组合”。通过把工作拆分到多个小函数中,函数组合能让我们以各种方式组合这些小函数来构建大函数,有点像乐高积木。
【可选阅读】一个函数应该接受多少个参数?
作者:Paul Hudson 2020 年 5 月 28 日 已针对 Xcode 16.4 更新
乍一看,这个问题就像 “一根绳子有多长?” 一样,没有一个确切、固定的答案 —— 一个函数可以不接受任何参数,也可以接受 20 个参数。
这当然是事实,但当一个函数接受很多参数时 —— 可能是 6 个或更多,不过这是非常主观的 —— 你就需要开始思考这个函数是不是承担了太多的工作。
- 它真的需要这 6 个参数吗?
- 这个函数能不能拆分成多个接受更少参数的小函数?
- 这些参数能不能以某种方式进行分组?
我们之后会介绍一些解决这类问题的技巧,但有一个重要的经验教训需要了解:这被称为 “代码异味”—— 即我们代码中的某些地方暗示了程序结构存在潜在问题。
【练习题】编写函数
问题 1/12:这段代码是有效的 Swift 代码吗?正确还是错误?
func readUserInput() {
while true {
print("Reading user input...")
break
}
readUserInput()问题 2/12:这段代码是有效的 Swift 代码吗?正确还是错误?
function applyFix() {
print("The fix is applied!")
}问题 3/12:这段代码是有效的 Swift 代码吗?正确还是错误?
func listOriginalStarWarsMovies() {
for i in 4...6 {
print("Episode \(i)")
}
}问题 4/12:这段代码是有效的 Swift 代码吗?正确还是错误?
func shareToTwitter() {
print("Sharing...")
}
shareToTwitter()问题 5/12:这段代码是有效的 Swift 代码吗?正确还是错误?
func scoreGoal() {
print("Gooooaaaal!")
}
scoreGoal(1)
scoreGoal(2)
scoreGoal(3)问题 6/12:这段代码是有效的 Swift 代码吗?正确还是错误?
func printWelcome() {
print("Hi there!")
}
printWelcome()问题 7/12:这段代码是有效的 Swift 代码吗?正确还是错误?
func shipOrders() {
let orders = [1, 2, 3, 4, 5]
for order in orders {
print("Shipping order \(id)")
}
}
shipOrders()问题 8/12:这段代码是有效的 Swift 代码吗?正确还是错误?
func sendMessage() {
print("Sending message...")
}
sendmessage()问题 9/12:这段代码是有效的 Swift 代码吗?正确还是错误?
func playMusic() {
print("Here's some Ed Sheeran.")
}问题 10/12:这段代码是有效的 Swift 代码吗?正确还是错误?
func showHelp() {
print("Welcome to MyApp.")
print("Click the button to start.")
}
showHelp()问题 11/12:这段代码是有效的 Swift 代码吗?正确还是错误?
func study {
print("It's time to study.")
print("I'm studying!")
print("Bored now; time for Netflix.")
}
study()问题 12/12:这段代码是有效的 Swift 代码吗?正确还是错误?
func doNothing() { }
doNothing()【练习题】接受参数
问题 1/12:这段代码是有效的 Swift 代码吗?正确还是错误?
func count(to: Int) {
for i in 1...to {
print("I'm counting: \(i)")
}
}问题 2/12:这段代码是有效的 Swift 代码吗?正确还是错误?
func walkDogs(destination: String) {
print("Let's go for a walk to \(destination).")
}
walkDogs(to: "the park")问题 3/12:这段代码是有效的 Swift 代码吗?正确还是错误?
func format(number: Int) {
print("The number is \(number).")
}
format(number: 32)问题 4/12:这段代码是有效的 Swift 代码吗?正确还是错误?
func check(age: Int) {
if age >= 18 {
print("You're an adult.")
} else {
print("You're a minor.")
}
}
check(age: 18)问题 5/12:这段代码是有效的 Swift 代码吗?正确还是错误?
func driveRace(laps: String) {
for i in 1...laps {
print("Another lap!")
}
}
driveRace(laps: 100)问题 6/12:这段代码是有效的 Swift 代码吗?正确还是错误?
func open(gifts: [Int]) {
for gift in gifts {
print("It's a \(gift) - thank you!")
}
}
open(gifts: ["guitar", "pair of socks"])问题 7/12:这段代码是有效的 Swift 代码吗?正确还是错误?
func calculateWages(people: Int) {
let total = people * 30_000
print("The total is \(total)")
}
calculatewages(people: 10)问题 8/12:这段代码是有效的 Swift 代码吗?正确还是错误?
func square(numbers: [Int]) {
for number in numbers {
let squared = number * number
print("\(number) squared is \(squared).")
}
}
square(numbers: [2, 3, 4])问题 9/12:这段代码是有效的 Swift 代码吗?正确还是错误?
func sendTweet(text: String) {
print("Posting to Twitter: \(text)")
}问题 10/12:这段代码是有效的 Swift 代码吗?正确还是错误?
func runDistance(kilometers Int) {
for _ in 1...kilometers {
print("Let's run another kilometer...")
}
}问题 11/12:这段代码是有效的 Swift 代码吗?正确还是错误?
func makeBand(names: [String]) {
print("Let's start a band...")
for name in names {
print("\(name) wants to join!")
}
}
makeBand(names: ["John", "Paul"])问题 12/12:这段代码是有效的 Swift 代码吗?正确还是错误?
func buyCar(price: Int) {
switch price {
case 0...20_000:
print("This seems cheap.")
case 20_001...50_000:
print("This seems like a reasonable car.")
case 50_001...100_000:
print("This had better be a good car.")
}
}7.2 如何从函数返回值
作者:Paul Hudson 2021 年 11 月 24 日 已更新至 Xcode 16.4
你已经了解了如何创建函数以及如何向函数添加参数,但函数通常还会将数据返回—— 它们执行一些计算,然后将计算结果返回给调用点。
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(),会得到一个所有字母按字母顺序排列的新字符串。所以,如果你对两个字符串都这样做,就可以使用 == 来比较它们,看看它们的字母是否相同。
试着自己编写这个函数吧。再次强调,如果你遇到困难也不用担心 —— 这对你来说都是全新的内容,努力记住新知识是学习过程的一部分。我很快会给你看解决方案,但请先自己尝试一下。
还在看吗?好吧,这是一个示例解决方案:
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() 函数,它接受两个十进制数并返回另一个十进制数:
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() 并返回结果。
这个函数也可以简化为一行代码,并且可以去掉 return 关键字 —— 试试看。和往常一样,我之后会给你看我的解决方案,但你自己尝试很重要。
还在看吗?好吧,这是我的解决方案:
func pythagoras(a: Double, b: Double) -> Double {
sqrt(a * a + b * b)
}在我们继续之前,我还想提最后一件事:如果你的函数不返回值,你仍然可以单独使用 return 来强制函数提前退出。例如,也许你要检查输入是否符合预期,如果不符合,你想在继续之前立即退出函数。
【可选阅读】在 Swift 函数中什么时候不需要 return 关键字?
作者:Paul Hudson 2024 年 5 月 6 日 已更新至 Xcode 16.4
我们在 Swift 中使用 return 关键字从函数返回值,但有一个特定的情况不需要它:当我们的函数只包含一个表达式时。
现在,“表达式” 这个词我不常使用,但在这里理解它很重要。当我们编写程序时,我们会做这样的事情:
5 + 8或者这样:
greet("Paul")这些代码行都会解析为一个单一的值:5 + 8 解析为 13,greet("Paul") 可能返回字符串 “Hi, Paul!”
即使是一些较长的代码也会解析为一个单一的值。例如,如果我们有三个布尔常量是这样的:
let isAdmin = true
let isOwner = false
let isEditingEnabled = false那么这行代码会解析为一个单一的值:
isOwner == true && isEditingEnabled || isAdmin == true结果会是 “true”,因为尽管 isOwner 是 false,但 isAdmin 是 true,所以整个表达式为 true。
所以,我们编写的很多代码都可以简化为一个单一的值。但也有很多代码不能简化为一个单一的值。例如,这里的值是什么:
let name = "Otis"是的,这创建了一个常量,但它本身并不会成为一个值 —— 我们不能写 return let name = "Otis"。
同样,我们可能会执行这样的操作:
if name == "Maeve" {
print("Hello, Maeve!")
print("How are you?")
}这也不能成为一个单一的值,因为其中有两个函数调用。
现在,所有这些都很重要,因为这些分类有各自的名称:当我们的代码可以简化为一个单一的值,如 true、false、“Hello” 或 19 时,我们称之为表达式。表达式是可以赋值给变量或使用 print() 打印的东西。另一方面,当我们执行诸如创建变量、开始循环或检查条件等操作时,我们称之为语句。
这一切都很重要,因为当函数中只有一个表达式时,Swift 允许我们跳过 return 关键字。所以,这两个函数做的是同样的事情:
func doMath() -> Int {
return 5 + 5
}
func doMoreMath() -> Int {
5 + 5
}记住,里面的表达式可以想多长就多长,但它不能包含任何语句 —— 不能有新的变量等等。
Swift 在这方面非常智能,它会自动允许我们以类似的方式使用简单的 if 和 switch 值 —— 只要它们直接返回值,而不是尝试创建新变量等。
例如,这是允许的:
func greet(name: String) -> String {
if name == "Taylor Swift" {
"Oh wow!"
} else {
"Hello, \(name)"
}
}条件的两个部分都直接返回了一个字符串,所以是允许的。然而,这是不允许的:
func greet(name: String) -> String {
if name == "Taylor Swift" {
"Oh wow!"
} else {
let greeting = "Hello, \(name)"
return greeting
}
}这试图在返回之前创建一个新的 greeting 常量。
本质上,if 本身可以成为一个表达式,只要 if 的每个分支 —— 它可能产生的每个结果 —— 本身都是一个单一的表达式。
这允许我们将条件的结果直接赋给一个新值,乍一看有点奇怪:
func greet(name: String) -> String {
let response = if name == "Taylor Swift" {
"Oh wow!"
} else {
"Hello, \(name)"
}
return response
}这起初通常有点难以理解,但试着把它想成一个三元条件运算符:
func greet(name: String) -> String {
let response = name == "Taylor Swift" ? "Oh wow!" : "Hello, \(name)"
return response
}【练习题】返回值
问题 1/12:这段代码是有效的 Swift 吗 —— 对还是错?
func read(books: [String]) -> Bool {
for book in books {
print("I'm reading \(book)")
}
return true
}问题 2/12:这段代码是有效的 Swift 吗 —— 对还是错?
func writeToLog(message: String) -> Bool {
if message != "" {
print("Log: \(message)")
return true
} else {
return false
}
}问题 3/12:这段代码是有效的 Swift 吗 —— 对还是错?
func check(scores: [Int]) {
for score in scores {
if score < 80 {
return false
}
}
return true
}
check(scores: [100, 90, 100, 85])问题 4/12:这段代码是有效的 Swift 吗 —— 对还是错?
func burnCandles(count: Int) -> Int {
for _ in 1...count {
print("I'm lighting a candle")
}
return true
}问题 5/12:这段代码是有效的 Swift 吗 —— 对还是错?
func paintHouse(color: String) -> Bool {
if color == "tartan" {
return false
}
}问题 6/12:这段代码是有效的 Swift 吗 —— 对还是错?
func format(number: Int) -> String {
return "The number is \(number)"
}问题 7/12:这段代码是有效的 Swift 吗 —— 对还是错?
func countMultiplesOf10(numbers: [Int]) -> Int {
var result = 0
for number in numbers {
if number.isMultiple(of: 10) {
result += 1
}
}
return result
}
countMultiplesOf10(numbers: [5, 10, 15, 20, 25])问题 8/12:这段代码是有效的 Swift 吗 —— 对还是错?
func giveDog(food: String) -> String {
if food == "treat" {
"The dog ate it"
}
}问题 9/12:这段代码是有效的 Swift 吗 —— 对还是错?
func estimateCost(units: Int) -> Int {
switch units {
case 0...10:
return "\(units * 10)"
case 11...50:
return "\(units * 9)"
case 51...100:
return "\(units * 8)"
default:
return "We can't make that many."
}
}问题 10/12:这段代码是有效的 Swift 吗 —— 对还是错?
func allTestsPassed(tests: [Bool]) -> Bool {
for test in tests {
if test == false {
return false
}
}
return true
}问题 11/12:这段代码是有效的 Swift 吗 —— 对还是错?
func playPiano(song: String) -> String {
retrurn "I'm going to play \(song) on my piano."
}问题 12/12:这段代码是有效的 Swift 吗 —— 对还是错?
func isEveryoneAdult(ages: [Int]) -> Bool {
for age in ages {
if age < 18 {
return false
}
}
return true
}
isEveryoneAdult(ages: [10, 20, 16, 24])7.3 如何从函数返回多个值
作者:Paul Hudson 2021 年 10 月 15 日 已针对 Xcode 16.4 更新
当你想从函数返回单个值时,你可以在函数的左大括号前写一个箭头和数据类型,就像这样:
func isUppercase(string: String) -> Bool {
string == string.uppercased()
}这段代码将一个字符串与其大写版本进行比较。如果该字符串已经完全是大写的,那么它不会有任何变化,两个字符串将完全相同;否则它们会不同,== 将返回 false。
如果你想从函数返回两个或更多值,你可以使用数组。例如,下面这个函数返回用户的详细信息:
func getUser() -> [String] {
["泰勒", "斯威夫特"]
}
let user = getUser()
print("姓名:\(user[0]) \(user[1])")这存在问题,因为很难记住 user[0] 和 user[1] 分别代表什么,而且如果我们调整数组中的数据,user[0] 和 user[1] 可能会变成其他内容,或者可能根本不存在。
我们也可以使用字典,但这也有它自身的问题:
func getUser() -> [String: String] {
[
"firstName": "泰勒",
"lastName": "斯威夫特"
]
}
let user = getUser()
print("姓名:\(user["firstName", default: "匿名"]) \(user["lastName", default: "匿名"])")是的,我们现在给用户数据的各个部分起了有意义的名称,但看看调用 print() 的地方 —— 尽管我们知道 firstName 和 lastName 都会存在,但我们仍然需要提供默认值,以防情况不符合我们的预期。
这两种解决方案都不太好,但 Swift 有一种解决方案,那就是元组。和数组、字典以及集合一样,元组允许我们将多个数据放入一个变量中,但不同于那些其他选项,元组有固定的大小,并且可以包含多种数据类型。
当函数返回元组时,我们的函数是这样的:
func getUser() -> (firstName: String, lastName: String) {
(firstName: "泰勒", lastName: "斯威夫特")
}
let user = getUser()
print("姓名:\(user.firstName) \(user.lastName)")让我们来分解一下:
- 我们的返回类型现在是
(firstName: String, lastName: String),这是一个包含两个字符串的元组。 - 我们元组中的每个字符串都有一个名称。这些名称不需要加引号:它们是元组中每个项的特定名称,与我们在字典中使用的任意键不同。
- 在函数内部,我们返回一个包含所有我们承诺的元素的元组,并附加到这些名称上:
firstName被设置为 “泰勒”,等等。 - 当我们调用
getUser()时,我们可以使用键名来读取元组的值:firstName、lastName等。
我知道元组看起来和字典非常相似,但它们是不同的:
- 当你访问字典中的值时,Swift 无法提前知道它们是否存在。是的,我们知道
user["firstName"]会在那里,但 Swift 不能确定,所以我们需要提供一个默认值。 - 当你访问元组中的值时,Swift确实可以提前知道它是可用的,因为元组表明它会可用。
- 我们使用
user.firstName来访问值:它不是字符串,所以也没有拼写错误的可能。 - 我们的字典可能包含除了
"firstName"之外的数百个其他值,但我们的元组不能 —— 我们必须列出它将包含的所有值,因此它保证包含所有这些值,而没有其他值。
因此,元组相对于字典有一个关键优势:我们精确地指定了哪些值会存在以及它们的类型,而字典可能包含也可能不包含我们要查询的值。
使用元组时,还有三件重要的事情需要知道。
首先,如果你从函数返回一个元组,Swift 已经知道你给元组中的每个项起的名称,所以你在使用 return 时不需要重复它们。所以,这段代码和我们之前的元组代码做的是同样的事情:
func getUser() -> (firstName: String, lastName: String) {
("泰勒", "斯威夫特")
}其次,有时你会发现你得到的元组中的元素没有名称。这时,你可以使用从 0 开始的数字索引来访问元组的元素,就像这样:
func getUser() -> (String, String) {
("泰勒", "斯威夫特")
}
let user = getUser()
print("姓名:\(user.0) \(user.1)")这些数字索引也适用于有命名元素的元组,但我一直觉得使用名称更好。
最后,如果一个函数返回一个元组,你实际上可以根据需要将元组拆分成单独的值。
为了理解我的意思,先看一下这段代码:
func getUser() -> (firstName: String, lastName: String) {
(firstName: "泰勒", lastName: "斯威夫特")
}
let user = getUser()
let firstName = user.firstName
let lastName = user.lastName
print("姓名:\(firstName) \(lastName)")回到 getUser() 的命名版本,当元组返回时,我们先将其中的元素复制到单独的变量中,然后再使用它们。这里没有什么新东西;我们只是在稍微移动数据。
然而,我们可以跳过第一步 —— 不将元组赋值给 user,然后从那里复制各个值,而是可以将从 getUser() 返回的值直接拆分成两个单独的常量,就像这样:
let (firstName, lastName) = getUser()
print("姓名:\(firstName) \(lastName)")这种语法一开始可能会让你有点困惑,但它实际上只是我们之前代码的简写:将我们从 getUser() 得到的包含两个元素的元组转换成两个单独的常量。
事实上,如果你不需要元组中的所有值,你可以更进一步,使用 _ 告诉 Swift 忽略元组的那部分:
let (firstName, _) = getUser()
print("姓名:\(firstName)")【可选阅读】在 Swift 中应该何时使用数组、集合或元组?
作者:Paul Hudson 2021 年 8 月 22 日 已针对 Xcode 16.4 更新
因为数组、集合和元组的工作方式略有不同,所以确保选择正确的类型来存储数据是很重要的,这样才能保证数据存储正确且高效。
记住:数组保持顺序且可以有重复项,集合是无序的且不能有重复项,元组内部有固定数量的固定类型的值。
所以:
- 如果你想存储一个游戏字典中的所有单词,这些单词没有重复项且顺序无关紧要,那么你会选择集合。
- 如果你想存储用户阅读过的所有文章,如果顺序无关紧要(如果你只关心他们是否读过),你可以使用集合;如果顺序有关系,你可以使用数组。
- 如果你想存储一个视频游戏的高分列表,这个列表的顺序很重要,并且可能包含重复项(如果两个玩家得到相同的分数),那么你会使用数组。
- 如果你想存储待办事项列表,当顺序可预测时效果最好,所以你应该使用数组。
- 如果你想精确地存储两个字符串,或者精确地存储两个字符串和一个整数,或者精确地存储三个布尔值,或者类似的情况,你应该使用元组。
【练习题】元组
问题 1/6:关于元组,以下哪些陈述是正确的?
- 选项 1: 元组必须始终创建为变量。
- 选项 2: 你可以给元组的项命名。
问题 2/6:关于元组,以下哪些陈述是正确的?
- 选项 1: 你可以使用数字位置访问元组的项。
- 选项 2: 元组中的所有值必须是唯一的。
问题 3/6:关于元组,以下哪些陈述是正确的?
- 选项 1: 元组将值存储在一个单一的值中。
- 选项 2: 没有人真正理解元组。
问题 4/6:关于元组,以下哪些陈述是正确的?
- 选项 1: 元组不能包含多行字符串。
- 选项 2: 元组是通过将项放在括号内创建的。
问题 5/6:关于元组,以下哪些陈述是正确的?
- 选项 1: 你不能更改元组项的类型。
- 选项 2: 元组只能包含字符串。
问题 6/6:关于元组,以下哪些陈述是正确的?
- 选项 1: 元组的大小是固定的。
- 选项 2: 元组与数组相同。
【练习题】数组 vs 集合 vs 元组
问题 1/6:以下哪些最适合存储为数组?
- 选项 1: 用户的地址。
- 选项 2: 聊天程序中的消息。
问题 2/6:以下哪些最适合存储为数组?
- 选项 1: 拼字游戏中有效的单词列表。
- 选项 2: 未来 10 天的天气预报。
问题 3/6:以下哪些最适合存储为数组?
- 选项 1: 一首诗的诗句。
- 选项 2: 用户是否登录。
问题 4/6:以下哪些最适合存储为数组?
- 选项 1: 一张专辑中的歌曲。
- 选项 2: 当前温度。
问题 5/6:以下哪些最适合存储为数组?
- 选项 1: 一个孩子每月的身高测量值。
- 选项 2: Twitter 上所有用户名的列表。
问题 6/6:以下哪些最适合存储为数组?
- 选项 1: 视频游戏的高分。
- 选项 2: 一个人的名字。
7.4 如何自定义参数标签
作者:Paul Hudson 2021 年 10 月 25 日 已针对 Xcode 16.4 更新
你已经了解了 Swift 开发者是如何命名函数参数的,因为这能让函数调用时更容易记住参数的作用。例如,我们可以编写一个函数来掷一定次数的骰子,用参数来控制骰子的面数和掷的次数:
func rollDice(sides: Int, count: Int) -> [Int] {
// 从一个空数组开始
var rolls = [Int]()
// 按需要掷骰子
for _ in 1...count {
// 将每次结果添加到数组中
let roll = Int.random(in: 1...sides)
rolls.append(roll)
}
// 返回所有掷骰子的结果
return rolls
}
let rolls = rollDice(sides: 6, count: 4)即使六个月后再看这段代码,我相信rollDice(sides: 6, count: 4)也是相当容易理解的。
这种为外部使用命名参数的方式在 Swift 中非常重要,实际上,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)在调用点这样读起来好多了 —— 你可以直接大声说 “print times table for 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 给了我们两种控制参数名称的重要方法:我们可以为外部参数名称使用_,这样就不会用到它;或者在那里添加第二个名称,这样我们就同时有了外部和内部参数名称。
提示: 之前我提到过,从技术上讲,你传入函数的值称为 “实参”,而在函数内部接收的值称为 “形参”。这就有点混乱了,因为现在在函数定义中,实参标签和形参名称并存。就像我说的,我会用 “参数” 这个词来指代两者,当需要区分时,你会看到我用 “外部参数名称” 和 “内部参数名称” 来区分它们。
【可选阅读】什么时候应该省略参数标签?
作者:Paul Hudson 2021 年 10 月 25 日 已针对 Xcode 16.4 更新
如果我们为函数参数的外部标签使用下划线,Swift 允许我们完全不给该参数命名。这在 Swift 开发的某些领域是非常常见的做法,特别是在不使用 SwiftUI 的应用程序中,但在很多其他情况下你也会想要使用这种方式。
跳过参数名称的主要原因是,当你的函数名是一个动词,而第一个参数是该动词所作用的名词时。例如:
- 问候一个人应该是
greet(taylor),而不是greet(person: taylor) - 买一件产品应该是
buy(toothbrush),而不是buy(item: toothbrush) - 找一个客户应该是
find(customer),而不是find(user: customer)
当参数标签可能与你要传入的任何内容的名称相同时,这一点尤为重要:
- 唱一首歌应该是
sing(song),而不是sing(song: song) - 启用一个闹钟应该是
enable(alarm),而不是enable(alarm: alarm) - 读一本书应该是
read(book),而不是read(book: book)
在 SwiftUI 出现之前,应用程序是使用苹果的 UIKit、AppKit 和 WatchKit 框架构建的,这些框架是用一种名为 Objective-C 的较旧语言设计的。在那种语言中,函数的第一个参数总是不命名的,所以当你在 Swift 中使用这些框架时,你会看到很多函数的第一个参数标签使用下划线,以保持与 Objective-C 的互操作性。
【练习题】省略参数标签
问题 1/12:这段代码是有效的 Swift 代码 —— 对还是错?
func makeBurger(withCheese: Bool) {
if cheese {
print("Here's a cheeseburger")
} else {
print("Here's a regular burger")
}
}问题 2/12:这段代码是有效的 Swift 代码 —— 对还是错?
func sumItems(_ items: [Int]) -> Int {
var total = 0
for item in items {
total += item
}
return total
}问题 3/12:这段代码是有效的 Swift 代码 —— 对还是错?
func printLogMessage(message: String) -> Bool {
print("Log: \(message)")
return true
}
printLogMessage("Something went wrong!")问题 4/12:这段代码是有效的 Swift 代码 —— 对还是错?
func bounceOnTrampoline(times: Int) {
for _ in 1...times {
print("Boing!")
}
}问题 5/12:这段代码是有效的 Swift 代码 —— 对还是错?
func greet(_ name: String) {
print("Hi, \(name)!")
}问题 6/12:这段代码是有效的 Swift 代码 —— 对还是错?
func countPoodles(dogs: [String]) -> Int {
var sum = 0
for dog in dogs {
if dog == "Poodle" {
sum += 1
}
}
return sum
}
countPoodles(["Mollie", "Penny", "Poppy"])问题 7/12:这段代码是有效的 Swift 代码 —— 对还是错?
func climbMountain(_ name: String) {
print("I'm going to climb \(name).")
}
climbMountain("Everest")问题 8/12:这段代码是有效的 Swift 代码 —— 对还是错?
func isEveryoneCanadian(_ birthCountries: [String]) {
for country in birthCountries {
if country != "Canada" {
return false
}
}
return true
}问题 9/12:这段代码是有效的 Swift 代码 —— 对还是错?
func square(_ number: Int) -> Int {
return number * number
}问题 10/12:这段代码是有效的 Swift 代码 —— 对还是错?
func formatLength(length length: Int) {
print("That measures \(length)cm.")
}
formatLength(95)问题 11/12:这段代码是有效的 Swift 代码 —— 对还是错?
func evaluateJavaScript(_ input: String) {
print("Yup, that's JavaScript alright.")
}问题 12/12:这段代码是有效的 Swift 代码 —— 对还是错?
func addStudentToClass(_ name: String) {
print("Welcome to the class, \(student)!")
}