Swift闭包基础要点

闭包是自包含的函数代码块,可以捕获和存储其所在上下文中任意常量和变量的引用,被称为包裹常量和变量。Swift会自动管理在捕获过程中涉及的内存操作。

闭包的形式:

  • 全局函数:一个有名字但不会捕获任何值的闭包
  • 嵌套函数:一个有名字并可以捕获其封闭函数域内值的闭包
  • 闭包表达式:一个利用轻量级语法所写的可以捕获上下文中变量或常量值的匿名闭包

闭包表达式

闭包表达式一般形式:

1
2
3
4
{
(parameters)->returnType in
statements
}

示例:

Swift标准库提供了sorted(by:)方法,它会根据你所提供的用于排序的闭包函数将已知类型数组中的值进行排序。一旦排序完成,sorted(by:)方法会返回一个与原数组大小相同,包含同类型元素且元素已正确排序的新数组。下面的闭包表达式使用sorted(by:)方法对一个Sring类型的数组进行字母逆序排序:

1
2
3
4
5
let names = ["Baylee","Tom","Barry","Diniella"]
//sorted(by:)函数的参数需要是个闭包,这是个内联闭包
reverseNames = names.sorted(by:{(s1: String, s2: String) -> Bool in
return s1 > s2 //statements
})

说明:如果第一个字符串(s1)大于第二个字符串(s2),backward(::) 函数会返回 true,表示在新的数组中 s1 应该出现在 s2 前。对于字符串中的字符来说,“大于”表示“按照字母顺序较晚出现”。这意味着字母 “B” 大于字母 “A” ,字符串 “Tom” 大于字符串 “Tim”。该闭包将进行字母逆序排序,”Barry” 将会排在 “Alex” 之前。

根据上下文推断类型

因为排序闭包函数是作为sorted(by:)方法的参数传入的,Swift可以推断其参数和返回值的型。sorted(by:)方法被一个字符串数组调用,因此其参数必须是(String, String) -> Bool类型的函数。这意味着(String, String)Bool类型并不需要作为闭包表达式定义的一部分。因为所有的类型都可以被正确推断:

1
2
3
reverseNames = names.sorted(by: { s1,s2 in return s1 > s2 } )
print(reverseNames)
//输出:["Tom", "Diniella", "Baylee", "Barry"]

单表达式闭包隐式返回

单行表达式闭包可以通过省略return关键字来隐式返回单行表达式的结果:

1
reverseNames = names.sorted(by:{ s1, s2 in s1 > s2})

参数名称缩写

Swift自动为内联闭包提供了参数名称缩写功能,可以直接通过$0,$1,$2来顺序调用闭包的参数。

如果你在闭包表达式中使用参数名称缩写,则可以在闭包定义中省略参数列表,并且对应参数名称缩写的类型会通过函数类型进行推断。in关键字也同样可以被省略,因为此时表达式完全由闭包函数体构成:

1
2
//$0,$1表示闭包中第一个和第二个String类型的参数
reverseNames = names.sorted(by:{ $0 > $1 })

运算符方法

Swift的String类型定义了关于>的字符串实现,其作为一个函数接受两个String类型的参数并返回Bool类型的值。而这正好与sorted(by:)方法的参数需要的函数类型相符,所以可以简单地传递一个大于号(>),Swift可以自动推断出你想使用大于号的字符串函数实现:

1
reverseNames = names.sorted(by: >)

尾随闭包

如果你需要将一个很长的闭包表达式作为最后一个参数传递给函数,可以使用尾随闭包来增强函数的可读性。尾随闭包是一个书写在函数括号之后的闭包表达式,函数支持将其作为最后一个参数调用。在使用尾随闭包时,你不用写出它的参数标签:

1
2
3
4
5
6
7
8
9
10
11
12
13
func someFunctionThatTakesAClosure(closure: () -> Void) {
// 函数体部分
}

// 不使用尾随闭包进行函数调用
someFunctionThatTakesAClosure(closure: {
// 闭包主体部分
})

// 使用尾随闭包进行函数调用
someFunctionThatTakesAClosure() {
// 闭包主体部分
}

在闭包表达式语法一节中作为 sorted(by:) 方法参数的字符串排序闭包可以改写为:

1
reversedNames = names.sorted() { $0 > $1 }

如果闭包表达式是函数或方法的唯一参数,则当你使用尾随闭包时,你甚至可以把 () 省略掉:

1
reversedNames = names.sorted { $0 > $1 }

Swift的Array类型有一个map(_:)方法,这个方法获取一个闭包表达式作为其唯一参数。该闭包函数会为数组中的每一个元素调用一次,并返回该元素所映射的值。具体的映射方式和返回值类型由闭包来指定。

当提供给数组的闭包应用于每个数组元素后,map(_:)方法将会返回一个新的数组,数组中包含了与原数组中的元素一一对应的映射后的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let digitNames = [0:"zero",1:"one",2:"two",3:"three",4:"four",5:"five",6:"six",7:"seven",8:"eight",9:"nine"]

//将被转换成String的整型数组
let numbers = [16,58,510]
let strings = numbers.map{
(number) -> String in
var number = number
var output = ""
repeat{
//digitNames下标后跟着一个叹号,因为字典下标返回一个可选值,标明该键不存在时会查找失败。叹号用于强制解包存储在下标的可选类型的返回值中的String类型的值。
output = digitNames[number % 10]! + output
number /= 10
}while number > 0
return output
}

值捕获

闭包可以在其被定义的上下文中捕获常量和变量。可以捕获值的闭包的最简单的形式就是嵌套函数。嵌套函数可以捕获其他外部函数所有的参数以及定义的常量和变量。

1
2
3
4
5
6
7
8
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}

捕获引用保证了 runningTotal 和 amount 变量在调用完 makeIncrementer 后不会消失,并且保证了在下一次执行 incrementer 函数时,runningTotal 依旧存在。

调用makeIncrementer:

1
2
3
4
5
6
7
let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen()
// 返回的值为10
incrementByTen()
// 返回的值为20
incrementByTen()
// 返回的值为30

如果你创建了另一个 incrementer,它会有属于自己的引用,指向一个全新、独立的 runningTotal 变量:

1
2
3
let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// 返回的值为7

再次调用原来的 incrementByTen 会继续增加它自己的 runningTotal 变量,该变量和 incrementBySeven 中捕获的变量没有任何联系:

1
2
incrementByTen()
// 返回的值为40

如果你将闭包赋值给一个类实例的属性,并且该闭包通过访问该实例或其成员而捕获了该实例,你将在闭包和该实例间创建一个循环强引用。Swift 使用捕获列表来打破这种循环强引用

闭包是引用类型

无论你将函数或闭包赋值给一个常量还是变量,你实际上都是将常量或变量的值设置为对应函数或闭包的引用。这也意味着如果你将闭包赋值给了两个不同的常量或变量,两个值都会指向同一个闭包:

1
2
3
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// 返回的值为50

逃逸闭包

当一个闭包作为参数传到一个函数中,但是这个闭包在函数返回之后才被执行,我们称该闭包从函数中逃逸。当你定义接受闭包作为参数的函数时,你可以在参数名之前标注 @escaping,用来指明这个闭包是允许“逃逸”出这个函数的。

一种能使闭包“逃逸”出函数的方法是,将这个闭包保存在一个函数外部定义的变量中。举个例子,很多启动异步操作的函数接受一个闭包参数作为 completion handler。这类函数会在异步操作开始之后立刻返回,但是闭包直到异步操作结束后才会被调用。在这种情况下,闭包需要“逃逸”出函数,因为闭包需要在函数返回之后被调用:

1
2
3
4
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}

someFunctionWithEscapingClosure(_:)函数接受一个闭包作为参数,该闭包被添加到一个函数外定义的数组中。如果你不将这个参数标记为@escaping,就会得到一个编译错误。将一个闭包标记为 @escaping 意味着你必须在闭包中显式地引用 self。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func someFunctionWithNonescapingClosure(closure: () -> Void) {
closure()
}

class SomeClass {
var x = 10
func doSomething() {
someFunctionWithEscapingClosure { self.x = 100 }
someFunctionWithNonescapingClosure { x = 200 }
}
}

let instance = SomeClass()
instance.doSomething()
print(instance.x)
// 打印出 "200"

completionHandlers.first?()
print(instance.x)
// 打印出 "100"

/*
传递到 someFunctionWithEscapingClosure(_:) 中的闭包是一个逃逸闭包,
这意味着它需要显式地引用 self。相对的,传递到
someFunctionWithNonescapingClosure(_:)
中的闭包是一个非逃逸闭包,这意味着它可以隐式引用 self
*/

自动闭包?????

自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。这种闭包不接受任何参数,当它被调用时,会返回被包装在其中的表达式的值。可以省略闭包的花括号,而用一个普通的表达式来代替显式的闭包。

自动闭包能够延迟求值,因为直到调用这个闭包,代码段才会被执行。

1
2
3
4
5
6
7
8
9
10
11
12
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// 打印出 "5"

let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// 打印出 "5"

print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// 打印出 "4"