Skip to content

第9天 闭包、函数作为参数

做好准备,因为今天我们要讲 Swift 中很多人难以理解的第一个知识点。请记住 Flip Wilson 的定律:“如果你不往机器里投几个五分镍币,就别指望能中头奖。”

今天的主题是闭包,它有点有点像匿名函数 —— 我们可以直接创建并赋值给变量的函数,或者传入其他函数中来定制它们的行为。没错,你没看错:将一个函数作为参数传入另一个函数。

闭包确实很难。 我这么说不是想让你打退堂鼓,只是想让你提前知道,如果你觉得闭包难以理解或记住,没关系 —— 我们都经历过这个阶段!

有时候闭包的语法可能会让你觉得眼花缭乱,在你学习今天的课程时,这一点会非常明显。如果你觉得有点难以承受 —— 如果你盯着一些代码,却不能 100% 确定它的意思 —— 那就回到上一个视频再看一遍,给你的记忆一点提示。你会发现下面的测试和可选阅读链接比平时多,希望能帮助你巩固知识。

SwiftUI 大量使用闭包,所以花时间弄清楚闭包的原理是值得的。是的,闭包可能是 Swift 中最复杂的特性,但这有点像骑自行车上山 —— 一旦你到达山顶,一旦你掌握了闭包,一切都会变得容易得多。

今天你有三个教程要学习,还有一个总结和另一个 checkpoint。 和往常一样,完成每个视频后,会有一些可选的额外阅读材料和简短测试,帮助你确保理解了所教的内容。这次你会发现这些内容相当多,因为闭包确实需要一些时间来理解,所以不要害怕去探索!

9.1 如何创建和使用闭包

作者:Paul Hudson 2021 年 11 月 8 日 已针对 Xcode 16.4 更新

在 Swift 中,函数是很强大的东西。是的,你已经了解了如何调用它们、向它们传递值以及让它们返回数据,但你还可以将它们赋值给变量、将函数传入其他函数,甚至从函数中返回函数。

例如:

swift
func greetUser() {
    print("Hi there!")
}

greetUser()

var greetCopy = greetUser
greetCopy()

这里创建了一个简单的函数并调用它,然后创建了该函数的一个副本并调用这个副本。因此,它会两次打印相同的消息。

重要提示: 当你复制一个函数时,不要在函数名后写括号 —— 应该是var greetCopy = greetUser,而不是var greetCopy = greetUser()。如果加上括号,你就是在调用这个函数,并将其返回值赋给其他东西。

但如果你想跳过创建一个单独的函数,而只是直接将功能赋给一个常量或变量呢?事实证明,你也可以做到这一点:

swift
let sayHello = {
    print("Hi there!")
}

sayHello()

Swift 给了它一个华丽的名字 ——闭包表达式,这其实是一种花哨的说法,意思是我们刚刚创建了一个闭包 —— 一段可以传递并在需要时调用的代码。这个闭包没有名字,但除此之外,它实际上就是一个不接受参数也不返回值的函数。

如果你希望闭包接受参数,就需要用一种特殊的方式来编写。你知道,闭包的开始和结束都用大括号,这意味着我们不能把控制参数或返回值的代码放在大括号外面。所以,Swift 有一个巧妙的解决方法:我们可以把这些信息放在大括号里面,就像这样:

swift
let sayHello = { (name: String) -> String in
    "Hi \(name)!"
}

我在这里加了一个额外的关键字 —— 你注意到了吗?就是in关键字,它紧跟在闭包的参数和返回类型后面。再次说明,对于普通函数,参数和返回类型会放在大括号外面,但对于闭包我们不能这样做。所以,in用于标记参数和返回类型的结束 —— 后面的所有内容都是闭包本身的主体。这是有原因的,你很快就会自己明白。

与此同时,你可能会有一个更基本的问题:“我到底为什么需要这些东西?” 我知道,闭包看起来确实很晦涩。更糟糕的是,它们看起来既晦涩复杂 —— 很多人第一次接触闭包时都非常困惑,因为它们是复杂的东西,而且似乎永远不会有用。

然而,你会发现闭包在 Swift 中被广泛使用,几乎在 SwiftUI 的所有地方都能用到。说真的,你会在每个 SwiftUI 应用中使用它们,有时甚至会用到几百次 —— 可能不一定是上面看到的这种形式,但你确实会大量使用它们。

为了理解闭包为什么如此有用,我首先想介绍一下函数类型。你已经知道整数的类型是Int,小数的类型是Double等等,现在我想让你思考一下函数也有类型。

我们来看看我们之前写的greetUser()函数:它不接受参数,不返回值,也不抛出错误。如果我们要为greetCopy写一个类型注解,会是这样的:

swift
var greetCopy: () -> Void = greetUser

我们来分解一下:

  1. 空括号表示一个不接受参数的函数。
  2. 箭头的含义和创建函数时一样:我们即将声明函数的返回类型。
  3. Void表示 “无”—— 这个函数不返回任何东西。有时你可能会看到它被写成(),但我们通常避免这样做,因为这可能会与空参数列表混淆。

每个函数的类型都取决于它接收和返回的数据。这听起来可能很简单,但其中隐藏着一个重要的注意点:它接收的数据的名称并不是函数类型的一部分。

我们可以用更多的代码来证明这一点:

swift
func getUserData(for id: Int) -> String {
    if id == 1989 {
        return "Taylor Swift"
    } else {
        return "Anonymous"
    }
}

let data: (Int) -> String = getUserData
let user = data(1989)
print(user)

一开始很简单:这是一个接受整数并返回字符串的函数。但是当我们获取函数的一个副本时,函数的类型不包括外部参数名for,所以调用副本时我们用data(1989)而不是data(for: 1989)

巧妙的是,这个规则也适用于所有闭包 —— 你可能已经注意到,我之前没有实际使用我们写的sayHello闭包,那是因为我不想让你对调用时缺少参数名产生疑问。现在我们来调用它:

swift
sayHello("Taylor")

这里没有使用参数名,就像我们复制函数时一样。所以,再次强调:只有当我们直接调用函数时,外部参数名才重要,当我们创建闭包或者先获取函数的副本时,外部参数名并不重要。

你可能还在想,这一切到底有什么意义,答案马上就会揭晓。你还记得我说过可以对数组使用sorted()来排序其元素吗?

这意味着我们可以写出这样的代码:

swift
let team = ["Gloria", "Suzanne", "Piper", "Tiffany", "Tasha"]
let sortedTeam = team.sorted()
print(sortedTeam)

这真的很简洁,但如果我们想控制排序呢 —— 如果我们希望有一个人因为是队长而总是排在第一位,其余的人按字母顺序排序呢?

实际上,sorted()允许我们传入一个自定义的排序函数来实现这一点。这个函数必须接受两个字符串,如果第一个字符串应该排在第二个字符串前面,就返回true,如果第一个字符串应该排在第二个字符串后面,就返回false

如果 Suzanne 是队长,这个函数会是这样的:

swift
func captainFirstSorted(name1: String, name2: String) -> Bool {
    if name1 == "Suzanne" {
        return true
    } else if name2 == "Suzanne" {
        return false
    }

    return name1 < name2
}

所以,如果第一个名字是 Suzanne,返回true,这样name1就会排在name2前面。另一方面,如果name2是 Suzanne,返回false,这样name1就会排在name2后面。如果两个名字都不是 Suzanne,就用<进行正常的字母排序。

就像我说的,sorted()可以传入一个函数来创建自定义的排序顺序,只要这个函数 a) 接受两个字符串,并且 b) 返回一个布尔值,sorted()就可以使用它。

我们新写的captainFirstSorted()函数正好符合这些要求,所以我们可以直接使用它:

swift
let captainFirstTeam = team.sorted(by: captainFirstSorted)
print(captainFirstTeam)

运行这段代码时,它会打印出["Suzanne", "Gloria", "Piper", "Tasha", "Tiffany"],完全符合我们的预期。

到目前为止,我们已经介绍了两个看似非常不同的东西。首先,我们可以创建闭包作为匿名函数,将它们存储在常量和变量中:

swift
let sayHello = {
    print("Hi there!")
}

sayHello()

而且我们还能够将函数传入其他函数,就像我们将captainFirstSorted()传入sorted()一样:

swift
let captainFirstTeam = team.sorted(by: captainFirstSorted)

闭包的强大之处在于我们可以将这两者结合起来:sorted()需要一个接受两个字符串并返回一个布尔值的函数,它并不在乎这个函数是用func正式创建的,还是用闭包提供的。

所以,我们可以再次调用sorted(),但这次不传入captainFirstTeam()函数,而是直接写一个新的闭包:写一个左大括号,列出参数和返回类型,写上in,然后放入我们标准的函数代码。

一开始这可能会让你很头疼。 这并不是因为你不够聪明理解不了闭包,或者不适合 Swift 编程,只是因为闭包确实很难。别担心 —— 我们会想办法让它变得更容易!

好了,让我们编写一些新的代码,使用闭包来调用sorted()

swift
let captainFirstTeam = team.sorted(by: { (name1: String, name2: String) -> Bool in
    if name1 == "Suzanne" {
        return true
    } else if name2 == "Suzanne" {
        return false
    }

    return name1 < name2
})

这里一下子出现了大量的语法,我不怪你会觉得头疼,但我希望你至少能看到闭包的一点好处:像sorted()这样的函数允许我们传入自定义代码来调整它们的工作方式,而且可以直接传入 —— 我们不需要为了这一次使用而专门写一个新函数。

现在你已经理解了闭包是什么,让我们看看能不能让它们更容易阅读。

【可选阅读】闭包到底是什么,为什么 Swift 这么喜欢它们?

作者:Paul Hudson 2020 年 6 月 5 日 已针对 Xcode 16.4 更新

来吧,承认吧:你肯定问过自己这个问题。如果你没问过,我会很惊讶,因为闭包是 Swift 最强大的特性之一,但也无疑是最让人困惑的特性。

所以,如果你现在在想 “哇,闭包真难”,别灰心 —— 它们确实很难,你觉得难,说明你的大脑在正常运转。

Swift 中使用闭包最常见的原因之一是存储功能 —— 可以说 “这是我想让你在某个时候做的一些工作,但不一定是现在。” 举几个例子:

  1. 延迟后运行一些代码。
  2. 动画完成后运行一些代码。
  3. 下载完成后运行一些代码。
  4. 用户从菜单中选择一个选项后运行一些代码。

闭包让我们可以把一些功能包装在一个变量里,然后存储起来。我们也可以从函数中返回它,再把这个闭包存储到其他地方。

学习的时候,闭包读起来有点费劲 —— 尤其是当它们接受和 / 或返回自己的参数时。但没关系:小步前进,如果卡住了就回溯,你会没问题的。

【可选阅读】为什么 Swift 的闭包参数在大括号里面?

作者:Paul Hudson 2020 年 5 月 28 日 已针对 Xcode 16.4 更新

闭包和函数都可以接受参数,但它们接受参数的方式大不相同。这是一个接受字符串和整数的函数:

swift
func pay(user: String, amount: Int) {
    // 代码
}

这是用闭包写成的完全相同的东西:

swift
let payment = { (user: String, amount: Int) in
    // 代码
}

如你所见,参数移到了大括号里面,in关键字用于标记参数列表的结束和闭包体本身的开始。

闭包把参数放在大括号里面是为了避免让 Swift 混淆:如果我们写成let payment = (user: String, amount: Int),那看起来就像我们在尝试创建一个元组,而不是一个闭包,这会很奇怪。

仔细想想,把参数列表放在大括号里也巧妙地体现了整个东西是存储在变量中的一个代码块 —— 参数列表和闭包体都是同一坨代码的一部分,并存储在我们的变量中。

把参数列表放在大括号里也说明了in关键字的重要性 —— 没有它,Swift 很难知道你的闭包体实际从哪里开始,因为没有第二组大括号。

【可选阅读】如何从不带参数的闭包中返回一个值?

作者:Paul Hudson 2020 年 5 月 28 日 已针对 Xcode 16.4 更新

Swift 中的闭包有独特的语法,这确实把它们和简单的函数区分开来,其中一个容易造成 confusion 的地方是我们如何接受和返回参数。

首先,这是一个接受一个参数且不返回任何值的闭包:

swift
let payment = { (user: String) in
    print("Paying \(user)…")
}

这是一个接受一个参数且返回一个布尔值的闭包:

swift
let payment = { (user: String) -> Bool in
    print("Paying \(user)…")
    return true
}

如果你想返回一个值但不接受任何参数,你不能只写-> Bool in——Swift 不会明白你的意思。相反,你应该对参数列表使用空括号,就像这样:

swift
let payment = { () -> Bool in
    print("Paying an anonymous person…")
    return true
}

仔细想想,这和标准函数的写法是一样的,你会写成func payment() -> Bool

【练习题】创建基本的闭包

问题 1/12:这段代码包含一个有效的 Swift 闭包 —— 对还是错?

swift
func signAutograph(to name: String) {
	print("To \(name), my #1 fan")
}

signAutograph(to: "Lisa")

问题 2/12:这段代码包含一个有效的 Swift 闭包 —— 对还是错?

swift
var paintPicture() {
	print("Where are my watercolors?")
}

问题 3/12:这段代码包含一个有效的 Swift 闭包 —— 对还是错?

swift
let learnSwift = {
	print("Closures are like functions")
}
learnSwift()

问题 4/12:这段代码包含一个有效的 Swift 闭包 —— 对还是错?

swift
let greetUser = {
	print("Hi there!")
}
greetUser()

问题 5/12:这段代码包含一个有效的 Swift 闭包 —— 对还是错?

swift
var connectVPN = {
	print("Connected!")
}
connectVPN()

问题 6/12:这段代码包含一个有效的 Swift 闭包 —— 对还是错?

swift
takeCruise = {
	print("A week of vacation!")
}

问题 7/12:这段代码包含一个有效的 Swift 闭包 —— 对还是错?

swift
let sing {
	print("Tralala")
}
sing()

问题 8/12:这段代码包含一个有效的 Swift 闭包 —— 对还是错?

swift
var meetFriends = {
	print("Let's watch a movie")
}
meetfriends()

问题 9/12:这段代码包含一个有效的 Swift 闭包 —— 对还是错?

swift
let walkDog = {
	print("Let's go to the park")
}

问题 10/12:这段代码包含一个有效的 Swift 闭包 —— 对还是错?

swift
let upgrade() = {
	print("Upgrading...")
}
upgrade()

问题 11/12:这段代码包含一个有效的 Swift 闭包 —— 对还是错?

swift
var castVote = {
	print("I voted!")
}
castVote()

问题 12/12:这段代码包含一个有效的 Swift 闭包 —— 对还是错?

swift
var takeMedicine = {
	print("I feel a little better")
}

【练习题】在闭包中接受参数

问题 1/12:这段代码是有效的 Swift—— 对还是错?

swift
var cleanRoom = { (name: String) in
	print("I'm cleaning the \(name).")
}
cleanRoom("kitchen")

问题 2/12:这段代码是有效的 Swift—— 对还是错?

swift
var sendMessage = { (message: String) in
	if message != "" {
		print("Sending to Twitter: \(message)")
	} else {
		print("Your message was empty.")
	}
}

问题 3/12:这段代码是有效的 Swift—— 对还是错?

swift
var click = { (button: Int) in
	if button >= 0 {
		print("Button \(button) was clicked.")
	} else {
		print("That button doesn't exist.")
	}
}

问题 4/12:这段代码是有效的 Swift—— 对还是错?

swift
printDocument = { (copies: Int) in
	for _ in 1...copies {
		print("Printing document...")
	}
}

问题 5/12:这段代码是有效的 Swift—— 对还是错?

swift
var shareWinnings = { (amount: Double) in
	let half = amount / 2.0
	print("It's \(half) for me and \(half) for you.")
}
shareWinnings("50")

问题 6/12:这段代码是有效的 Swift—— 对还是错?

swift
var pickFruit = { (name: String) in
	switch name {
	case strawberry:
		fallthrough
	case raspberry:
		print("Strawberries and raspberries are half price!")
	default:
		print("We don't have those.")
	}
}

pickFruit("strawberry")

问题 7/12:这段代码是有效的 Swift—— 对还是错?

swift
let calculateResult = { (Int) in
	if answer == 42 {
		print("You're correct!")
	} else {
		print("Try again.")
	}
}

问题 8/12:这段代码是有效的 Swift—— 对还是错?

swift
let fixCar = { (problem: String) in
	print("I fixed the \(problem).")
}

问题 9/12:这段代码是有效的 Swift—— 对还是错?

swift
let makeReservation = { (people: Int) in
	print("I'd like a table for \(people), please.")
}

问题 10/12:这段代码是有效的 Swift—— 对还是错?

swift
var cutGrass = { (length currentLength: Int) in
	switch currentLength {
	case 0...1:
		print("That's too short")
	case 1...3:
		print("It's already the right length")
	default:
		print("That's perfect.")
	}
}

问题 11/12:这段代码是有效的 Swift—— 对还是错?

swift
let watchTV = { (channel: String) in
	print("I'm going to watch some \(channel)")
}
watch_TV("BBC News")

问题 12/12:这段代码是有效的 Swift—— 对还是错?

swift
let rowBoat = { (distance: Int) in
	for _ in 1...distance {
		print("I'm rowing 1km.")
	}
}
rowBoat(5)

【练习题】从闭包中返回值

问题 1/12:这段代码是有效的 Swift—— 对还是错?

swift
var flyDrone = { (hasPermit: Bool) -> Bool in
	if hasPermit {
		print("Let's find somewhere safe!")
		return true
	}
	print("That's against the law.")
	return false
}

问题 2/12:这段代码是有效的 Swift—— 对还是错?

swift
let shovelSnow = { (depth) -> String in
	return "OK, I can do this..."
}

问题 3/12:这段代码是有效的 Swift—— 对还是错?

swift
let measureSize = { (inches: Int) -> String in
	switch inches {
	case 0...26:
		return "XS"
	case 27...30:
		return "S"
	case 31...34:
		return "M"
	case 35...38:
		return "L"
	default:
		return "XL"
	}
}
measureSize(36)

问题 4/12:这段代码是有效的 Swift—— 对还是错?

swift
func callNumber = { (number: Int) -> String in
	return "Calling now..."
}

问题 5/12:这段代码是有效的 Swift—— 对还是错?

swift
let goSurfing = { (waveHeight: Int) -> Int in
	if waveHeight < 5 {
		return "Let's go!"
	} else if waveHeight < 10 {
		return "This could be tricky"
	} else if waveHeight < 20 {
		return "Only a pro could do that"
	} else {
		return "No way!"
	}
}

问题 6/12:这段代码是有效的 Swift—— 对还是错?

swift
var difficultyRating = { (trick: String) -> Int in
	if trick == "ollie" {
		return 1
	} else if trick == "Yoyo Plant" {
		return 3
	} else if trick == "900" {
		return 5
	} else {
		return 0
	}
}
print(difficultyRating("ollie"))

问题 7/12:这段代码是有效的 Swift—— 对还是错?

swift
let convertNumerals = { (numeral: String) -> String in
	switch numeral {
	case "I":
		return "1"
	case "II":
		return "2"
	case "III":
		return "3"
	}
}
print(convertNumerals("II"))

问题 8/12:这段代码是有效的 Swift—— 对还是错?

swift
var goToWork = { (hours: String) -> Bool in
	print("I'm going to work")
	for _ in 1...hours {
		print("I'm chatting to friends on Facebook.")
	}
	print("I'm going home")
	return true
}

问题 9/12:这段代码是有效的 Swift—— 对还是错?

swift
var costToShootMovie = { (location: String) -> Int in
	if location == "UK" {
		return 1_000_000
	} else if location == "US" {
		return 5_000_000
	} else {
		return 500_000
	}
}

问题 10/12:这段代码是有效的 Swift—— 对还是错?

swift
let writeEssay = { (topic: String) -> String in
	return "Here's an essay on \(topic)."
}

问题 11/12:这段代码是有效的 Swift—— 对还是错?

swift
var buyMagazine = { (name: String) -> Int in
	let amount = 10
	print("\(name) costs \(amount)")
	return amount
}
buyMagazine(name: "Wired")

问题 12/12:这段代码是有效的 Swift—— 对还是错?

swift
let bakeBirthdayCake = { (name: String) -> Int in
	print("I've made a cake for \(name); here's the bill.")
	return 50
}

9.2 如何使用尾随闭包和简写语法

作者:Paul Hudson 2021 年 10 月 9 日 已针对 Xcode 16.4 更新

Swift 有一些技巧可以减少闭包带来的语法冗余,但首先让我们回顾一下问题所在。以下是上一章结尾处的代码:

swift
let team = ["Gloria", "Suzanne", "Piper", "Tiffany", "Tasha"]

let captainFirstTeam = team.sorted(by: { (name1: String, name2: String) -> Bool in
    if name1 == "Suzanne" {
        return true
    } else if name2 == "Suzanne" {
        return false
    }

    return name1 < name2
})

print(captainFirstTeam)

如果你还记得,sorted()可以接受任何类型的函数来进行自定义排序,但有一个规则:该函数必须接受数组中的两个元素(这里是两个字符串),并在第一个字符串应该排在第二个字符串之前时返回true

需要明确的是,这个函数必须是这样的 —— 如果它不返回任何值,或者只接受一个字符串,那么 Swift 会拒绝编译我们的代码。

仔细想想:在这段代码中,我们提供给sorted()的函数必须接收两个字符串并返回一个布尔值,那我们为什么要在闭包中重复这些信息呢?

答案是:我们不需要。我们不需要指定两个参数的类型,因为它们一定是字符串;也不需要指定返回类型,因为它一定是布尔值。所以,我们可以将代码重写为:

swift
let captainFirstTeam = team.sorted(by: { name1, name2 in

这已经减少了代码中的冗余,但我们还可以更进一步:当一个函数接受另一个函数作为参数时(就像sorted()那样),Swift 允许使用一种特殊的语法,叫做尾随闭包语法。它看起来像这样:

swift
let captainFirstTeam = team.sorted { name1, name2 in
    if name1 == "Suzanne" {
        return true
    } else if name2 == "Suzanne" {
        return false
    }

    return name1 < name2
}

我们不再将闭包作为参数传递,而是直接开始编写闭包 —— 这样就可以移除开头的(by:和结尾的右括号。希望你现在能明白为什么参数列表和in要放在闭包内部了,因为如果它们在外面,看起来会更奇怪!

Swift 还有最后一种方法可以让闭包更简洁:Swift 可以使用简写语法自动为我们提供参数名。使用这种语法,我们甚至不需要再写name1, name2 in,而是依靠 Swift 为我们提供的特殊命名值:$0$1,分别代表第一个和第二个字符串。

使用这种语法,我们的代码会变得更短:

swift
let captainFirstTeam = team.sorted {
    if $0 == "Suzanne" {
        return true
    } else if $1 == "Suzanne" {
        return false
    }

    return $0 < $1
}

我把这个放在最后讲,是因为它不像其他语法那样一目了然 —— 有些人看到这种语法会反感,因为它不够清晰,这也没关系。

就我个人而言,我不会在这里使用它,因为我们每个值都使用了不止一次,但如果我们的sorted()调用更简单 —— 例如,我们只想进行反向排序 —— 那么我会使用:

swift
let reverseTeam = team.sorted {
    return $0 > $1
}

所以,in用于标记参数和返回类型的结束 —— 之后的所有内容都是闭包本身的主体。这是有原因的,你很快就会自己体会到。

这里我把比较符号从<换成了>,这样我们就得到了一个反向排序,但现在我们的代码只有一行,所以可以去掉return,让它变得更简洁:

swift
let reverseTeam = team.sorted { $0 > $1 }

关于何时使用简写语法,何时不使用,并没有固定的规则,但为了方便理解,我会在以下情况不使用简写语法:

  1. 闭包的代码很长。
  2. $0及类似的参数被多次使用。
  3. 有三个或更多参数(例如$2$3等)。

如果你仍然对闭包的强大之处存有疑虑,让我们再看两个例子。

首先,filter()函数让我们可以对数组中的每个元素运行一些代码,并返回一个新的数组,其中包含所有使该函数返回true的元素。因此,我们可以像这样找出所有名字以 T 开头的团队成员:

swift
let tOnly = team.filter { $0.hasPrefix("T") }
print(tOnly)

这将打印出["Tiffany", "Tasha"],因为这是仅有的两个名字以 T 开头的团队成员。

其次,map()函数让我们可以使用自己选择的代码转换数组中的每个元素,并返回一个包含所有转换后元素的新数组:

swift
let uppercaseTeam = team.map { $0.uppercased() }
print(uppercaseTeam)

这将打印出["GLORIA", "SUZANNE", "PIPER", "TIFFANY", "TASHA"],因为它将每个名字都转换为大写,并根据结果生成了一个新数组。

提示: 使用map()时,返回的类型不必与开始的类型相同 —— 例如,你可以将一个整数数组转换为一个字符串数组。

就像我说的,在 SwiftUI 中你会大量使用闭包:

  1. 当你在屏幕上创建数据列表时,SwiftUI 会要求你提供一个函数,该函数接受列表中的一个项目并将其转换为可以在屏幕上显示的内容。
  2. 当你创建一个按钮时,SwiftUI 会要求你提供一个函数,用于在按钮被按下时执行,以及另一个函数用于生成按钮的内容 —— 例如一张图片或一些文本等。
  3. 即使只是垂直堆叠文本片段,也是使用闭包来完成的。

是的,每次 SwiftUI 需要这些的时候,你都可以创建单独的函数,但相信我:你不会这么做的。闭包让这类代码变得非常自然,而且我相信你会惊讶于 SwiftUI 如何利用它们来生成极其简洁、清晰的代码。

【可选阅读】为什么 Swift 有尾随闭包语法?

作者:Paul Hudson 2020 年 5 月 28 日 已针对 Xcode 16.4 更新

尾随闭包语法旨在让 Swift 代码更易于阅读,不过有些人更倾向于避免使用它。

我们先从一个简单的例子开始。下面是一个函数,它接受一个 Double 类型的参数,然后是一个包含要执行的操作的闭包:

swift
func animate(duration: Double, animations: () -> Void) {
    print("开始一个时长为 \(duration) 秒的动画……")
    animations()
}

(如果你好奇的话,这是一个真实且非常常见的 UIKit 函数的简化版本!)

我们可以不使用尾随闭包来调用这个函数,像这样:

swift
animate(duration: 3, animations: {
    print("淡出图像")
})

这很常见。很多人不使用尾随闭包,这也没关系。但更多的 Swift 开发者看到末尾的 }) 时会有点不舒服 —— 这看起来不太美观。

尾随闭包可以帮我们简化代码,同时还能去掉 animations 参数标签。上面那个函数调用就变成了这样:

swift
animate(duration: 3) {
    print("淡出图像")
}

当闭包的含义与函数名直接相关时,尾随闭包的效果最好 —— 你能明白这个闭包在做什么,因为函数名叫 animate()(动画)。

如果你不确定是否应该使用尾随闭包,我的建议是先到处都用用看。用一两个月后,你就有足够的使用经验来回头做更清晰的判断了,但希望你能习惯它们,因为它们在 Swift 中真的很常见!

【可选阅读】什么时候应该使用简写参数名?

作者:Paul Hudson 2021 年 3 月 11 日 已针对 Xcode 16.4 更新

在处理闭包时,Swift 给我们提供了一种特殊的简写参数语法,能让闭包的编写极其简洁。这种语法会自动将参数名编号为 $0$1$2 等等 —— 我们不能在自己的代码中使用这样的名称,所以当你看到它们时,就能立刻明白这是闭包的简写语法。

至于什么时候应该使用它们,这真的得 “视情况而定”:

  1. 参数多吗?如果参数很多,简写语法就不再有用了,实际上还会适得其反 —— 你需要用来和 $0 比较的是 $3 还是 $4 呢?给它们起个实际的名字,含义就会更清晰。
  2. 这个函数常用吗?随着你的 Swift 技能不断提升,你会发现有那么几个 —— 可能 10 个左右 —— 极其常用的函数会用到闭包,所以阅读你代码的人会很容易理解 $0 是什么意思。
  3. 简写名称在你的方法中使用了好几次吗?如果需要引用 $0 超过两三次,或许你应该给它起一个真正的名字。

重要的是你的代码要易于阅读和理解。有时候这意味着要写得简短简洁,但并非总是如此 —— 要根据具体情况来选择是否使用简写语法。

【练习题】简写参数名

问题 1/6:当将闭包作为函数参数传递时,以下哪些陈述是正确的?

  • 选项 1: 简写参数写成 #0#1 等等。
  • 选项 2: 简写参数写成 $0$1 等等。

问题 2/6:当将闭包作为函数参数传递时,以下哪些陈述是正确的?

  • 选项 1: 如果只有一行代码,可以省略 return
  • 选项 2: 简写语法只能用于 Swift 的内置函数,比如 print()

问题 3/6:当将闭包作为函数参数传递时,以下哪些陈述是正确的?

  • 选项 1: Swift 会自动提供 $ 变量;我们不需要自己定义。
  • 选项 2: 简写参数不能用于字符串插值。

问题 4/6:当将闭包作为函数参数传递时,以下哪些陈述是正确的?

  • 选项 1: 使用简写参数时,不需要列出要接受的参数。
  • 选项 2: 使用简写语法时,必须列出要接受的参数。

问题 5/6:当将闭包作为函数参数传递时,以下哪些陈述是正确的?

  • 选项 1: Swift 可以推断返回类型。
  • 选项 2: 简写语法会自动提供最多三个参数。

问题 6/6:当将闭包作为函数参数传递时,以下哪些陈述是正确的?

  • 选项 1: Swift 可以推断闭包接受的参数。
  • 选项 2: 简写语法只能用于整数。

9.3 如何接受函数作为参数

作者:Paul Hudson 2021 年 11 月 8 日 已针对 Xcode 16.4 更新

我想探讨的最后一个与闭包相关的话题是,如何编写能够接受其他函数作为参数的函数。这对于闭包来说尤为重要,因为涉及到尾随闭包语法,但无论如何,这都是一项有用的技能。

之前我们看过这样一段代码:

swift
func greetUser() {
    print("Hi there!")
}

greetUser()

var greetCopy: () -> Void = greetUser
greetCopy()

我特意在里面添加了类型注解,因为这正是我们在指定函数作为参数时要用到的:我们告诉 Swift,该函数接受哪些参数以及它的返回类型。

再一次提醒你:这种语法一开始可能会让人觉得有点难以理解!下面是一个函数,它通过重复调用另一个函数若干次来生成一个整数数组:

swift
func makeArray(size: Int, using generator: () -> Int) -> [Int] {
    var numbers = [Int]()

    for _ in 0..<size {
        let newNumber = generator()
        numbers.append(newNumber)
    }

    return numbers
}

我们来逐步分析一下:

  1. 这个函数名为makeArray()。它有两个参数,其中一个是我们想要的整数数量,并且返回一个整数数组。
  2. 第二个参数是一个函数。这个函数本身不接受任何参数,但每次调用时会返回一个整数。
  3. makeArray()内部,我们创建一个新的空整数数组,然后按照要求的次数进行循环。
  4. 每次循环时,我们调用作为参数传入的generator函数。它会返回一个新的整数,我们将这个整数放入numbers数组中。
  5. 最后返回完成的数组。

makeArray()的函数体大部分都很简单:重复调用一个函数来生成整数,将每个值添加到数组中,然后返回整个数组。

复杂的部分是第一行:

swift
func makeArray(size: Int, using generator: () -> Int) -> [Int] {

这里有两组括号和两组返回类型,所以一开始可能会让人觉得混乱。如果你把它拆分来看,应该就能线性地理解了:

  1. 我们正在创建一个新函数。
  2. 这个函数名为makeArray()
  3. 第一个参数是一个名为size的整数。
  4. 第二个参数是一个名为generator的函数,它本身不接受任何参数,返回一个整数。
  5. 整个makeArray()函数返回一个整数数组。

这样一来,我们就可以生成任意大小的整数数组,只需传入一个用于生成每个数字的函数:

swift
let rolls = makeArray(size: 50) {
    Int.random(in: 1...20)
}

print(rolls)

记住,这种功能也适用于专门定义的函数,所以我们可以这样写:

swift
func generateNumber() -> Int {
    Int.random(in: 1...20)
}

let newRolls = makeArray(size: 50, using: generateNumber)
print(newRolls)

这会调用generateNumber()50 次来填充数组。

在你学习 Swift 和 SwiftUI 的过程中,需要知道如何接受函数作为参数的情况并不多,但至少现在你对它的工作原理和重要性有了一些了解。

在我们继续之前,还有最后一点:你可以让你的函数接受多个函数参数,在这种情况下,你可以指定多个尾随闭包。这种语法在 SwiftUI 中非常常见,所以至少有必要让你了解一下。

为了演示这一点,下面是一个接受三个函数参数的函数,每个参数都不接受任何参数且不返回任何值:

swift
func doImportantWork(first: () -> Void, second: () -> Void, third: () -> Void) {
    print("即将开始第一项工作")
    first()
    print("即将开始第二项工作")
    second()
    print("即将开始第三项工作")
    third()
    print("完成!")
}

我在里面添加了额外的print()调用,以模拟在调用firstsecondthird之间进行的特定工作。

调用这个函数时,第一个尾随闭包和我们已经使用过的一样,但第二个和第三个的格式有所不同:你要结束前一个闭包的花括号,然后写上外部参数名和一个冒号,再开始另一个花括号。

看起来是这样的:

swift
doImportantWork {
    print("这是第一项工作")
} second: {
    print("这是第二项工作")
} third: {
    print("这是第三项工作")
}

三个尾随闭包的情况并不像你想象的那么少见。例如,在 SwiftUI 中创建一个内容部分时,就会用到三个尾随闭包:一个用于内容本身,一个用于放在上方的标题,一个用于放在下方的页脚。

【可选阅读】为什么要将闭包用作参数?

作者:Paul Hudson 2021 年 11 月 8 日 已针对 Xcode 16.4 更新

Swift 的闭包可以像其他任何数据类型一样使用,这意味着你可以将它们传入函数、复制它们等等。但当你刚开始学习时,可能会觉得这有点 “能做但没必要”—— 很难看出它的好处。

我能给出的最好例子之一是 Siri 与应用程序的集成方式。Siri 是一项可以在 iOS 设备上任何地方运行的系统服务,但它能够与应用程序进行通信 —— 你可以用 Lyft 预订车程,可以用 Carrot Weather 查看天气等等。在幕后,Siri 会在后台启动应用程序的一小部分来传递我们的语音请求,然后将应用程序的响应显示在 Siri 的用户界面中。

现在想想:如果我的应用程序表现不佳,需要 10 秒才能响应 Siri,会发生什么?记住,用户实际上看不到我的应用程序,只能看到 Siri,所以在他们看来,Siri 好像完全卡住了。

这会是非常糟糕的用户体验,所以苹果采用了闭包:它在后台启动我们的应用程序,并传入一个闭包,我们完成工作后就可以调用这个闭包。然后,我们的应用程序可以花任意长的时间来弄清楚需要做什么工作,完成后调用这个闭包将数据发送回 Siri。使用闭包来返回数据,而不是从函数中返回值,这意味着 Siri 不需要等待函数完成,因此它可以保持用户界面的交互性 —— 不会冻结。

另一个常见的例子是发起网络请求。普通的 iPhone 每秒可以做几十亿件事情,但连接到日本的服务器需要半秒甚至更长时间 —— 与设备上事情发生的速度相比,这几乎是极其缓慢的。所以,当我们从互联网请求数据时,我们会使用闭包:“请获取这个数据,完成后运行这个闭包。” 同样,这意味着我们不会在进行一些缓慢的工作时,强制用户界面冻结。

【练习题】闭包作为参数

问题 1/12:这段代码是有效的 Swift 代码吗?是或否?

swift
var swift {
	print("Cool - I can use closures!")
}
func writeCode(using language: () -> Void) {
	language()
	print("That'll be eleventy billion dollars, please.")
}

问题 2/12:这段代码是有效的 Swift 代码吗?是或否?

swift
var playWithDog = {
	print("Fetch!")
}
func play(using playType: () -> Void) -> String {
	print("Let's play a game")
	playType()
}
play(using: playWithDog)

问题 3/12:这段代码是有效的 Swift 代码吗?是或否?

swift
var makeFromStraw = {
	print("Let's build it out of straw")
}
func buildHouse(using buildingStyle: () -> Void) {
	buildingStyle
	print("It's ready - can anyone blow it down?")
}

问题 4/12:这段代码是有效的 Swift 代码吗?是或否?

swift
let awesomeTalk = {
	print("Here's a great talk!")
}
func deliverTalk(name: String, type: () -> Void) {
	print("My talk is called \(name)")
	type()
}
deliverTalk(name: "My Awesome Talk", type: awesomeTalk)

问题 5/12:这段代码是有效的 Swift 代码吗?是或否?

swift
let swanDive = {
	print("SWAN DIVE!")
}
func performDive(type dive: () -> Void) {
	print("I'm climbing up to the top")
	dive()
}
performDive(type: swanDive)

问题 6/12:这段代码是有效的 Swift 代码吗?是或否?

swift
let helicopterTravel = {
	print("Get to the chopper!")
}
func travel(by travelMeans: () -> Void) {
	print("Let's go on vacation...")
	travelMeans()
}
travel(by: helicopterTravel)

问题 7/12:这段代码是有效的 Swift 代码吗?是或否?

swift
let evilRobot = {
	print("EXTERMINATE")
}
func buildRobot(personality: () -> Void) {
	print("Time to turn on the robot!")
	personality()
}
buildRobot(using: evilRobot)

问题 8/12:这段代码是有效的 Swift 代码吗?是或否?

swift
var goOnBike = {
	print("I'll take my bicycle.")
}
func race(using vehicleType: () -> Void) {
	print("Let's race!")
	vehicleType()
}

问题 9/12:这段代码是有效的 Swift 代码吗?是或否?

swift
var payCash = {
	print("Here's the money.")
}
func buyClothes(item: String, using payment: () -> Void) {
	print("I'll take this \(item).")
	payment()
}
buyClothes(item: "jacket", using: payCash)

问题 10/12:这段代码是有效的 Swift 代码吗?是或否?

swift
let resignation = { (name: String) in
	print("Dear \(name), I'm outta here!")
}
func printDocument(contents: () -> Void) {
	print("Connecting to printer...")
	print("Sending document...")
	contents()
}
printDocument(contents: resignation)

问题 11/12:这段代码是有效的 Swift 代码吗?是或否?

swift
let driveSafely = {
	return "I'm being a considerate driver"
}
func drive(using driving: () -> Void) {
	print("Let's get in the car")
	driving()
	print("We're there!")
}
drive(using: driveSafely)

问题 12/12:这段代码是有效的 Swift 代码吗?是或否?

swift
var learnWithUnwrap = {
	print("Hey, this is fun!")
}
func learnSwift(using approach: () -> Void) {
	print("I'm learning Swift")
	approach()
}
learnSwift(using: learnWithUnwrap)

【练习题】尾随闭包语法

问题 1/12:这段代码是有效的 Swift 代码吗?是或否?

swift
func holdClass(name: String, lesson: () -> Void) {
	print("Welcome to \(name)!")
	lesson()
	print("Make sure your homework is done by next week.")
}
holdClass("Philosophy 101", lesson:) {
	print("All we are is dust in the wind, dude.")
}

问题 2/12:这段代码是有效的 Swift 代码吗?是或否?

swift
func phoneFriend(conversation: () -> Void) {
	print("Calling 555-1234...")
	conversation()
}
phoneFriend:
	print("Hello!")
	print("A foreign prince wants to give you $5 million.")
	print("What are your bank details?")

问题 3/12:这段代码是有效的 Swift 代码吗?是或否?

swift
func doTricks(_ tricks: () -> Void) {
	print("Start recording now!")
	tricks()
	print("Did you get all that?")
}

问题 4/12:这段代码是有效的 Swift 代码吗?是或否?

swift
func tendGarden(activities: () -> Void) {
	print("I love gardening")
	activities()
}
tendGarden {
	print("Let's grow some roses!")
}

问题 5/12:这段代码是有效的 Swift 代码吗?是或否?

swift
func makeCake(instructions: () -> Void) {
	print("Wash hands")
	print("Collect ingredients")
	instructions()
	print("Here's your cake!")
}
makeCake {
	print("Mix egg and flour")
}

问题 6/12:这段代码是有效的 Swift 代码吗?是或否?

swift
func brewTea(steps: ()) {
	print("Get tea")
	print("Get milk")
	print("Get sugar")
	steps()
}
brewTea {
	print("Brew tea in teapot.")
	print("Add milk to cup")
	print("Pour tea into cup")
	print("Add sugar to taste.")
}

问题 7/12:这段代码是有效的 Swift 代码吗?是或否?

swift
func assembleToy(instruction: () -> Void) {
	instructions()
	print("It's done!")
}
assembleToy {
	print("Grok the glib")
	print("Flop the flip")
	print("Click the clack")
}

问题 8/12:这段代码是有效的 Swift 代码吗?是或否?

swift
func knitSweater(then: () -> Void) {
	print("Buy wool")
	for _ in 1...100 {
		print("Knit knit knit...")
	}
	action()
}
knitSweater {
	print("Who wants to buy a sweater?")
}

问题 9/12:这段代码是有效的 Swift 代码吗?是或否?

swift
func repeatAction(count: Int, action: () -> Void) {
	for _ in 0..<count {
		action()
	}
}
repeatAction(count: 5) {
	print("Hello, world!")
}

问题 10/12:这段代码是有效的 Swift 代码吗?是或否?

swift
func clean(tasks: () -> Void) {
	print("It's time to clean the house.")
	tasks()
}
clean [
	print("I'm going to clean the kitchen.")
	print("I'm going to tidy the study.")
	print("I'm going to nuke the kids' room.")
]

问题 11/12:这段代码是有效的 Swift 代码吗?是或否?

swift
func goCamping(then action: () -> Void) {
	print("We're going camping!")
	action()
}
goCamping {
	print("Sing songs")
	print("Put up tent")
	print("Attempt to sleep")
}

问题 12/12:这段代码是有效的 Swift 代码吗?是或否?

swift
func goOnVacation(to destination: String, _ activities: () -> Void) {
	print("Packing bags...")
	print("Getting on plane to \(destination)...")
	activities()
	print("Time to go home!")
}
goOnVacation(to: "Mexico") {
	print("Go sightseeing")
	print("Relax in sun")
	print("Go hiking")
}

9.4 总结:闭包

作者:Paul Hudson 2021 年 10 月 9 日 已更新至 Xcode 16.4

在前几章中,我们已经介绍了很多关于闭包的内容,现在来回顾一下:

  • 在 Swift 中,你可以复制函数,复制后的函数与原函数功能相同,只是会丢失外部参数名。
  • 所有函数都有类型,就像其他数据类型一样。这包括它们接收的参数以及返回类型,返回类型可能是 Void(也称为 “无”)。
  • 你可以通过赋值给常量或变量直接创建闭包。
  • 接收参数或有返回值的闭包必须在其大括号内声明这些内容,后面跟关键字 in
  • 函数能够接收其他函数作为参数。它们必须预先声明这些函数必须使用的数据类型,Swift 会确保遵循这些规则。
  • 在这种情况下,除了传递一个专门的函数,你也可以传递一个闭包 —— 你可以直接创建一个闭包。Swift 允许这两种方式都能工作。
  • 当将闭包作为函数参数传递时,如果 Swift 能够自动推断出闭包内部的类型,你不需要显式写出这些类型。返回值也是如此 —— 如果 Swift 能推断出来,你就不需要指定。
  • 如果一个函数的一个或多个最后参数是函数,你可以使用尾随闭包语法。
  • 你也可以使用简写参数名,如 $0$1,但我建议只在某些情况下使用。
  • 你可以创建自己的接收函数作为参数的函数,不过实际上,知道如何使用它们比知道如何创建它们重要得多。

在 Swift 语言的各个部分中,我认为闭包是最难学习的一部分。不仅其语法一开始看起来有点刺眼,而且将一个函数传递到另一个函数中的概念也需要一点时间来理解。

所以,如果你读完这些章节后感觉脑袋快要爆炸了,那很好 —— 这意味着你已经对闭包有了一半的理解!

9.5 检查点 5

作者:Paul Hudson 2021 年 10 月 9 日 已更新至 Xcode 16.4

掌握了闭包之后,是时候尝试一个使用闭包的小编码挑战了。

你已经接触过 sorted()filter()map(),我希望你把它们串联起来使用 —— 调用一个,再调用另一个,然后再调用另一个,不使用临时变量。

你的输入是这样的:

swift
let luckyNumbers = [7, 4, 38, 21, 16, 15, 12, 33, 31, 49]

你的任务是:

  1. 过滤掉所有偶数
  2. 将数组按升序排序
  3. 把它们映射成格式为 “7 is a lucky number” 的字符串
  4. 打印结果数组,每行一个元素

所以,你的输出应该如下:

swift
7 is a lucky number
15 is a lucky number
21 is a lucky number
31 is a lucky number
33 is a lucky number
49 is a lucky number

如果你需要提示,下面有一些,但说实话,你应该能够凭记忆或参考本书最近的章节来解决这个问题。

还在看吗?好吧,这里有一些提示:

  1. 你需要使用 filter()sorted()map() 函数。
  2. 函数的运行顺序很重要 —— 如果你先把数组转换成字符串,sorted() 会进行字符串排序而不是整数排序。这意味着 15 会排在 7 前面,因为 Swift 会比较 “15” 中的 “1” 和 “7”。
  3. 要串联这些函数,可以使用 luckyNumbers.first { }.second { },显然要在里面放入真正的函数调用。
  4. 你应该使用 isMultiple(of:) 来移除偶数。

今天,又一个 Swift 的关键知识点被你掌握了 —— 干得好!现在做件明智的事,把你的进展发布到网上:这会迫使你用自己的话来描述这些内容,也会鼓励你继续学习明天的内容。

本站使用 VitePress 制作