今天我们一起来学习闭包(Closure)。对于初学Swift的人来说,闭包可能是第一道坎,它难以理解但极其有用。即使一开始觉得困难,也值得花时间去掌握,因为它遍布Swift和SwiftUI的各个角落。
闭包可以被看作匿名的函数块。你可以创建它并直接赋值给变量,或者传递给其他函数以自定义其行为。没错,你可以将一个函数作为参数传递给另一个函数。
掌握闭包确实需要一些努力。它的语法起初可能令人望而生畏,但请记住,几乎所有Swift开发者都经历过这个阶段。如果你在看代码时感到不确定,回去再看一遍,慢慢理解。这就像骑自行车上山,一旦到达山顶,一切都会变得轻松。
如何创建和使用闭包
函数在Swift中是一等公民。你不仅可以调用它们,还可以将它们赋值给变量、作为参数传递,甚至从函数中返回函数。
例如,先定义一个函数,然后复制它:
func greetUser() {
print(“Hi there!”)
}
greetUser()
var greetCopy = greetUser
greetCopy()
这段代码会打印两次“Hi there!”。注意,复制函数时不要加括号(),否则你将调用函数并存储其返回值,而不是函数本身。
更直接的方法是使用闭包表达式创建一个匿名函数块:
let sayHello = {
print(“Hi there!”)
}
sayHello()
这个闭包没有参数,也不返回值。如果你想让它接受参数和返回值,语法略有不同:
let sayHello = { (name: String) -> String in
“Hi \(name)!”
}
这里出现了in关键字,它用来分隔闭包的参数/返回类型定义和其主体代码。因为闭包的参数和返回值必须写在大括号{}内部,in起到了标记分界的作用。
你可能会问:为什么需要闭包?它们看起来复杂且晦涩。然而,闭包在Swift中无处不在,尤其是在SwiftUI中。理解闭包是编写现代Swift代码的关键。
要深入理解闭包,需要先了解函数类型。就像整数有Int类型,函数也有自己的类型。考虑之前的greetUser函数,它不接受参数,不返回值。它的类型是() -> Void。
()表示没有参数。->指向返回类型。Void表示“无”,即不返回任何内容。有时也写作(),但为避免混淆,通常使用Void。
每个函数的类型都由其接收和返回的数据类型决定。一个重要的细节是:函数的外部参数名不属于其类型的一部分。通过以下代码可以验证:
func getUserData(for id: Int) -> String {
if id == 1989 {
return “Taylor Swift”
} else {
return “Anonymous”
}
}
let data: (Int) -> String = getUserData
let user = data(1989) // 注意,这里调用时没有使用外部参数名‘for:‘
print(user)
当我们复制getUserData函数时,复本data的类型是(Int) -> String,调用时只需传入Int,而不需要原来的外部参数名for:。这个规则同样适用于闭包的调用。
闭包的实用性在哪里?一个经典例子是数组的sorted()方法。默认情况下,它可以排序:
let team = [“Gloria”, “Suzanne”, “Piper”, “Tiffany”, “Tasha”]
let sortedTeam = team.sorted()
print(sortedTeam)
但如果你想自定义排序规则呢?比如,让队长“Suzanne”始终排在最前面,其他人按字母顺序排列。sorted(by:)方法允许你传入一个自定义排序函数。这个函数必须接受两个相同类型的元素(这里是String),并返回一个Bool,指示第一个元素是否应该排在第二个元素之前。
我们可以先定义一个这样的函数:
func captainFirstSorted(name1: String, name2: String) -> Bool {
if name1 == “Suzanne” {
return true
} else if name2 == “Suzanne” {
return false
}
return name1 < name2
}
然后将其传递给sorted(by:):
let captainFirstTeam = team.sorted(by: captainFirstSorted)
print(captainFirstTeam) // 输出: [“Suzanne”, “Gloria”, “Piper”, “Tasha”, “Tiffany”]
闭包的强大之处在于,你无需预先定义命名函数。你可以直接将一个闭包(匿名函数)传递给sorted(by:):
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
})
虽然代码量看起来不少,但它的好处是:你可以直接将定制逻辑内联写入,而无需为了单一用途专门创建一个函数。接下来,我们将看到如何让这段代码变得更简洁。
为什么Swift那么喜欢使用闭包?
闭包是Swift最强大也最具挑战的特性之一。Swift(尤其是SwiftUI)广泛使用闭包的一个核心原因是其存储和延迟执行的能力。你可以说“在将来某个时刻执行这段代码”,例如:
- 延迟一段时间后执行。
- 动画完成后执行。
- 网络下载完成后执行。
- 用户选择菜单项后执行。
闭包让我们能将功能“打包”进一个变量,存储起来,并在需要时执行或传递。
为什么Swift的闭包参数在大括号内?
闭包和普通函数声明参数的位置不同。普通函数参数在括号内:
func pay(user: String, amount: Int) { }
而闭包将参数列表放在大括号内:
let payment = { (user: String, amount: Int) in
// 代码
}
这样做主要是为了避免语法歧义。如果写成let payment = (user: String, amount: Int),看起来就像在创建一个元组。将参数放在大括号内,配合in关键字,清晰地定义了闭包这个完整的、可存储的代码块的结构。
如何从不带参数的闭包返回值?
声明闭包的返回类型时,如果闭包没有参数,必须使用空括号()。
- 无参数无返回值:
let payment = { () -> Void in print(“Paying an anonymous person…”) } - 无参数但有返回值:
let payment = { () -> Bool in print(“Paying an anonymous person…”) return true }
这与声明函数func payment() -> Bool的逻辑是一致的。
如何使用尾部闭包和速记语法
为了使闭包代码更简洁,Swift提供了多种语法糖。回顾之前的复杂闭包调用:
let captainFirstTeam = team.sorted(by: { (name1: String, name2: String) -> Bool in
// … 排序逻辑
})
第一步:类型推断
由于sorted(by:)明确要求一个(String, String) -> Bool类型的函数,我们可以省略闭包内的参数类型和返回类型声明:
let captainFirstTeam = team.sorted(by: { name1, name2 in
// … 排序逻辑
})
第二步:尾部闭包语法
如果函数的最后一个参数是闭包,你可以使用尾部闭包语法。将闭包写在函数调用的小括号()之后。
let captainFirstTeam = team.sorted { name1, name2 in
if name1 == “Suzanne” {
return true
} else if name2 == “Suzanne” {
return false
}
return name1 < name2
}
这移除了by:标签和参数列表外的括号,代码看起来更清爽。
第三步:速记参数名
Swift为闭包提供了自动生成的速记参数名:$0, $1, $2……分别代表第一个、第二个、第三个参数。使用它们可以进一步简化:
let captainFirstTeam = team.sorted {
if $0 == “Suzanne” {
return true
} else if $1 == “Suzanne” {
return false
}
return $0 < $1
}
当逻辑非常简单时,比如只是反转排序,代码可以精简到极致:
let reverseTeam = team.sorted { $0 > $1 }
// 甚至可以省略‘return‘,因为只有一行表达式
let reverseTeam = team.sorted { $0 > $1 }
何时使用速记语法? 没有硬性规定,但个人建议在以下情况避免使用:
- 闭包逻辑较长、较复杂。
- 速记参数名(如
$0)被多次重复使用。 - 参数超过两个(
$2,$3),可读性会下降。
闭包的简洁语法使其在处理集合时非常强大,例如:
filter: 筛选数组中满足条件的元素。let tOnly = team.filter { $0.hasPrefix(“T”) } // [“Tiffany”, “Tasha”]map: 将数组中的每个元素转换成另一种形式。let uppercaseTeam = team.map { $0.uppercased() } // [“GLORIA”, “SUZANNE”, …]
在SwiftUI中,闭包更是无处不在,用于定义视图内容、处理按钮动作、配置列表等。
Swift为什么使用尾部闭包语法?
尾部闭包语法旨在提升代码的可读性,特别是在闭包体较长时。对比以下两种写法:
// 传统写法
animate(duration: 3, animations: {
print(“Fade out the image”)
})
// 尾部闭包写法
animate(duration: 3) {
print(“Fade out the image”)
}
尾部闭包写法消除了结尾的}),使代码结构更清晰。当函数名(如animate)能清晰表达闭包所执行的操作时,这种语法显得非常自然。建议初学者积极尝试使用尾部闭包,以熟悉这种Swift中常见的编码风格。
如何接受函数作为参数
最后,我们来探讨如何编写接受函数作为参数的函数。这在创建高阶函数时非常有用。
首先,回忆函数类型注解:
var greetCopy: () -> Void = greetUser
编写一个生成数组的函数,它接受一个“生成器”函数作为参数:
func makeArray(size: Int, using generator: () -> Int) -> [Int] {
var numbers = [Int]()
for _ in 0..<size {
let newNumber = generator() // 调用传入的函数
numbers.append(newNumber)
}
return numbers
}
这个函数做了两件事:
- 它名为
makeArray,接受一个Int类型的size参数和一个() -> Int类型的generator参数,返回一个[Int]。 - 在函数体内,它循环
size次,每次调用generator函数来获取一个整数,并将其添加到数组中。
调用这个函数时,你可以传入一个闭包:
let rolls = makeArray(size: 50) {
Int.random(in: 1…20)
}
也可以传入一个预定义的函数:
func generateNumber() -> Int {
Int.random(in: 1…20)
}
let newRolls = makeArray(size: 50, using: generateNumber)
多个尾部闭包
一个函数可以接受多个函数参数。在调用时,第一个闭包使用标准的尾部闭包语法,后续的闭包则需要带上参数标签。
func doImportantWork(first: () -> Void, second: () -> Void, third: () -> Void) {
print(“About to start first work”)
first()
print(“About to start second work”)
second()
print(“About to start third work”)
third()
print(“Done!”)
}
// 调用
doImportantWork {
print(“This is the first work”)
} second: {
print(“This is the second work”)
} third: {
print(“This is the third work”)
}
这种多尾部闭包的语法在SwiftUI中很常见,例如用于同时提供视图的主体、页眉和页脚。
为什么要使用闭包作为参数?
使用闭包作为参数的核心优势在于异步执行和非阻塞。它允许你将“将来要做什么”这个任务打包,交给另一个系统或函数去安排执行,而当前代码可以继续运行,不被阻塞。
- 系统集成(如Siri):当Siri需要你的应用处理一个请求时,它会在后台启动你的应用并传入一个闭包。你的应用可以在完成所有耗时工作(如网络请求、数据处理)后,调用这个闭包来回传结果。这样Siri的界面在整个过程中都能保持响应,不会因为等待你的应用而冻结。
- 网络请求:从网络获取数据是典型的耗时操作。通过使用闭包回调,你可以发起请求后立即返回,让UI保持交互性。当网络数据返回时,再执行闭包中的代码来处理数据、更新界面。
闭包使得这种“发起请求 -> 立即返回 -> 未来处理结果”的模式变得非常优雅和易于管理。
总结:闭包
让我们回顾一下关于闭包的核心要点:
- 函数副本:你可以复制函数,但复制体会丢失原始函数的外部参数名。
- 函数类型:所有函数都有类型,格式为
(参数类型) -> 返回类型。Void表示无返回值。 - 创建闭包:可以直接将闭包表达式赋值给常量或变量。
- 闭包语法:带参数和返回值的闭包,其声明必须放在大括号内,并用
in关键字与主体分隔。 - 函数参数:函数可以接受其他函数作为参数。调用时,你可以传递一个命名函数,也可以直接内联一个闭包。
- 类型推断:在闭包上下文中,Swift通常可以推断参数和返回值的类型,允许你省略它们的显式声明。
- 尾部闭包:当函数的最后一个参数是闭包时,可以使用尾部闭包语法,让代码更清晰。
- 速记参数:可以使用
$0、$1等速记参数名来指代闭包的参数,但应酌情使用以保证代码可读性。 - 编写高阶函数:你可以创建自己的、接受函数作为参数的函数。
闭包无疑是Swift学习曲线中最陡峭的部分之一。它的语法和概念需要时间去适应。如果你在学完本章后感到有些头晕,那是完全正常的——这意味着你已经踏上了掌握Swift核心特性的正确道路。