闭包(Closures)
闭包可以捕获和存储其所在上下文中任意常量和变量的引用。 这就是所谓的闭合并包裹着这些常量和变量,俗称闭包。Swift 会为您管理在捕获过程中涉及到的所有内存操作。
注意:
如果您不熟悉捕获(capturing)这个概念也不用担心,您可以在 值捕获 章节对其进行详细了解。
在函数 章节中介绍的全局和嵌套函数实际上也是特殊的闭包,闭包采取如下三种形式之一:
- 全局函数是一个有名字但不会捕获任何值的闭包
- 嵌套函数是一个有名字并可以捕获其封闭函数域内值的闭包
- 闭包表达式是一个利用轻量级语法所写的可以捕获其上下文中变量或常量值的匿名闭包
Swift 的闭包表达式拥有简洁的风格,并鼓励在常见场景中进行语法优化,主要优化如下:
- 利用上下文推断参数和返回值类型
- 隐式返回单表达式闭包,即单表达式闭包可以省略
return
关键字 - 参数名称缩写
- 尾随(Trailing)闭包语法
闭包表达式(Closure Expressions)
嵌套函数 是一个在较复杂函数中方便进行命名和定义自包含代码模块的方式。当然,有时候撰写小巧的没有完整定义和命名的类函数结构也是很有用处的,尤其是在您处理一些函数并需要将另外一些函数作为该函数的参数时。
闭包表达式是一种利用简洁语法构建内联闭包的方式。 闭包表达式提供了一些语法优化,使得撰写闭包变得简单明了。 下面闭包表达式的例子通过使用几次迭代展示了sort
函数定义和语法优化的方式。 每一次迭代都用更简洁的方式描述了相同的功能。
sort 函数(The Sort Function)
Swift 标准库提供了sort
函数,会根据您提供的基于输出类型排序的闭包函数将已知类型数组中的值进行排序。 一旦排序完成,函数会返回一个与原数组大小相同的新数组,该数组中包含已经正确排序的同类型元素。
下面的闭包表达式示例使用sort
函数对一个String
类型的数组进行字母逆序排序,以下是初始数组值:
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
sort
函数需要传入两个参数:
- 已知类型的数组
- 闭包函数,该闭包函数需要传入与数组类型相同的两个值,并返回一个布尔类型值来告诉
sort
函数当排序结束后传入的第一个参数排在第二个参数前面还是后面。如果第一个参数值出现在第二个参数值前面,排序闭包函数需要返回true
,反之返回false
。
该例子对一个String
类型的数组进行排序,因此排序闭包函数类型需为(String, String) -> Bool
。
提供排序闭包函数的一种方式是撰写一个符合其类型要求的普通函数,并将其作为sort
函数的第二个参数传入:
func backwards(s1: String, s2: String) -> Bool {
return s1 > s2
}
var reversed = sort(names, backwards)
// reversed 为 ["Ewa", "Daniella", "Chris", "Barry", "Alex"]
如果第一个字符串 (s1
) 大于第二个字符串 (s2
),backwards
函数返回true
,表示在新的数组中s1
应该出现在s2
前。 对于字符串中的字符来说,“大于” 表示 “按照字母顺序较晚出现”。 这意味着字母"B"
大于字母"A"
,字符串"Tom"
大于字符串"Tim"
。 其将进行字母逆序排序,"Barry"
将会排在"Alex"
之后。
然而,这是一个相当冗长的方式,本质上只是写了一个单表达式函数 (a > b)。 在下面的例子中,利用闭合表达式语法可以更好的构造一个内联排序闭包。
闭包表达式语法(Closure Expression Syntax)
闭包表达式语法有如下一般形式:
{ (parameters) -> returnType in
statements
}
闭包表达式语法可以使用常量、变量和inout
类型作为参数,不提供默认值。 也可以在参数列表的最后使用可变参数。 元组也可以作为参数和返回值。
下面的例子展示了之前backwards
函数对应的闭包表达式版本的代码:
reversed = sort(names, { (s1: String, s2: String) -> Bool in
return s1 > s2
})
需要注意的是内联闭包参数和返回值类型声明与backwards
函数类型声明相同。 在这两种方式中,都写成了(s1: String, s2: String) -> Bool
。 然而在内联闭包表达式中,函数和返回值类型都写在大括号内,而不是大括号外。
闭包的函数体部分由关键字in
引入。 该关键字表示闭包的参数和返回值类型定义已经完成,闭包函数体即将开始。
因为这个闭包的函数体部分如此短以至于可以将其改写成一行代码:
reversed = sort(names, { (s1: String, s2: String) -> Bool in return s1 > s2 } )
这说明sort
函数的整体调用保持不变,一对圆括号仍然包裹住了函数中整个参数集合。而其中一个参数现在变成了内联闭包(相比于backwards
版本的代码)。
根据上下文推断类型(Inferring Type From Context)
因为排序闭包函数是作为sort
函数的参数进行传入的,Swift可以推断其参数和返回值的类型。 sort
期望第二个参数是类型为(String, String) -> Bool
的函数,因此实际上String
,String
和Bool
类型并不需要作为闭包表达式定义中的一部分。 因为所有的类型都可以被正确推断,返回箭头 (->
) 和围绕在参数周围的括号也可以被省略:
reversed = sort(names, { s1, s2 in return s1 > s2 } )
实际上任何情况下,通过内联闭包表达式构造的闭包作为参数传递给函数时,都可以推断出闭包的参数和返回值类型,这意味着您几乎不需要利用完整格式构造任何内联闭包。
单表达式闭包隐式返回(Implicit Return From Single-Expression Clossures)
单行表达式闭包可以通过隐藏return
关键字来隐式返回单行表达式的结果,如上版本的例子可以改写为:
reversed = sort(names, { s1, s2 in s1 > s2 } )
在这个例子中,sort
函数的第二个参数函数类型明确了闭包必须返回一个Bool
类型值。 因为闭包函数体只包含了一个单一表达式 (s1 > s2
),该表达式返回Bool
类型值,因此这里没有歧义,return
关键字可以省略。
参数名称缩写(Shorthand Argument Names)
Swift 自动为内联函数提供了参数名称缩写功能,您可以直接通过$0
,$1
,$2
来顺序调用闭包的参数。
如果您在闭包表达式中使用参数名称缩写,您可以在闭包参数列表中省略对其的定义,并且对应参数名称缩写的类型会通过函数类型进行推断。 in
关键字也同样可以被省略,因为此时闭包表达式完全由闭包函数体构成:
reversed = sort(names, { $0 > $1 } )
在这个例子中,$0
和$1
表示闭包中第一个和第二个String
类型的参数。
运算符函数(Operator Functions)
实际上还有一种更简短的方式来撰写上面例子中的闭包表达式。 Swift 的String
类型定义了关于大于号 (>
) 的字符串实现,其作为一个函数接受两个String
类型的参数并返回Bool
类型的值。 而这正好与sort
函数的第二个参数需要的函数类型相符合。 因此,您可以简单地传递一个大于号,Swift可以自动推断出您想使用大于号的字符串函数实现:
reversed = sort(names, >)
运算符函数
让已有的运算符也可以对自定义的类和结构进行运算,这称为运算符重载。
这个例子展示了如何用+
让一个自定义的结构做加法。算术运算符+
是一个两目运算符,因为它有两个操作数,而且它必须出现在两个操作数之间。
例子中定义了一个名为Vector2D
的二维坐标向量 (x,y)
的结构,然后定义了让两个Vector2D
的对象相加的运算符函数。
struct Vector2D {
var x = 0.0, y = 0.0
}
@infix func + (left: Vector2D, right: Vector2D) -> Vector2D {
return Vector2D(x: left.x + right.x, y: left.y + right.y)
}
该运算符函数定义了一个全局的+
函数,这个函数需要两个Vector2D
类型的参数,返回值也是Vector2D
类型。需要定义和实现一个中置运算的时候,在关键字func
之前写上属性 @infix
就可以了。
在这个代码实现中,参数被命名为了left
和right
,代表+
左边和右边的两个Vector2D
对象。函数返回了一个新的Vector2D
的对象,这个对象的x
和y
分别等于两个参数对象的x
和y
的和。
这个函数是全局的,而不是Vector2D
结构的成员方法,所以任意两个Vector2D
对象都可以使用这个中置运算符。
let vector = Vector2D(x: 3.0, y: 1.0)
let anotherVector = Vector2D(x: 2.0, y: 4.0)
let combinedVector = vector + anotherVector
// combinedVector 是一个新的Vector2D, 值为 (5.0, 5.0)
这个例子实现两个向量 (3.0,1.0)
和 (2.0,4.0)
相加,得到向量 (5.0,5.0)
的过程。如下图示:
前置和后置运算符
上个例子演示了一个双目中置运算符的自定义实现,同样我们也可以玩标准单目运算符的实现。单目运算符只有一个操作数,在操作数之前就是前置的,如-a
; 在操作数之后就是后置的,如i++
。
实现一个前置或后置运算符时,在定义该运算符的时候于关键字func
之前标注 @prefix
或 @postfix
属性。
@prefix func - (vector: Vector2D) -> Vector2D {
return Vector2D(x: -vector.x, y: -vector.y)
}
这段代码为Vector2D
类型提供了单目减运算-a
,@prefix
属性表明这是个前置运算符。
对于数值,单目减运算符可以把正数变负数,把负数变正数。对于Vector2D
,单目减运算将其x
和y
都进进行单目减运算。
let positive = Vector2D(x: 3.0, y: 4.0)
let negative = -positive
// negative 为 (-3.0, -4.0)
let alsoPositive = -negative
// alsoPositive 为 (3.0, 4.0)
组合赋值运算符
组合赋值是其他运算符和赋值运算符一起执行的运算。如+=
把加运算和赋值运算组合成一个操作。实现一个组合赋值符号需要使用@assignment
属性,还需要把运算符的左参数设置成inout
,因为这个参数会在运算符函数内直接修改它的值。
@assignment func += (inout left: Vector2D, right: Vector2D) {
left = left + right
}
因为加法运算在之前定义过了,这里无需重新定义。所以,加赋运算符函数使用已经存在的高级加法运算符函数来执行左值加右值的运算。
var original = Vector2D(x: 1.0, y: 2.0)
let vectorToAdd = Vector2D(x: 3.0, y: 4.0)
original += vectorToAdd
// original 现在为 (4.0, 6.0)
你可以将 @assignment
属性和 @prefix
或 @postfix
属性起来组合,实现一个Vector2D
的前置运算符。
@prefix @assignment func ++ (inout vector: Vector2D) -> Vector2D {
vector += Vector2D(x: 1.0, y: 1.0)
return vector
}
这个前置使用了已经定义好的高级加赋运算,将自己加上一个值为 (1.0,1.0)
的对象然后赋给自己,然后再将自己返回。
var toIncrement = Vector2D(x: 3.0, y: 4.0)
let afterIncrement = ++toIncrement
// toIncrement 现在是 (4.0, 5.0)
// afterIncrement 现在也是 (4.0, 5.0)
注意:
默认的赋值符是不可重载的。只有组合赋值符可以重载。三目条件运算符a?b:c
也是不可重载。
比较运算符
Swift无所知道自定义类型是否相等或不等,因为等于或者不等于由你的代码说了算了。所以自定义的类和结构要使用比较符==
或!=
就需要重载。
定义相等运算符函数跟定义其他中置运算符雷同:
@infix func == (left: Vector2D, right: Vector2D) -> Bool {
return (left.x == right.x) && (left.y == right.y)
}
@infix func != (left: Vector2D, right: Vector2D) -> Bool {
return !(left == right)
}
上述代码实现了相等运算符==
来判断两个Vector2D
对象是否有相等的值,相等的概念就是它们有相同的x
值和相同的y
值,我们就用这个逻辑来实现。接着使用==
的结果实现了不相等运算符!=
。
现在我们可以使用这两个运算符来判断两个Vector2D
对象是否相等。
let twoThree = Vector2D(x: 2.0, y: 3.0)
let anotherTwoThree = Vector2D(x: 2.0, y: 3.0)
if twoThree == anotherTwoThree {
println("这两个向量是相等的.")
}
// prints "这两个向量是相等的."
尾随闭包(Trailing Closures)
如果您需要将一个很长的闭包表达式作为最后一个参数传递给函数,可以使用尾随闭包来增强函数的可读性。 尾随闭包是一个书写在函数括号之后的闭包表达式,函数支持将其作为最后一个参数调用。
func someFunctionThatTakesAClosure(closure: () -> ()) {
// 函数体部分
}
// 以下是不使用尾随闭包进行函数调用
someFunctionThatTakesAClosure({
// 闭包主体部分
})
// 以下是使用尾随闭包进行函数调用
someFunctionThatTakesAClosure() {
// 闭包主体部分
}
注意:
如果函数只需要闭包表达式一个参数,当您使用尾随闭包时,您甚至可以把()
省略掉。
在上例中作为sort
函数参数的字符串排序闭包可以改写为:
reversed = sort(names) { $0 > $1 }
当闭包非常长以至于不能在一行中进行书写时,尾随闭包变得非常有用。 举例来说,Swift 的Array
类型有一个map
方法,其获取一个闭包表达式作为其唯一参数。 数组中的每一个元素调用一次该闭包函数,并返回该元素所映射的值(也可以是不同类型的值)。 具体的映射方式和返回值类型由闭包来指定。
当提供给数组闭包函数后,map
方法将返回一个新的数组,数组中包含了与原数组一一对应的映射后的值。
下例介绍了如何在map
方法中使用尾随闭包将Int
类型数组[16,58,510]
转换为包含对应String
类型的数组["OneSix", "FiveEight", "FiveOneZero"]
:
let digitNames = [
0: "Zero", 1: "One", 2: "Two", 3: "Three", 4: "Four",
5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
]
let numbers = [16, 58, 510]
如上代码创建了一个数字位和它们名字映射的英文版本字典。 同时定义了一个准备转换为字符串的整型数组。
您现在可以通过传递一个尾随闭包给numbers
的map
方法来创建对应的字符串版本数组。 需要注意的时调用numbers.map
不需要在map
后面包含任何括号,因为其只需要传递闭包表达式这一个参数,并且该闭包表达式参数通过尾随方式进行撰写:
let strings = numbers.map {
(var number) -> String in
var output = ""
while number > 0 {
output = digitNames[number % 10]! + output
number /= 10
}
return output
}
// strings 常量被推断为字符串类型数组,即 String[]
// 其值为 ["OneSix", "FiveEight", "FiveOneZero"]
map
在数组中为每一个元素调用了闭包表达式。 您不需要指定闭包的输入参数number
的类型,因为可以通过要映射的数组类型进行推断。
闭包number
参数被声明为一个变量参数(变量的具体描述请参看常量参数和变量参数),因此可以在闭包函数体内对其进行修改。闭包表达式制定了返回类型为String
,以表明存储映射值的新数组类型为String
。
闭包表达式在每次被调用的时候创建了一个字符串并返回。 其使用求余运算符 (number % 10) 计算最后一位数字并利用digitNames
字典获取所映射的字符串。
注意:
字典digitNames
下标后跟着一个叹号 (!),因为字典下标返回一个可选值 (optional value),表明即使该 key 不存在也不会查找失败。
在上例中,它保证了number % 10
可以总是作为一个digitNames
字典的有效下标 key。
因此叹号可以用于强制解析 (force-unwrap) 存储在可选下标项中的String
类型值。
从digitNames
字典中获取的字符串被添加到输出的前部,逆序建立了一个字符串版本的数字。 (在表达式number % 10
中,如果number为16,则返回6,58返回8,510返回0)。
number
变量之后除以10。 因为其是整数,在计算过程中未除尽部分被忽略。 因此 16变成了1,58变成了5,510变成了51。
整个过程重复进行,直到number /= 10
为0,这时闭包会将字符串输出,而map
函数则会将字符串添加到所映射的数组中。
上例中尾随闭包语法在函数后整洁封装了具体的闭包功能,而不再需要将整个闭包包裹在map
函数的括号内。
捕获值(Capturing Values)
闭包可以在其定义的上下文中捕获常量或变量。 即使定义这些常量和变量的原域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。
Swift最简单的闭包形式是嵌套函数,也就是定义在其他函数的函数体内的函数。 嵌套函数可以捕获其外部函数所有的参数以及定义的常量和变量。
下例为一个叫做makeIncrementor
的函数,其包含了一个叫做incrementor
嵌套函数。 嵌套函数incrementor
从上下文中捕获了两个值,runningTotal
和amount
。 之后makeIncrementor
将incrementor
作为闭包返回。 每次调用incrementor
时,其会以amount
作为增量增加runningTotal
的值。
func makeIncrementor(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementor() -> Int {
runningTotal += amount
return runningTotal
}
return incrementor
}
makeIncrementor
返回类型为() -> Int
。 这意味着其返回的是一个函数,而不是一个简单类型值。 该函数在每次调用时不接受参数只返回一个Int
类型的值。 关于函数返回其他函数的内容,请查看函数类型作为返回类型。
makeIncrementor
函数定义了一个整型变量runningTotal
(初始为0) 用来存储当前跑步总数。 该值通过incrementor
返回。
makeIncrementor
有一个Int
类型的参数,其外部命名为forIncrement
, 内部命名为amount
,表示每次incrementor
被调用时runningTotal
将要增加的量。
incrementor
函数用来执行实际的增加操作。 该函数简单地使runningTotal
增加amount
,并将其返回。
如果我们单独看这个函数,会发现看上去不同寻常:
func incrementor() -> Int {
runningTotal += amount
return runningTotal
}
incrementor
函数并没有获取任何参数,但是在函数体内访问了runningTotal
和amount
变量。这是因为其通过捕获在包含它的函数体内已经存在的runningTotal
和amount
变量而实现。
由于没有修改amount
变量,incrementor
实际上捕获并存储了该变量的一个副本,而该副本随着incrementor
一同被存储。
然而,因为每次调用该函数的时候都会修改runningTotal
的值,incrementor
捕获了当前runningTotal
变量的引用,而不是仅仅复制该变量的初始值。捕获一个引用保证了当makeIncrementor
结束时候并不会消失,也保证了当下一次执行incrementor
函数时,runningTotal
可以继续增加。
注意:
Swift 会决定捕获引用还是拷贝值。
您不需要标注amount
或者runningTotal
来声明在嵌入的incrementor
函数中的使用方式。
Swift 同时也处理runingTotal
变量的内存管理操作,如果不再被incrementor
函数使用,则会被清除。
下面代码为一个使用makeIncrementor
的例子:
let incrementByTen = makeIncrementor(forIncrement: 10)
该例子定义了一个叫做incrementByTen
的常量,该常量指向一个每次调用会加10的incrementor
函数。 调用这个函数多次可以得到以下结果:
incrementByTen()
// 返回的值为10
incrementByTen()
// 返回的值为20
incrementByTen()
// 返回的值为30
如果您创建了另一个incrementor
,其会有一个属于自己的独立的runningTotal
变量的引用。 下面的例子中,incrementBySevne
捕获了一个新的runningTotal
变量,该变量和incrementByTen
中捕获的变量没有任何联系:
let incrementBySeven = makeIncrementor(forIncrement: 7)
incrementBySeven()
// 返回的值为7
incrementByTen()
// 返回的值为40
注意:
如果您将闭包赋值给一个类实例的属性,并且该闭包通过指向该实例或其成员来捕获了该实例,您将创建一个在闭包和实例间的强引用环。
Swift 使用捕获列表来打破这种强引用环。更多信息,请参考 闭包引起的循环强引用。
闭包引起的循环强引用
前面我们看到了循环强引用环是在两个类实例属性互相保持对方的强引用时产生的,还知道了如何用弱引用和无主引用来打破循环强引用。
循环强引用还会发生在当你将一个闭包赋值给类实例的某个属性,并且这个闭包体中又使用了实例。这个闭包体中可能访问了实例的某个属性,例如self.someProperty
,或者闭包中调用了实例的某个方法,例如self.someMethod
。这两种情况都导致了闭包 “捕获" self
,从而产生了循环强引用。
循环强引用的产生,是因为闭包和类相似,都是引用类型。当你把一个闭包赋值给某个属性时,你也把一个引用赋值给了这个闭包。实质上,这跟之前的问题是一样的-两个强引用让彼此一直有效。但是,和两个类实例不同,这次一个是类实例,另一个是闭包。
Swift 提供了一种优雅的方法来解决这个问题,称之为闭包占用列表(closuer capture list)。同样的,在学习如何用闭包占用列表破坏循环强引用之前,先来了解一下循环强引用是如何产生的,这对我们是很有帮助的。
下面的例子为你展示了当一个闭包引用了self
后是如何产生一个循环强引用的。例子中定义了一个叫HTMLElement
的类,用一种简单的模型表示 HTML 中的一个单独的元素:
class HTMLElement {
let name: String
let text: String?
@lazy var asHTML: () -> String = {
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
println("\(name) is being deinitialized")
}
}
HTMLElement
类定义了一个name
属性来表示这个元素的名称,例如代表段落的"p",或者代表换行的"br"。HTMLElement
还定义了一个可选属性text
,用来设置和展现 HTML 元素的文本。
除了上面的两个属性,HTMLElement
还定义了一个lazy
属性asHTML
。这个属性引用了一个闭包,将name
和text
组合成 HTML 字符串片段。该属性是() -> String
类型,或者可以理解为“一个没有参数,返回String
的函数”。
默认情况下,闭包赋值给了asHTML
属性,这个闭包返回一个代表 HTML 标签的字符串。如果text
值存在,该标签就包含可选值text
;如果text
不存在,该标签就不包含文本。对于段落元素,根据text
是"some text"还是nil
,闭包会返回"<p>some text</p>
"或者"<p />
"。
可以像实例方法那样去命名、使用asHTML
属性。然而,由于asHTML
是闭包而不是实例方法,如果你想改变特定元素的 HTML 处理的话,可以用自定义的闭包来取代默认值。
注意:
asHTML
声明为lazy
属性,因为只有当元素确实需要处理为HTML输出的字符串时,才需要使用asHTML
。也就是说,在默认的闭包中可以使用self
,因为只有当初始化完成以及self
确实存在后,才能访问lazy
属性。
HTMLElement
类只提供一个构造函数,通过name
和text
(如果有的话)参数来初始化一个元素。该类也定义了一个析构函数,当HTMLElement
实例被销毁时,打印一条消息。
下面的代码展示了如何用HTMLElement
类创建实例并打印消息。
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
println(paragraph!.asHTML())
// prints"hello, world"
注意:
上面的paragraph
变量定义为可选HTMLElement
,因此我们可以赋值nil
给它来演示循环强引用。
不幸的是,上面写的HTMLElement
类产生了类实例和asHTML
默认值的闭包之间的循环强引用。循环强引用如下图所示:
实例的asHTML
属性持有闭包的强引用。但是,闭包在其闭包体内使用了self
(引用了self.name
和self.text
),因此闭包捕获了self
,这意味着闭包又反过来持有了HTMLElement
实例的强引用。这样两个对象就产生了循环强引用。(更多关于闭包捕获值的信息,请参考值捕获)。
注意:
虽然闭包多次使用了self
,它只捕获HTMLElement
实例的一个强引用。
如果设置paragraph
变量为nil
,打破它持有的HTMLElement
实例的强引用,HTMLElement
实例和它的闭包都不会被销毁,也是因为循环强引用:
paragraph = nil
注意HTMLElementdeinitializer
中的消息并没有别打印,证明了HTMLElement
实例并没有被销毁。
解决闭包引起的循环强引用
在定义闭包时同时定义捕获列表作为闭包的一部分,通过这种方式可以解决闭包和类实例之间的循环强引用。捕获列表定义了闭包体内捕获一个或者多个引用类型的规则。跟解决两个类实例间的循环强引用一样,声明每个捕获的引用为弱引用或无主引用,而不是强引用。应当根据代码关系来决定使用弱引用还是无主引用。
注意:
Swift 有如下要求:只要在闭包内使用self
的成员,就要用self.someProperty
或者self.someMethod
(而不只是someProperty
或someMethod
)。这提醒你可能会不小心就捕获了self
。
定义捕获列表
捕获列表中的每个元素都是由weak
或者unowned
关键字和实例的引用(如self
或someInstance
)成对组成。每一对都在方括号中,通过逗号分开。
捕获列表放置在闭包参数列表和返回类型之前:
@lazy var someClosure: (Int, String) -> String = {
[unowned self] (index: Int, stringToProcess: String) -> String in
// closure body goes here
}
如果闭包没有指定参数列表或者返回类型,则可以通过上下文推断,那么可以捕获列表放在闭包开始的地方,跟着是关键字in
:
@lazy var someClosure: () -> String = {
[unowned self] in
// closure body goes here
}
弱引用和无主引用
当闭包和捕获的实例总是互相引用时并且总是同时销毁时,将闭包内的捕获定义为无主引用。
相反的,当捕获引用有时可能会是nil
时,将闭包内的捕获定义为弱引用。弱引用总是可选类型,并且当引用的实例被销毁后,弱引用的值会自动置为nil
。这使我们可以在闭包内检查它们是否存在。
注意:
如果捕获的引用绝对不会置为nil
,应该用无主引用,而不是弱引用。
前面的HTMLElement
例子中,无主引用是正确的解决循环强引用的方法。这样编写HTMLElement
类来避免循环强引用:
class HTMLElement {
let name: String
let text: String?
@lazy var asHTML: () -> String = {
[unowned self] in
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
println("\(name) is being deinitialized")
}
}
上面的HTMLElement
实现和之前的实现一致,只是在asHTML
闭包中多了一个捕获列表。这里,捕获列表是[unowned self]
,表示“用无主引用而不是强引用来捕获self
”。
和之前一样,我们可以创建并打印HTMLElement
实例:
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
println(paragraph!.asHTML())
// prints "<p>hello, world</p>"
使用捕获列表后引用关系如下图所示:
这一次,闭包以无主引用的形式捕获self
,并不会持有HTMLElement
实例的强引用。如果将paragraph
赋值为nil
,HTMLElement
实例将会被销毁,并能看到它的析构函数打印出的消息。
paragraph = nil
// prints "p is being deinitialized"
可选链(Optional Chaining)
可选链(Optional Chaining)是一种可以请求和调用属性、方法及子脚本的过程,它的可选性体现于请求或调用的目标当前可能为空(nil
)。如果可选的目标有值,那么调用就会成功;相反,如果选择的目标为空(nil
),则这种调用将返回空(nil
)。多次请求或调用可以被链接在一起形成一个链,如果任何一个节点为空(nil
)将导致整个链失效。
注意:
Swift 的可选链和 Objective-C 中的消息为空有些相像,但是 Swift 可以使用在任意类型中,并且失败与否可以被检测到。
可选链可替代强制解析
通过在想调用的属性、方法、或子脚本的可选值(optional value
)(非空)后面放一个问号,可以定义一个可选链。这一点很像在可选值后面放一个叹号来强制拆得其封包内的值。它们的主要的区别在于当可选值为空时可选链即刻失败,然而一般的强制解析将会引发运行时错误。
为了反映可选链可以调用空(nil
),不论你调用的属性、方法、子脚本等返回的值是不是可选值,它的返回结果都是一个可选值。你可以利用这个返回值来检测你的可选链是否调用成功,有返回值即成功,返回nil则失败。
调用可选链的返回结果与原本的返回结果具有相同的类型,但是原本的返回结果被包装成了一个可选值,当可选链调用成功时,一个应该返回Int
的属性将会返回Int?
。
下面几段代码将解释可选链和强制解析的不同。
首先定义两个类Person
和Residence
。
class Person {
var residence: Residence?
}
class Residence {
var numberOfRooms = 1
}
Residence
具有一个Int
类型的numberOfRooms
,其值为 1。Person
具有一个可选residence
属性,它的类型是Residence?
。
如果你创建一个新的Person
实例,它的residence
属性由于是被定义为可选型的,此属性将默认初始化为空:
let john = Person()
如果你想使用感叹号(!
)强制解析获得这个人residence
属性numberOfRooms
属性值,将会引发运行时错误,因为这时没有可以供解析的residence
值。
let roomCount = john.residence!.numberOfRooms
//将导致运行时错误
当john.residence
不是nil
时,会运行通过,且会将roomCount
设置为一个int
类型的合理值。然而,如上所述,当residence
为空时,这个代码将会导致运行时错误。
可选链提供了一种另一种获得numberOfRooms
的方法。利用可选链,使用问号来代替原来!
的位置:
if let roomCount = john.residence?.numberOfRooms {
println("John's residence has \(roomCount) room(s).")
} else {
println("Unable to retrieve the number of rooms.")
}
// 打印 "Unable to retrieve the number of rooms.
这告诉 Swift 来链接可选residence?
属性,如果residence
存在则取回numberOfRooms
的值。
因为这种尝试获得numberOfRooms
的操作有可能失败,可选链会返回Int?
类型值,或者称作“可选Int
”。当residence
是空的时候(上例),选择Int
将会为空,因此会出先无法访问numberOfRooms
的情况。
要注意的是,即使numberOfRooms是非可选Int
(Int?
)时这一点也成立。只要是通过可选链的请求就意味着最后numberOfRooms
总是返回一个Int?
而不是Int
。
你可以自己定义一个Residence
实例给john.residence
,这样它就不再为空了:
john.residence = Residence()
john.residence
现在有了实际存在的实例而不是nil了。如果你想使用和前面一样的可选链来获得numberOfRoooms
,它将返回一个包含默认值 1 的Int?
:
if let roomCount = john.residence?.numberOfRooms {
println("John's residence has \(roomCount) room(s).")
} else {
println("Unable to retrieve the number of rooms.")
}
// 打印 "John's residence has 1 room(s)"。
为可选链定义模型类
你可以使用可选链来多层调用属性,方法,和子脚本。这让你可以利用它们之间的复杂模型来获取更底层的属性,并检查是否可以成功获取此类底层属性。
后面的代码定义了四个将在后面使用的模型类,其中包括多层可选链。这些类是由上面的Person
和Residence
模型通过添加一个Room
和一个Address
类拓展来。
Person
类定义与之前相同。
class Person {
var residence: Residence?
}
Residence
类比之前复杂些。这次,它定义了一个变量 rooms
,它被初始化为一个Room[]
类型的空数组:
class Residence {
var rooms = Room[]()
var numberOfRooms: Int {
return rooms.count
}
subscript(i: Int) -> Room {
return rooms[i]
}
func printNumberOfRooms() {
println("The number of rooms is \(numberOfRooms)")
}
var address: Address?
}
因为Residence
存储了一个Room
实例的数组,它的numberOfRooms
属性值不是一个固定的存储值,而是通过计算而来的。numberOfRooms
属性值是由返回rooms
数组的count
属性值得到的。
为了能快速访问rooms
数组,Residence
定义了一个只读的子脚本,通过插入数组的元素角标就可以成功调用。如果该角标存在,子脚本则将该元素返回。
Residence
中也提供了一个printNumberOfRooms
的方法,即简单的打印房间个数。
最后,Residence
定义了一个可选属性叫address
(address?
)。Address
类的属性将在后面定义。 用于rooms
数组的Room
类是一个很简单的类,它只有一个name
属性和一个设定room
名的初始化器。
class Room {
let name: String
init(name: String) { self.name = name }
}
这个模型中的最终类叫做Address
。它有三个类型是String?
的可选属性。前面两个可选属性buildingName
和 buildingNumber
作为地址的一部分,是定义某个建筑物的两种方式。第三个属性street
,用于命名地址的街道名:
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier() -> String? {
if buildingName {
return buildingName
} else if buildingNumber {
return buildingNumber
} else {
return nil
}
}
}
Address
类还提供了一个buildingIdentifier
的方法,它的返回值类型为String?
。这个方法检查buildingName
和buildingNumber
的属性,如果buildingName
有值则将其返回,或者如果buildingNumber
有值则将其返回,再或如果没有一个属性有值,返回空。
通过可选链调用属性
正如上面“ 可选链可替代强制解析”中所述,你可以利用可选链的可选值获取属性,并且检查属性是否获取成功。然而,你不能使用可选链为属性赋值。
使用上述定义的类来创建一个人实例,并再次尝试后去它的numberOfRooms
属性:
let john = Person()
if let roomCount = john.residence?.numberOfRooms {
println("John's residence has \(roomCount) room(s).")
} else {
println("Unable to retrieve the number of rooms.")
}
// 打印 "Unable to retrieve the number of rooms。
由于john.residence
是空,所以这个可选链和之前一样失败了,但是没有运行时错误
通过可选链调用方法
你可以使用可选链的来调用可选值的方法并检查方法调用是否成功。即使这个方法没有返回值,你依然可以使用可选链来达成这一目的。
Residence
的printNumberOfRooms
方法会打印numberOfRooms
的当前值。方法如下:
func printNumberOfRooms(){
println(“The number of rooms is \(numberOfRooms)”)
}
这个方法没有返回值。但是,没有返回值类型的函数和方法有一个隐式的返回值类型Void
(参见Function Without Return Values)。
如果你利用可选链调用此方法,这个方法的返回值类型将是Void?
,而不是Void
,因为当通过可选链调用方法时返回值总是可选类型(optional type)。即使这个方法本身没有定义返回值,你也可以使用if
语句来检查是否能成功调用printNumberOfRooms
方法:如果方法通过可选链调用成功,printNumberOfRooms
的隐式返回值将会是Void
,如果没有成功,将返回nil
:
if john.residence?.printNumberOfRooms() {
println("It was possible to print the number of rooms.")
} else {
println("It was not possible to print the number of rooms.")
}
// 打印 "It was not possible to print the number of rooms."。
使用可选链调用子脚本
你可以使用可选链来尝试从子脚本获取值并检查子脚本的调用是否成功,然而,你不能通过可选链来设置子代码。
注意:
当你使用可选链来获取子脚本的时候,你应该将问号放在子脚本括号的前面而不是后面。可选链的问号一般直接跟在表达语句的后面。
下面这个例子用在Residence
类中定义的子脚本来获取john.residence
数组中第一个房间的名字。因为john.residence
现在是nil
,子脚本的调用失败了。
if let firstRoomName = john.residence?[0].name {
println("The first room name is \(firstRoomName).")
} else {
println("Unable to retrieve the first room name.")
}
// 打印 "Unable to retrieve the first room name."。
在子代码调用中可选链的问号直接跟在john.residence
的后面,在子脚本括号的前面,因为john.residence
是可选链试图获得的可选值。
如果你创建一个Residence
实例给john.residence
,且在他的rooms
数组中有一个或多个Room
实例,那么你可以使用可选链通过Residence
子脚本来获取在rooms
数组中的实例了:
let johnsHouse = Residence()
johnsHouse.rooms += Room(name: "Living Room")
johnsHouse.rooms += Room(name: "Kitchen")
john.residence = johnsHouse
if let firstRoomName = john.residence?[0].name {
println("The first room name is \(firstRoomName).")
} else {
println("Unable to retrieve the first room name.")
}
// 打印 "The first room name is Living Room."。
连接多层链接
你可以将多层可选链连接在一起,可以掘取模型内更下层的属性方法和子脚本。然而多层可选链不能再添加比已经返回的可选值更多的层。 也就是说:
如果你试图获得的类型不是可选类型,由于使用了可选链它将变成可选类型。 如果你试图获得的类型已经是可选类型,由于可选链它也不会提高可选性。
因此:
如果你试图通过可选链获得Int
值,不论使用了多少层链接返回的总是Int?
。 相似的,如果你试图通过可选链获得Int?
值,不论使用了多少层链接返回的总是Int?
。
下面的例子试图获取john
的residence
属性里的address
的street
属性。这里使用了两层可选链来联系residence
和address
属性,它们两者都是可选类型:
if let johnsStreet = john.residence?.address?.street {
println("John's street name is \(johnsStreet).")
} else {
println("Unable to retrieve the address.")
}
// 打印 "Unable to retrieve the address.”。
john.residence
的值现在包含一个Residence
实例,然而john.residence.address
现在是nil
,因此john.residence?.address?.street
调用失败。
从上面的例子发现,你试图获得street
属性值。这个属性的类型是String?
。因此尽管在可选类型属性前使用了两层可选链,john.residence?.address?.street
的返回值类型也是String?
。
如果你为Address
设定一个实例来作为john.residence.address
的值,并为address
的street
属性设定一个实际值,你可以通过多层可选链来得到这个属性值。
let johnsAddress = Address()
johnsAddress.buildingName = "The Larches"
johnsAddress.street = "Laurel Street"
john.residence!.address = johnsAddress
if let johnsStreet = john.residence?.address?.street {
println("John's street name is \(johnsStreet).")
} else {
println("Unable to retrieve the address.")
}
// 打印 "John's street name is Laurel Street."。
值得注意的是,“!
”符号在给john.residence.address
分配address
实例时的使用。john.residence
属性是一个可选类型,因此你需要在它获取address
属性之前使用!
解析以获得它的实际值。
链接可选返回值的方法
前面的例子解释了如何通过可选链来获得可选类型属性值。你也可以通过可选链调用一个返回可选类型值的方法并按需链接该方法的返回值。
下面的例子通过可选链调用了Address
类中的buildingIdentifier
方法。这个方法的返回值类型是String?
。如上所述,这个方法在可选链调用后最终的返回值类型依然是String?
:
if let buildingIdentifier = john.residence?.address?.buildingIdentifier() {
println("John's building identifier is \(buildingIdentifier).")
}
// 打印 "John's building identifier is The Larches."。
如果你还想进一步对方法返回值执行可选链,将可选链问号符放在方法括号的后面:
if let upper = john.residence?.address?.buildingIdentifier()?.uppercaseString {
println("John's uppercase building identifier is \(upper).")
}
// 打印 "John's uppercase building identifier is THE LARCHES."。
注意:
在上面的例子中,你将可选链问号符放在括号后面是因为你想要链接的可选值是buildingIdentifier
方法的返回值,不是buildingIdentifier
方法本身。