今天我们一起来学习闭包(Closure),闭包对于初学Swift的人来说是难以理解的第一件事,但是如果不去尝试我们可能永远也不会理解它的具体用法。所以无论如何,我们都要尝试去使用它,毕竟它很有用。

闭包有点像匿名的函数——我们可以创建函数并直接将其赋值给变量,或者将其传递给其他函数以自定义其工作方式。是的,你没看错:将一个函数作为参数传递到另一个函数中。

闭包真的很难。我这么说并不是想让你放弃,只是想让你提前知道,如果你觉得闭包难理解或难记忆,没关系,我们都经历过!

有时,闭包的语法会让你的眼睛有点吃不消,这一点在你学习今天的课程时会非常明显。如果你觉得有点吃力——如果你盯着一些代码却不能百分百确定它的意思—只需返回去再看一遍,给你的记忆一点提示。

SwiftUI 广泛使用闭包,因此值得花时间了解一下。是的,闭包可能是 Swift 中最复杂的功能,但这有点像骑自行车上山,一旦你到达山顶,一旦你掌握了闭包,一切都会变得容易得多。

现在就让我们开始吧!

如何创建和使用闭包

函数在 Swift 中非常强大。是的,您已经看到了如何调用函数、传入值和返回数据,但您还可以将函数赋值给变量、将函数传入函数,甚至从函数返回函数。

例如:

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

greetUser()

var greetCopy = greetUser
greetCopy()

它创建了一个很简单的函数并调用了它,但随后又创建了该函数的副本并调用了副本。因此,它会打印两次相同的信息。

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

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

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

sayHello()

Swift 给它起了一个高大上的名字——闭包表达式(closure expression),这是一种花哨的说法,表示我们刚刚创建了一个闭包——我们可以随时传递和调用的代码块。这个闭包没有名字,但它实际上是一个不带参数、不返回值的函数。

如果你想让闭包接受参数,就需要用一种特殊的方式来编写。你看,闭包以大括号开始和结束,这意味着我们不能在大括号外写代码来控制参数或返回值。因此,Swift 有一个巧妙的变通方法:我们可以将相同的信息放在大括号内,就像这样:

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

我在这里多加了一个关键字,你发现了吗?它是 in 关键字,直接出现在闭包的参数和返回类型之后。同样,如果是普通函数,参数和返回类型会出现在大括号外,但闭包不能这样做。因此,in 用于标记参数和返回类型的结束——之后的所有内容都是闭包的主体。这样做是有原因的,你很快就会明白。

与此同时,你可能会有一个更基本的问题: “我为什么需要这些东西?” 我知道,闭包看起来确实非常晦涩难懂。更糟糕的是,它们看起来既晦涩又复杂——很多很多人在初次接触闭包时都会感到非常吃力,因为它们是复杂的怪兽,看起来好像永远都用不上。

然而,正如你将看到的那样,闭包在 Swift 中被广泛使用,几乎遍布 SwiftUI 的每一个角落。说真的,在你编写的每个 SwiftUI 应用程序中都会用到它们,有时甚至会用到上百次——也许不一定是你上面看到的这种形式,但你会经常用到它。

要了解闭包为何如此有用,我首先要向你介绍函数类型。我们已经看到整数有 Int 类型,小数有 Double 类型等,现在我想让你思考函数也有类型。

以我们之前写的 greetUser() 函数为例:它不接受任何参数,不返回任何值,也不会出错。如果我们把它写成 greetCopy 的类型注解,我们会这样写:

var greetCopy: () -> Void = greetUser

让我们来分析一下…

  1. 空括号表示函数不带参数。
  2. 箭头的含义与创建函数时的含义相同:我们即将声明函数的返回类型。
  3. 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)
print(user)

一开始很简单:这是一个接受整数并返回字符串的函数。但当我们复制这个函数时,函数的类型并不包括 for 外部参数的名称,因此在调用复制的函数时,我们使用的是 data(1989) 而不是 data(for:1989)。

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

sayHello("Taylor")

这就像我们复制函数一样,不使用参数名。所以,还是那句话:外部参数名只有在我们直接调用函数时才重要,而不是在我们创建闭包或首先拷贝函数时。

你可能还在想为什么这些都重要,这一切都将变得清晰起来。还记得我说过我们可以在数组中使用 sorted() 来对元素进行排序吗?

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

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

这确实很棒,但如果我们想控制排序呢?如果我们总是想让一个人排在第一位,因为他是队长,而其他人则按字母顺序排序呢?

实际上,sorted() 允许我们传递一个自定义排序函数来控制排序。这个函数必须接受两个字符串,如果第一个字符串排序在第二个字符串之前,则返回 true;如果第一个字符串排序在第二个字符串之后,则返回 false。

如果Suzanne是队长,函数将如下所示:

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() 所做的,所以我们可以直接使用它:

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

运行后,它将打印出[“Suzanne”、”Gloria”、”Piper”、”Tasha”、”Tiffany”],完全符合我们的要求。

现在,我们已经完成了两个看似完全不同的任务。首先,我们可以将闭包创建为匿名函数,并将其存储在常量和变量中:

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

sayHello()

我们还可以将函数传递给其他函数,就像我们将 captainFirstSorted() 传递给 sorted() 一样:

let captainFirstTeam = team.sorted(by: captainFirstSorted)

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

因此,我们可以再次调用 sorted(),但不是传入 captainFirstTeam() 函数,而是启动一个新的闭包:写一个开放括号,列出它的参数和返回类型,写入,然后写入我们的标准函数代码。

一开始你会觉得很费劲。这并不是因为你不够聪明,不懂闭包,或者不适合 Swift 编程,而是闭包真的很难。别担心,我们将研究如何让它变得更简单!

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

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
})

这一下子就包含了一大段语法,我想再说一遍,这会变得更容易——在下一章中,我们将研究如何减少代码量,以便更容易看清发生了什么。

但首先,我想分析一下这里发生了什么:

  1. 我们像以前一样调用 sorted() 函数。
  2. 我们传递的不是一个函数,而是一个闭包——从 by: 后面的开头括号到最后一行的结尾括号都是闭包的一部分。
  3. 在闭包内部,我们列出了 sorted() 将传递给我们的两个参数,即两个字符串。我们还指出闭包将返回一个布尔值,然后用 in 标记闭包代码的开始。
  4. 其他都是正常的函数代码。
    再次强调,这里有很多语法,如果你感到头疼,没人会责怪你,但我希望你至少能看到闭包的一点好处:像 sorted() 这样的函数可以让我们传递自定义代码来调整它们的工作方式,而且是直接传递——我们不需要为了这一种用法而编写一个新函数。

现在你了解了什么是闭包,让我们看看能否让它们更容易阅读…

什么是闭包?为什么Swift那么喜欢使用闭包?

我相信你一定问过自己这个问题。如果你没有,我会很惊讶,因为闭包是 Swift 最强大的功能之一,但也很容易让人感到困惑。

所以,如果你坐在这里想 “哇,闭包真难!”,请不要沮丧——它们确实很难,你觉得它们难,说明你的大脑在正确地运行。

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

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

闭包让我们可以将某些功能封装在一个变量中,然后存储在某个地方。我们也可以从函数中返回,然后将闭包存储在其他地方。

在学习过程中,闭包有点难读,尤其是当它们接受和/或返回自己的参数时。不过没关系:小步前进,遇到困难时再回头看,这样就不会有问题了。

为什么 Swift 的闭包参数在大括号内?

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

func pay(user: String, amount: Int) {
    // code
}

这里写的是一模一样的闭包:

let payment = { (user: String, amount: Int) in
    // code
}

正如你所看到的,参数被移到了大括号内,而 in 关键字则用来标记参数列表的结束和闭包主体的开始。

闭包将参数放在括号内是为了避免混淆 Swift:如果我们写的是 let payment = (user: String, amount: Int),那么看起来就像是在创建一个元组,而不是一个闭包,这就很奇怪了。

仔细想想,将参数放在大括号内还能巧妙地捕捉到整个过程是存储在变量中的一个数据块——参数列表和闭包主体都是同一段代码的一部分,并存储在我们的变量中。

将参数列表放在大括号内说明了 in 关键字的重要性——如果没有这个关键字,Swift 就很难知道闭包主体究竟从哪里开始,因为没有第二组大括号。

如何从不带参数的闭包返回值?

Swift 中的闭包拥有与简单函数截然不同的语法,其中一个容易引起混淆的地方是我们如何接受和返回参数。

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

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

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

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

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

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

如果你仔细想想,这和写 func payment() -> Bool 的标准函数是一样的。

如何使用尾部闭包和速记语法

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() 提供的函数必须提供两个字符串并返回一个布尔值,那么我们为什么还要在闭包中重复自己的操作呢?

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

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

这已经减少了代码中的杂乱无章,但我们还可以更进一步:当一个函数接受另一个函数作为参数时(如 sorted()),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,而是依赖 Swift 为我们提供的特殊命名值:$0 和 $1,分别代表第一个和第二个字符串。

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

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

    return $0 < $1
}

我把这个放在最后,是因为它不像其他语法那样一目了然——有些人甚至看到这个语法就讨厌它,因为它不太清楚,这没关系。

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

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

因此,in 用于标记参数和返回类型的结束,之后的所有内容都是闭包的主体。这样做是有原因的,你很快就会明白。

在这里,我将比较从 < 翻转到了 >,因此我们得到了一个反向排序,但现在我们只需一行代码,因此我们可以删除返回值,将其缩减到几乎没有:

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

关于何时使用速记语法,何时不使用速记语法,并没有固定的规则,但为了以防万一,除非出现以下情况,否则我会使用速记语法:

  1. 闭包代码较长。
  2. $0 和其系列参数的使用次数超过一次。
  3. 有三个或三个以上的参数(例如:$2、$3 等)。

如果你还不相信闭包的威力,让我们再来看两个例子。

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

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

这将打印出[“Tiffany”, “Tasha”],因为只有这两位队员的名字是以 T 开头的。

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

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

这将打印出[“GLORIA”, “SUZANNE”, “PIPER”, “TIFFANY”, “TASHA”],因为它将每个名字都大写了,并从结果中产生了一个新数组。

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

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

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

是的,你可以在 SwiftUI 的每次操作中都创建单独的函数,但相信我:你不会这样做。闭包让这种代码变得完全自然,我想你会惊讶于 SwiftUI 是如何使用闭包编写出非常简单、整洁的代码的。

Swift 为什么使用尾部闭包语法?

尾部闭包语法是为了让 Swift 代码更易于阅读而设计的,尽管有些人喜欢避免使用它。

我们先来看一个简单的例子。下面是一个函数,它接受一个 Double,然后接受一个包含要做的更改的闭包:

func animate(duration: Double, animations: () -> Void) {
    print("Starting a \(duration) second animation…")
    animations()
}

(如果你想知道,这是一个真实的、非常常见的 UIKit 函数的简化版本!)。

我们可以像这样调用该函数,而不使用尾部闭包:

animate(duration: 3, animations: {
    print("Fade out the image")
})

这很常见。很多人不使用尾部闭包,这没关系。但更多的 Swift 开发人员看到结尾的 }) 会有点畏缩——这并不令人愉快。

尾部闭包让我们可以清理干净,同时还能移除动画参数标签。同样的函数调用会变成这样:

animate(duration: 3) {
    print("Fade out the image")
}

当尾部闭包的含义直接与函数名称相连时,尾部闭包的效果最好——因为函数名为 animate(),所以你可以看到闭包在做什么。

如果你不确定是否要使用尾部闭包,我的建议是开始在所有地方使用它们。一旦你使用了一两个月,你就会有足够的经验来回顾并做出更明确的决定,但希望你能习惯它们,因为它们在 Swift 中真的很常见!

什么情况下应该使用速记参数名?

在使用闭包时,Swift 为我们提供了一种特殊的参数速记语法,让我们可以非常简洁地编写闭包。这种语法会自动将参数名称编号为 $0、$1、$2 等,我们无法在自己的代码中使用这些名称,所以当你看到它们时,就会立即明白这是闭包的速记语法。

至于什么时候应该使用它们,那就 “看情况 “了:

  1. 参数多吗?如果是这样,速记语法就不再有用,事实上会适得其反——是$3还是$4,你需要与$0进行比较,给它们起个实际的名字,它们的含义就会更清楚。
  2. 函数是否常用?随着 Swift 技能的提高,你会开始意识到有少数(可能有 10 个左右)极其常用的函数会使用闭包,因此其他人在阅读你的代码时会很容易理解 $0 的含义。
  3. 速记名称在你的方法中是否多次使用?如果你需要引用 $0 的次数超过两三次,你也许应该给它一个真正的名字。

重要的是,你的代码要易于阅读和理解。有时,这意味着代码要简短,但并不总是如此——根据具体情况选择速记语法。

如何接受函数作为参数

我们还想了解最后一个与闭包相关的话题,那就是如何编写接受其他函数作为参数的函数。这一点对于闭包尤为重要,因为它使用了尾部闭包语法,但无论如何,这都是一项有用的技能。

前面我们学习了这段代码:

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

greetUser()

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

我故意在这里添加了类型注解,因为这正是我们在指定函数作为参数时使用的注解:我们告诉 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. 每次循环时,我们都会调用作为参数传递进来的生成器函数。这会返回一个新的整数,因此我们将其放入数字数组中。
  5. 最后返回完成的数组。

makeArray() 的主体部分非常简单:重复调用函数生成整数,将每个值添加到数组中,然后全部返回。

复杂的部分是第一行:

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

在这里,我们有两组括号和两组返回类型,所以一开始可能会有点混乱。如果把它拆分开来,就可以线性阅读了:

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

因此,我们现在可以制作任意大小的整数数组,只需传入一个用于生成每个数字的函数即可:

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

print(rolls)

请记住,同样的功能也适用于专用函数,所以我们可以这样写:

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

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

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

在你学习 Swift 和 SwiftUI 的过程中,需要了解如何接受函数作为参数的次数屈指可数,但至少现在您已经了解了它的工作原理和重要原因。

在我们继续之前还有最后一件事:如果需要,你可以让函数接受多个函数参数,在这种情况下,你可以指定多个尾部闭包。这里的语法在 SwiftUI 中非常常见,所以在这里至少要向大家展示一下。

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

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!")
}

我在其中添加了额外的 print() 调用,以模拟在调用第一、第二和第三个闭包之间进行的特定工作。

在调用时,第一个尾部闭包与我们已经使用过的完全相同,但第二个和第三个闭包的格式不同:你要结束前一个闭包的括号,然后写入外部参数名称和一个冒号,然后开始另一个括号。

如下所示:

doImportantWork {
    print("This is the first work")
} second: {
    print("This is the second work")
} third: {
    print("This is the third work")
}

使用三个尾部闭包并不像你想象的那样罕见。例如,在 SwiftUI 中制作一个内容部分需要使用三个尾部闭包:一个用于内容本身,一个用于放在上方的页眉,一个用于放在下方的页脚。

为什么要使用闭包作为参数?

Swift 的闭包可以像使用其他类型的数据一样使用,这意味着你可以将它们传递到函数中,获取它们的副本,等等。但是,当你刚刚学习时,你可能会觉得这很像 “你可以这样做,但并不意味着你应该这样做”——你很难看到这样做的好处。

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

现在想想:如果我的应用表现不佳,需要 10 秒钟才能响应 Siri 呢?请记住,用户实际上看不到我的应用,只能看到 Siri,因此从他们的角度看,Siri 似乎完全冻结了。

这样的用户体验会很糟糕,因此苹果使用闭包来代替:它会在后台启动我们的应用,并传递给我们一个闭包,我们可以在完成后调用它。这样,我们的应用就可以花尽可能长的时间来找出需要完成的工作,而当我们完成工作后,我们就可以调用闭包向 Siri 发回数据。使用闭包发回数据而不是从函数返回值,意味着 Siri 无需等待函数完成,因此它可以保持用户界面的交互性——不会冻结。

另一个常见的例子是发出网络请求。iPhone 平均每秒可以处理几十亿件事情,但连接到日本的服务器却需要半秒甚至更长的时间,与设备上的运行速度相比,这几乎是冰川般的速度。因此,当我们从互联网上请求数据时,我们都是以闭包的方式进行的: “请获取这些数据,完成后运行这个闭包”。同样,这意味着我们不会在一些缓慢的工作正在进行时强制冻结我们的用户界面。

总结:闭包

在前面,我们已经介绍了很多关于闭包的知识,现在让我们来回顾一下:

  • 你可以在 Swift 中复制函数,除了会丢失外部参数名称外,它们的工作方式与原始函数相同。
  • 所有函数都有类型,就像其他数据类型一样。这包括它们接收的参数和返回类型,返回类型可能是 Void,也称为 “无”。
  • 通过赋值给常量或变量,可以直接创建闭包。
  • 接受参数或返回值的闭包必须在括号内声明,并在后面加上关键字 in。
  • 函数可以接受其他函数作为参数。它们必须事先声明这些函数必须使用哪些数据,Swift 会确保这些规则得到遵守。
  • 在这种情况下,你也可以传递一个闭包,而不是传递一个专用函数——你可以直接创建一个闭包。Swift 允许这两种方法同时使用。
  • 在传递闭包作为函数参数时,如果 Swift 可以自动计算出闭包内部的类型,那么您就不需要明确写出闭包内部的类型。返回值的情况也是如此,如果 Swift 可以自动计算出返回值,你就不需要指定返回值。
  • 如果函数的一个或多个最终参数是函数,你可以使用尾部闭包语法。
  • 你也可以使用诸如 $0 和 $1之类的速记参数名,但我建议只有在某些情况下才这样做。
  • 你可以自己创建接受函数作为参数的函数,不过在实践中,知道如何使用这些函数要比知道如何创建它们重要得多。

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

所以,如果你读完了本章,感觉头都要炸了,那就太好了——这意味着你对闭包的理解已经成功了一半!

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注